parent
8bcd6ce863
commit
18b798e8ce
97 changed files with 3916 additions and 276 deletions
@ -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'; |
||||||
|
} |
||||||
@ -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, |
||||||
|
) { |
||||||
|
} |
||||||
|
} |
||||||
@ -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]); |
||||||
|
} |
||||||
|
} |
||||||
@ -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(); |
||||||
|
} |
||||||
|
} |
||||||
@ -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); |
||||||
|
} |
||||||
|
} |
||||||
@ -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; |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -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); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -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'))], |
||||||
|
)))], |
||||||
|
]); |
||||||
|
} |
||||||
|
} |
||||||
@ -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, |
||||||
|
); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -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; |
||||||
|
} |
||||||
|
} |
||||||
Loading…
Reference in new issue