编译期注解语法支持

pull/34/head
韩天峰 1 month ago
parent 8bcd6ce863
commit 18b798e8ce
  1. 1
      cli.php
  2. 30
      examples/override/ns.php
  3. 7
      phpunit/code/arrayable-dynamic-field.php
  4. 8
      phpunit/code/arrayable-explicit-fields.php
  5. 12
      phpunit/code/arrayable-generated-method-conflict.php
  6. 7
      phpunit/code/arrayable-invalid-argument.php
  7. 6
      phpunit/code/arrayable-invalid-target.php
  8. 13
      phpunit/code/arrayable-parent-visible-fields.php
  9. 71
      phpunit/code/arrayable.php
  10. 10
      phpunit/code/compile-time-attribute-duplicate.php
  11. 82
      phpunit/code/compile_time_attributes.php
  12. 35
      phpunit/code/compiler_api/library_import_php.php
  13. 11
      phpunit/code/constructor-existing.php
  14. 14
      phpunit/code/constructor-parent-final.php
  15. 11
      phpunit/code/constructor-parent-none.php
  16. 14
      phpunit/code/constructor-parent-optional.php
  17. 14
      phpunit/code/constructor-parent-private.php
  18. 14
      phpunit/code/constructor-parent-required.php
  19. 7
      phpunit/code/constructor-static.php
  20. 12
      phpunit/code/generated-method-declared-conflict.php
  21. 15
      phpunit/code/generated-method-final-parent-conflict.php
  22. 10
      phpunit/code/generated-method-generated-conflict.php
  23. 14
      phpunit/code/generated-method-parent-conflict.php
  24. 11
      phpunit/code/getter-hook-property.php
  25. 10
      phpunit/code/getter-readonly-property.php
  26. 6
      phpunit/code/hot-cold-conflict.php
  27. 46
      phpunit/code/hot-cold.php
  28. 6
      phpunit/code/hot-invalid-target.php
  29. 57
      phpunit/code/language_attributes.php
  30. 106
      phpunit/code/methods-for-inheritance.php
  31. 19
      phpunit/code/methods-for-interface-target.php
  32. 24
      phpunit/code/methods-for-keyword-conflict-reversed.php
  33. 24
      phpunit/code/methods-for-keyword-conflict.php
  34. 42
      phpunit/code/methods-for.php
  35. 12
      phpunit/code/must-use-discard.php
  36. 16
      phpunit/code/must-use-method-discard.php
  37. 3
      phpunit/code/not-empty-arrow-function.php
  38. 3
      phpunit/code/not-null-arrow-function.php
  39. 19
      phpunit/code/not-null-nullable-warning.php
  40. 16
      phpunit/code/override-arguments.php
  41. 16
      phpunit/code/override-attribute-private-parent.php
  42. 16
      phpunit/code/override-constructor.php
  43. 17
      phpunit/code/override-duplicate.php
  44. 7
      phpunit/code/override-interface-missing.php
  45. 6
      phpunit/code/override-invalid-target.php
  46. 9
      phpunit/code/override-missing.php
  47. 14
      phpunit/code/override-trait-missing.php
  48. 62
      phpunit/code/override-valid.php
  49. 10
      phpunit/code/parameter-validation-order.php
  50. 12
      phpunit/code/printer-arrayable-field-types.php
  51. 12
      phpunit/code/printer-generated-method-conflict.php
  52. 11
      phpunit/code/printer-parent-private-field.php
  53. 8
      phpunit/code/printer-private-fields.php
  54. 7
      phpunit/code/printer-static-field.php
  55. 10
      phpunit/code/setter-hook-property.php
  56. 7
      phpunit/code/setter-readonly-property.php
  57. 3
      phpunit/code/validate-arrow-function.php
  58. 13
      phpunit/code/validate-compatible-types.php
  59. 9
      phpunit/code/validate-duplicate.php
  60. 6
      phpunit/code/validate-incompatible-array.php
  61. 6
      phpunit/code/validate-incompatible-email-int.php
  62. 6
      phpunit/code/validate-invalid-target.php
  63. 6
      phpunit/code/validate-sanitize.php
  64. 11
      phpunit/code/with-hook-property.php
  65. 10
      phpunit/code/with-readonly-property.php
  66. 532
      phpunit/src/ClassTest.php
  67. 47
      phpunit/src/CompileTimeAttributeRegistryTest.php
  68. 81
      phpunit/src/CompilerBaseApiTest.php
  69. 2
      phpunit/src/Entity/FunctionDefTest.php
  70. 50
      src/CompilerBase.php
  71. 93
      src/Diagnostics/CompileTimeAttributeDiagnostic.php
  72. 19
      src/Diagnostics/CompilerDiagnosticTrait.php
  73. 10
      src/Entity/ClassDef.php
  74. 8
      src/Entity/FunctionDef.php
  75. 26
      src/Exception/CompileTimeAttributeError.php
  76. 3
      src/Generator/FiberGenerator.php
  77. 34
      src/Generator/LibraryImportStubGenerator.php
  78. 77
      src/Parser/MethodCallTrait.php
  79. 150
      src/Parser/UniversalMethodCall.php
  80. 178
      src/Preprocessor.php
  81. 80
      src/Transform/ArrayableLowering.php
  82. 96
      src/Transform/ClassFieldSelection.php
  83. 141
      src/Transform/CompileTimeAttribute.php
  84. 123
      src/Transform/CompileTimeAttributeRegistry.php
  85. 102
      src/Transform/ConstructorLowering.php
  86. 35
      src/Transform/FunctionAttributeLowering.php
  87. 19
      src/Transform/GetterLowering.php
  88. 26
      src/Transform/NotEmptyLowering.php
  89. 48
      src/Transform/NotNullLowering.php
  90. 117
      src/Transform/ParameterValidationLowering.php
  91. 114
      src/Transform/PrinterLowering.php
  92. 46
      src/Transform/PropertyMethodLowering.php
  93. 219
      src/Transform/ValidateLowering.php
  94. 115
      src/Transform/Visitor.php
  95. 406
      src/Translator.php
  96. 12
      src/gen_stub.php
  97. 51
      src/polyfills.php

@ -1,5 +1,4 @@
#!/usr/bin/env php #!/usr/bin/env php
<?php <?php
require __DIR__ . '/src/polyfills.php';
include $argv[1]; include $argv[1];
main($argc, $argv); main($argc, $argv);

@ -0,0 +1,30 @@
<?php
namespace App {
class Base
{
function foo2()
{
var_dump(__METHOD__);
}
}
use \override as myoverride;
class Child extends Base
{
#[myoverride]
function foo()
{
var_dump(__LINE__ . '()' . __METHOD__);
}
}
}
namespace {
function main()
{
$c = new App\Child();
$c->foo();
}
}

@ -0,0 +1,7 @@
<?php
#[Arrayable(fields: ['missing'])]
class ArrayableDynamicField
{
public int $id = 1;
}

@ -0,0 +1,8 @@
<?php
#[Arrayable(fields: ['id', 'hidden'])]
class ArrayableExplicitFields
{
private int $id = 1;
protected array $hidden = [];
}

@ -0,0 +1,12 @@
<?php
#[Arrayable]
class ArrayableGeneratedMethodConflict
{
public string $name = 'TypePHP';
public function toArray(): array
{
return ['name' => $this->name];
}
}

@ -0,0 +1,7 @@
<?php
#[Arrayable(fields: 'id')]
class InvalidArrayableArgument
{
public int $id = 1;
}

@ -0,0 +1,6 @@
<?php
#[Arrayable]
function invalidArrayableTarget(): void
{
}

@ -0,0 +1,13 @@
<?php
class ArrayableParentVisibleFieldsBase
{
public int $id = 1;
protected string $name = 'TypePHP';
}
#[Arrayable(fields: ['id', 'name', 'local'])]
class ArrayableParentVisibleFieldsChild extends ArrayableParentVisibleFieldsBase
{
private array $local = [];
}

@ -0,0 +1,71 @@
<?php
class ArrayableBase
{
public int $baseId = 1;
protected string $hidden = 'hidden';
}
#[Arrayable(fields: ['name', 'baseId'])]
#[Printer(['name'])]
class ArrayableUser extends ArrayableBase
{
public string $name = '张三';
public string $email = 'user@example.com';
}
#[Arrayable]
class ArrayableDefaults extends ArrayableBase
{
public string $name = 'default';
public static int $shared = 1;
}
#[Arrayable([])]
class EmptyArrayable
{
public int $id = 1;
}
#[Arrayable(['lateId'])]
class LateFieldArrayable extends LateFieldBase
{
}
class LateFieldBase
{
public int $lateId = 9;
}
#[Arrayable]
class LateCustomArrayable extends LateCustomArrayableBase
{
public int $id = 1;
}
class LateCustomArrayableBase
{
public function toArray(): array
{
return ['custom' => 8];
}
}
function main(): void
{
$user = new ArrayableUser();
$data = $user->toArray();
echo $data['name'];
echo $data['baseId'];
echo count($data);
echo $user;
echo $user->toString();
$defaults = (new ArrayableDefaults())->toArray();
echo $defaults['baseId'];
echo $defaults['name'];
echo count($defaults);
echo count((new EmptyArrayable())->toArray());
echo (new LateFieldArrayable())->toArray()['lateId'];
echo (new LateCustomArrayable())->toArray()['custom'];
}

@ -0,0 +1,10 @@
<?php
use \Getter as ReadProperty;
class CompileTimeAttributeDuplicate
{
#[Getter]
#[ReadProperty]
private string $name = 'TypePHP';
}

@ -2,8 +2,11 @@
namespace CompileTimeAttributes; namespace CompileTimeAttributes;
use \Arrayable;
use \Getter; use \Getter;
use \Validate;
use \NotNull; use \NotNull;
use \NotEmpty;
use \Printer; use \Printer;
use \Setter; use \Setter;
use \With; use \With;
@ -15,7 +18,8 @@ class PrintableBase
protected string $ignored = 'hidden'; protected string $ignored = 'hidden';
} }
#[Printer] #[Printer(fields: ['id', 'name'])]
#[Arrayable(['baseId', 'name'])]
class User extends PrintableBase class User extends PrintableBase
{ {
public int $id = 2; public int $id = 2;
@ -33,12 +37,34 @@ class User extends PrintableBase
class CustomPrinterBase class CustomPrinterBase
{ {
public function toString(): string public function __toString(): string
{ {
return 'custom'; return 'custom';
} }
} }
class CustomArrayableBase
{
public function toArray(): array
{
return ['custom' => true];
}
}
#[Arrayable]
class CustomArrayableChild extends CustomArrayableBase
{
public int $value = 1;
}
#[Arrayable]
class DefaultArrayable extends PrintableBase
{
public string $name = 'default';
protected string $hidden = 'hidden';
public static int $shared = 1;
}
#[Printer] #[Printer]
class CustomPrinterChild extends CustomPrinterBase class CustomPrinterChild extends CustomPrinterBase
{ {
@ -53,7 +79,7 @@ class LatePrinterChild extends LatePrinterBase
class LatePrinterBase class LatePrinterBase
{ {
public function toString(): string public function __toString(): string
{ {
return 'late'; return 'late';
} }
@ -73,21 +99,65 @@ function requireValue(#[NotNull] int $value): int
return $value; return $value;
} }
function requireEmail(
#[NotEmpty]
#[Validate(FILTER_VALIDATE_EMAIL, message: 'Invalid email')]
string $email,
): string {
return $email;
}
function requirePort(
#[Validate(
FILTER_VALIDATE_INT,
options: ['options' => ['min_range' => 1, 'max_range' => 65535]],
)]
int $port,
): int {
return $port;
}
function requireBoolean(#[Validate(FILTER_VALIDATE_BOOLEAN)] bool $value): bool
{
return $value;
}
function main(): void function main(): void
{ {
$requireName = function (#[NotNull] string $name): string { $requireName = function (#[NotNull] string $name): string {
return $name; return $name;
}; };
$requireNonEmptyName = function (#[NotEmpty] string $name): string {
return $name;
};
$requireValidEmail = function (
#[Validate(FILTER_VALIDATE_EMAIL)] string $email,
): string {
return $email;
};
$user = new User(); $user = new User();
$user->setNickname('php'); $user->setNickname('php');
$copy = $user->withNickname('cpp'); $copy = $user->withNickname('cpp');
echo $user->getNickname(); echo $user->getNickname();
echo $copy->getNickname(); echo $copy->getNickname();
echo $user->toString(); echo $user;
echo (new CustomPrinterChild())->toString(); $userData = $user->toArray();
echo (new LatePrinterChild())->toString(); echo $userData['baseId'];
echo $userData['name'];
$defaultData = (new DefaultArrayable())->toArray();
echo $defaultData['baseId'];
echo $defaultData['name'];
echo (new CustomArrayableChild())->toArray()['custom'];
echo new CustomPrinterChild();
echo new LatePrinterChild();
echo requireValue(1); echo requireValue(1);
echo $requireName('typephp'); echo $requireName('typephp');
echo $requireNonEmptyName('typephp');
echo $requireValidEmail('typephp@example.com');
echo requireEmail('user@example.com');
echo requirePort(9501);
echo requireBoolean(false);
echo $requireName('typephp');
$promoted = new PromotedProperties(1); $promoted = new PromotedProperties(1);
$promoted->setValue(2); $promoted->setValue(2);

@ -2,20 +2,28 @@
namespace LibraryApi; namespace LibraryApi;
use \ExtensionProvider as Provider; use \Arrayable;
use \MethodsFor as Provider;
use \Constructor;
use \Validate;
use \Getter; use \Getter;
use \Hot;
use \NotNull; use \NotNull;
use \NoExport as Internal; use \NoExport as Internal;
use \Override;
use \MustUse;
use \Printer; use \Printer;
use \Cold;
use \Setter; use \Setter;
use \Type; use \Type;
use \With; use \With;
#[Printer] #[Printer(fields: ['value', 'doubled'])]
#[Arrayable(['value'])]
class Counter class Counter
{ {
public const int STEP = 2; public const int STEP = 2;
#[Getter, Setter, With] #[Constructor, Getter, Setter, With]
public int $value = 1; public int $value = 1;
public int $doubled { public int $doubled {
get { get {
@ -32,7 +40,8 @@ class Counter
return $this->value; return $this->value;
} }
public function label(#[NotNull] string $value): string #[MustUse, Cold]
public function label(#[NotNull, Validate(FILTER_VALIDATE_EMAIL)] string $value): string
{ {
return $value; return $value;
} }
@ -63,6 +72,7 @@ class InternalStringExtension
} }
} }
#[MustUse, Hot]
function twice(int $value): int function twice(int $value): int
{ {
return $value * 2; return $value * 2;
@ -73,3 +83,20 @@ function internal_twice(int $value = 2): int
{ {
return $value * 2; return $value * 2;
} }
class LibraryParent
{
public function version(): int
{
return 1;
}
}
class LibraryChild extends LibraryParent
{
#[Override]
public function version(): int
{
return 2;
}
}

@ -0,0 +1,11 @@
<?php
class InvalidConstructor
{
#[Constructor]
private int $value;
public function __construct()
{
}
}

@ -0,0 +1,14 @@
<?php
class ConstructorFinalParent
{
final protected function __construct()
{
}
}
class ConstructorFinalChild extends ConstructorFinalParent
{
#[Constructor]
private int $id;
}

@ -0,0 +1,11 @@
<?php
class ConstructorWithoutParentConstructor
{
}
class ConstructorWithoutParentConstructorChild extends ConstructorWithoutParentConstructor
{
#[Constructor]
private int $id;
}

@ -0,0 +1,14 @@
<?php
class ConstructorOptionalParent
{
public function __construct(string $label = 'parent')
{
}
}
class ConstructorOptionalChild extends ConstructorOptionalParent
{
#[Constructor]
private int $id;
}

@ -0,0 +1,14 @@
<?php
class ConstructorPrivateParent
{
private function __construct(string $label)
{
}
}
class ConstructorPrivateChild extends ConstructorPrivateParent
{
#[Constructor]
private int $id;
}

@ -0,0 +1,14 @@
<?php
class ConstructorRequiredParent
{
public function __construct(string $label)
{
}
}
class ConstructorRequiredChild extends ConstructorRequiredParent
{
#[Constructor]
private int $id;
}

@ -0,0 +1,7 @@
<?php
class InvalidStaticConstructor
{
#[Constructor]
private static int $value;
}

@ -0,0 +1,12 @@
<?php
class GeneratedMethodDeclaredConflict
{
#[Getter]
private string $name;
public function getNAME(): string
{
return $this->name;
}
}

@ -0,0 +1,15 @@
<?php
class GeneratedMethodFinalConflictParent
{
final public function withName(string $name): static
{
return $this;
}
}
class GeneratedMethodFinalConflictChild extends GeneratedMethodFinalConflictParent
{
#[With]
private string $name;
}

@ -0,0 +1,10 @@
<?php
class GeneratedMethodGeneratedConflict
{
#[Getter]
private string $name;
#[Getter]
private string $Name;
}

@ -0,0 +1,14 @@
<?php
class GeneratedMethodConflictParent
{
protected function setName(string $name): void
{
}
}
class GeneratedMethodConflictChild extends GeneratedMethodConflictParent
{
#[Setter]
private string $name;
}

@ -0,0 +1,11 @@
<?php
class GetterHookProperty
{
#[Getter]
public int $value {
get {
return 1;
}
}
}

@ -0,0 +1,10 @@
<?php
readonly class GetterReadonlyProperty
{
public function __construct(
#[Getter]
private int $value,
) {
}
}

@ -0,0 +1,6 @@
<?php
#[Hot, Cold]
function conflictingTemperature(): void
{
}

@ -0,0 +1,46 @@
<?php
namespace FunctionTemperature {
use \Cold;
use \Hot;
#[Hot]
function frequentlyUsed(int $value): int
{
return $value + 1;
}
#[Cold]
function errorMessage(string $message): string
{
return 'Error: ' . $message;
}
class Worker
{
#[Hot]
public function run(int $value): int
{
return frequentlyUsed($value);
}
#[Cold]
public function fail(string $message): string
{
return errorMessage($message);
}
}
}
namespace {
function main(): void
{
$worker = new \FunctionTemperature\Worker();
echo $worker->run(1);
echo $worker->fail('test');
}
}

@ -0,0 +1,6 @@
<?php
#[Hot]
class InvalidHotTarget
{
}

@ -0,0 +1,57 @@
<?php
namespace LanguageAttributes;
use \Constructor;
use \Getter;
use \MustUse;
use \NotEmpty;
use \NotNull;
function add(int $left, int $right): int
{
$result = $left + $right;
return $result;
}
#[MustUse]
function calculate(int $value): int
{
return add($value, 1);
}
function nullable(#[NotNull] ?int $value): int
{
return $value;
}
function nonEmpty(#[NotEmpty] string $value): string
{
return $value;
}
class User
{
#[Constructor, Getter]
private int $id;
#[Constructor, Getter]
private string $name = 'typephp';
#[MustUse]
public function displayName(): string
{
return strtoupper($this->name);
}
}
function main(): void
{
$value = calculate(1);
$user = new User(1);
$name = $user->displayName();
echo $value;
echo $name;
echo nullable(0);
echo nonEmpty('0');
}

@ -0,0 +1,106 @@
<?php
#[MethodsFor('*')]
class HierarchyKeywordMethods
{
public static function keywordWins(any $value): string
{
return 'keyword';
}
}
class HierarchyBase
{
public function keywordWins(): string
{
return 'member';
}
public function realWins(): string
{
return 'member';
}
}
class HierarchyChild extends HierarchyBase
{
}
#[MethodsFor(HierarchyBase::class)]
class HierarchyBaseMethods
{
public static function inheritedExtension(HierarchyBase $value): string
{
return 'base';
}
public static function nearestExtension(HierarchyBase $value): string
{
return 'base';
}
}
#[MethodsFor(HierarchyChild::class)]
class HierarchyChildMethods
{
public static function nearestExtension(HierarchyChild $value): string
{
return 'child';
}
}
#[MethodsFor(Type::Object)]
class HierarchyObjectMethods
{
public static function objectFallback(object $value): string
{
return 'object';
}
public static function realWins(object $value): string
{
return 'extension';
}
public static function declaredMethod(object $value): string
{
return 'extension';
}
}
interface HierarchyContract
{
public function declaredMethod(): string;
}
class HierarchyImplementation implements HierarchyContract
{
public function declaredMethod(): string
{
return 'declared';
}
}
function hierarchy_calls(
HierarchyChild $child,
HierarchyBase $base,
HierarchyContract $contract,
object $object,
): void {
echo $child->keywordWins();
echo $child->realWins();
echo $child->inheritedExtension();
echo $child->nearestExtension();
echo $base->nearestExtension();
echo $child->objectFallback();
echo $contract->declaredMethod();
echo $contract->objectFallback();
echo $object->objectFallback();
}
function hierarchy_nullable_call(?HierarchyChild $child): void
{
// A nullable receiver is not statically guaranteed to be an object, so it
// must not use the Type::Object fallback.
echo $child->objectFallback();
}

@ -0,0 +1,19 @@
<?php
interface InvalidMethodsForContract
{
}
#[MethodsFor(InvalidMethodsForContract::class)]
class InvalidInterfaceMethods
{
public static function inspect(InvalidMethodsForContract $value): string
{
return 'invalid';
}
}
function methods_for_interface_target(InvalidMethodsForContract $value): string
{
return $value->inspect();
}

@ -0,0 +1,24 @@
<?php
#[MethodsFor(Type::String)]
class ReversedConflictingStringMethods
{
public static function inspect(string $value): string
{
return $value;
}
}
#[MethodsFor('*')]
class ReversedConflictingKeywordMethods
{
public static function inspect(any $value): string
{
return 'keyword';
}
}
function methods_for_reversed_keyword_conflict(string $value): string
{
return $value->inspect();
}

@ -0,0 +1,24 @@
<?php
#[MethodsFor('*')]
class ConflictingKeywordMethods
{
public static function inspect(any $value): string
{
return 'keyword';
}
}
#[MethodsFor(Type::String)]
class ConflictingStringMethods
{
public static function inspect(string $value): string
{
return $value;
}
}
function methods_for_keyword_conflict(string $value): string
{
return $value->inspect();
}

@ -0,0 +1,42 @@
<?php
namespace MethodsForAttribute {
use \MethodsFor as ForType;
use \Type;
#[ForType(Type::String)]
class StringMethods
{
public static function surround(string $value, string $left, string $right): string
{
return $left . $value . $right;
}
}
class User
{
public string $name = 'TypePHP';
}
#[\MethodsFor(User::class)]
class UserMethods
{
public static function displayName(User $user): string
{
return $user->name;
}
}
}
namespace {
function main(): void
{
$name = 'TypePHP';
echo $name->surround('<', '>');
echo (new \MethodsForAttribute\User())->displayName();
}
}

@ -0,0 +1,12 @@
<?php
#[MustUse]
function result(): int
{
return 1;
}
function main(): void
{
result();
}

@ -0,0 +1,16 @@
<?php
class ResultFactory
{
#[MustUse]
public function create(): int
{
return 1;
}
}
function main(): void
{
$factory = new ResultFactory();
$factory->create();
}

@ -0,0 +1,3 @@
<?php
$validate = fn (#[NotEmpty] string $value): string => $value;

@ -0,0 +1,3 @@
<?php
$validate = fn (#[NotNull] ?string $value): ?string => $value;

@ -0,0 +1,19 @@
<?php
function nullableValue(#[NotNull] ?string $value): ?string
{
return $value;
}
function unionNullableValue(#[NotNull] string|null $value): string|null
{
return $value;
}
function main(): void
{
$nullableValue = function (#[NotNull] ?string $value): ?string {
return $value;
};
echo $nullableValue('typephp');
}

@ -0,0 +1,16 @@
<?php
class OverrideArgumentsParent
{
public function value(): void
{
}
}
class OverrideArgumentsChild extends OverrideArgumentsParent
{
#[\Override(true)]
public function value(): void
{
}
}

@ -0,0 +1,16 @@
<?php
class OverridePrivateParent
{
private function value(): void
{
}
}
class OverridePrivateChild extends OverridePrivateParent
{
#[\Override]
public function value(): void
{
}
}

@ -0,0 +1,16 @@
<?php
class OverrideConstructorParent
{
public function __construct()
{
}
}
class OverrideConstructorChild extends OverrideConstructorParent
{
#[\Override]
public function __construct()
{
}
}

@ -0,0 +1,17 @@
<?php
class OverrideDuplicateParent
{
public function value(): void
{
}
}
class OverrideDuplicateChild extends OverrideDuplicateParent
{
#[\Override]
#[\Override]
public function value(): void
{
}
}

@ -0,0 +1,7 @@
<?php
interface OverrideInterfaceMissing
{
#[\Override]
public function missing(): void;
}

@ -0,0 +1,6 @@
<?php
#[\Override]
function overrideInvalidTarget(): void
{
}

@ -0,0 +1,9 @@
<?php
class OverrideMissing
{
#[\Override]
public function missing(): void
{
}
}

@ -0,0 +1,14 @@
<?php
trait OverrideTraitMissingMethod
{
#[\Override]
public function missing(): void
{
}
}
class OverrideTraitConsumer
{
use OverrideTraitMissingMethod;
}

@ -0,0 +1,62 @@
<?php
namespace OverrideValid;
use \Override as Replaces;
class ParentClass
{
public function inherited(): string
{
return 'parent';
}
}
interface Named
{
public function name(): string;
}
class ChildClass extends ParentClass implements Named
{
#[Replaces]
public function inherited(): string
{
return 'child';
}
#[\Override]
public function name(): string
{
return 'child';
}
}
interface ChildNamed extends Named
{
#[\Override]
public function name(): string;
}
trait InheritedMethod
{
#[\Override]
public function inherited(): string
{
return 'trait';
}
}
class TraitChild extends ParentClass
{
use InheritedMethod;
}
class InternalInterfaceImplementation implements \Stringable
{
#[\Override]
public function __toString(): string
{
return 'typephp';
}
}

@ -0,0 +1,10 @@
<?php
function validatedValue(
#[Validate(FILTER_VALIDATE_EMAIL)]
#[NotEmpty]
#[NotNull]
?string $value,
): ?string {
return $value;
}

@ -0,0 +1,12 @@
<?php
#[Printer(fields: ['id', 'name', 'tags', 'enabled', 'value'])]
#[Arrayable(fields: ['id', 'name', 'tags', 'enabled', 'value'])]
class PrinterArrayableFieldTypes
{
private int $id = 1;
private string $name = 'TypePHP';
protected array $tags = ['a', 'b'];
private bool $enabled = true;
private mixed $value = null;
}

@ -0,0 +1,12 @@
<?php
#[Printer]
class PrinterGeneratedMethodConflict
{
public string $name = 'TypePHP';
public function __toString(): string
{
return $this->name;
}
}

@ -0,0 +1,11 @@
<?php
class PrinterParentPrivateFieldBase
{
private string $secret = 'hidden';
}
#[Printer(fields: ['secret'])]
class PrinterParentPrivateFieldChild extends PrinterParentPrivateFieldBase
{
}

@ -0,0 +1,8 @@
<?php
#[Printer(['id', 'hidden'])]
class PrinterPrivateFields
{
private int $id = 1;
private string $hidden = 'secret';
}

@ -0,0 +1,7 @@
<?php
#[Printer(fields: ['shared'])]
class PrinterStaticField
{
public static string $shared = 'TypePHP';
}

@ -0,0 +1,10 @@
<?php
class SetterHookProperty
{
#[Setter]
public int $value {
set(int $value) {
}
}
}

@ -0,0 +1,7 @@
<?php
class SetterReadonlyProperty
{
#[Setter]
private readonly int $value;
}

@ -0,0 +1,3 @@
<?php
$validate = fn (#[Validate(FILTER_VALIDATE_EMAIL)] string $value): string => $value;

@ -0,0 +1,13 @@
<?php
function emailFromUnion(
#[Validate(FILTER_VALIDATE_EMAIL)] int|string $email,
): string|int {
return $email;
}
function integerArray(
#[Validate(FILTER_VALIDATE_INT, FILTER_REQUIRE_ARRAY)] array $values,
): array {
return $values;
}

@ -0,0 +1,9 @@
<?php
function validateDuplicate(
#[Validate(FILTER_VALIDATE_INT)]
#[Validate(FILTER_VALIDATE_INT)]
int $value,
): int {
return $value;
}

@ -0,0 +1,6 @@
<?php
function invalidArrayType(
#[Validate(FILTER_VALIDATE_INT)] array $values,
): void {
}

@ -0,0 +1,6 @@
<?php
function invalidEmailType(
#[Validate(FILTER_VALIDATE_EMAIL)] int $email,
): void {
}

@ -0,0 +1,6 @@
<?php
#[Validate(FILTER_VALIDATE_EMAIL)]
function invalidFilterTarget(): void
{
}

@ -0,0 +1,6 @@
<?php
function invalidFilter(
#[Validate(FILTER_SANITIZE_EMAIL)] string $email,
): void {
}

@ -0,0 +1,11 @@
<?php
class WithHookProperty
{
#[With]
public int $value {
get {
return 1;
}
}
}

@ -0,0 +1,10 @@
<?php
readonly class WithReadonlyProperty
{
public function __construct(
#[With]
private int $value,
) {
}
}

@ -21,11 +21,189 @@ class ClassTest extends \BaseTest
$this->compile('getter-function.php'); $this->compile('getter-function.php');
} }
public function testCompileTimeAttributeRejectsAliasesOfTheSameAttributeRepeated(): void
{
$this->expectException(\TypePhp\Exception\SyntaxError::class);
$this->expectExceptionMessage('Getter cannot be repeated on the same declaration');
$this->compile('compile-time-attribute-duplicate.php');
}
public function testGetterSupportsReadonlyProperties(): void
{
$this->compile('getter-readonly-property.php');
}
public function testGetterRejectsHookProperties(): void
{
$this->expectException(\TypePhp\Exception\SyntaxError::class);
$this->expectExceptionMessage('Getter cannot be applied to properties with hooks');
$this->compile('getter-hook-property.php');
}
public function testSetterRejectsReadonlyProperties(): void
{
$this->expectException(\TypePhp\Exception\SyntaxError::class);
$this->expectExceptionMessage('Setter cannot be applied to readonly properties');
$this->compile('setter-readonly-property.php');
}
public function testSetterRejectsHookProperties(): void
{
$this->expectException(\TypePhp\Exception\SyntaxError::class);
$this->expectExceptionMessage('Setter cannot be applied to properties with hooks');
$this->compile('setter-hook-property.php');
}
public function testWithRejectsReadonlyPropertiesIncludingReadonlyClasses(): void
{
$this->expectException(\TypePhp\Exception\SyntaxError::class);
$this->expectExceptionMessage('With cannot be applied to readonly properties');
$this->compile('with-readonly-property.php');
}
public function testWithRejectsHookProperties(): void
{
$this->expectException(\TypePhp\Exception\SyntaxError::class);
$this->expectExceptionMessage('With cannot be applied to properties with hooks');
$this->compile('with-hook-property.php');
}
public function testGeneratedMethodRejectsDeclaredMethodConflictCaseInsensitively(): void
{
$this->exec('Duplicate method `getName`', 'generated-method-declared-conflict.php');
}
public function testGeneratedMethodConflictDiagnosticPointsToAttributeAndDeclaration(): void
{
try {
$this->compile('generated-method-declared-conflict.php');
$this->fail('Expected generated Getter conflict');
} catch (\TypePhp\Exception\TestError $error) {
$file = realpath(__DIR__ . '/../code/generated-method-declared-conflict.php');
$this->assertNotFalse($file);
$message = $error->getMessage();
$this->assertStringContainsString('compile-time attribute: #[Getter]', $message);
$this->assertStringContainsString('target: property $name', $message);
$this->assertStringContainsString('source: ' . $file . ':5', $message);
$this->assertStringContainsString('conflict source: declaration at ' . $file . ':8', $message);
}
}
public function testGeneratedMethodsRejectEachOtherCaseInsensitively(): void
{
$this->exec('Duplicate method `getName`', 'generated-method-generated-conflict.php');
}
public function testPrinterGeneratedMethodUsesNormalDuplicateMethodValidation(): void
{
$this->exec('Duplicate method `__toString`', 'printer-generated-method-conflict.php');
}
public function testArrayableGeneratedMethodUsesNormalDuplicateMethodValidation(): void
{
$this->exec('Duplicate method `toArray`', 'arrayable-generated-method-conflict.php');
}
public function testGeneratedMethodMayOverrideCompatibleParentMethod(): void
{
$this->compile('generated-method-parent-conflict.php');
}
public function testGeneratedMethodObeysFinalParentMethodRule(): void
{
$this->exec(
'Cannot override final method `GeneratedMethodFinalConflictParent::withName()`',
'generated-method-final-parent-conflict.php'
);
}
public function testCompileTimeGeneratedPropertyMethodsPrinterAndNotNull(): void public function testCompileTimeGeneratedPropertyMethodsPrinterAndNotNull(): void
{ {
$this->compile('compile_time_attributes.php'); $this->compile('compile_time_attributes.php');
} }
public function testArrayableAndPrinterFieldSelection(): void
{
$this->compile('arrayable.php');
}
public function testArrayableRejectsNonClassTargets(): void
{
$this->expectException(\TypePhp\Exception\SyntaxError::class);
$this->expectExceptionMessage('Arrayable can only be applied to named classes');
$this->compile('arrayable-invalid-target.php');
}
public function testArrayableAcceptsExplicitFieldsWithoutVisibilityFiltering(): void
{
$this->compile('arrayable-explicit-fields.php');
}
public function testArrayableRejectsDynamicFields(): void
{
$this->expectException(\TypePhp\Exception\SyntaxError::class);
$this->expectExceptionMessage(
'Arrayable field `missing` must be a declared instance property accessible from the class'
);
$this->compile('arrayable-dynamic-field.php');
}
public function testArrayableAcceptsPublicAndProtectedParentFields(): void
{
$this->compile('arrayable-parent-visible-fields.php');
}
public function testPrinterAcceptsPrivateSelectedFields(): void
{
$this->compile('printer-private-fields.php');
}
public function testPrinterRejectsPrivateParentFields(): void
{
$this->expectException(\TypePhp\Exception\SyntaxError::class);
$this->expectExceptionMessage(
'Printer field `secret` must be a declared instance property accessible from the class'
);
$this->compile('printer-parent-private-field.php');
}
public function testPrinterRejectsStaticFields(): void
{
$this->expectException(\TypePhp\Exception\SyntaxError::class);
$this->expectExceptionMessage(
'Printer field `shared` must be a declared instance property accessible from the class'
);
$this->compile('printer-static-field.php');
}
public function testPrinterConvertsNonStringFieldsAndArrayablePreservesValues(): void
{
global $translator;
$compiler = \TypePhp\CompilerTest::create(ROOT_PATH);
$translator = $compiler;
$testFile = __DIR__ . '/../code/printer-arrayable-field-types.php';
$compiler->addFiles([$testFile]);
$compiler->prepareFile($testFile);
$cppFile = $compiler->convertFile($testFile);
$code = file_get_contents($cppFile);
$printerStart = strpos($code, 'php_printerarrayablefieldtypes____tostring');
$arrayableStart = strpos($code, 'php_printerarrayablefieldtypes__toarray');
$this->assertNotFalse($printerStart);
$this->assertNotFalse($arrayableStart);
$printerCode = substr($code, $printerStart, $arrayableStart - $printerStart);
$arrayableCode = substr($code, $arrayableStart);
$this->assertSame(4, substr_count($printerCode, 'php::toString('));
$this->assertStringNotContainsString('php::toString(', $arrayableCode);
}
public function testArrayableRejectsNonArrayFields(): void
{
$this->expectException(\TypePhp\Exception\SyntaxError::class);
$this->expectExceptionMessage('Arrayable $fields must be an array literal');
$this->compile('arrayable-invalid-argument.php');
}
public function testNotNullRejectsNonParameterTargets(): void public function testNotNullRejectsNonParameterTargets(): void
{ {
$this->expectException(\TypePhp\Exception\SyntaxError::class); $this->expectException(\TypePhp\Exception\SyntaxError::class);
@ -33,6 +211,360 @@ class ClassTest extends \BaseTest
$this->compile('not-null-invalid-target.php'); $this->compile('not-null-invalid-target.php');
} }
public function testNotNullRejectsArrowFunctionParameters(): void
{
$this->expectException(\TypePhp\Exception\SyntaxError::class);
$this->expectExceptionMessage('NotNull is not supported on arrow function parameters');
$this->compile('not-null-arrow-function.php');
}
public function testNotNullWarnsForExplicitlyNullableParameters(): void
{
global $translator;
$compiler = \TypePhp\CompilerTest::create(ROOT_PATH);
$translator = $compiler;
$reporter = new class implements \TypePhp\Diagnostics\DiagnosticReporter {
/** @var list<string> */
public array $warnings = [];
public function fatal(string $message): never
{
throw new \TypePhp\Exception\TestError($message);
}
public function warning(\PhpParser\Node $node, string $file, string $message): void
{
$this->warnings[] = $message;
}
};
$compiler->setDiagnosticReporter($reporter);
$testFile = __DIR__ . '/../code/not-null-nullable-warning.php';
$compiler->addFiles([$testFile]);
$compiler->prepareFile($testFile);
$this->assertSame([
'NotNull is applied to nullable parameter `$value`',
'NotNull is applied to nullable parameter `$value`',
'NotNull is applied to nullable parameter `$value`',
], $reporter->warnings);
}
public function testParameterValidationUsesFixedSemanticOrder(): void
{
$parser = (new \PhpParser\ParserFactory())->createForHostVersion();
$ast = $parser->parse(file_get_contents(__DIR__ . '/../code/parameter-validation-order.php'));
$traverser = new \PhpParser\NodeTraverser();
$traverser->addVisitor(new \PhpParser\NodeVisitor\NameResolver(null, ['replaceNodes' => false]));
$traverser->addVisitor(new \TypePhp\Transform\Visitor());
$stmts = $traverser->traverse($ast);
$function = $stmts[0];
$this->assertInstanceOf(\PhpParser\Node\Stmt\Function_::class, $function);
$this->assertInstanceOf(\PhpParser\Node\Expr\BinaryOp\Identical::class, $function->stmts[0]->cond);
$this->assertInstanceOf(\PhpParser\Node\Expr\Empty_::class, $function->stmts[1]->cond);
$this->assertInstanceOf(\PhpParser\Node\Expr\BinaryOp\Identical::class, $function->stmts[2]->cond);
$this->assertInstanceOf(\PhpParser\Node\Expr\FuncCall::class, $function->stmts[2]->cond->left);
$this->assertSame('filter_var', strtolower($function->stmts[2]->cond->left->name->toString()));
}
public function testNotEmptyRejectsArrowFunctionParameters(): void
{
$this->expectException(\TypePhp\Exception\SyntaxError::class);
$this->expectExceptionMessage('NotEmpty is not supported on arrow function parameters');
$this->compile('not-empty-arrow-function.php');
}
public function testValidateRejectsArrowFunctionParameters(): void
{
$this->expectException(\TypePhp\Exception\SyntaxError::class);
$this->expectExceptionMessage('Validate is not supported on arrow function parameters');
$this->compile('validate-arrow-function.php');
}
public function testValidateRejectsSanitizeFilters(): void
{
$this->expectException(\TypePhp\Exception\SyntaxError::class);
$this->expectExceptionMessage('Validate only accepts FILTER_VALIDATE_* filters');
$this->compile('validate-sanitize.php');
}
public function testValidateRejectsProvablyIncompatibleScalarType(): void
{
$this->expectException(\TypePhp\Exception\SyntaxError::class);
$this->expectExceptionMessage(
'Validate filter FILTER_VALIDATE_EMAIL is incompatible with parameter `$email` declared as `int`'
);
$this->compile('validate-incompatible-email-int.php');
}
public function testValidateRejectsArrayWithoutArrayMode(): void
{
$this->expectException(\TypePhp\Exception\SyntaxError::class);
$this->expectExceptionMessage(
'Validate filter FILTER_VALIDATE_INT is incompatible with parameter `$values` declared as `array`'
);
$this->compile('validate-incompatible-array.php');
}
public function testValidateAllowsCompatibleUnionAndExplicitArrayMode(): void
{
$this->compile('validate-compatible-types.php');
}
public function testValidateRejectsNonParameterTargets(): void
{
$this->expectException(\TypePhp\Exception\SyntaxError::class);
$this->expectExceptionMessage('Validate can only be applied to function or method parameters');
$this->compile('validate-invalid-target.php');
}
public function testValidateUsesCentralDuplicateAttributeValidation(): void
{
$this->expectException(\TypePhp\Exception\SyntaxError::class);
$this->expectExceptionMessage('Validate cannot be repeated on the same declaration');
$this->compile('validate-duplicate.php');
}
public function testCompileTimeAttributeDiagnosticsContainTargetAndBothConflictSources(): void
{
try {
$this->compile('validate-duplicate.php');
$this->fail('Expected duplicate Validate diagnostic');
} catch (\TypePhp\Exception\SyntaxError $error) {
$message = $error->getMessage();
$file = realpath(__DIR__ . '/../code/validate-duplicate.php');
$this->assertNotFalse($file);
$this->assertStringContainsString('target: parameter $value', $message);
$this->assertStringContainsString('source: ' . $file . ':4', $message);
$this->assertStringContainsString(
'conflict source: #[Validate] at ' . $file . ':5',
$message,
);
}
}
public function testLanguageCompileTimeAttributes(): void
{
$this->compile('language_attributes.php');
}
public function testMethodsForSupportsAliasesAndObjectTargets(): void
{
$this->compile('methods-for.php');
}
public function testMethodsForUsesKeywordClassHierarchyAndObjectFallbackPriority(): void
{
global $translator;
$compiler = \TypePhp\CompilerTest::create(ROOT_PATH);
$translator = $compiler;
$testFile = __DIR__ . '/../code/methods-for-inheritance.php';
$compiler->addFiles([$testFile]);
$compiler->prepareFile($testFile);
$cppFile = $compiler->convertFile($testFile);
$cpp = file_get_contents($cppFile);
$this->assertStringContainsString('php_hierarchykeywordmethods__keywordwins(', $cpp);
$this->assertStringContainsString('php::echo(php_hierarchybase__realwins(child));', $cpp);
$this->assertStringNotContainsString(
'php::echo(php::toString(php_hierarchyobjectmethods__realwins(',
$cpp,
);
$this->assertStringNotContainsString(
'php::echo(php::toString(php_hierarchyobjectmethods__declaredmethod(',
$cpp,
);
$this->assertStringContainsString('php_hierarchybasemethods__inheritedextension(', $cpp);
$this->assertMatchesRegularExpression(
'/php_hierarchychildmethods__nearestextension\([^,\n]+, child\)/',
$cpp,
);
$this->assertMatchesRegularExpression(
'/php_hierarchybasemethods__nearestextension\([^,\n]+, base\)/',
$cpp,
);
$this->assertSame(
3,
substr_count($cpp, 'php::echo(php::toString(php_hierarchyobjectmethods__objectfallback('),
);
}
public function testMethodsForRejectsKeywordAndTargetSpecificNameConflict(): void
{
$this->expectException(\TypePhp\Exception\TestError::class);
$this->expectExceptionMessage(
'conflicts with keyword extension method *::inspect()'
);
$this->compile('methods-for-keyword-conflict.php');
}
public function testMethodsForKeywordConflictDoesNotDependOnDeclarationOrder(): void
{
$this->expectException(\TypePhp\Exception\TestError::class);
$this->expectExceptionMessage(
'Keyword extension method *::inspect() conflicts with extension method php::str::inspect()'
);
$this->compile('methods-for-keyword-conflict-reversed.php');
}
public function testMethodsForRejectsInterfaceTargets(): void
{
$this->expectException(\TypePhp\Exception\TestError::class);
$this->expectExceptionMessage(
'MethodsFor target InvalidMethodsForContract must be a class; interfaces are not supported'
);
$this->compile('methods-for-interface-target.php');
}
public function testHotAndColdFunctionAttributes(): void
{
$this->compile('hot-cold.php');
}
public function testHotAndColdCannotBeCombined(): void
{
$this->expectException(\TypePhp\Exception\SyntaxError::class);
$this->expectExceptionMessage('Hot and Cold cannot be applied to the same function or method');
$this->compile('hot-cold-conflict.php');
}
public function testHotRejectsNonFunctionTargets(): void
{
$this->expectException(\TypePhp\Exception\SyntaxError::class);
$this->expectExceptionMessage('Hot can only be applied to functions or methods');
$this->compile('hot-invalid-target.php');
}
public function testMustUseRejectsDiscardedReturnValue(): void
{
$this->exec('must be used', 'must-use-discard.php');
}
public function testMustUseRejectsDiscardedMethodReturnValue(): void
{
$this->exec('must be used', 'must-use-method-discard.php');
}
public function testOverrideAcceptsParentInterfaceTraitAndNamespaceAliasMatches(): void
{
$this->compile('override-valid.php');
}
public function testOverrideRequiresMatchingParentMethod(): void
{
$this->exec(
'OverrideMissing::missing() has #[\\Override] attribute, but no matching parent method exists',
'override-missing.php',
);
}
public function testOverrideNeverMatchesConstructor(): void
{
$this->exec(
'OverrideConstructorChild::__construct() has #[\\Override] attribute, but no matching parent method exists',
'override-constructor.php',
);
}
public function testOverrideDoesNotMatchPrivateParentMethod(): void
{
$this->exec(
'OverridePrivateChild::value() has #[\\Override] attribute, but no matching parent method exists',
'override-attribute-private-parent.php',
);
}
public function testOverrideOnTraitMethodIsValidatedAtUseSite(): void
{
$this->exec(
'OverrideTraitConsumer::missing() has #[\\Override] attribute, but no matching parent method exists',
'override-trait-missing.php',
);
}
public function testOverrideOnRootInterfaceRequiresParentMethod(): void
{
$this->exec(
'OverrideInterfaceMissing::missing() has #[\\Override] attribute, but no matching parent method exists',
'override-interface-missing.php',
);
}
public function testOverrideRejectsNonMethodTargets(): void
{
$this->expectException(\TypePhp\Exception\SyntaxError::class);
$this->expectExceptionMessage('Override can only be applied to methods');
$this->compile('override-invalid-target.php');
}
public function testOverrideRejectsArguments(): void
{
$this->expectException(\TypePhp\Exception\SyntaxError::class);
$this->expectExceptionMessage('Override does not accept arguments');
$this->compile('override-arguments.php');
}
public function testOverrideCannotBeRepeated(): void
{
$this->expectException(\TypePhp\Exception\SyntaxError::class);
$this->expectExceptionMessage('Override cannot be repeated on the same declaration');
$this->compile('override-duplicate.php');
}
public function testConstructorRejectsExistingConstructor(): void
{
$this->exec('Duplicate method `__construct`', 'constructor-existing.php');
}
public function testConstructorRejectsStaticProperties(): void
{
$this->expectException(\TypePhp\Exception\SyntaxError::class);
$this->expectExceptionMessage('Constructor can only be applied to instance properties');
$this->compile('constructor-static.php');
}
public function testConstructorCallsParentConstructorWithoutRequiredArguments(): void
{
global $translator;
$compiler = \TypePhp\CompilerTest::create(ROOT_PATH);
$translator = $compiler;
$testFile = __DIR__ . '/../code/constructor-parent-optional.php';
$compiler->addFiles([$testFile]);
$compiler->prepareFile($testFile);
$cppFile = $compiler->convertFile($testFile);
$this->assertStringContainsString(
'// Stmt_Expression(Expr_StaticCall)',
file_get_contents($cppFile),
);
}
public function testConstructorAllowsParentWithoutConstructor(): void
{
$this->compile('constructor-parent-none.php');
}
public function testConstructorRejectsParentConstructorWithRequiredArguments(): void
{
$this->exec(
'parent constructor `ConstructorRequiredParent::__construct()` requires 1 argument(s)',
'constructor-parent-required.php'
);
}
public function testConstructorDoesNotCallPrivateParentConstructor(): void
{
$this->compile('constructor-parent-private.php');
}
public function testConstructorRejectsFinalParentConstructor(): void
{
$this->exec(
'Cannot override final method `ConstructorFinalParent::__construct()`',
'constructor-parent-final.php'
);
}
public function testReAssignThis() public function testReAssignThis()
{ {
$this->exec('Cannot re-assign $this', 're-assign-this.php'); $this->exec('Cannot re-assign $this', 're-assign-this.php');

@ -0,0 +1,47 @@
<?php
use PHPUnit\Framework\TestCase;
use TypePhp\Transform\CompileTimeAttributeRegistry;
final class CompileTimeAttributeRegistryTest extends TestCase
{
public function testEveryBuiltInCompileTimeAttributeHasCompleteMetadata(): void
{
$expected = [
'MethodsFor', 'NoExport', 'Getter', 'Setter', 'With', 'Printer', 'Arrayable',
'NotNull', 'NotEmpty', 'Validate', 'Override', 'MustUse', 'Hot', 'Cold', 'Constructor',
];
$this->assertSame($expected, CompileTimeAttributeRegistry::names());
foreach (CompileTimeAttributeRegistry::all() as $definition) {
$this->assertNotEmpty($definition['targets']);
$this->assertNotSame('', $definition['argument_parser']);
$this->assertNotSame('', $definition['phase']);
$this->assertIsBool($definition['preserve_in_library_stub']);
$this->assertFalse($definition['repeatable']);
}
$this->assertNotContains('NoExport', CompileTimeAttributeRegistry::names(true));
$this->assertContains('Getter', CompileTimeAttributeRegistry::names(true));
$this->assertContains('Override', CompileTimeAttributeRegistry::names(true));
$this->assertSame(
['Override', 'MustUse', 'Hot', 'Cold'],
CompileTimeAttributeRegistry::namesForPhase(CompileTimeAttributeRegistry::PHASE_ENTER),
);
}
public function testRegistryMatchesPublicCompileTimeAttributeDeclarations(): void
{
$source = file_get_contents(ROOT_PATH . '/src/polyfills.php');
$this->assertNotFalse($source);
preg_match_all(
'/#\[Attribute\([^\]]+\)\]\s+final readonly class ([A-Za-z_][A-Za-z0-9_]*)/',
$source,
$matches,
);
$declaredByTypePhp = array_values(array_filter(
CompileTimeAttributeRegistry::names(),
static fn (string $name): bool => $name !== 'Override',
));
$this->assertSame($declaredByTypePhp, $matches[1]);
}
}

@ -831,6 +831,22 @@ YAML);
$this->assertStringNotContainsString('property_map = {}', $code); $this->assertStringNotContainsString('property_map = {}', $code);
} }
public function testArrayableKeywordConversionCallsGeneratedMethodAtRuntime(): void
{
global $translator;
$translator = $this->compiler;
$testFile = ROOT_PATH . '/phpunit/code/arrayable.php';
$this->compiler->addFiles([$testFile]);
$this->compiler->prepareFile($testFile);
$cppFile = $this->compiler->convertFile($testFile);
$cpp = file_get_contents($cppFile);
$this->assertStringContainsString('data = php::toArray(user);', $cpp);
$this->assertStringContainsString('php::Array php_arrayableuser__toarray(', $cpp);
$this->assertStringContainsString('php::Str php_arrayableuser____tostring(', $cpp);
}
public function testLibraryFunctionHeaderExportsDefaultValueHelpersWithoutLiteralStorage(): void public function testLibraryFunctionHeaderExportsDefaultValueHelpersWithoutLiteralStorage(): void
{ {
global $translator; global $translator;
@ -848,8 +864,13 @@ YAML);
$header = file_get_contents($headerFile); $header = file_get_contents($headerFile);
$this->assertStringContainsString('#pragma once', $header); $this->assertStringContainsString('#pragma once', $header);
$this->assertStringContainsString('TYPEPHP_PRIME2_API __declspec(dllexport)', $header); $this->assertStringContainsString('#include <typephp_helper.h>', $header);
$this->assertStringContainsString('TYPEPHP_PRIME2_API __declspec(dllimport)', $header); $this->assertStringContainsString('# define TYPEPHP_PRIME2_API TYPEPHP_SYMBOL_EXPORT', $header);
$this->assertStringContainsString('# define TYPEPHP_PRIME2_API TYPEPHP_SYMBOL_IMPORT', $header);
$this->assertStringNotContainsString('__declspec(', $header);
$this->assertStringNotContainsString('__attribute__(', $header);
$this->assertStringNotContainsString('defined(_WIN32)', $header);
$this->assertStringNotContainsString('defined(__GNUC__)', $header);
$this->assertStringContainsString( $this->assertStringContainsString(
'TYPEPHP_PRIME2_API php::Str php_exported_defaults_arg_0_default_value();', 'TYPEPHP_PRIME2_API php::Str php_exported_defaults_arg_0_default_value();',
$header $header
@ -899,8 +920,8 @@ YAML);
$this->compiler->genFunctionDeclarations($headerFile); $this->compiler->genFunctionDeclarations($headerFile);
$header = file_get_contents($headerFile); $header = file_get_contents($headerFile);
$this->assertStringContainsString('TYPEPHP_PRIME2_IMPORT __declspec(dllimport)', $header); $this->assertStringContainsString('#define TYPEPHP_PRIME2_IMPORT TYPEPHP_SYMBOL_IMPORT', $header);
$this->assertStringContainsString('TYPEPHP_PRIME2_API __declspec(dllexport)', $header); $this->assertStringContainsString('# define TYPEPHP_PRIME2_API TYPEPHP_SYMBOL_EXPORT', $header);
$this->assertStringContainsString( $this->assertStringContainsString(
'TYPEPHP_PRIME2_IMPORT php::Array php_exported_defaults(', 'TYPEPHP_PRIME2_IMPORT php::Array php_exported_defaults(',
$header $header
@ -926,9 +947,10 @@ YAML);
ROOT_PATH . '/phpunit/code/compiler_api/library_import_global.php', ROOT_PATH . '/phpunit/code/compiler_api/library_import_global.php',
]; ];
$this->compiler->addFiles($files); $this->compiler->addFiles($files);
$cppFiles = [];
foreach ($files as $file) { foreach ($files as $file) {
$this->compiler->prepareFile($file); $this->compiler->prepareFile($file);
$this->compiler->convertFile($file); $cppFiles[$file] = $this->compiler->convertFile($file);
$this->assertStringNotContainsString( $this->assertStringNotContainsString(
'NoExport', 'NoExport',
file_get_contents($this->compiler->getArgInfoHeaderFile($file)), file_get_contents($this->compiler->getArgInfoHeaderFile($file)),
@ -937,15 +959,24 @@ YAML);
'Getter', 'Getter',
file_get_contents($this->compiler->getArgInfoHeaderFile($file)), file_get_contents($this->compiler->getArgInfoHeaderFile($file)),
); );
foreach (['NotNull', 'Printer', 'Setter', 'With'] as $attribute) { foreach (\TypePhp\Transform\CompileTimeAttributeRegistry::names() as $attribute) {
$this->assertStringNotContainsString( $this->assertStringNotContainsString(
$attribute, $attribute,
file_get_contents($this->compiler->getArgInfoHeaderFile($file)), file_get_contents($this->compiler->getArgInfoHeaderFile($file)),
); );
} }
} }
$phpCpp = file_get_contents($cppFiles[$files[0]]);
$this->assertStringContainsString(
'TYPEPHP_HOT_ATTRIBUTE php::Int php_libraryapi__twice(',
$phpCpp,
);
$this->assertStringContainsString(
'TYPEPHP_COLD_ATTRIBUTE php::Str php_libraryapi__counter__label(',
$phpCpp,
);
$provider = $this->invokeMethod('getClass', 'LibraryApi\\InternalStringExtension'); $provider = $this->invokeMethod('getClass', 'LibraryApi\\InternalStringExtension');
$this->assertSame(Type::STR, $provider->extensionProviderTarget); $this->assertSame(Type::STR, $provider->methodsForTarget);
$stubFile = $this->compiler->genLibraryImportStub($files); $stubFile = $this->compiler->genLibraryImportStub($files);
$stub = file_get_contents($stubFile); $stub = file_get_contents($stubFile);
@ -955,16 +986,20 @@ YAML);
$this->assertStringContainsString('class Counter', $stub); $this->assertStringContainsString('class Counter', $stub);
$this->assertStringContainsString('public const int STEP = 2;', $stub); $this->assertStringContainsString('public const int STEP = 2;', $stub);
$this->assertStringContainsString('public int $value = 1;', $stub); $this->assertStringContainsString('public int $value = 1;', $stub);
$this->assertStringContainsString('#[\Getter, \Setter, \With]', $stub); $this->assertStringContainsString('#[\Constructor, \Getter, \Setter, \With]', $stub);
$this->assertStringContainsString('#[\Printer]', $stub); $this->assertStringContainsString("#[\Printer(fields: ['value', 'doubled'])]", $stub);
$this->assertStringContainsString('#[\NotNull]', $stub); $this->assertStringContainsString("#[\Arrayable(['value'])]", $stub);
$this->assertStringContainsString('#[\NotNull, \Validate(FILTER_VALIDATE_EMAIL)]', $stub);
$this->assertStringContainsString('#[\MustUse, \Cold]', $stub);
$this->assertStringContainsString('#[\MustUse, \Hot]', $stub);
$this->assertStringContainsString('#[\Override]', $stub);
$this->assertMatchesRegularExpression( $this->assertMatchesRegularExpression(
'/public int \$doubled\s*\{\s*get\s*\{\s*\}\s*set\(int \$value\)\s*\{\s*\}\s*\}/s', '/public int \$doubled\s*\{\s*get\s*\{\s*\}\s*set\(int \$value\)\s*\{\s*\}\s*\}/s',
$stub, $stub,
); );
$this->assertStringContainsString('function add(int $amount = self::STEP): int', $stub); $this->assertStringContainsString('function add(int $amount = self::STEP): int', $stub);
$this->assertMatchesRegularExpression( $this->assertMatchesRegularExpression(
'/function label\(\s*#\[\\\\NotNull\]\s*string \$value\s*\): string/s', '/function label\(\s*#\[\\\\NotNull, \\\\Validate\(FILTER_VALIDATE_EMAIL\)\]\s*string \$value\s*\): string/s',
$stub, $stub,
); );
$this->assertStringContainsString('function twice(int $value): int', $stub); $this->assertStringContainsString('function twice(int $value): int', $stub);
@ -986,13 +1021,25 @@ YAML);
$this->compiler->genFunctionDeclarations($libraryHeaderFile); $this->compiler->genFunctionDeclarations($libraryHeaderFile);
$libraryHeader = file_get_contents($libraryHeaderFile); $libraryHeader = file_get_contents($libraryHeaderFile);
$this->assertStringContainsString( $this->assertStringContainsString(
'TYPEPHP_PRIME2_API php::Int php_libraryapi__twice(', 'TYPEPHP_PRIME2_API TYPEPHP_HOT_ATTRIBUTE php::Int php_libraryapi__twice(',
$libraryHeader,
);
$this->assertStringContainsString(
'TYPEPHP_PRIME2_API TYPEPHP_COLD_ATTRIBUTE php::Str php_libraryapi__counter__label(',
$libraryHeader, $libraryHeader,
); );
$this->assertStringContainsString( $this->assertStringContainsString(
'TYPEPHP_PRIME2_API php::Int php_libraryapi__counter__getvalue(', 'TYPEPHP_PRIME2_API php::Int php_libraryapi__counter__getvalue(',
$libraryHeader, $libraryHeader,
); );
$this->assertStringContainsString(
'TYPEPHP_PRIME2_API php::Array php_libraryapi__counter__toarray(',
$libraryHeader,
);
$this->assertStringContainsString(
'TYPEPHP_PRIME2_API php::Str php_libraryapi__counter____tostring(',
$libraryHeader,
);
$this->assertStringContainsString( $this->assertStringContainsString(
'extern php::Int php_libraryapi__internal_twice(', 'extern php::Int php_libraryapi__internal_twice(',
$libraryHeader, $libraryHeader,
@ -1047,7 +1094,15 @@ YAML);
$header, $header,
); );
$this->assertStringContainsString( $this->assertStringContainsString(
'TYPEPHP_PRIME2_IMPORT php::Int php_libraryapi__twice(', 'TYPEPHP_PRIME2_IMPORT php::Array php_libraryapi__counter__toarray(',
$header,
);
$this->assertStringContainsString(
'TYPEPHP_PRIME2_IMPORT php::Str php_libraryapi__counter____tostring(',
$header,
);
$this->assertStringContainsString(
'TYPEPHP_PRIME2_IMPORT TYPEPHP_HOT_ATTRIBUTE php::Int php_libraryapi__twice(',
$header, $header,
); );
$this->assertStringContainsString( $this->assertStringContainsString(

@ -17,6 +17,8 @@ class FunctionDefTest extends TestCase
$this->assertEquals('', $fn->namespace); $this->assertEquals('', $fn->namespace);
$this->assertFalse($fn->method); $this->assertFalse($fn->method);
$this->assertFalse($fn->stub); $this->assertFalse($fn->stub);
$this->assertFalse($fn->hot);
$this->assertFalse($fn->cold);
$this->assertEmpty($fn->argInfoList); $this->assertEmpty($fn->argInfoList);
$this->assertEquals(0, $fn->argCountRequired); $this->assertEquals(0, $fn->argCountRequired);
$this->assertEquals('', $fn->params); $this->assertEquals('', $fn->params);

@ -16,6 +16,7 @@ use TypePhp\Entity\ArgInfo;
use TypePhp\Context\FunctionContext; use TypePhp\Context\FunctionContext;
use TypePhp\Context\CompilationStateTrait; use TypePhp\Context\CompilationStateTrait;
use TypePhp\Diagnostics\CompilerDiagnosticTrait; use TypePhp\Diagnostics\CompilerDiagnosticTrait;
use TypePhp\Diagnostics\CompileTimeAttributeDiagnostic;
use TypePhp\Diagnostics\CliDiagnosticReporter; use TypePhp\Diagnostics\CliDiagnosticReporter;
use TypePhp\Diagnostics\DiagnosticReporter; use TypePhp\Diagnostics\DiagnosticReporter;
use TypePhp\Diagnostics\ThrowingDiagnosticReporter; use TypePhp\Diagnostics\ThrowingDiagnosticReporter;
@ -1527,6 +1528,7 @@ class CompilerBase implements PropertyAccessContext
switch ($class) { switch ($class) {
case 'Stmt_Expression': case 'Stmt_Expression':
$v->expr->setAttribute(self::ATTR_STATEMENT_EXPRESSION, true); $v->expr->setAttribute(self::ATTR_STATEMENT_EXPRESSION, true);
$this->assertMustUseResultIsConsumed($v->expr);
if ($this->inGeneratorBody && $v->expr instanceof Expr\Yield_) { if ($this->inGeneratorBody && $v->expr instanceof Expr\Yield_) {
$result = $this->parseYieldStmt($v->expr); $result = $this->parseYieldStmt($v->expr);
} elseif ($this->inGeneratorBody && $v->expr instanceof Expr\YieldFrom) { } elseif ($this->inGeneratorBody && $v->expr instanceof Expr\YieldFrom) {
@ -1634,6 +1636,54 @@ class CompilerBase implements PropertyAccessContext
return $code; return $code;
} }
protected function assertMustUseResultIsConsumed(NodeAbstract $expr): void
{
$functionDef = $this->resolveCalledFunctionDef($expr);
if ($functionDef?->mustUse) {
$target = ($functionDef->method ? 'method ' : 'function ') . $functionDef->name . '()';
$this->error(CompileTimeAttributeDiagnostic::formatPositions(
'The return value of `' . $functionDef->name . '()` must be used',
'MustUse',
$target,
$functionDef->sourceFile,
$functionDef->startLine,
'discarded call',
$this->file,
$expr->getStartLine(),
));
}
}
protected function resolveCalledFunctionDef(NodeAbstract $expr): ?FunctionDef
{
if ($expr instanceof Expr\FuncCall && $expr->name instanceof Node\Name) {
$name = $this->parseIdentifier($expr->name);
$native = $this->findNativeFunction($name);
return $native ? $this->getFunction($native) : null;
}
if ($expr instanceof Expr\MethodCall && $expr->name instanceof Node\Identifier) {
$class = $this->detectClassOfExpr($expr->var);
if ($class === '' && $expr->var instanceof Expr\Variable && is_string($expr->var->name)) {
$var = $this->parseVariable($expr->var);
$class = $var === 'this_' ? $this->getFullClassName() : $this->getDeclaredObjectType($var);
}
return $class === '' ? null : $this->findAotMethodFunctionDef($class, $expr->name->toString());
}
if ($expr instanceof Expr\StaticCall && $expr->class instanceof Node\Name
&& $expr->name instanceof Node\Identifier) {
$class = $this->parseIdentifier($expr->class);
if ($class === 'self' || $class === 'static') {
$class = $this->getFullClassName();
} elseif ($class === 'parent') {
$class = $this->classDef?->extends ?? '';
} else {
$class = $this->getNamespacedClassName($class);
}
return $class === '' ? null : $this->findAotMethodFunctionDef($class, $expr->name->toString());
}
return null;
}
protected function parseEcho(mixed $v): string protected function parseEcho(mixed $v): string
{ {
$lines = []; $lines = [];

@ -0,0 +1,93 @@
<?php
/**
* This file is part of TypePHP.
*
* @link https://www.swoole.com/
* @contact service@swoole.com
*/
namespace TypePhp\Diagnostics;
use PhpParser\Node;
use PhpParser\Node\Stmt;
final class CompileTimeAttributeDiagnostic
{
public const GENERATED_BY = 'typephpGeneratedByCompileTimeAttribute';
public const GENERATED_TARGET = 'typephpCompileTimeAttributeTarget';
public static function format(
string $message,
string $attribute,
Node $target,
string $file,
?Node $source = null,
?string $conflictAttribute = null,
?Node $conflictSource = null,
): string {
$source ??= $target;
$context = '[compile-time attribute: #[' . $attribute . ']; target: ' .
self::describeTarget($target) . '; source: ' . $file . ':' . $source->getStartLine();
if ($conflictSource !== null) {
$context .= $conflictAttribute === null
? '; conflict source: declaration at ' . $file . ':' . $conflictSource->getStartLine()
: '; conflict source: #[' . $conflictAttribute . '] at ' .
$file . ':' . $conflictSource->getStartLine();
}
return $message . ' ' . $context . ']';
}
public static function markGenerated(Node $generated, string $attribute, Node $target): void
{
$generated->setAttribute(self::GENERATED_BY, $attribute);
$generated->setAttribute(self::GENERATED_TARGET, $target);
}
public static function formatPositions(
string $message,
string $attribute,
string $target,
string $sourceFile,
int $sourceLine,
?string $conflictLabel = null,
?string $conflictFile = null,
?int $conflictLine = null,
): string {
$context = '[compile-time attribute: #[' . $attribute . ']; target: ' . $target .
'; source: ' . $sourceFile . ':' . $sourceLine;
if ($conflictFile !== null && $conflictLine !== null) {
$context .= '; conflict source: ' . ($conflictLabel ?? 'declaration') . ' at ' .
$conflictFile . ':' . $conflictLine;
}
return $message . ' ' . $context . ']';
}
public static function describeTarget(Node $node): string
{
if ($node instanceof Stmt\Function_) {
return 'function ' . $node->name->toString() . '()';
}
if ($node instanceof Stmt\ClassMethod) {
return 'method ' . $node->name->toString() . '()';
}
if ($node instanceof Stmt\Property) {
return 'property ' . implode(', ', array_map(
static fn (Node\PropertyItem $property): string => '$' . $property->name->toString(),
$node->props,
));
}
if ($node instanceof Node\Param && is_string($node->var->name)) {
return ($node->isPromoted() ? 'promoted property/parameter ' : 'parameter ') . '$' . $node->var->name;
}
if ($node instanceof Stmt\ClassLike) {
return strtolower(str_replace('Stmt_', '', $node->getType())) . ' ' . ($node->name?->toString() ?? 'anonymous');
}
if ($node instanceof Node\Expr\Closure) {
return 'anonymous function';
}
if ($node instanceof Node\Expr\ArrowFunction) {
return 'arrow function';
}
return $node->getType();
}
}

@ -32,6 +32,25 @@ trait CompilerDiagnosticTrait
$this->getDiagnosticReporter()->warning($node, $this->file, $msg); $this->getDiagnosticReporter()->warning($node, $this->file, $msg);
} }
protected function fatalCompileTimeAttribute(
Node $target,
string $attribute,
string $message,
?Node $source = null,
?string $conflictAttribute = null,
?Node $conflictSource = null,
): never {
$this->error(CompileTimeAttributeDiagnostic::format(
$message,
$attribute,
$target,
$this->file,
$source,
$conflictAttribute,
$conflictSource,
));
}
protected function errorUndefinedVariable(Variable $node): never protected function errorUndefinedVariable(Variable $node): never
{ {
$this->fatalError($node, "The variable `\${$node->name}` is undefined"); $this->fatalError($node, "The variable `\${$node->name}` is undefined");

@ -34,9 +34,15 @@ class ClassDef extends ClassLikeDef
public bool $enum = false; public bool $enum = false;
/** Whether this class and its methods are part of the public ABI of a library build. */ /** Whether this class and its methods are part of the public ABI of a library build. */
public bool $exported = true; public bool $exported = true;
public ?string $extensionProviderTarget = null; public ?string $methodsForTarget = null;
/** Whether #[Printer] generated this class's own toString() method. */ /** Whether #[Printer] generated this class's own __toString() method. */
public bool $printerGenerated = false; public bool $printerGenerated = false;
/** @var list<string>|null Explicit fields, or null to include every public instance property. */
public ?array $printerFields = null;
/** Whether #[Arrayable] generated this class's own toArray() method. */
public bool $arrayableGenerated = false;
/** @var list<string>|null Explicit fields, or null to include every public instance property. */
public ?array $arrayableFields = null;
/** /**
* Backing type for backed enums ('int' or 'string'), null for pure enums. * Backing type for backed enums ('int' or 'string'), null for pure enums.

@ -32,6 +32,14 @@ class FunctionDef
public bool $returnTypeUndeclared = false; public bool $returnTypeUndeclared = false;
public bool $returnsByRef = false; public bool $returnsByRef = false;
public bool $generator = false; public bool $generator = false;
/** The call result must not be discarded as a statement expression. */
public bool $mustUse = false;
/** The method must override an inherited class or interface method. */
public bool $overrideRequired = false;
/** Prefer optimizing this function for frequently executed paths. */
public bool $hot = false;
/** Prefer optimizing this function for rarely executed paths. */
public bool $cold = false;
/** Number of fixed positional values returned through the internal tuple fast path. */ /** Number of fixed positional values returned through the internal tuple fast path. */
public int $multiReturnCount = 0; public int $multiReturnCount = 0;
/** Source file containing this function definition. */ /** Source file containing this function definition. */

@ -0,0 +1,26 @@
<?php
/**
* This file is part of TypePHP.
*
* @link https://www.swoole.com/
* @contact service@swoole.com
*/
namespace TypePhp\Exception;
use PhpParser\Node;
final class CompileTimeAttributeError extends SyntaxError
{
public function __construct(
string $message,
public readonly Node $target,
public readonly ?string $attribute = null,
public readonly ?Node $attributeSource = null,
public readonly ?string $conflictAttribute = null,
public readonly ?Node $conflictSource = null,
?\Throwable $previous = null,
) {
parent::__construct($message, 0, $previous);
}
}

@ -212,7 +212,8 @@ trait FiberGenerator
private function doGenFiberGeneratorFunction(Function_|ClassMethod $v, FunctionDef $functionDef, string $nativeName): string private function doGenFiberGeneratorFunction(Function_|ClassMethod $v, FunctionDef $functionDef, string $nativeName): string
{ {
$functionDeclCode = Type::VAR . ' ' . self::PREFIX . $nativeName . '('; $functionDeclCode = $this->getFunctionOptimizationAttribute($functionDef)
. Type::VAR . ' ' . self::PREFIX . $nativeName . '(';
if ($this->class) { if ($this->class) {
$functionDeclCode .= Type::OBJECT . ' &this_'; $functionDeclCode .= Type::OBJECT . ' &this_';
if ($functionDef->params) { if ($functionDef->params) {

@ -14,6 +14,8 @@ use PhpParser\NodeTraverser;
use PhpParser\NodeVisitor\NameResolver; use PhpParser\NodeVisitor\NameResolver;
use PhpParser\Parser; use PhpParser\Parser;
use PhpParser\PrettyPrinter; use PhpParser\PrettyPrinter;
use TypePhp\Transform\CompileTimeAttribute;
use TypePhp\Transform\CompileTimeAttributeRegistry;
final class LibraryImportStubGenerator final class LibraryImportStubGenerator
{ {
@ -110,8 +112,12 @@ final class LibraryImportStubGenerator
) !== 1, ) !== 1,
); );
$stmt->setAttribute('comments', array_values($comments)); $stmt->setAttribute('comments', array_values($comments));
$this->filterAttributesForLibraryStub($stmt);
if ($stmt instanceof Node\Stmt\Function_) { if ($stmt instanceof Node\Stmt\Function_) {
foreach ($stmt->params as $param) {
$this->filterAttributesForLibraryStub($param);
}
$stmt->stmts = []; $stmt->stmts = [];
return $stmt; return $stmt;
} }
@ -127,10 +133,15 @@ final class LibraryImportStubGenerator
&& !($stmt instanceof Node\Stmt\Interface_)) { && !($stmt instanceof Node\Stmt\Interface_)) {
$member->stmts = []; $member->stmts = [];
} }
$this->filterAttributesForLibraryStub($member);
foreach ($member->params as $param) {
$this->filterAttributesForLibraryStub($param);
}
$members[] = $member; $members[] = $member;
continue; continue;
} }
if ($member instanceof Node\Stmt\Property) { if ($member instanceof Node\Stmt\Property) {
$this->filterAttributesForLibraryStub($member);
foreach ($member->hooks as $hook) { foreach ($member->hooks as $hook) {
if ($hook->body !== null) { if ($hook->body !== null) {
$hook->body = []; $hook->body = [];
@ -159,8 +170,7 @@ final class LibraryImportStubGenerator
} }
foreach ($node->attrGroups as $group) { foreach ($node->attrGroups as $group) {
foreach ($group->attrs as $attribute) { foreach ($group->attrs as $attribute) {
$parts = $attribute->name->getParts(); if (CompileTimeAttribute::is($attribute, 'NoExport')) {
if (count($parts) === 1 && strcasecmp($parts[0], 'NoExport') === 0) {
return true; return true;
} }
} }
@ -168,4 +178,24 @@ final class LibraryImportStubGenerator
return false; return false;
} }
private function filterAttributesForLibraryStub(Node $node): void
{
if (!property_exists($node, 'attrGroups')) {
return;
}
foreach ($node->attrGroups as $groupIndex => $group) {
foreach ($group->attrs as $attributeIndex => $attribute) {
$definition = CompileTimeAttributeRegistry::get(CompileTimeAttribute::resolvedName($attribute));
if ($definition !== null && !$definition['preserve_in_library_stub']) {
unset($group->attrs[$attributeIndex]);
}
}
$group->attrs = array_values($group->attrs);
if ($group->attrs === []) {
unset($node->attrGroups[$groupIndex]);
}
}
$node->attrGroups = array_values($node->attrGroups);
}
} }

@ -362,6 +362,11 @@ trait MethodCallTrait
} }
return $this->genToConvertCall($object, $methodName, $receiverType); return $this->genToConvertCall($object, $methodName, $receiverType);
} }
// MethodsFor('*') extensions apply to every receiver type.
$kwExt = $this->findKeywordExtensionMethod($methodName);
if ($kwExt) {
return $this->parseUniversalMethodCall($expr, $object, $methodName, $kwExt, $this->isVarExpr($expr->var));
}
// A provider targeting Type::Any only applies when // A provider targeting Type::Any only applies when
// the receiver's static type is actually mixed/any. // the receiver's static type is actually mixed/any.
if ($receiverType === Type::VAR) { if ($receiverType === Type::VAR) {
@ -370,11 +375,6 @@ trait MethodCallTrait
return $this->parseUniversalMethodCall($expr, $object, $methodName, $anyExtension, $this->isVarExpr($expr->var)); return $this->parseUniversalMethodCall($expr, $object, $methodName, $anyExtension, $this->isVarExpr($expr->var));
} }
} }
// ExtensionProvider('*') extensions apply to every receiver type.
$kwExt = $this->findKeywordExtensionMethod($methodName);
if ($kwExt) {
return $this->parseUniversalMethodCall($expr, $object, $methodName, $kwExt, $this->isVarExpr($expr->var));
}
} }
// 可转为原生调用的 MethodCall // 可转为原生调用的 MethodCall
@ -412,14 +412,22 @@ trait MethodCallTrait
} }
} }
} catch (DynamicCall) { } catch (DynamicCall) {
$extension = $this->findObjectExtensionMethod($class, $methodName); $extension = $this->findObjectExtensionMethod(
$class,
$methodName,
$this->isDefinitelyObjectReceiver($expr->var, $object, $class, $type),
);
if ($extension !== null) { if ($extension !== null) {
return $this->parseUniversalMethodCall($expr, $object, $methodName, $extension); return $this->parseUniversalMethodCall($expr, $object, $methodName, $extension);
} }
$magicMethod = true; $magicMethod = true;
} }
if (!$nativeFunc) { if (!$nativeFunc) {
$extension = $this->findObjectExtensionMethod($class, $methodName); $extension = $this->findObjectExtensionMethod(
$class,
$methodName,
$this->isDefinitelyObjectReceiver($expr->var, $object, $class, $type),
);
if ($extension !== null) { if ($extension !== null) {
return $this->parseUniversalMethodCall($expr, $object, $methodName, $extension); return $this->parseUniversalMethodCall($expr, $object, $methodName, $extension);
} }
@ -450,7 +458,11 @@ trait MethodCallTrait
} }
$extensionClass = $this->detectClassOfExpr($expr->var); $extensionClass = $this->detectClassOfExpr($expr->var);
$extension = $this->findObjectExtensionMethod($extensionClass, $methodName); $extension = $this->findObjectExtensionMethod(
$extensionClass,
$methodName,
$this->isDefinitelyObjectReceiver($expr->var, $object, $extensionClass, $type),
);
if ($extension !== null) { if ($extension !== null) {
return $this->parseUniversalMethodCall($expr, $object, $methodName, $extension, false); return $this->parseUniversalMethodCall($expr, $object, $methodName, $extension, false);
} }
@ -483,6 +495,55 @@ trait MethodCallTrait
} }
} }
private function isDefinitelyObjectReceiver(
Expr $receiver,
string $object,
string $class,
string $type,
): bool {
if ($type !== Type::OBJECT && $class === '') {
return false;
}
if ($this->isVarExpr($receiver)) {
foreach ($this->functionDef?->argInfoList ?? [] as $argument) {
if ($argument->name === $object && $argument->nullable) {
return false;
}
}
}
if ($this->isPropertyFetch($receiver) && $this->getNativePropertyDef($receiver)?->nullable) {
return false;
}
$calledFunction = $this->resolveCalledFunctionDef($receiver);
if ($calledFunction !== null && $this->typeNodeAllowsNull($calledFunction->returnTypeNode)) {
return false;
}
return true;
}
private function typeNodeAllowsNull(?Node $type): bool
{
if ($type instanceof Node\NullableType) {
return true;
}
if (!$type instanceof Node\UnionType) {
return false;
}
foreach ($type->types as $member) {
if ($member instanceof Node\Identifier && strtolower($member->name) === 'null') {
return true;
}
if ($member instanceof Node\Name && strtolower($member->toString()) === 'null') {
return true;
}
}
return false;
}
protected function parseStaticCall(Expr\StaticCall $expr): string protected function parseStaticCall(Expr\StaticCall $expr): string
{ {

@ -13,7 +13,7 @@ use PhpParser\NodeAbstract;
trait UniversalMethodCall trait UniversalMethodCall
{ {
private ?array $extensionProviderMethods = null; private ?array $methodsForRegistry = null;
protected const array UNIVERSAL_METHODS = [ protected const array UNIVERSAL_METHODS = [
Type::INT => [ Type::INT => [
'add' => ['handler' => 'calc_op', 'op' => '+', 'return_type' => Type::INT, 'min_args' => 1, 'max_args' => 1], 'add' => ['handler' => 'calc_op', 'op' => '+', 'return_type' => Type::INT, 'min_args' => 1, 'max_args' => 1],
@ -338,17 +338,20 @@ trait UniversalMethodCall
return $ext ? $ext['return_type'] : null; return $ext ? $ext['return_type'] : null;
} }
private function getExtensionProviderMethods(): array private function getMethodsForRegistry(): array
{ {
if ($this->extensionProviderMethods !== null) { if ($this->methodsForRegistry !== null) {
return $this->extensionProviderMethods; return $this->methodsForRegistry;
} }
$registry = []; $registry = [];
foreach ($this->symbols->classes() as $provider) { foreach ($this->symbols->classes() as $provider) {
$target = $provider->extensionProviderTarget; $target = $provider->methodsForTarget;
if ($target === null) { if ($target === null) {
continue; continue;
} }
if (!$this->isBuiltinMethodsForTarget($target) && $this->isInterface($target)) {
$this->error("MethodsFor target {$target} must be a class; interfaces are not supported");
}
foreach ($provider->methods as $method) { foreach ($provider->methods as $method) {
if (str_starts_with($method->name, '__')) { if (str_starts_with($method->name, '__')) {
continue; continue;
@ -357,21 +360,36 @@ trait UniversalMethodCall
continue; continue;
} }
if (!($method->flags & \PhpParser\Modifiers::STATIC)) { if (!($method->flags & \PhpParser\Modifiers::STATIC)) {
$this->error("Extension provider method {$provider->getNamespacedName(false)}::{$method->name}() must be static"); $this->error("MethodsFor method {$provider->getNamespacedName(false)}::{$method->name}() must be static");
} }
$function = $method->functionDef; $function = $method->functionDef;
if ($function === null || empty($function->argInfoList)) { if ($function === null || empty($function->argInfoList)) {
$this->error("Extension provider method {$provider->getNamespacedName(false)}::{$method->name}() must declare a receiver parameter"); $this->error("MethodsFor method {$provider->getNamespacedName(false)}::{$method->name}() must declare a receiver parameter");
} }
$receiver = $function->argInfoList[0]; $receiver = $function->argInfoList[0];
if ($receiver->byRef || !$this->extensionReceiverMatchesTarget($receiver, $target)) { if ($receiver->byRef || !$this->extensionReceiverMatchesTarget($receiver, $target)) {
$this->error("Invalid receiver parameter for extension provider method {$provider->getNamespacedName(false)}::{$method->name}()"); $this->error("Invalid receiver parameter for MethodsFor method {$provider->getNamespacedName(false)}::{$method->name}()");
} }
$targetKey = strtolower(ltrim($target, '\\')); $targetKey = strtolower(ltrim($target, '\\'));
$methodKey = strtolower($method->name); $methodKey = strtolower($method->name);
if (isset($registry[$targetKey][$methodKey])) { if (isset($registry[$targetKey][$methodKey])) {
$this->error("Duplicate extension method {$target}::{$method->name}()"); $this->error("Duplicate extension method {$target}::{$method->name}()");
} }
if ($target === '*') {
foreach ($registry as $registeredTarget => $registeredMethods) {
if ($registeredTarget !== '*' && isset($registeredMethods[$methodKey])) {
$this->error(
"Keyword extension method *::{$method->name}() conflicts with extension method "
. "{$registeredTarget}::{$method->name}()"
);
}
}
} elseif (isset($registry['*'][$methodKey])) {
$this->error(
"Extension method {$target}::{$method->name}() conflicts with keyword extension method "
. "*::{$method->name}()"
);
}
$registry[$targetKey][$methodKey] = [ $registry[$targetKey][$methodKey] = [
'handler' => 'provider_extension', 'handler' => 'provider_extension',
'fn' => $this->getNativeName($method->name, $provider->namespace, $provider->name), 'fn' => $this->getNativeName($method->name, $provider->namespace, $provider->name),
@ -382,7 +400,16 @@ trait UniversalMethodCall
]; ];
} }
} }
return $this->extensionProviderMethods = $registry; return $this->methodsForRegistry = $registry;
}
private function isBuiltinMethodsForTarget(string $target): bool
{
return $target === '*' || in_array($target, [
Type::VAR, Type::INT, Type::FLOAT, Type::BOOL, Type::STR,
Type::ARRAY, Type::OBJECT, Type::STREAM, Type::BIGINT,
Type::BIGFLOAT, Type::DECIMAL, Type::BOX,
], true);
} }
private function extensionReceiverMatchesTarget($receiver, string $target): bool private function extensionReceiverMatchesTarget($receiver, string $target): bool
@ -390,12 +417,7 @@ trait UniversalMethodCall
if ($target === '*') { if ($target === '*') {
return $receiver->type === Type::VAR; return $receiver->type === Type::VAR;
} }
$builtinTargets = [ if (!$this->isBuiltinMethodsForTarget($target)) {
Type::VAR, Type::INT, Type::FLOAT, Type::BOOL, Type::STR,
Type::ARRAY, Type::OBJECT, Type::STREAM, Type::BIGINT,
Type::BIGFLOAT, Type::DECIMAL, Type::BOX,
];
if (!in_array($target, $builtinTargets, true)) {
return $receiver->type === Type::OBJECT && $this->isSameClassName($receiver->declaredClass, $target); return $receiver->type === Type::OBJECT && $this->isSameClassName($receiver->declaredClass, $target);
} }
return $receiver->type === $target; return $receiver->type === $target;
@ -403,7 +425,7 @@ trait UniversalMethodCall
private function findProviderExtension(string $target, string $method): ?array private function findProviderExtension(string $target, string $method): ?array
{ {
return $this->getExtensionProviderMethods()[strtolower(ltrim($target, '\\'))][strtolower($method)] ?? null; return $this->getMethodsForRegistry()[strtolower(ltrim($target, '\\'))][strtolower($method)] ?? null;
} }
protected const array TO_CONVERT_FN = [ protected const array TO_CONVERT_FN = [
@ -445,21 +467,95 @@ trait UniversalMethodCall
} }
/** /**
* Look up a statically compiled object extension in the object's own * Look up an object extension using the receiver's static class hierarchy.
* namespace. The class prefix and method suffix must match the declared * Real methods always win. Extensions are searched on the exact static
* names without converting between camelCase and snake_case. This lookup * class, then its parents from nearest to farthest, and finally Type::Object
* is only used by the named MethodCall AST path; dynamic method names and * when the receiver is statically known to be an object. __call() remains
* StaticCall nodes deliberately do not use it. * the last fallback in the caller.
* Real methods are resolved before this fallback, while __call() is used
* only if no valid extension exists.
*/ */
protected function findObjectExtensionMethod(string $class, string $method): ?array protected function findObjectExtensionMethod(
{ string $class,
string $method,
bool $receiverIsDefinitelyObject,
): ?array {
$class = ltrim($class, '\\'); $class = ltrim($class, '\\');
if ($class === '' || !$this->hasClass($class)) { if ($class !== '' && $this->objectTypeDeclaresMethod($class, $method)) {
return null; return null;
} }
return $this->findProviderExtension($class, $method);
$visited = [];
$current = $class;
while ($current !== '') {
$key = strtolower(ltrim($current, '\\'));
if (isset($visited[$key])) {
break;
}
$visited[$key] = true;
$extension = $this->findProviderExtension($current, $method);
if ($extension !== null) {
return $extension;
}
$classDef = $this->getClassDef($current);
if ($classDef !== null) {
$current = $classDef->extends;
continue;
}
$reflection = Reflection::getClass($current);
$parent = $reflection?->getParentClass();
$current = $parent === false || $parent === null ? '' : $parent->getName();
}
return $receiverIsDefinitelyObject
? $this->findProviderExtension(Type::OBJECT, $method)
: null;
}
private function objectTypeDeclaresMethod(string $class, string $method): bool
{
return $this->classOrInterfaceDeclaresMethod($class, $method, []);
}
/** @param array<string, true> $visited */
private function classOrInterfaceDeclaresMethod(string $type, string $method, array $visited): bool
{
$type = ltrim($type, '\\');
$key = strtolower($type);
if ($type === '' || isset($visited[$key])) {
return false;
}
$visited[$key] = true;
$classDef = $this->getClassDef($type);
if ($classDef !== null) {
if ($classDef->hasMethod($method) || $classDef->hasAbstractMethod($method)) {
return true;
}
foreach ($classDef->implements as $interface) {
if ($this->classOrInterfaceDeclaresMethod($interface, $method, $visited)) {
return true;
}
}
return $classDef->extends !== ''
&& $this->classOrInterfaceDeclaresMethod($classDef->extends, $method, $visited);
}
if ($this->hasInterface($type)) {
$interfaceDef = $this->getInterface($type);
if ($interfaceDef->hasMethod($method)) {
return true;
}
foreach ($interfaceDef->extendsList ?: ($interfaceDef->extends ? [$interfaceDef->extends] : []) as $parent) {
if ($this->classOrInterfaceDeclaresMethod($parent, $method, $visited)) {
return true;
}
}
return false;
}
$reflection = Reflection::getClass($type);
return $reflection?->hasMethod($method) ?? false;
} }
/** /**

@ -17,9 +17,13 @@ use TypePhp\Entity\FunctionDef;
use TypePhp\Entity\InterfaceDef; use TypePhp\Entity\InterfaceDef;
use TypePhp\Entity\MethodDef; use TypePhp\Entity\MethodDef;
use TypePhp\Entity\PropertyDef; use TypePhp\Entity\PropertyDef;
use TypePhp\Diagnostics\CompileTimeAttributeDiagnostic;
use TypePhp\Exception\SyntaxError; use TypePhp\Exception\SyntaxError;
use TypePhp\Transform\PropertyHookLowering; use TypePhp\Transform\PropertyHookLowering;
use TypePhp\Transform\PrinterLowering; use TypePhp\Transform\PrinterLowering;
use TypePhp\Transform\ArrayableLowering;
use TypePhp\Transform\ClassFieldSelection;
use TypePhp\Transform\FunctionAttributeLowering;
use TypePhp\Transform\Visitor; use TypePhp\Transform\Visitor;
use PhpParser\Modifiers; use PhpParser\Modifiers;
use PhpParser\ConstExprEvaluator; use PhpParser\ConstExprEvaluator;
@ -139,7 +143,10 @@ class Preprocessor extends CompilerBase
$traverser = new NodeTraverser(); $traverser = new NodeTraverser();
$traverser->addVisitor(new NameResolver(null, ['replaceNodes' => false])); $traverser->addVisitor(new NameResolver(null, ['replaceNodes' => false]));
$traverser->addVisitor(new Visitor()); $traverser->addVisitor(new Visitor(
fn (Node $node, string $message) => $this->warning($node, $message),
$this->file,
));
$stmts = $traverser->traverse($ast); $stmts = $traverser->traverse($ast);
$this->validateUnsupportedAttributeArguments($stmts); $this->validateUnsupportedAttributeArguments($stmts);
@ -227,7 +234,12 @@ class Preprocessor extends CompilerBase
continue; continue;
} }
if ($attribute->args !== []) { if ($attribute->args !== []) {
$this->fatalError($attribute, 'NoExport does not accept arguments'); $this->fatalCompileTimeAttribute(
$node,
'NoExport',
'NoExport does not accept arguments',
$attribute,
);
} }
return true; return true;
} }
@ -534,6 +546,17 @@ class Preprocessor extends CompilerBase
} }
$functionDef = new FunctionDef($fnName, $returnType, $this->namespace); $functionDef = new FunctionDef($fnName, $returnType, $this->namespace);
$functionDef->mustUse = (bool) $v->getAttribute(FunctionAttributeLowering::MUST_USE_ATTRIBUTE, false);
$functionDef->overrideRequired = (bool) $v->getAttribute(FunctionAttributeLowering::OVERRIDE_ATTRIBUTE, false);
$functionDef->hot = (bool) $v->getAttribute(FunctionAttributeLowering::HOT_ATTRIBUTE, false);
$functionDef->cold = (bool) $v->getAttribute(FunctionAttributeLowering::COLD_ATTRIBUTE, false);
if ($functionDef->mustUse && $returnType === Type::VOID) {
$this->fatalCompileTimeAttribute(
$v,
'MustUse',
'MustUse cannot be applied to a function or method returning void',
);
}
$functionDef->exported = !($this->classDef?->exported === false || $this->hasNoExportAttribute($v)); $functionDef->exported = !($this->classDef?->exported === false || $this->hasNoExportAttribute($v));
$functionDef->returnClass = $class; $functionDef->returnClass = $class;
// Record late-bound return type keywords so they can be re-resolved to // Record late-bound return type keywords so they can be re-resolved to
@ -663,7 +686,7 @@ class Preprocessor extends CompilerBase
$this->classDef = new ClassDef($this->class, $flags, $this->namespace); $this->classDef = new ClassDef($this->class, $flags, $this->namespace);
$this->classDef->exported = !$this->hasNoExportAttribute($class); $this->classDef->exported = !$this->hasNoExportAttribute($class);
$this->classDef->extensionProviderTarget = $this->parseExtensionProviderTarget($class); $this->classDef->methodsForTarget = $this->parseMethodsForTarget($class);
$this->addClass($fullClassName, $this->classDef); $this->addClass($fullClassName, $this->classDef);
if (!empty($class->extends)) { if (!empty($class->extends)) {
@ -696,20 +719,37 @@ class Preprocessor extends CompilerBase
$this->symbolDeclInFile[$fullClassNameLower] = $this->file; $this->symbolDeclInFile[$fullClassNameLower] = $this->file;
if ($class instanceof Node\Stmt\Class_) { if ($class instanceof Node\Stmt\Class_) {
$generatedPrinter = false; $generatedPrinter = null;
$generatedArrayable = null;
foreach ($class->getMethods() as $method) { foreach ($class->getMethods() as $method) {
if ($method->getAttribute(PrinterLowering::GENERATED_ATTRIBUTE)) { if ($method->getAttribute(PrinterLowering::GENERATED_ATTRIBUTE)) {
$generatedPrinter = true; $generatedPrinter = $method;
break; }
if ($method->getAttribute(ArrayableLowering::GENERATED_ATTRIBUTE)) {
$generatedArrayable = $method;
} }
} }
if ($generatedPrinter && $this->parentHasMethod($this->classDef->extends, 'toString')) { if ($generatedPrinter !== null) {
PrinterLowering::removeGeneratedMethod($class);
} elseif ($generatedPrinter) {
$this->classDef->printerGenerated = true; $this->classDef->printerGenerated = true;
$this->classDef->printerFields = $generatedPrinter->getAttribute(PrinterLowering::FIELDS_ATTRIBUTE);
$properties = $this->classDef->printerFields
?? [...$this->parentPublicProperties($this->classDef->extends), ...ClassFieldSelection::ownPublicProperties($class)];
PrinterLowering::rebuildGeneratedMethod( PrinterLowering::rebuildGeneratedMethod(
$class, $class,
[...$this->parentPublicProperties($this->classDef->extends), ...PrinterLowering::ownPublicProperties($class)], $properties,
$this->classDef->printerFields,
$this->classStringProperties($this->classDef),
);
}
if ($generatedArrayable !== null) {
$this->classDef->arrayableGenerated = true;
$this->classDef->arrayableFields = $generatedArrayable->getAttribute(ArrayableLowering::FIELDS_ATTRIBUTE);
$properties = $this->classDef->arrayableFields
?? [...$this->parentPublicProperties($this->classDef->extends), ...ClassFieldSelection::ownPublicProperties($class)];
ArrayableLowering::rebuildGeneratedMethod(
$class,
$properties,
$this->classDef->arrayableFields,
); );
} }
} }
@ -774,30 +814,6 @@ class Preprocessor extends CompilerBase
return $code; return $code;
} }
public function shouldGeneratePrinter(string $class): bool
{
$classDef = $this->getClassDef(ltrim($class, '\\'));
if ($classDef === null) {
return true;
}
// A child may be discovered before its parent during the initial
// project scan. Reconcile the provisional method once every class is
// available, before conversion and arginfo generation begin.
if ($classDef->printerGenerated && $this->parentHasMethod($classDef->extends, 'toString')) {
$generated = $classDef->removeMethod('toString');
if ($generated?->functionDef !== null) {
foreach ($this->symbols->functions() as $name => $functionDef) {
if ($functionDef === $generated->functionDef) {
$this->symbols->removeFunction($name);
break;
}
}
}
$classDef->printerGenerated = false;
}
return $classDef->printerGenerated;
}
/** @return list<string> */ /** @return list<string> */
protected function parentPublicProperties(string $parent): array protected function parentPublicProperties(string $parent): array
{ {
@ -817,35 +833,75 @@ class Preprocessor extends CompilerBase
return array_values(array_unique($properties)); return array_values(array_unique($properties));
} }
protected function parentHasMethod(string $parent, string $method): bool /** @return list<string> */
protected function selectableProperties(ClassDef $classDef): array
{ {
$properties = [];
$parent = $classDef->extends;
while ($parent !== '') { while ($parent !== '') {
$classDef = $this->getClassDef($parent); $parentDef = $this->getClassDef($parent);
if ($classDef === null) { if ($parentDef === null) {
return $this->isInternalClass($parent) && method_exists($parent, $method); break;
} }
if ($classDef->hasMethod($method) || $classDef->hasAbstractMethod($method)) { foreach ($parentDef->properties as $property) {
return true; if (!$property->isStatic() && !$property->isPrivate()) {
$properties[] = $property->name;
}
} }
$parent = $classDef->extends; $parent = $parentDef->extends;
} }
return false; foreach ($classDef->properties as $property) {
if (!$property->isStatic()) {
$properties[] = $property->name;
}
}
return array_values(array_unique($properties));
}
/** @return list<string> */
protected function classStringProperties(ClassDef $classDef): array
{
$types = [];
if ($classDef->extends !== '') {
$parent = $this->getClassDef($classDef->extends);
if ($parent !== null) {
foreach ($this->classStringProperties($parent) as $property) {
$types[$property] = true;
}
}
}
foreach ($classDef->properties as $property) {
if (!$property->isStatic()) {
$types[$property->name] = $property->type === Type::STR;
}
}
return array_keys(array_filter($types));
} }
protected function parseExtensionProviderTarget(Node\Stmt\Class_|Node\Stmt\Trait_|Node\Stmt\Enum_ $class): ?string protected function parseMethodsForTarget(Node\Stmt\Class_|Node\Stmt\Trait_|Node\Stmt\Enum_ $class): ?string
{ {
foreach ($class->attrGroups as $groupIndex => $group) { foreach ($class->attrGroups as $groupIndex => $group) {
foreach ($group->attrs as $attributeIndex => $attribute) { foreach ($group->attrs as $attributeIndex => $attribute) {
if (!$this->isRootCompileTimeAttribute($attribute, 'ExtensionProvider')) { if (!$this->isRootCompileTimeAttribute($attribute, 'MethodsFor')) {
continue; continue;
} }
if (!$class instanceof Node\Stmt\Class_) { if (!$class instanceof Node\Stmt\Class_) {
$this->fatalError($class, 'ExtensionProvider can only be applied to classes'); $this->fatalCompileTimeAttribute(
$class,
'MethodsFor',
'MethodsFor can only be applied to classes',
$attribute,
);
} }
if (count($attribute->args) !== 1) { if (count($attribute->args) !== 1) {
$this->fatalError($attribute, 'ExtensionProvider expects exactly one target'); $this->fatalCompileTimeAttribute(
$class,
'MethodsFor',
'MethodsFor expects exactly one target',
$attribute,
);
} }
$target = $this->parseExtensionProviderTargetValue($attribute->args[0]->value, $attribute); $target = $this->parseMethodsForTargetValue($attribute->args[0]->value, $attribute, $class);
unset($group->attrs[$attributeIndex]); unset($group->attrs[$attributeIndex]);
$group->attrs = array_values($group->attrs); $group->attrs = array_values($group->attrs);
if (empty($group->attrs)) { if (empty($group->attrs)) {
@ -858,7 +914,11 @@ class Preprocessor extends CompilerBase
return null; return null;
} }
private function parseExtensionProviderTargetValue(Node\Expr $value, NodeAbstract $errorNode): string private function parseMethodsForTargetValue(
Node\Expr $value,
NodeAbstract $errorNode,
Node\Stmt\Class_ $class,
): string
{ {
if ($value instanceof Node\Scalar\String_ && $value->value === '*') { if ($value instanceof Node\Scalar\String_ && $value->value === '*') {
return '*'; return '*';
@ -889,7 +949,12 @@ class Preprocessor extends CompilerBase
return $targets[$constant]; return $targets[$constant];
} }
} }
$this->fatalError($errorNode, "ExtensionProvider target must be '*', Type::*, or ClassName::class"); $this->fatalCompileTimeAttribute(
$class,
'MethodsFor',
"MethodsFor target must be '*', Type::*, or ClassName::class",
$errorNode,
);
} }
protected function buildLiteralArrayInitPlan(Node\Expr\Array_ $defaultNode): ArrayInitPlan protected function buildLiteralArrayInitPlan(Node\Expr\Array_ $defaultNode): ArrayInitPlan
@ -1316,7 +1381,20 @@ class Preprocessor extends CompilerBase
if (!$abstract) { if (!$abstract) {
$this->methodDef = new MethodDef($flags, $name); $this->methodDef = new MethodDef($flags, $name);
$this->methodDef->node = $v;
if ($this->classDef->hasMethod($name)) { if ($this->classDef->hasMethod($name)) {
$generatedBy = $v->getAttribute(CompileTimeAttributeDiagnostic::GENERATED_BY);
$generatedTarget = $v->getAttribute(CompileTimeAttributeDiagnostic::GENERATED_TARGET);
if (is_string($generatedBy) && $generatedTarget instanceof Node) {
$this->fatalCompileTimeAttribute(
$generatedTarget,
$generatedBy,
"Duplicate method `{$this->method}`",
$generatedTarget,
null,
$this->classDef->getMethod($name)->node,
);
}
$this->fatalError($v, "Duplicate method `{$this->method}`"); $this->fatalError($v, "Duplicate method `{$this->method}`");
} }
$this->prepareFunction($v); $this->prepareFunction($v);
@ -1330,6 +1408,7 @@ class Preprocessor extends CompilerBase
$this->fatalError($v, "Non-abstract class {$this->class} contains abstract method {$v->name}"); $this->fatalError($v, "Non-abstract class {$this->class} contains abstract method {$v->name}");
} }
$this->methodDef = new MethodDef($flags, $name); $this->methodDef = new MethodDef($flags, $name);
$this->methodDef->node = $v;
$this->methodDef->functionDef = $this->parseFunctionDecl($v); $this->methodDef->functionDef = $this->parseFunctionDecl($v);
$this->methodDef->functionDef->method = true; $this->methodDef->functionDef->method = true;
$this->checkRequiredArgNum($name, $this->methodDef, $v); $this->checkRequiredArgNum($name, $this->methodDef, $v);
@ -1449,6 +1528,7 @@ class Preprocessor extends CompilerBase
} }
$this->method = $methodName; $this->method = $methodName;
$methodDef = new MethodDef($this->parseModifiers($stmt->flags), $methodName); $methodDef = new MethodDef($this->parseModifiers($stmt->flags), $methodName);
$methodDef->node = $stmt;
$methodDef->functionDef = $this->parseFunctionDecl($stmt); $methodDef->functionDef = $this->parseFunctionDecl($stmt);
$methodDef->functionDef->method = true; $methodDef->functionDef->method = true;
$this->interfaceDef->addMethod($methodDef); $this->interfaceDef->addMethod($methodDef);

@ -0,0 +1,80 @@
<?php
/**
* This file is part of TypePHP.
*
* @link https://www.swoole.com/
* @contact service@swoole.com
*/
namespace TypePhp\Transform;
use PhpParser\Modifiers;
use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\Stmt;
use TypePhp\Diagnostics\CompileTimeAttributeDiagnostic;
final class ArrayableLowering
{
public const GENERATED_ATTRIBUTE = 'typephpArrayableGenerated';
public const FIELDS_ATTRIBUTE = 'typephpArrayableFields';
public static function lowerClass(Stmt\Class_ $class): void
{
$attribute = CompileTimeAttribute::find($class, 'Arrayable');
if ($attribute === null) {
return;
}
$fields = ClassFieldSelection::parse($attribute, 'Arrayable');
CompileTimeAttribute::remove($class, 'Arrayable');
self::appendGeneratedMethod(
$class,
$fields ?? ClassFieldSelection::ownPublicProperties($class),
$fields,
);
}
/**
* @param list<string> $properties
* @param list<string>|null $fields
*/
public static function rebuildGeneratedMethod(Stmt\Class_ $class, array $properties, ?array $fields): void
{
self::removeGeneratedMethod($class);
self::appendGeneratedMethod($class, array_values(array_unique($properties)), $fields);
}
public static function removeGeneratedMethod(Stmt\Class_ $class): void
{
foreach ($class->stmts as $index => $stmt) {
if ($stmt instanceof Stmt\ClassMethod && $stmt->getAttribute(self::GENERATED_ATTRIBUTE)) {
unset($class->stmts[$index]);
}
}
$class->stmts = array_values($class->stmts);
}
/**
* @param list<string> $properties
* @param list<string>|null $fields
*/
private static function appendGeneratedMethod(Stmt\Class_ $class, array $properties, ?array $fields): void
{
$items = [];
foreach ($properties as $property) {
$items[] = new Expr\ArrayItem(
new Expr\PropertyFetch(new Expr\Variable('this'), $property),
new Node\Scalar\String_($property),
);
}
$method = new Stmt\ClassMethod('toArray', [
'flags' => Modifiers::PUBLIC,
'returnType' => new Node\Identifier('array'),
'stmts' => [new Stmt\Return_(new Expr\Array_($items))],
]);
$method->setAttribute(self::GENERATED_ATTRIBUTE, true);
$method->setAttribute(self::FIELDS_ATTRIBUTE, $fields);
CompileTimeAttributeDiagnostic::markGenerated($method, 'Arrayable', $class);
$class->stmts[] = $method;
}
}

@ -0,0 +1,96 @@
<?php
/**
* This file is part of TypePHP.
*
* @link https://www.swoole.com/
* @contact service@swoole.com
*/
namespace TypePhp\Transform;
use PhpParser\Modifiers;
use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\Stmt;
use TypePhp\Exception\SyntaxError;
final class ClassFieldSelection
{
/**
* @return list<string>|null Null selects every public instance property.
*/
public static function parse(Node\Attribute $attribute, string $name): ?array
{
if ($attribute->args === []) {
return null;
}
if (count($attribute->args) !== 1) {
throw new SyntaxError($name . ' accepts only the optional $fields argument');
}
$argument = $attribute->args[0];
if ($argument->name !== null && $argument->name->toString() !== 'fields') {
throw new SyntaxError($name . ' has an unknown argument $' . $argument->name->toString());
}
if ($argument->unpack || !$argument->value instanceof Expr\Array_) {
throw new SyntaxError($name . ' $fields must be an array literal of property names');
}
$fields = [];
foreach ($argument->value->items as $item) {
if ($item === null || $item->unpack || $item->key !== null
|| !$item->value instanceof Node\Scalar\String_) {
throw new SyntaxError($name . ' $fields must be a list of property-name strings');
}
$field = $item->value->value;
if (in_array($field, $fields, true)) {
throw new SyntaxError($name . ' field `' . $field . '` is specified more than once');
}
$fields[] = $field;
}
return $fields;
}
/** @return list<string> */
public static function ownPublicProperties(Stmt\Class_ $class): array
{
$properties = [];
foreach ($class->stmts as $stmt) {
if ($stmt instanceof Stmt\Property && $stmt->isPublic() && !$stmt->isStatic()) {
foreach ($stmt->props as $property) {
$properties[] = $property->name->toString();
}
}
if ($stmt instanceof Stmt\ClassMethod && $stmt->name->toLowerString() === '__construct') {
foreach ($stmt->params as $param) {
if ($param->isPromoted() && ($param->flags & Modifiers::PUBLIC) && is_string($param->var->name)) {
$properties[] = $param->var->name;
}
}
}
}
return array_values(array_unique($properties));
}
/**
* @param list<string>|null $selected
* @param list<string> $available
* @return list<string>
*/
public static function resolve(?array $selected, array $available, string $name): array
{
$available = array_values(array_unique($available));
if ($selected === null) {
return $available;
}
foreach ($selected as $field) {
if (!in_array($field, $available, true)) {
throw new SyntaxError(
$name . ' field `' . $field . '` must be a declared instance property accessible from the class'
);
}
}
return $selected;
}
}

@ -9,10 +9,84 @@
namespace TypePhp\Transform; namespace TypePhp\Transform;
use PhpParser\Node; use PhpParser\Node;
use TypePhp\Exception\CompileTimeAttributeError;
use TypePhp\Exception\SyntaxError; use TypePhp\Exception\SyntaxError;
final class CompileTimeAttribute final class CompileTimeAttribute
{ {
public static function validateNode(Node $node): void
{
if (!property_exists($node, 'attrGroups')) {
return;
}
$found = [];
foreach ($node->attrGroups as $group) {
foreach ($group->attrs as $attribute) {
$definition = CompileTimeAttributeRegistry::get(self::resolvedName($attribute));
if ($definition === null) {
continue;
}
$key = strtolower($definition['name']);
$found[$key][] = $attribute;
if (!self::matchesTarget($node, $definition['targets'])) {
throw new CompileTimeAttributeError(
$definition['target_error'],
$node,
$definition['name'],
$attribute,
);
}
self::validateArguments($attribute, $definition['name']);
}
}
foreach ($found as $attributes) {
$definition = CompileTimeAttributeRegistry::get(self::resolvedName($attributes[0]));
if (!$definition['repeatable'] && count($attributes) > 1) {
throw new CompileTimeAttributeError(
$definition['name'] . ' cannot be repeated on the same declaration',
$node,
$definition['name'],
$attributes[0],
$definition['name'],
$attributes[1],
);
}
foreach ($definition['conflicts'] as $conflict) {
if (isset($found[strtolower($conflict)])) {
$target = $definition['targets'] === [
CompileTimeAttributeRegistry::TARGET_FUNCTION,
CompileTimeAttributeRegistry::TARGET_METHOD,
] ? 'function or method' : 'declaration';
throw new CompileTimeAttributeError(
$definition['name'] . ' and ' . $conflict . ' cannot be applied to the same ' . $target,
$node,
$definition['name'],
$attributes[0],
$conflict,
$found[strtolower($conflict)][0],
);
}
}
}
}
public static function find(Node $node, string $name): ?Node\Attribute
{
if (!property_exists($node, 'attrGroups')) {
return null;
}
foreach ($node->attrGroups as $group) {
foreach ($group->attrs as $attribute) {
if (self::is($attribute, $name)) {
return $attribute;
}
}
}
return null;
}
public static function has(Node $node, string $name): bool public static function has(Node $node, string $name): bool
{ {
if (!property_exists($node, 'attrGroups')) { if (!property_exists($node, 'attrGroups')) {
@ -50,19 +124,80 @@ final class CompileTimeAttribute
return $found; return $found;
} }
public static function remove(Node $node, string $name): bool
{
$found = false;
foreach ($node->attrGroups as $groupIndex => $group) {
foreach ($group->attrs as $attributeIndex => $attribute) {
if (self::is($attribute, $name)) {
$found = true;
unset($group->attrs[$attributeIndex]);
}
}
$group->attrs = array_values($group->attrs);
if ($group->attrs === []) {
unset($node->attrGroups[$groupIndex]);
}
}
$node->attrGroups = array_values($node->attrGroups);
return $found;
}
public static function is(Node\Attribute $attribute, string $name): bool public static function is(Node\Attribute $attribute, string $name): bool
{
return strcasecmp(self::resolvedName($attribute), ltrim($name, '\\')) === 0;
}
public static function resolvedName(Node\Attribute $attribute): string
{ {
$resolvedName = $attribute->name->getAttribute('resolvedName') $resolvedName = $attribute->name->getAttribute('resolvedName')
?? $attribute->name->getAttribute('namespacedName') ?? $attribute->name->getAttribute('namespacedName')
?? $attribute->name; ?? $attribute->name;
return ltrim($resolvedName->toString(), '\\');
return strcasecmp(ltrim($resolvedName->toString(), '\\'), $name) === 0;
} }
private static function validateArguments(Node\Attribute $attribute, string $name): void private static function validateArguments(Node\Attribute $attribute, string $name): void
{ {
if ($attribute->args !== []) { $definition = CompileTimeAttributeRegistry::get($name);
if ($definition !== null
&& $definition['argument_parser'] === CompileTimeAttributeRegistry::ARGUMENTS_NONE
&& $attribute->args !== []) {
throw new SyntaxError($name . ' does not accept arguments'); throw new SyntaxError($name . ' does not accept arguments');
} }
} }
/** @param list<string> $targets */
private static function matchesTarget(Node $node, array $targets): bool
{
if (in_array(CompileTimeAttributeRegistry::TARGET_CLASS, $targets, true)
&& $node instanceof Node\Stmt\Class_) {
return true;
}
if (in_array(CompileTimeAttributeRegistry::TARGET_NAMED_CLASS, $targets, true)
&& $node instanceof Node\Stmt\Class_ && $node->name !== null) {
return true;
}
if (in_array(CompileTimeAttributeRegistry::TARGET_CLASS_LIKE, $targets, true)
&& $node instanceof Node\Stmt\ClassLike) {
return true;
}
if (in_array(CompileTimeAttributeRegistry::TARGET_FUNCTION, $targets, true)
&& $node instanceof Node\Stmt\Function_) {
return true;
}
if (in_array(CompileTimeAttributeRegistry::TARGET_METHOD, $targets, true)
&& $node instanceof Node\Stmt\ClassMethod) {
return true;
}
if (in_array(CompileTimeAttributeRegistry::TARGET_PROPERTY, $targets, true)
&& ($node instanceof Node\Stmt\Property || ($node instanceof Node\Param && $node->isPromoted()))) {
return true;
}
if (in_array(CompileTimeAttributeRegistry::TARGET_DECLARED_PROPERTY, $targets, true)
&& $node instanceof Node\Stmt\Property) {
return true;
}
return in_array(CompileTimeAttributeRegistry::TARGET_PARAMETER, $targets, true)
&& $node instanceof Node\Param;
}
} }

@ -0,0 +1,123 @@
<?php
/**
* This file is part of TypePHP.
*
* @link https://www.swoole.com/
* @contact service@swoole.com
*/
namespace TypePhp\Transform;
final class CompileTimeAttributeRegistry
{
public const TARGET_CLASS = 'class';
public const TARGET_NAMED_CLASS = 'named_class';
public const TARGET_CLASS_LIKE = 'class_like';
public const TARGET_FUNCTION = 'function';
public const TARGET_METHOD = 'method';
public const TARGET_PROPERTY = 'property';
public const TARGET_DECLARED_PROPERTY = 'declared_property';
public const TARGET_PARAMETER = 'parameter';
public const ARGUMENTS_NONE = 'none';
public const ARGUMENTS_METHODS_FOR = 'methods_for';
public const ARGUMENTS_FIELDS = 'fields';
public const ARGUMENTS_VALIDATE = 'validate';
public const PHASE_PREPROCESS = 'preprocess';
public const PHASE_ENTER = 'enter';
public const PHASE_FUNCTION_LEAVE = 'function_leave';
public const PHASE_CLASS_LEAVE = 'class_leave';
/**
* @return array<string, array{
* name: string,
* targets: list<string>,
* target_error: string,
* repeatable: bool,
* argument_parser: string,
* conflicts: list<string>,
* phase: string,
* preserve_in_library_stub: bool
* }>
*/
public static function all(): array
{
static $definitions = null;
if ($definitions !== null) {
return $definitions;
}
$definitions = [];
$add = static function (
string $name,
array $targets,
string $targetError,
string $argumentParser,
string $phase,
bool $preserveInLibraryStub = true,
array $conflicts = [],
bool $repeatable = false,
) use (&$definitions): void {
$definitions[strtolower($name)] = [
'name' => $name,
'targets' => $targets,
'target_error' => $targetError,
'repeatable' => $repeatable,
'argument_parser' => $argumentParser,
'conflicts' => $conflicts,
'phase' => $phase,
'preserve_in_library_stub' => $preserveInLibraryStub,
];
};
$add('MethodsFor', [self::TARGET_NAMED_CLASS], 'MethodsFor can only be applied to classes', self::ARGUMENTS_METHODS_FOR, self::PHASE_PREPROCESS);
$add('NoExport', [self::TARGET_CLASS_LIKE, self::TARGET_FUNCTION, self::TARGET_METHOD], 'NoExport can only be applied to classes, functions, or methods', self::ARGUMENTS_NONE, self::PHASE_PREPROCESS, false);
foreach (['Getter', 'Setter', 'With'] as $name) {
$add($name, [self::TARGET_PROPERTY], $name . ' can only be applied to instance properties', self::ARGUMENTS_NONE, self::PHASE_CLASS_LEAVE);
}
foreach (['Printer', 'Arrayable'] as $name) {
$add($name, [self::TARGET_NAMED_CLASS], $name . ' can only be applied to named classes', self::ARGUMENTS_FIELDS, self::PHASE_CLASS_LEAVE);
}
foreach (['NotNull', 'NotEmpty'] as $name) {
$add($name, [self::TARGET_PARAMETER], $name . ' can only be applied to function or method parameters', self::ARGUMENTS_NONE, self::PHASE_FUNCTION_LEAVE);
}
$add('Validate', [self::TARGET_PARAMETER], 'Validate can only be applied to function or method parameters', self::ARGUMENTS_VALIDATE, self::PHASE_FUNCTION_LEAVE);
$add('Override', [self::TARGET_METHOD], 'Override can only be applied to methods', self::ARGUMENTS_NONE, self::PHASE_ENTER);
$add('MustUse', [self::TARGET_FUNCTION, self::TARGET_METHOD], 'MustUse can only be applied to functions or methods', self::ARGUMENTS_NONE, self::PHASE_ENTER);
$add('Hot', [self::TARGET_FUNCTION, self::TARGET_METHOD], 'Hot can only be applied to functions or methods', self::ARGUMENTS_NONE, self::PHASE_ENTER, true, ['Cold']);
$add('Cold', [self::TARGET_FUNCTION, self::TARGET_METHOD], 'Cold can only be applied to functions or methods', self::ARGUMENTS_NONE, self::PHASE_ENTER, true, ['Hot']);
$add('Constructor', [self::TARGET_DECLARED_PROPERTY], 'Constructor can only be applied to instance properties', self::ARGUMENTS_NONE, self::PHASE_CLASS_LEAVE);
return $definitions;
}
public static function get(string $name): ?array
{
return self::all()[strtolower(ltrim($name, '\\'))] ?? null;
}
/** @return list<string> */
public static function names(bool $preservedInLibraryStubOnly = false): array
{
$names = [];
foreach (self::all() as $definition) {
if (!$preservedInLibraryStubOnly || $definition['preserve_in_library_stub']) {
$names[] = $definition['name'];
}
}
return $names;
}
/** @return list<string> */
public static function namesForPhase(string $phase): array
{
$names = [];
foreach (self::all() as $definition) {
if ($definition['phase'] === $phase) {
$names[] = $definition['name'];
}
}
return $names;
}
}

@ -0,0 +1,102 @@
<?php
/**
* This file is part of TypePHP.
*
* @link https://www.swoole.com/
* @contact service@swoole.com
*/
namespace TypePhp\Transform;
use PhpParser\Modifiers;
use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\Param;
use PhpParser\Node\Stmt;
use TypePhp\Exception\SyntaxError;
use TypePhp\Exception\CompileTimeAttributeError;
use TypePhp\Diagnostics\CompileTimeAttributeDiagnostic;
final class ConstructorLowering
{
public const GENERATED_ATTRIBUTE = 'typephpConstructorGenerated';
public static function validateTarget(Node $node): void
{
if (!CompileTimeAttribute::has($node, 'Constructor')) {
return;
}
if (!$node instanceof Stmt\Property || $node->isStatic()) {
throw new SyntaxError('Constructor can only be applied to instance properties');
}
}
public static function lowerClassLike(Stmt\Class_|Stmt\Trait_|Stmt\Enum_ $class): void
{
$properties = [];
$target = null;
foreach ($class->stmts as $stmt) {
if (!$stmt instanceof Stmt\Property || !CompileTimeAttribute::has($stmt, 'Constructor')) {
continue;
}
if (!$class instanceof Stmt\Class_) {
throw new SyntaxError('Constructor properties can only be declared in classes');
}
$attribute = CompileTimeAttribute::find($stmt, 'Constructor');
CompileTimeAttribute::consume($stmt, 'Constructor');
$target ??= $stmt;
foreach ($stmt->props as $property) {
$properties[] = [
$property->name->toString(),
$stmt->type,
$property->default,
$stmt->getAttributes(),
$stmt,
$attribute,
];
}
}
if ($properties === []) {
return;
}
$params = [];
$stmts = [];
$optionalSeen = false;
$optionalTarget = null;
$optionalAttribute = null;
foreach ($properties as [$name, $type, $default, $attributes, $propertyTarget, $attributeSource]) {
if ($default !== null) {
$optionalSeen = true;
$optionalTarget ??= $propertyTarget;
$optionalAttribute ??= $attributeSource;
} elseif ($optionalSeen) {
throw new CompileTimeAttributeError(
'Constructor required properties cannot follow properties with default values',
$propertyTarget,
'Constructor',
$attributeSource,
'Constructor',
$optionalAttribute ?? $optionalTarget,
);
}
$params[] = new Param(
new Expr\Variable($name),
default: $default === null ? null : clone $default,
type: $type === null ? null : clone $type,
attributes: $attributes,
);
$stmts[] = new Stmt\Expression(new Expr\Assign(
new Expr\PropertyFetch(new Expr\Variable('this'), $name),
new Expr\Variable($name),
));
}
$constructor = new Stmt\ClassMethod('__construct', [
'flags' => Modifiers::PUBLIC,
'params' => $params,
'stmts' => $stmts,
]);
$constructor->setAttribute(self::GENERATED_ATTRIBUTE, true);
CompileTimeAttributeDiagnostic::markGenerated($constructor, 'Constructor', $target ?? $class);
$class->stmts[] = $constructor;
}
}

@ -0,0 +1,35 @@
<?php
/**
* This file is part of TypePHP.
*
* @link https://www.swoole.com/
* @contact service@swoole.com
*/
namespace TypePhp\Transform;
use PhpParser\Node;
use PhpParser\Node\Stmt;
use TypePhp\Exception\SyntaxError;
final class FunctionAttributeLowering
{
public const MUST_USE_ATTRIBUTE = 'typephpMustUse';
public const OVERRIDE_ATTRIBUTE = 'typephpOverride';
public const HOT_ATTRIBUTE = 'typephpHot';
public const COLD_ATTRIBUTE = 'typephpCold';
public static function lower(Node $node): void
{
foreach (CompileTimeAttributeRegistry::namesForPhase(CompileTimeAttributeRegistry::PHASE_ENTER) as $name) {
if (!CompileTimeAttribute::has($node, $name)) {
continue;
}
if (!$node instanceof Stmt\Function_ && !$node instanceof Stmt\ClassMethod) {
throw new SyntaxError($name . ' can only be applied to functions or methods');
}
CompileTimeAttribute::consume($node, $name);
$node->setAttribute('typephp' . $name, true);
}
}
}

@ -14,6 +14,7 @@ use PhpParser\Node\Expr;
use PhpParser\Node\Param; use PhpParser\Node\Param;
use PhpParser\Node\Stmt; use PhpParser\Node\Stmt;
use TypePhp\Exception\SyntaxError; use TypePhp\Exception\SyntaxError;
use TypePhp\Diagnostics\CompileTimeAttributeDiagnostic;
final class GetterLowering final class GetterLowering
{ {
@ -40,6 +41,9 @@ final class GetterLowering
/** @return list<Stmt\ClassMethod> */ /** @return list<Stmt\ClassMethod> */
public static function lowerProperty(Stmt\Property $property): array public static function lowerProperty(Stmt\Property $property): array
{ {
if (CompileTimeAttribute::has($property, 'Getter') && $property->hooks !== []) {
throw new SyntaxError('Getter cannot be applied to properties with hooks');
}
if (!CompileTimeAttribute::consume($property, 'Getter')) { if (!CompileTimeAttribute::consume($property, 'Getter')) {
return []; return [];
} }
@ -49,7 +53,7 @@ final class GetterLowering
$methods[] = self::createGetter( $methods[] = self::createGetter(
$prop->name->toString(), $prop->name->toString(),
$property->type, $property->type,
$property->getAttributes(), $property,
); );
} }
return $methods; return $methods;
@ -57,23 +61,28 @@ final class GetterLowering
public static function lowerPromotedProperty(Param $param): ?Stmt\ClassMethod public static function lowerPromotedProperty(Param $param): ?Stmt\ClassMethod
{ {
if (CompileTimeAttribute::has($param, 'Getter') && $param->hooks !== []) {
throw new SyntaxError('Getter cannot be applied to properties with hooks');
}
if (!$param->isPromoted() || !is_string($param->var->name) || !CompileTimeAttribute::consume($param, 'Getter')) { if (!$param->isPromoted() || !is_string($param->var->name) || !CompileTimeAttribute::consume($param, 'Getter')) {
return null; return null;
} }
return self::createGetter($param->var->name, $param->type, $param->getAttributes()); return self::createGetter($param->var->name, $param->type, $param);
} }
private static function createGetter(string $property, ?Node $type, array $attributes): Stmt\ClassMethod private static function createGetter(string $property, ?Node $type, Node $target): Stmt\ClassMethod
{ {
return new Stmt\ClassMethod('get' . ucfirst($property), [ $method = new Stmt\ClassMethod('get' . ucfirst($property), [
'flags' => Modifiers::PUBLIC, 'flags' => Modifiers::PUBLIC,
'returnType' => $type === null ? null : clone $type, 'returnType' => $type === null ? null : clone $type,
'stmts' => [new Stmt\Return_(new Expr\PropertyFetch( 'stmts' => [new Stmt\Return_(new Expr\PropertyFetch(
new Expr\Variable('this'), new Expr\Variable('this'),
$property, $property,
))], ))],
], $attributes); ], $target->getAttributes());
CompileTimeAttributeDiagnostic::markGenerated($method, 'Getter', $target);
return $method;
} }
} }

@ -0,0 +1,26 @@
<?php
/**
* This file is part of TypePHP.
*
* @link https://www.swoole.com/
* @contact service@swoole.com
*/
namespace TypePhp\Transform;
use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\Stmt;
final class NotEmptyLowering
{
public static function createCheck(string $name): Stmt\If_
{
return new Stmt\If_(new Expr\Empty_(new Expr\Variable($name)), [
'stmts' => [new Stmt\Expression(new Expr\Throw_(new Expr\New_(
new Node\Name\FullyQualified('ValueError'),
[new Node\Arg(new Node\Scalar\String_('Parameter $' . $name . ' must not be empty'))],
)))],
]);
}
}

@ -10,48 +10,20 @@ namespace TypePhp\Transform;
use PhpParser\Node; use PhpParser\Node;
use PhpParser\Node\Expr; use PhpParser\Node\Expr;
use PhpParser\Node\Param;
use PhpParser\Node\Stmt; use PhpParser\Node\Stmt;
use TypePhp\Exception\SyntaxError;
final class NotNullLowering final class NotNullLowering
{ {
public static function validateTarget(Node $node): void public static function createCheck(string $name): Stmt\If_
{ {
if (CompileTimeAttribute::has($node, 'NotNull') && !$node instanceof Param) { return new Stmt\If_(new Expr\BinaryOp\Identical(
throw new SyntaxError('NotNull can only be applied to function or method parameters'); new Expr\Variable($name),
} new Expr\ConstFetch(new Node\Name('null')),
} ), [
'stmts' => [new Stmt\Expression(new Expr\Throw_(new Expr\New_(
public static function lowerFunction(Stmt\Function_|Stmt\ClassMethod|Expr\Closure $function): void new Node\Name\FullyQualified('ValueError'),
{ [new Node\Arg(new Node\Scalar\String_('Parameter $' . $name . ' must not be null'))],
$checks = []; )))],
foreach ($function->params as $param) { ]);
if (!CompileTimeAttribute::consume($param, 'NotNull')) {
continue;
}
if ($function->stmts === null || !is_string($param->var->name)) {
throw new SyntaxError('NotNull requires a concrete function or method parameter');
}
$name = $param->var->name;
$checks[] = new Stmt\If_(new Expr\Empty_(new Expr\Variable($name)), [
'stmts' => [new Stmt\Expression(new Expr\Throw_(new Expr\New_(
new Node\Name\FullyQualified('ValueError'),
[new Node\Arg(new Node\Scalar\String_('Parameter $' . $name . ' must not be empty'))],
)))],
]);
}
if ($checks !== []) {
$function->stmts = [...$checks, ...$function->stmts];
}
}
public static function rejectArrowFunction(Expr\ArrowFunction $function): void
{
foreach ($function->params as $param) {
if (CompileTimeAttribute::has($param, 'NotNull')) {
throw new SyntaxError('NotNull is not supported on arrow function parameters');
}
}
} }
} }

@ -0,0 +1,117 @@
<?php
/**
* This file is part of TypePHP.
*
* @link https://www.swoole.com/
* @contact service@swoole.com
*/
namespace TypePhp\Transform;
use PhpParser\Node\Expr;
use PhpParser\Node\NullableType;
use PhpParser\Node\Param;
use PhpParser\Node\Stmt;
use PhpParser\Node\UnionType;
use TypePhp\Exception\CompileTimeAttributeError;
use TypePhp\Exception\SyntaxError;
final class ParameterValidationLowering
{
public static function lowerFunction(
Stmt\Function_|Stmt\ClassMethod|Expr\Closure $function,
?callable $warning = null,
): void
{
$checks = [];
foreach ($function->params as $param) {
if (!is_string($param->var->name)) {
continue;
}
$name = $param->var->name;
$notNull = CompileTimeAttribute::find($param, 'NotNull');
if ($notNull !== null) {
if ($notNull->args !== []) {
throw new CompileTimeAttributeError(
'NotNull does not accept arguments',
$param,
'NotNull',
$notNull,
);
}
if ($warning !== null && self::isExplicitlyNullable($param)) {
$warning($param, 'NotNull is applied to nullable parameter `$' . $name . '`');
}
$checks[] = NotNullLowering::createCheck($name);
}
$notEmpty = CompileTimeAttribute::find($param, 'NotEmpty');
if ($notEmpty !== null) {
if ($notEmpty->args !== []) {
throw new CompileTimeAttributeError(
'NotEmpty does not accept arguments',
$param,
'NotEmpty',
$notEmpty,
);
}
$checks[] = NotEmptyLowering::createCheck($name);
}
$validate = CompileTimeAttribute::find($param, 'Validate');
if ($validate !== null) {
try {
$checks[] = ValidateLowering::createCheck($param, $validate);
} catch (SyntaxError $error) {
throw new CompileTimeAttributeError(
$error->getMessage(),
$param,
'Validate',
$validate,
previous: $error,
);
}
}
CompileTimeAttribute::remove($param, 'NotNull');
CompileTimeAttribute::remove($param, 'NotEmpty');
CompileTimeAttribute::remove($param, 'Validate');
}
if ($checks !== []) {
if ($function->stmts === null) {
throw new SyntaxError('Parameter validation requires a concrete function or method');
}
$function->stmts = [...$checks, ...$function->stmts];
}
}
private static function isExplicitlyNullable(Param $param): bool
{
if ($param->type instanceof NullableType) {
return true;
}
if (!$param->type instanceof UnionType) {
return false;
}
foreach ($param->type->types as $type) {
if (strcasecmp($type->toString(), 'null') === 0) {
return true;
}
}
return false;
}
public static function rejectArrowFunction(Expr\ArrowFunction $function): void
{
foreach ($function->params as $param) {
foreach (['NotNull', 'NotEmpty', 'Validate'] as $name) {
$attribute = CompileTimeAttribute::find($param, $name);
if ($attribute !== null) {
throw new CompileTimeAttributeError(
$name . ' is not supported on arrow function parameters',
$param,
$name,
$attribute,
);
}
}
}
}
}

@ -12,41 +12,47 @@ use PhpParser\Modifiers;
use PhpParser\Node; use PhpParser\Node;
use PhpParser\Node\Expr; use PhpParser\Node\Expr;
use PhpParser\Node\Stmt; use PhpParser\Node\Stmt;
use TypePhp\Exception\SyntaxError; use TypePhp\Diagnostics\CompileTimeAttributeDiagnostic;
final class PrinterLowering final class PrinterLowering
{ {
public const GENERATED_ATTRIBUTE = 'typephpPrinterGenerated'; public const GENERATED_ATTRIBUTE = 'typephpPrinterGenerated';
public const FIELDS_ATTRIBUTE = 'typephpPrinterFields';
public static function validateTarget(Node $node): void public static function lowerClass(Stmt\Class_ $class): void
{ {
if (!CompileTimeAttribute::has($node, 'Printer')) { $attribute = CompileTimeAttribute::find($class, 'Printer');
if ($attribute === null) {
return; return;
} }
if (!$node instanceof Stmt\Class_ || $node->name === null) { $fields = ClassFieldSelection::parse($attribute, 'Printer');
throw new SyntaxError('Printer can only be applied to named classes'); CompileTimeAttribute::remove($class, 'Printer');
} self::appendGeneratedMethod(
$class,
$fields ?? ClassFieldSelection::ownPublicProperties($class),
$fields,
self::ownStringProperties($class),
);
} }
public static function lowerClass(Stmt\Class_ $class, bool $generate = true): void /**
{ * @param list<string> $properties
if (!CompileTimeAttribute::consume($class, 'Printer') || !$generate) { * @param list<string>|null $fields
return; */
} public static function rebuildGeneratedMethod(
foreach ($class->getMethods() as $method) { Stmt\Class_ $class,
if ($method->name->toLowerString() === 'tostring') { array $properties,
return; ?array $fields,
} array $stringProperties = [],
} ): void
self::appendGeneratedMethod($class, self::ownPublicProperties($class));
}
/** @param list<string> $properties */
public static function rebuildGeneratedMethod(Stmt\Class_ $class, array $properties): void
{ {
self::removeGeneratedMethod($class); self::removeGeneratedMethod($class);
self::appendGeneratedMethod($class, array_values(array_unique($properties))); self::appendGeneratedMethod(
$class,
array_values(array_unique($properties)),
$fields,
$stringProperties,
);
} }
public static function removeGeneratedMethod(Stmt\Class_ $class): void public static function removeGeneratedMethod(Stmt\Class_ $class): void
@ -59,19 +65,55 @@ final class PrinterLowering
$class->stmts = array_values($class->stmts); $class->stmts = array_values($class->stmts);
} }
/**
* @param list<string> $properties
* @param list<string>|null $fields
* @param list<string> $stringProperties
*/
private static function appendGeneratedMethod(
Stmt\Class_ $class,
array $properties,
?array $fields,
array $stringProperties,
): void
{
$expression = new Node\Scalar\String_($class->name->toString() . '(');
foreach ($properties as $index => $property) {
$prefix = ($index === 0 ? '' : ', ') . $property . '=';
$value = new Expr\PropertyFetch(new Expr\Variable('this'), $property);
if (!in_array($property, $stringProperties, true)) {
$value = new Expr\MethodCall($value, new Node\Identifier('toString'));
}
$expression = new Expr\BinaryOp\Concat(
new Expr\BinaryOp\Concat($expression, new Node\Scalar\String_($prefix)),
$value,
);
}
$expression = new Expr\BinaryOp\Concat($expression, new Node\Scalar\String_(')'));
$method = new Stmt\ClassMethod('__toString', [
'flags' => Modifiers::PUBLIC,
'returnType' => new Node\Identifier('string'),
'stmts' => [new Stmt\Return_($expression)],
]);
$method->setAttribute(self::GENERATED_ATTRIBUTE, true);
$method->setAttribute(self::FIELDS_ATTRIBUTE, $fields);
CompileTimeAttributeDiagnostic::markGenerated($method, 'Printer', $class);
$class->stmts[] = $method;
}
/** @return list<string> */ /** @return list<string> */
public static function ownPublicProperties(Stmt\Class_ $class): array private static function ownStringProperties(Stmt\Class_ $class): array
{ {
$properties = []; $properties = [];
foreach ($class->stmts as $stmt) { foreach ($class->stmts as $stmt) {
if ($stmt instanceof Stmt\Property && $stmt->isPublic() && !$stmt->isStatic()) { if ($stmt instanceof Stmt\Property && self::isStringType($stmt->type)) {
foreach ($stmt->props as $property) { foreach ($stmt->props as $property) {
$properties[] = $property->name->toString(); $properties[] = $property->name->toString();
} }
} }
if ($stmt instanceof Stmt\ClassMethod && $stmt->name->toLowerString() === '__construct') { if ($stmt instanceof Stmt\ClassMethod && $stmt->name->toLowerString() === '__construct') {
foreach ($stmt->params as $param) { foreach ($stmt->params as $param) {
if ($param->isPromoted() && ($param->flags & Modifiers::PUBLIC) && is_string($param->var->name)) { if ($param->isPromoted() && is_string($param->var->name) && self::isStringType($param->type)) {
$properties[] = $param->var->name; $properties[] = $param->var->name;
} }
} }
@ -80,24 +122,8 @@ final class PrinterLowering
return $properties; return $properties;
} }
/** @param list<string> $properties */ private static function isStringType(?Node $type): bool
private static function appendGeneratedMethod(Stmt\Class_ $class, array $properties): void
{ {
$expression = new Node\Scalar\String_($class->name->toString() . '('); return $type instanceof Node\Identifier && strtolower($type->name) === 'string';
foreach ($properties as $index => $property) {
$prefix = ($index === 0 ? '' : ', ') . $property . '=';
$expression = new Expr\BinaryOp\Concat(
new Expr\BinaryOp\Concat($expression, new Node\Scalar\String_($prefix)),
new Expr\PropertyFetch(new Expr\Variable('this'), $property),
);
}
$expression = new Expr\BinaryOp\Concat($expression, new Node\Scalar\String_(')'));
$method = new Stmt\ClassMethod('toString', [
'flags' => Modifiers::PUBLIC,
'returnType' => new Node\Identifier('string'),
'stmts' => [new Stmt\Return_($expression)],
]);
$method->setAttribute(self::GENERATED_ATTRIBUTE, true);
$class->stmts[] = $method;
} }
} }

@ -14,6 +14,7 @@ use PhpParser\Node\Expr;
use PhpParser\Node\Param; use PhpParser\Node\Param;
use PhpParser\Node\Stmt; use PhpParser\Node\Stmt;
use TypePhp\Exception\SyntaxError; use TypePhp\Exception\SyntaxError;
use TypePhp\Diagnostics\CompileTimeAttributeDiagnostic;
final class PropertyMethodLowering final class PropertyMethodLowering
{ {
@ -36,8 +37,9 @@ final class PropertyMethodLowering
} }
/** @return list<Stmt\ClassMethod> */ /** @return list<Stmt\ClassMethod> */
public static function lowerProperty(Stmt\Property $property): array public static function lowerProperty(Stmt\Property $property, bool $classReadonly = false): array
{ {
self::validatePropertySemantics($property, $property->hooks !== [], $property->isReadonly() || $classReadonly);
$setter = CompileTimeAttribute::consume($property, 'Setter'); $setter = CompileTimeAttribute::consume($property, 'Setter');
$with = CompileTimeAttribute::consume($property, 'With'); $with = CompileTimeAttribute::consume($property, 'With');
if (!$setter && !$with) { if (!$setter && !$with) {
@ -48,21 +50,22 @@ final class PropertyMethodLowering
foreach ($property->props as $prop) { foreach ($property->props as $prop) {
$name = $prop->name->toString(); $name = $prop->name->toString();
if ($setter) { if ($setter) {
$methods[] = self::createSetter($name, $property->type, $property->getAttributes()); $methods[] = self::createSetter($name, $property->type, $property);
} }
if ($with) { if ($with) {
$methods[] = self::createWith($name, $property->type, $property->getAttributes()); $methods[] = self::createWith($name, $property->type, $property);
} }
} }
return $methods; return $methods;
} }
/** @return list<Stmt\ClassMethod> */ /** @return list<Stmt\ClassMethod> */
public static function lowerPromotedProperty(Param $param): array public static function lowerPromotedProperty(Param $param, bool $classReadonly = false): array
{ {
if (!$param->isPromoted() || !is_string($param->var->name)) { if (!$param->isPromoted() || !is_string($param->var->name)) {
return []; return [];
} }
self::validatePropertySemantics($param, $param->hooks !== [], $param->isReadonly() || $classReadonly);
$setter = CompileTimeAttribute::consume($param, 'Setter'); $setter = CompileTimeAttribute::consume($param, 'Setter');
$with = CompileTimeAttribute::consume($param, 'With'); $with = CompileTimeAttribute::consume($param, 'With');
if (!$setter && !$with) { if (!$setter && !$with) {
@ -71,17 +74,32 @@ final class PropertyMethodLowering
$methods = []; $methods = [];
if ($setter) { if ($setter) {
$methods[] = self::createSetter($param->var->name, $param->type, $param->getAttributes()); $methods[] = self::createSetter($param->var->name, $param->type, $param);
} }
if ($with) { if ($with) {
$methods[] = self::createWith($param->var->name, $param->type, $param->getAttributes()); $methods[] = self::createWith($param->var->name, $param->type, $param);
} }
return $methods; return $methods;
} }
private static function createSetter(string $property, ?Node $type, array $attributes): Stmt\ClassMethod private static function validatePropertySemantics(Node $node, bool $hasHooks, bool $readonly): void
{ {
return new Stmt\ClassMethod('set' . ucfirst($property), [ foreach (self::ATTRIBUTES as $attribute) {
if (!CompileTimeAttribute::has($node, $attribute)) {
continue;
}
if ($hasHooks) {
throw new SyntaxError($attribute . ' cannot be applied to properties with hooks');
}
if ($readonly) {
throw new SyntaxError($attribute . ' cannot be applied to readonly properties');
}
}
}
private static function createSetter(string $property, ?Node $type, Node $target): Stmt\ClassMethod
{
$method = new Stmt\ClassMethod('set' . ucfirst($property), [
'flags' => Modifiers::PUBLIC, 'flags' => Modifiers::PUBLIC,
'params' => [new Param(new Expr\Variable($property), type: $type === null ? null : clone $type)], 'params' => [new Param(new Expr\Variable($property), type: $type === null ? null : clone $type)],
'returnType' => new Node\Identifier('void'), 'returnType' => new Node\Identifier('void'),
@ -89,12 +107,14 @@ final class PropertyMethodLowering
new Expr\PropertyFetch(new Expr\Variable('this'), $property), new Expr\PropertyFetch(new Expr\Variable('this'), $property),
new Expr\Variable($property), new Expr\Variable($property),
))], ))],
], $attributes); ], $target->getAttributes());
CompileTimeAttributeDiagnostic::markGenerated($method, 'Setter', $target);
return $method;
} }
private static function createWith(string $property, ?Node $type, array $attributes): Stmt\ClassMethod private static function createWith(string $property, ?Node $type, Node $target): Stmt\ClassMethod
{ {
return new Stmt\ClassMethod('with' . ucfirst($property), [ $method = new Stmt\ClassMethod('with' . ucfirst($property), [
'flags' => Modifiers::PUBLIC, 'flags' => Modifiers::PUBLIC,
'params' => [new Param(new Expr\Variable($property), type: $type === null ? null : clone $type)], 'params' => [new Param(new Expr\Variable($property), type: $type === null ? null : clone $type)],
'returnType' => new Node\Name('static'), 'returnType' => new Node\Name('static'),
@ -109,6 +129,8 @@ final class PropertyMethodLowering
)), )),
new Stmt\Return_(new Expr\Variable('clone')), new Stmt\Return_(new Expr\Variable('clone')),
], ],
], $attributes); ], $target->getAttributes());
CompileTimeAttributeDiagnostic::markGenerated($method, 'With', $target);
return $method;
} }
} }

@ -0,0 +1,219 @@
<?php
/**
* This file is part of TypePHP.
*
* @link https://www.swoole.com/
* @contact service@swoole.com
*/
namespace TypePhp\Transform;
use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\IntersectionType;
use PhpParser\Node\NullableType;
use PhpParser\Node\Param;
use PhpParser\Node\Stmt;
use PhpParser\Node\UnionType;
use TypePhp\Exception\SyntaxError;
final class ValidateLowering
{
public static function createCheck(Param $param, Node\Attribute $attribute): Stmt\If_
{
$parameter = is_string($param->var->name) ? $param->var->name : '';
[$filter, $options, $message] = self::parseArguments($parameter, $attribute);
self::assertParameterTypeCompatible($param, $filter, $options);
$call = new Expr\FuncCall(new Node\Name\FullyQualified('filter_var'), [
new Node\Arg(new Expr\Variable($parameter)),
new Node\Arg(new Node\Scalar\Int_($filter)),
new Node\Arg(self::withNullOnFailure($options)),
]);
return new Stmt\If_(new Expr\BinaryOp\Identical($call, new Expr\ConstFetch(new Node\Name('null'))), [
'stmts' => [new Stmt\Expression(new Expr\Throw_(new Expr\New_(
new Node\Name\FullyQualified('ValueError'),
[new Node\Arg($message)],
)))],
]);
}
/** @return array{int, Expr, Expr} */
private static function parseArguments(string $parameter, Node\Attribute $attribute): array
{
$values = [];
$positions = ['filter', 'options', 'message'];
foreach ($attribute->args as $index => $arg) {
$name = $arg->name?->toString() ?? ($positions[$index] ?? null);
if ($name === null || !in_array($name, $positions, true)) {
throw new SyntaxError('Validate has an unknown argument');
}
if (isset($values[$name])) {
throw new SyntaxError('Validate argument $' . $name . ' is specified more than once');
}
$values[$name] = $arg->value;
}
if (!isset($values['filter'])) {
throw new SyntaxError('Validate requires the $filter argument');
}
$filter = self::resolveFilter($values['filter']);
if (!in_array($filter, self::validationFilters(), true)) {
throw new SyntaxError('Validate only accepts FILTER_VALIDATE_* filters');
}
$options = isset($values['options']) ? clone $values['options'] : new Node\Scalar\Int_(0);
if (!$options instanceof Node\Scalar\Int_ && !$options instanceof Expr\Array_
&& !$options instanceof Expr\ConstFetch
&& !$options instanceof Expr\BinaryOp\BitwiseOr) {
throw new SyntaxError('Validate $options must be an integer flag or an array literal');
}
$defaultMessage = new Node\Scalar\String_('Parameter $' . $parameter . ' is invalid');
$message = isset($values['message']) ? clone $values['message'] : $defaultMessage;
if ($message instanceof Expr\ConstFetch && $message->name->toLowerString() === 'null') {
$message = $defaultMessage;
}
if (!$message instanceof Node\Scalar\String_ && !$message instanceof Expr\ConstFetch
&& !$message instanceof Expr\ClassConstFetch) {
throw new SyntaxError('Validate $message must be a string or null');
}
return [$filter, $options, $message];
}
private static function assertParameterTypeCompatible(Param $param, int $filter, Expr $options): void
{
if ($param->type === null || self::typeMayPassFilter($param->type, $filter, self::resolveFlags($options))) {
return;
}
$name = is_string($param->var->name) ? $param->var->name : '';
throw new SyntaxError(
'Validate filter ' . self::filterName($filter) . ' is incompatible with parameter `$' .
$name . '` declared as `' . $param->type->toString() . '`',
);
}
private static function typeMayPassFilter(Node $type, int $filter, ?int $flags): bool
{
if ($type instanceof NullableType) {
return self::typeMayPassFilter($type->type, $filter, $flags);
}
if ($type instanceof UnionType || $type instanceof IntersectionType) {
foreach ($type->types as $member) {
if (self::typeMayPassFilter($member, $filter, $flags)) {
return true;
}
}
return false;
}
if (!$type instanceof Node\Identifier) {
// Named object types may implement __toString(); without full class
// resolution they are not provably incompatible with filter_var().
return true;
}
$name = strtolower($type->name);
if ($name === 'array') {
return $flags === null
|| (bool) ($flags & (FILTER_REQUIRE_ARRAY | FILTER_FORCE_ARRAY));
}
if (!in_array($name, ['int', 'float', 'bool', 'true', 'false', 'null'], true)) {
return true;
}
return !in_array($filter, self::stringShapeFilters(), true);
}
/** @return list<int> */
private static function stringShapeFilters(): array
{
return array_values(array_filter([
defined('FILTER_VALIDATE_EMAIL') ? FILTER_VALIDATE_EMAIL : null,
defined('FILTER_VALIDATE_URL') ? FILTER_VALIDATE_URL : null,
defined('FILTER_VALIDATE_IP') ? FILTER_VALIDATE_IP : null,
defined('FILTER_VALIDATE_MAC') ? FILTER_VALIDATE_MAC : null,
], static fn ($value): bool => is_int($value)));
}
private static function resolveFlags(Expr $options): ?int
{
if ($options instanceof Node\Scalar\Int_) {
return $options->value;
}
if ($options instanceof Expr\ConstFetch) {
$name = ltrim($options->name->toString(), '\\');
return defined($name) && is_int(constant($name)) ? constant($name) : null;
}
if ($options instanceof Expr\BinaryOp\BitwiseOr) {
$left = self::resolveFlags($options->left);
$right = self::resolveFlags($options->right);
return $left === null || $right === null ? null : $left | $right;
}
if (!$options instanceof Expr\Array_) {
return null;
}
foreach ($options->items as $item) {
if ($item?->key instanceof Node\Scalar\String_ && $item->key->value === 'flags') {
return self::resolveFlags($item->value);
}
}
return 0;
}
private static function filterName(int $filter): string
{
foreach (get_defined_constants(true)['filter'] ?? [] as $name => $value) {
if ($value === $filter && str_starts_with($name, 'FILTER_VALIDATE_')) {
return $name;
}
}
return (string) $filter;
}
private static function resolveFilter(Expr $expr): int
{
if ($expr instanceof Node\Scalar\Int_) {
return $expr->value;
}
if ($expr instanceof Expr\ConstFetch) {
$name = ltrim($expr->name->toString(), '\\');
if (defined($name) && is_int(constant($name))) {
return constant($name);
}
}
throw new SyntaxError('Validate $filter must be a FILTER_VALIDATE_* constant');
}
/** @return list<int> */
private static function validationFilters(): array
{
$filters = [];
foreach (get_defined_constants(true)['filter'] ?? [] as $name => $value) {
if (str_starts_with($name, 'FILTER_VALIDATE_') && is_int($value)) {
$filters[] = $value;
}
}
return array_values(array_unique($filters));
}
private static function withNullOnFailure(Expr $options): Expr
{
$flag = new Node\Scalar\Int_(FILTER_NULL_ON_FAILURE);
if (!$options instanceof Expr\Array_) {
return new Expr\BinaryOp\BitwiseOr($options, $flag);
}
$flagsItem = null;
foreach ($options->items as $item) {
if ($item?->unpack) {
throw new SyntaxError('Validate $options does not support array unpacking');
}
if ($item !== null && $item->key instanceof Node\Scalar\String_ && $item->key->value === 'flags') {
$flagsItem = $item;
}
}
if ($flagsItem !== null) {
$flagsItem->value = new Expr\BinaryOp\BitwiseOr($flagsItem->value, $flag);
return $options;
}
$options->items[] = new Expr\ArrayItem($flag, new Node\Scalar\String_('flags'));
return $options;
}
}

@ -8,32 +8,42 @@
namespace TypePhp\Transform; namespace TypePhp\Transform;
use Closure;
use PhpParser\Node; use PhpParser\Node;
use PhpParser\Node\Stmt; use PhpParser\Node\Stmt;
use PhpParser\NodeVisitorAbstract; use PhpParser\NodeVisitorAbstract;
use TypePhp\Exception\CompileTimeAttributeError;
use TypePhp\Exception\SyntaxError;
use TypePhp\Diagnostics\CompileTimeAttributeDiagnostic;
class Visitor extends NodeVisitorAbstract class Visitor extends NodeVisitorAbstract
{ {
/** @param null|callable(Stmt\Class_): bool $printerPredicate */ /** @param null|Closure(Node, string): void $warning */
public function __construct(private $printerPredicate = null) public function __construct(
{ private readonly ?Closure $warning = null,
private readonly string $sourceFile = '',
) {
} }
public function enterNode(Node $node): null public function enterNode(Node $node): null
{ {
GetterLowering::validateTarget($node); $this->guard($node, static fn () => CompileTimeAttribute::validateNode($node));
PropertyMethodLowering::validateTarget($node); $this->guard($node, static fn () => FunctionAttributeLowering::lower($node));
NotNullLowering::validateTarget($node); $this->guard($node, static fn () => GetterLowering::validateTarget($node), 'Getter');
PrinterLowering::validateTarget($node); $this->guard($node, static fn () => PropertyMethodLowering::validateTarget($node));
$this->guard($node, static fn () => ConstructorLowering::validateTarget($node), 'Constructor');
return null; return null;
} }
public function leaveNode(Node $node): null public function leaveNode(Node $node): null
{ {
if ($node instanceof Stmt\Function_ || $node instanceof Stmt\ClassMethod || $node instanceof Node\Expr\Closure) { if ($node instanceof Stmt\Function_ || $node instanceof Stmt\ClassMethod || $node instanceof Node\Expr\Closure) {
NotNullLowering::lowerFunction($node); $this->guard(
$node,
fn () => ParameterValidationLowering::lowerFunction($node, $this->warning),
);
} elseif ($node instanceof Node\Expr\ArrowFunction) { } elseif ($node instanceof Node\Expr\ArrowFunction) {
NotNullLowering::rejectArrowFunction($node); $this->guard($node, static fn () => ParameterValidationLowering::rejectArrowFunction($node));
} }
if (!$node instanceof Stmt\Class_ && !$node instanceof Stmt\Trait_ && !$node instanceof Stmt\Enum_) { if (!$node instanceof Stmt\Class_ && !$node instanceof Stmt\Trait_ && !$node instanceof Stmt\Enum_) {
@ -41,34 +51,107 @@ class Visitor extends NodeVisitorAbstract
} }
$methods = []; $methods = [];
$classReadonly = $node instanceof Stmt\Class_ && $node->isReadonly();
foreach ($node->stmts as $stmt) { foreach ($node->stmts as $stmt) {
if ($stmt instanceof Stmt\Property) { if ($stmt instanceof Stmt\Property) {
array_push($methods, ...PropertyHookLowering::lowerProperty($stmt)); array_push($methods, ...PropertyHookLowering::lowerProperty($stmt));
array_push($methods, ...GetterLowering::lowerProperty($stmt)); array_push($methods, ...$this->guard(
array_push($methods, ...PropertyMethodLowering::lowerProperty($stmt)); $stmt,
static fn () => GetterLowering::lowerProperty($stmt),
'Getter',
));
array_push($methods, ...$this->guard(
$stmt,
static fn () => PropertyMethodLowering::lowerProperty($stmt, $classReadonly),
));
} elseif ($stmt instanceof Stmt\ClassMethod && $stmt->name->toLowerString() === '__construct') { } elseif ($stmt instanceof Stmt\ClassMethod && $stmt->name->toLowerString() === '__construct') {
foreach ($stmt->params as $param) { foreach ($stmt->params as $param) {
$marker = PropertyHookLowering::lowerPromotedProperty($param); $marker = PropertyHookLowering::lowerPromotedProperty($param);
if ($marker !== null) { if ($marker !== null) {
$methods[] = $marker; $methods[] = $marker;
} }
$getter = GetterLowering::lowerPromotedProperty($param); $getter = $this->guard(
$param,
static fn () => GetterLowering::lowerPromotedProperty($param),
'Getter',
);
if ($getter !== null) { if ($getter !== null) {
$methods[] = $getter; $methods[] = $getter;
} }
array_push($methods, ...PropertyMethodLowering::lowerPromotedProperty($param)); array_push($methods, ...$this->guard(
$param,
static fn () => PropertyMethodLowering::lowerPromotedProperty($param, $classReadonly),
));
} }
} }
} }
if ($methods !== []) { if ($methods !== []) {
array_push($node->stmts, ...$methods); array_push($node->stmts, ...$methods);
} }
$this->guard($node, static fn () => ConstructorLowering::lowerClassLike($node), 'Constructor');
if ($node instanceof Stmt\Class_) { if ($node instanceof Stmt\Class_) {
if (CompileTimeAttribute::has($node, 'Printer')) { if (CompileTimeAttribute::find($node, 'Printer') !== null) {
$generate = $this->printerPredicate === null || ($this->printerPredicate)($node); $this->guard($node, static fn () => PrinterLowering::lowerClass($node), 'Printer');
PrinterLowering::lowerClass($node, $generate); }
if (CompileTimeAttribute::find($node, 'Arrayable') !== null) {
$this->guard($node, static fn () => ArrayableLowering::lowerClass($node), 'Arrayable');
} }
} }
return null; return null;
} }
private function guard(Node $target, Closure $operation, ?string $attribute = null): mixed
{
try {
return $operation();
} catch (SyntaxError $error) {
if (str_contains($error->getMessage(), '[compile-time attribute:')) {
throw $error;
}
$source = $target;
$conflictAttribute = null;
$conflictSource = null;
if ($error instanceof CompileTimeAttributeError) {
$target = $error->target;
$attribute = $error->attribute ?? $attribute;
$source = $error->attributeSource ?? $target;
$conflictAttribute = $error->conflictAttribute;
$conflictSource = $error->conflictSource;
} else {
[$detected, $attributeSource] = $this->detectAttribute($target);
$attribute ??= $detected;
$source = $attributeSource ?? $target;
}
$attribute ??= 'unknown';
$file = $this->sourceFile !== '' ? $this->sourceFile : '<unknown>';
throw new SyntaxError(CompileTimeAttributeDiagnostic::format(
$error->getMessage(),
$attribute,
$target,
$file,
$source,
$conflictAttribute,
$conflictSource,
), 0, $error);
}
}
/** @return array{?string, ?Node} */
private function detectAttribute(Node $node): array
{
if (!property_exists($node, 'attrGroups')) {
return [null, null];
}
foreach ($node->attrGroups as $group) {
foreach ($group->attrs as $attribute) {
$definition = CompileTimeAttributeRegistry::get(CompileTimeAttribute::resolvedName($attribute));
if ($definition !== null) {
return [$definition['name'], $attribute];
}
}
}
return [null, null];
}
} }

@ -19,6 +19,7 @@ use TypePhp\Build\NativeBuilder;
use TypePhp\Build\PrecompiledHeaderManager; use TypePhp\Build\PrecompiledHeaderManager;
use TypePhp\Build\SourcePipelineTrait; use TypePhp\Build\SourcePipelineTrait;
use TypePhp\Config\ProjectYamlLoader; use TypePhp\Config\ProjectYamlLoader;
use TypePhp\Diagnostics\CompileTimeAttributeDiagnostic;
use TypePhp\Build\ResourceCompilationTrait; use TypePhp\Build\ResourceCompilationTrait;
use TypePhp\Entity\ArgInfo; use TypePhp\Entity\ArgInfo;
use TypePhp\Entity\ClassDef; use TypePhp\Entity\ClassDef;
@ -30,6 +31,7 @@ use TypePhp\Entity\MethodDef;
use TypePhp\Entity\PropertyDef; use TypePhp\Entity\PropertyDef;
use TypePhp\Exception\Redo; use TypePhp\Exception\Redo;
use TypePhp\Exception\Skip; use TypePhp\Exception\Skip;
use TypePhp\Exception\SyntaxError;
use TypePhp\Generator\DefaultArgumentGenerator; use TypePhp\Generator\DefaultArgumentGenerator;
use TypePhp\Generator\LibraryImportStubGenerator; use TypePhp\Generator\LibraryImportStubGenerator;
use TypePhp\Generator\Symbol; use TypePhp\Generator\Symbol;
@ -39,6 +41,7 @@ use TypePhp\Platform\Windows;
use TypePhp\Resolver\Reflection; use TypePhp\Resolver\Reflection;
use TypePhp\Resolver\ClassConstantValueTrait; use TypePhp\Resolver\ClassConstantValueTrait;
use TypePhp\Transform\Visitor; use TypePhp\Transform\Visitor;
use TypePhp\Transform\ConstructorLowering;
use PhpParser\Modifiers; use PhpParser\Modifiers;
use PhpParser\Node; use PhpParser\Node;
use PhpParser\NodeAbstract; use PhpParser\NodeAbstract;
@ -1561,7 +1564,9 @@ CODE;
{ {
$code = '#pragma once' . PHP_EOL . PHP_EOL; $code = '#pragma once' . PHP_EOL . PHP_EOL;
$code .= '#include <phpx.h>' . PHP_EOL; $code .= '#include <phpx.h>' . PHP_EOL;
$code .= '#include <typephp_helper.h>' . PHP_EOL;
$code .= '#include <typephp_fiber_generator.h>' . PHP_EOL; $code .= '#include <typephp_fiber_generator.h>' . PHP_EOL;
$code .= PHP_EOL;
if ($this->isBuildModeLib()) { if ($this->isBuildModeLib()) {
$code .= $this->genLibraryApiMacro($this->targetName); $code .= $this->genLibraryApiMacro($this->targetName);
@ -1600,10 +1605,11 @@ CODE;
} }
} }
$params = implode(', ', $list); $params = implode(', ', $list);
$code .= $functionDeclarationPrefix . ($func->returnsByRef ? Type::REF : $func->returnType) . ' ' . self::PREFIX . $name . '(' . $params . ');' . PHP_EOL; $functionAttribute = $this->getFunctionOptimizationAttribute($func);
$code .= $functionDeclarationPrefix . $functionAttribute . ($func->returnsByRef ? Type::REF : $func->returnType) . ' ' . self::PREFIX . $name . '(' . $params . ');' . PHP_EOL;
if ($func->hasMultiReturn()) { if ($func->hasMultiReturn()) {
$code .= 'namespace ' . self::MULTI_RETURN_NAMESPACE . ' {' . PHP_EOL; $code .= 'namespace ' . self::MULTI_RETURN_NAMESPACE . ' {' . PHP_EOL;
$code .= $functionDeclarationPrefix . $func->getMultiReturnCppType() . ' ' . self::PREFIX . $name . '(' . $params . ');' . PHP_EOL; $code .= $functionDeclarationPrefix . $functionAttribute . $func->getMultiReturnCppType() . ' ' . self::PREFIX . $name . '(' . $params . ');' . PHP_EOL;
$code .= '}' . PHP_EOL; $code .= '}' . PHP_EOL;
} }
} }
@ -1620,29 +1626,17 @@ CODE;
{ {
$apiMacro = $this->getNamedLibraryApiMacroName($library); $apiMacro = $this->getNamedLibraryApiMacroName($library);
$exportsMacro = $this->getNamedLibraryExportsMacroName($library); $exportsMacro = $this->getNamedLibraryExportsMacroName($library);
$code = "#if defined(_WIN32)\n"; $code = "#if defined({$exportsMacro})\n";
$code .= "# if defined({$exportsMacro})\n"; $code .= "# define {$apiMacro} TYPEPHP_SYMBOL_EXPORT\n";
$code .= "# define {$apiMacro} __declspec(dllexport)\n";
$code .= "# else\n";
$code .= "# define {$apiMacro} __declspec(dllimport)\n";
$code .= "# endif\n";
$code .= "#elif defined(__GNUC__) && __GNUC__ >= 4\n";
$code .= "# define {$apiMacro} __attribute__((visibility(\"default\")))\n";
$code .= "#else\n"; $code .= "#else\n";
$code .= "# define {$apiMacro}\n"; $code .= "# define {$apiMacro} TYPEPHP_SYMBOL_IMPORT\n";
return $code . "#endif\n\n"; return $code . "#endif\n\n";
} }
protected function genLibraryImportMacro(string $library): string protected function genLibraryImportMacro(string $library): string
{ {
$importMacro = $this->getNamedLibraryImportMacroName($library); $importMacro = $this->getNamedLibraryImportMacroName($library);
$code = "#if defined(_WIN32)\n"; return "#define {$importMacro} TYPEPHP_SYMBOL_IMPORT\n\n";
$code .= "# define {$importMacro} __declspec(dllimport)\n";
$code .= "#elif defined(__GNUC__) && __GNUC__ >= 4\n";
$code .= "# define {$importMacro} __attribute__((visibility(\"default\")))\n";
$code .= "#else\n";
$code .= "# define {$importMacro}\n";
return $code . "#endif\n\n";
} }
protected function getFunctionDeclarationPrefix(FunctionDef $function): string protected function getFunctionDeclarationPrefix(FunctionDef $function): string
@ -1656,6 +1650,17 @@ CODE;
return 'extern '; return 'extern ';
} }
protected function getFunctionOptimizationAttribute(FunctionDef $function): string
{
if ($function->hot) {
return 'TYPEPHP_HOT_ATTRIBUTE ';
}
if ($function->cold) {
return 'TYPEPHP_COLD_ATTRIBUTE ';
}
return '';
}
protected function isImportedFunction(FunctionDef $function): bool protected function isImportedFunction(FunctionDef $function): bool
{ {
return $function->importLibrary !== ''; return $function->importLibrary !== '';
@ -2287,10 +2292,7 @@ CODE;
$ast = $this->parser->parse($phpCode); $ast = $this->parser->parse($phpCode);
$traverser = new NodeTraverser(); $traverser = new NodeTraverser();
$traverser->addVisitor(new NameResolver(null, ['replaceNodes' => false])); $traverser->addVisitor(new NameResolver(null, ['replaceNodes' => false]));
$traverser->addVisitor(new Visitor(function (Node\Stmt\Class_ $class): bool { $traverser->addVisitor(new Visitor(sourceFile: $this->file));
$name = isset($class->namespacedName) ? $class->namespacedName->toString() : $class->name->toString();
return $this->shouldGeneratePrinter($name);
}));
$stmts = $traverser->traverse($ast); $stmts = $traverser->traverse($ast);
@ -2328,6 +2330,7 @@ CODE;
$this->parseConstDef($v); $this->parseConstDef($v);
break; break;
case 'Stmt_Interface': case 'Stmt_Interface':
$this->validateInterfaceOverrideAttributes($v);
break; break;
case 'Stmt_Nop': case 'Stmt_Nop':
break; break;
@ -2490,6 +2493,7 @@ CODE;
$this->parseGroupUse($v2); $this->parseGroupUse($v2);
break; break;
case 'Stmt_Interface': case 'Stmt_Interface':
$this->validateInterfaceOverrideAttributes($v2);
break; break;
default: default:
abort($v2); abort($v2);
@ -2864,6 +2868,109 @@ CODE;
return $typeNode ? $this->typeNodeToString($typeNode) : null; return $typeNode ? $this->typeNodeToString($typeNode) : null;
} }
private function configureGeneratedConstructorParentCall(Node\Stmt\Class_ $class): void
{
$constructor = null;
foreach ($class->getMethods() as $method) {
if ($method->getAttribute(ConstructorLowering::GENERATED_ATTRIBUTE, false)) {
$constructor = $method;
break;
}
}
if ($constructor === null || $this->classDef->extends === '') {
return;
}
$parent = $this->classDef->extends;
while ($parent !== '') {
$parentDef = $this->getClassDef($parent);
if ($parentDef === null) {
$reflection = Reflection::getClass($parent);
$parentConstructor = $reflection?->getConstructor();
if ($parentConstructor === null) {
return;
}
$owner = $parentConstructor->getDeclaringClass()->getName();
$this->applyGeneratedConstructorParentRule(
$constructor,
$owner,
$parentConstructor->getModifiers(),
$parentConstructor->getNumberOfRequiredParameters(),
$parentConstructor->isAbstract(),
);
return;
}
if ($parentDef->hasMethod('__construct')) {
$parentConstructor = $parentDef->getMethod('__construct');
$this->applyGeneratedConstructorParentRule(
$constructor,
$parent,
$parentConstructor->flags,
$parentConstructor->functionDef?->argCountRequired ?? 0,
false,
);
return;
}
if ($parentDef->hasAbstractMethod('__construct')) {
$parentConstructor = $parentDef->getAbstractMethod('__construct');
$this->applyGeneratedConstructorParentRule(
$constructor,
$parent,
$parentConstructor->flags,
$parentConstructor->functionDef?->argCountRequired ?? 0,
true,
);
return;
}
$parent = $parentDef->extends;
}
}
private function applyGeneratedConstructorParentRule(
Node\Stmt\ClassMethod $constructor,
string $parent,
int $flags,
int $requiredArguments,
bool $abstract,
): void {
$attributeTarget = $constructor->getAttribute(
\TypePhp\Diagnostics\CompileTimeAttributeDiagnostic::GENERATED_TARGET,
$constructor,
);
if (!$attributeTarget instanceof Node) {
$attributeTarget = $constructor;
}
if ($flags & Modifiers::FINAL) {
$this->fatalCompileTimeAttribute(
$attributeTarget,
'Constructor',
"Cannot override final method `{$parent}::__construct()`",
$attributeTarget,
);
}
if ($flags & Modifiers::PRIVATE) {
return;
}
if ($requiredArguments > 0) {
$this->fatalCompileTimeAttribute(
$attributeTarget,
'Constructor',
"Constructor cannot be generated because parent constructor `{$parent}::__construct()` " .
"requires {$requiredArguments} argument(s); declare `__construct()` explicitly",
$attributeTarget,
);
}
if ($abstract) {
return;
}
array_unshift($constructor->stmts, new Node\Stmt\Expression(new Node\Expr\StaticCall(
new Node\Name('parent'),
new Node\Identifier('__construct'),
)));
}
protected function parseClass(Node\Stmt\Class_|Node\Stmt\Trait_|Node\Stmt\Enum_ $class): string protected function parseClass(Node\Stmt\Class_|Node\Stmt\Trait_|Node\Stmt\Enum_ $class): string
{ {
$this->class = $this->parseIdentifier($class->name); $this->class = $this->parseIdentifier($class->name);
@ -2872,12 +2979,59 @@ CODE;
$this->fatalError($class, "class {$fullName} not found"); $this->fatalError($class, "class {$fullName} not found");
} }
$this->classDef = $this->getClass($fullName); $this->classDef = $this->getClass($fullName);
$this->parseExtensionProviderTarget($class); $this->parseMethodsForTarget($class);
if ($class instanceof Node\Stmt\Class_) {
$this->configureGeneratedConstructorParentCall($class);
}
if ($class instanceof Node\Stmt\Class_ && $this->classDef->printerGenerated) { if ($class instanceof Node\Stmt\Class_ && $this->classDef->printerGenerated) {
$available = [...$this->parentPublicProperties($this->classDef->extends), ...\TypePhp\Transform\ClassFieldSelection::ownPublicProperties($class)];
try {
$properties = \TypePhp\Transform\ClassFieldSelection::resolve(
$this->classDef->printerFields,
$this->classDef->printerFields === null
? $available
: $this->selectableProperties($this->classDef),
'Printer',
);
} catch (SyntaxError $error) {
throw new SyntaxError(CompileTimeAttributeDiagnostic::format(
$error->getMessage(),
'Printer',
$class,
$this->file,
), 0, $error);
}
\TypePhp\Transform\PrinterLowering::rebuildGeneratedMethod( \TypePhp\Transform\PrinterLowering::rebuildGeneratedMethod(
$class, $class,
[...$this->parentPublicProperties($this->classDef->extends), ...\TypePhp\Transform\PrinterLowering::ownPublicProperties($class)], $properties,
$this->classDef->printerFields,
$this->classStringProperties($this->classDef),
);
}
if ($class instanceof Node\Stmt\Class_ && $this->classDef->arrayableGenerated) {
$available = [...$this->parentPublicProperties($this->classDef->extends), ...\TypePhp\Transform\ClassFieldSelection::ownPublicProperties($class)];
try {
$properties = \TypePhp\Transform\ClassFieldSelection::resolve(
$this->classDef->arrayableFields,
$this->classDef->arrayableFields === null
? $available
: $this->selectableProperties($this->classDef),
'Arrayable',
);
} catch (SyntaxError $error) {
throw new SyntaxError(CompileTimeAttributeDiagnostic::format(
$error->getMessage(),
'Arrayable',
$class,
$this->file,
), 0, $error);
}
\TypePhp\Transform\ArrayableLowering::rebuildGeneratedMethod(
$class,
$properties,
$this->classDef->arrayableFields,
); );
} }
@ -2932,6 +3086,7 @@ CODE;
} }
} }
if (!$class instanceof Node\Stmt\Trait_) { if (!$class instanceof Node\Stmt\Trait_) {
$this->validateOverrideAttributes($class);
$this->checkInterfaceImplementations($class); $this->checkInterfaceImplementations($class);
$this->checkInheritedAbstractMethodsAreImplemented($class); $this->checkInheritedAbstractMethodsAreImplemented($class);
} }
@ -3284,7 +3439,8 @@ CODE;
? $this->functionDef->getMultiReturnCppType() ? $this->functionDef->getMultiReturnCppType()
: ($this->functionDef->returnsByRef ? Type::REF : $this->getReturnType()); : ($this->functionDef->returnsByRef ? Type::REF : $this->getReturnType());
$nativeName = self::PREFIX . $name; $nativeName = self::PREFIX . $name;
$functionDeclCode = $cppReturnType . ' ' . ($multiReturn ? $this->getMultiReturnImplName($name) : $nativeName) . '('; $functionAttribute = $this->getFunctionOptimizationAttribute($this->functionDef);
$functionDeclCode = $functionAttribute . $cppReturnType . ' ' . ($multiReturn ? $this->getMultiReturnImplName($name) : $nativeName) . '(';
if ($this->class) { if ($this->class) {
$functionDeclCode .= Type::OBJECT . ' &this_'; $functionDeclCode .= Type::OBJECT . ' &this_';
if ($this->classDef?->trait !== null && $this->methodDef?->parentMethodCalls) { if ($this->classDef?->trait !== null && $this->methodDef?->parentMethodCalls) {
@ -3340,7 +3496,7 @@ CODE;
: $argInfo->name, : $argInfo->name,
$this->functionDef->argInfoList, $this->functionDef->argInfoList,
)); ));
$code .= Type::ARRAY . ' ' . $nativeName . '(' . $this->functionDef->params . ') {' . PHP_EOL; $code .= $functionAttribute . Type::ARRAY . ' ' . $nativeName . '(' . $this->functionDef->params . ') {' . PHP_EOL;
$this->indentLevel++; $this->indentLevel++;
$code .= $this->getIndent() . 'return ' . Type::ARRAY . '(' . $this->getMultiReturnImplName($name) . '(' . $forwardArgs . '));' . PHP_EOL; $code .= $this->getIndent() . 'return ' . Type::ARRAY . '(' . $this->getMultiReturnImplName($name) . '(' . $forwardArgs . '));' . PHP_EOL;
$this->indentLevel--; $this->indentLevel--;
@ -3384,15 +3540,17 @@ CODE;
$methodDef = $classDef->getMethod($name); $methodDef = $classDef->getMethod($name);
if ($methodDef->flags & Modifiers::PRIVATE) { if ($methodDef->flags & Modifiers::PRIVATE) {
_error: _error:
$message = 'Cannot override private method `' . $extends . '::' . $name . '()`';
$this->fatalGeneratedMethodAttributeIfAny($v, $message, $extends, $name);
$this->fatalError($v, $this->fatalError($v,
'Cannot override private method `' . $message);
$extends . '::' . $name . '()`');
} }
if ($methodDef->flags & Modifiers::FINAL) { if ($methodDef->flags & Modifiers::FINAL) {
_final_error: _final_error:
$message = 'Cannot override final method `' . $extends . '::' . $name . '()`';
$this->fatalGeneratedMethodAttributeIfAny($v, $message, $extends, $name);
$this->fatalError($v, $this->fatalError($v,
'Cannot override final method `' . $message);
$extends . '::' . $name . '()`');
} }
$this->validateMethodOverrideSignature($v, $name, $this->methodDef, $methodDef, $extends); $this->validateMethodOverrideSignature($v, $name, $this->methodDef, $methodDef, $extends);
break; break;
@ -3429,6 +3587,24 @@ CODE;
return; return;
} }
// MustUse is part of the callable contract. An override may strengthen
// this guarantee, but it must not drop one promised by a parent class
// or interface.
if ($parentFuncDef->mustUse && !$childFuncDef->mustUse) {
$message = "Declaration of `{$className}::{$methodName}()` must be compatible with " .
"`{$parentClass}::{$methodName}()`";
$this->error(CompileTimeAttributeDiagnostic::formatPositions(
$message,
'MustUse',
"method {$parentClass}::{$methodName}()",
$parentFuncDef->sourceFile,
$parentFuncDef->startLine,
'override drops MustUse contract',
$this->file,
$v->getStartLine(),
));
}
if (!$this->isReturnTypeOverrideCompatible($childFuncDef, $parentFuncDef)) { if (!$this->isReturnTypeOverrideCompatible($childFuncDef, $parentFuncDef)) {
$this->fatalMethodOverrideIncompatible($v, $className, $methodName, $parentClass); $this->fatalMethodOverrideIncompatible($v, $className, $methodName, $parentClass);
} }
@ -3474,9 +3650,44 @@ CODE;
string $methodName, string $methodName,
string $parentClass string $parentClass
): void { ): void {
$this->fatalError($v, $message = "Declaration of `{$className}::{$methodName}()` must be compatible " .
"Declaration of `{$className}::{$methodName}()` must be compatible " . "with `{$parentClass}::{$methodName}()`";
"with `{$parentClass}::{$methodName}()`"); $this->fatalGeneratedMethodAttributeIfAny($v, $message, $parentClass, $methodName);
$this->fatalError($v, $message);
}
private function fatalGeneratedMethodAttributeIfAny(
NodeAbstract $method,
string $message,
string $parentClass,
string $methodName,
): void {
$attribute = $method->getAttribute(CompileTimeAttributeDiagnostic::GENERATED_BY);
$target = $method->getAttribute(CompileTimeAttributeDiagnostic::GENERATED_TARGET);
if (!is_string($attribute) || !$target instanceof Node) {
return;
}
$parentFunction = null;
$parentDef = $this->getClassDef($parentClass);
if ($parentDef instanceof ClassDef) {
if ($parentDef->hasMethod($methodName)) {
$parentFunction = $parentDef->getMethod($methodName)->functionDef;
} elseif ($parentDef->hasAbstractMethod($methodName)
&& isset($parentDef->abstractMethodDefs[strtolower($methodName)])) {
$parentFunction = $parentDef->getAbstractMethod($methodName)->functionDef;
}
}
$this->error(CompileTimeAttributeDiagnostic::formatPositions(
$message,
$attribute,
CompileTimeAttributeDiagnostic::describeTarget($target),
$this->file,
$target->getStartLine(),
$parentFunction === null ? null : 'parent method',
$parentFunction?->sourceFile,
$parentFunction?->startLine,
));
} }
private function isReturnTypeOverrideCompatible(FunctionDef $childFuncDef, FunctionDef $parentFuncDef): bool private function isReturnTypeOverrideCompatible(FunctionDef $childFuncDef, FunctionDef $parentFuncDef): bool
@ -3628,6 +3839,130 @@ CODE;
} }
} }
private function validateOverrideAttributes(Node\Stmt\Class_|Node\Stmt\Enum_ $classStmt): void
{
$methods = [...$this->classDef->methods, ...$this->classDef->abstractMethodDefs];
foreach ($methods as $methodDef) {
if (!$methodDef->functionDef?->overrideRequired) {
continue;
}
if ($this->hasMatchingOverrideDeclaration($this->classDef, $methodDef->name)) {
continue;
}
$this->fatalMissingOverride(
$methodDef->node ?? $classStmt,
$this->classDef->getNamespacedName(false),
$methodDef->name,
);
}
}
private function hasMatchingOverrideDeclaration(ClassDef $classDef, string $methodName): bool
{
if (strtolower($methodName) === '__construct') {
return false;
}
$current = $classDef;
while ($current->extends !== '') {
$parentName = $current->extends;
if ($current->inheritedFromInternalClass || $this->isInternalClass($parentName)) {
$modifiers = Reflection::getClassMethodModifiers($parentName, $methodName);
if ($modifiers !== null && !($modifiers & \ReflectionMethod::IS_PRIVATE)) {
return true;
}
break;
}
if (!$this->hasClass($parentName)) {
break;
}
$current = $this->getClass($parentName);
if ($current->hasMethod($methodName)) {
if (!($current->getMethod($methodName)->flags & Modifiers::PRIVATE)) {
return true;
}
} elseif ($current->hasAbstractMethod($methodName)) {
if (!($current->getMethodFlags($methodName) & Modifiers::PRIVATE)) {
return true;
}
}
}
foreach ($this->getClassImplementedInterfaces($classDef) as $interfaceName) {
if ($this->isInternalInterface($interfaceName)) {
if (Reflection::hasMethod($interfaceName, $methodName)) {
return true;
}
continue;
}
if ($this->hasInterface($interfaceName) && $this->getInterface($interfaceName)->hasMethod($methodName)) {
return true;
}
}
return false;
}
private function validateInterfaceOverrideAttributes(Node\Stmt\Interface_ $interfaceStmt): void
{
$name = $this->parseIdentifier($interfaceStmt->name);
$interfaceName = $this->namespace === '' ? $name : $this->namespace . '\\' . $name;
if (!$this->hasInterface($interfaceName)) {
return;
}
$interfaceDef = $this->getInterface($interfaceName);
foreach ($interfaceDef->methods as $methodDef) {
if (!$methodDef->functionDef?->overrideRequired) {
continue;
}
$visited = [];
if ($this->interfaceParentsHaveMethod($interfaceDef, $methodDef->name, $visited)) {
continue;
}
$this->fatalMissingOverride($methodDef->node ?? $interfaceStmt, $interfaceName, $methodDef->name);
}
}
private function fatalMissingOverride(NodeAbstract $node, string $className, string $methodName): never
{
$this->fatalCompileTimeAttribute(
$node,
'Override',
"{$className}::{$methodName}() has #[\\Override] attribute, " .
'but no matching parent method exists',
);
}
/** @param array<string, true> $visited */
private function interfaceParentsHaveMethod(
InterfaceDef $interfaceDef,
string $methodName,
array &$visited,
): bool {
foreach ($interfaceDef->extendsList ?: ($interfaceDef->extends ? [$interfaceDef->extends] : []) as $parentName) {
$key = strtolower($parentName);
if (isset($visited[$key])) {
continue;
}
$visited[$key] = true;
if ($this->isInternalInterface($parentName)) {
if (Reflection::hasMethod($parentName, $methodName)) {
return true;
}
continue;
}
if (!$this->hasInterface($parentName)) {
continue;
}
$parent = $this->getInterface($parentName);
if ($parent->hasMethod($methodName)
|| $this->interfaceParentsHaveMethod($parent, $methodName, $visited)) {
return true;
}
}
return false;
}
private function checkInterfaceImplementation(NodeAbstract $node, ClassDef $classDef, string $interfaceName): void private function checkInterfaceImplementation(NodeAbstract $node, ClassDef $classDef, string $interfaceName): void
{ {
if ($this->isInternalInterface($interfaceName)) { if ($this->isInternalInterface($interfaceName)) {
@ -3843,6 +4178,11 @@ CODE;
// Keep the AST node so trait-composed methods can report accurate // Keep the AST node so trait-composed methods can report accurate
// line numbers when validated for override compatibility later. // line numbers when validated for override compatibility later.
$this->methodDef->node = $v; $this->methodDef->node = $v;
if ($this->classDef->trait === null
&& $this->methodDef->functionDef?->overrideRequired
&& !$this->hasMatchingOverrideDeclaration($this->classDef, $name)) {
$this->fatalMissingOverride($v, $this->classDef->getNamespacedName(false), $name);
}
// 预处理阶段没有父类的信息,只能在实现阶段检查 // 预处理阶段没有父类的信息,只能在实现阶段检查
$this->checkParentMethodCanBeOverridden($v, $name); $this->checkParentMethodCanBeOverridden($v, $name);
$methodCodes[$name] = $this->parseFunction($v); $methodCodes[$name] = $this->parseFunction($v);

@ -3584,9 +3584,7 @@ class AttributeInfo {
foreach ($attrGroup->attrs as $attr) { foreach ($attrGroup->attrs as $attr) {
$parts = $attr->name->getParts(); $parts = $attr->name->getParts();
$compileTimeAttribute = count($parts) === 1 $compileTimeAttribute = count($parts) === 1
&& in_array(strtolower($parts[0]), [ && TypePhp\Transform\CompileTimeAttributeRegistry::get($parts[0]) !== null;
'extensionprovider', 'getter', 'noexport', 'notnull', 'printer', 'setter', 'with',
], true);
if ($compileTimeAttribute) { if ($compileTimeAttribute) {
continue; continue;
} }
@ -4513,13 +4511,7 @@ class FileInfo {
null, null,
['preserveOriginalNames' => true] ['preserveOriginalNames' => true]
)); ));
$nodeTraverser->addVisitor(new TypePhp\Transform\Visitor(static function (Stmt\Class_ $class): bool { $nodeTraverser->addVisitor(new TypePhp\Transform\Visitor());
if (!isset($GLOBALS['translator'])) {
return true;
}
$name = isset($class->namespacedName) ? $class->namespacedName->toString() : $class->name->toString();
return getTranslator()->shouldGeneratePrinter($name);
}));
$prettyPrinter = new class extends Standard { $prettyPrinter = new class extends Standard {
protected function pName_FullyQualified(PhpParser\Node\Name\FullyQualified $node): string { protected function pName_FullyQualified(PhpParser\Node\Name\FullyQualified $node): string {
return implode('\\', $node->getParts()); return implode('\\', $node->getParts());

@ -7,7 +7,7 @@
*/ */
#[Attribute(Attribute::TARGET_CLASS)] #[Attribute(Attribute::TARGET_CLASS)]
final readonly class ExtensionProvider final readonly class MethodsFor
{ {
public function __construct(public string $target) public function __construct(public string $target)
{ {
@ -37,6 +37,17 @@ final readonly class With
#[Attribute(Attribute::TARGET_CLASS)] #[Attribute(Attribute::TARGET_CLASS)]
final readonly class Printer final readonly class Printer
{ {
public function __construct(public ?array $fields = null)
{
}
}
#[Attribute(Attribute::TARGET_CLASS)]
final readonly class Arrayable
{
public function __construct(public ?array $fields = null)
{
}
} }
#[Attribute(Attribute::TARGET_PARAMETER)] #[Attribute(Attribute::TARGET_PARAMETER)]
@ -44,8 +55,44 @@ final readonly class NotNull
{ {
} }
#[Attribute(Attribute::TARGET_PARAMETER)]
final readonly class NotEmpty
{
}
#[Attribute(Attribute::TARGET_PARAMETER)]
final readonly class Validate
{
public function __construct(
public int $filter,
public int|array $options = 0,
public ?string $message = null,
) {
}
}
#[Attribute(Attribute::TARGET_FUNCTION | Attribute::TARGET_METHOD)]
final readonly class MustUse
{
}
#[Attribute(Attribute::TARGET_FUNCTION | Attribute::TARGET_METHOD)]
final readonly class Hot
{
}
#[Attribute(Attribute::TARGET_FUNCTION | Attribute::TARGET_METHOD)]
final readonly class Cold
{
}
#[Attribute(Attribute::TARGET_PROPERTY)]
final readonly class Constructor
{
}
/** /**
* Public compile-time type symbols shared by extension providers and std containers. * Public compile-time type symbols shared by MethodsFor providers and std containers.
* This root class is deliberately distinct from the compiler-internal TypePhp\Type. * This root class is deliberately distinct from the compiler-internal TypePhp\Type.
*/ */
final class Type final class Type

Loading…
Cancel
Save