parent
1e70321553
commit
b4919c7e76
6 changed files with 631 additions and 40 deletions
@ -0,0 +1,290 @@ |
|||||||
|
<?php |
||||||
|
/** |
||||||
|
* This file is part of TypePHP(AOT). |
||||||
|
* |
||||||
|
* @link https://www.swoole.com/aot/ |
||||||
|
* @contact service@swoole.com |
||||||
|
*/ |
||||||
|
|
||||||
|
use TypePhp\CompilerTest; |
||||||
|
use TypePhp\Exception\TestError; |
||||||
|
|
||||||
|
/** |
||||||
|
* Compound declarations must be rejected by the TypePHP front end, rather |
||||||
|
* than leaking into gen_stub.php or the C++ compiler. |
||||||
|
* @internal |
||||||
|
* @coversNothing |
||||||
|
*/ |
||||||
|
final class CompoundTypeDeclarationValidationTest extends PHPUnit\Framework\TestCase |
||||||
|
{ |
||||||
|
private string $testRoot; |
||||||
|
|
||||||
|
protected function setUp(): void |
||||||
|
{ |
||||||
|
$this->testRoot = sys_get_temp_dir() . '/typephp-compound-type-' . bin2hex(random_bytes(8)); |
||||||
|
mkdir($this->testRoot, 0777, true); |
||||||
|
} |
||||||
|
|
||||||
|
protected function tearDown(): void |
||||||
|
{ |
||||||
|
if (!is_dir($this->testRoot)) { |
||||||
|
return; |
||||||
|
} |
||||||
|
$iterator = new RecursiveIteratorIterator( |
||||||
|
new RecursiveDirectoryIterator($this->testRoot, FilesystemIterator::SKIP_DOTS), |
||||||
|
RecursiveIteratorIterator::CHILD_FIRST, |
||||||
|
); |
||||||
|
foreach ($iterator as $entry) { |
||||||
|
$entry->isDir() ? rmdir($entry->getPathname()) : unlink($entry->getPathname()); |
||||||
|
} |
||||||
|
rmdir($this->testRoot); |
||||||
|
} |
||||||
|
|
||||||
|
/** @dataProvider invalidNamedDeclarationProvider */ |
||||||
|
public function testInvalidNamedDeclarationFailsDuringPrepare(string $declaration, string $diagnostic): void |
||||||
|
{ |
||||||
|
[$compiler, $file] = $this->compilerFor("<?php\n{$declaration}\nfunction main(): void {}\n");
|
||||||
|
|
||||||
|
$this->expectException(TestError::class); |
||||||
|
$this->expectExceptionMessage($diagnostic); |
||||||
|
$compiler->prepareFile($file); |
||||||
|
} |
||||||
|
|
||||||
|
public static function invalidNamedDeclarationProvider(): iterable |
||||||
|
{ |
||||||
|
yield 'duplicate builtin union member' => [ |
||||||
|
'function broken(int|string|int $value): void {}', |
||||||
|
'Duplicate type int is redundant', |
||||||
|
]; |
||||||
|
yield 'duplicate resolved class union member' => [ |
||||||
|
'namespace App; use Vendor\Model as Item; function broken(Item|\Vendor\Model $value): void {}', |
||||||
|
'Duplicate type Vendor\Model is redundant', |
||||||
|
]; |
||||||
|
yield 'iterable includes array' => [ |
||||||
|
'function broken(iterable|array $value): void {}', |
||||||
|
'Duplicate type array is redundant', |
||||||
|
]; |
||||||
|
yield 'iterable includes Traversable' => [ |
||||||
|
'function broken(\Traversable|iterable $value): void {}', |
||||||
|
'Duplicate type Traversable is redundant', |
||||||
|
]; |
||||||
|
yield 'bool includes false' => [ |
||||||
|
'function broken(false|bool $value): void {}', |
||||||
|
'Duplicate type false is redundant', |
||||||
|
]; |
||||||
|
yield 'true and false must use bool' => [ |
||||||
|
'function broken(true|false $value): void {}', |
||||||
|
'Type contains both true and false, bool must be used instead', |
||||||
|
]; |
||||||
|
yield 'mixed in union' => [ |
||||||
|
'function broken(mixed|string $value): void {}', |
||||||
|
'Type mixed can only be used as a standalone type', |
||||||
|
]; |
||||||
|
yield 'void in union' => [ |
||||||
|
'function broken(): void|string {}', |
||||||
|
'Type void can only be used as a standalone type', |
||||||
|
]; |
||||||
|
yield 'never in union' => [ |
||||||
|
'function broken(): never|string {}', |
||||||
|
'Type never can only be used as a standalone type', |
||||||
|
]; |
||||||
|
yield 'nullable mixed' => [ |
||||||
|
'function broken(?mixed $value): void {}', |
||||||
|
'Type mixed cannot be marked as nullable since mixed already includes null', |
||||||
|
]; |
||||||
|
yield 'nullable null' => [ |
||||||
|
'function broken(?null $value): void {}', |
||||||
|
'null cannot be marked as nullable', |
||||||
|
]; |
||||||
|
yield 'nullable void' => [ |
||||||
|
'function broken(): ?void {}', |
||||||
|
'Void can only be used as a standalone type', |
||||||
|
]; |
||||||
|
yield 'nullable never' => [ |
||||||
|
'function broken(): ?never {}', |
||||||
|
'never can only be used as a standalone type', |
||||||
|
]; |
||||||
|
yield 'scalar intersection member' => [ |
||||||
|
'function broken(A&int $value): void {}', |
||||||
|
'Type int cannot be part of an intersection type', |
||||||
|
]; |
||||||
|
yield 'callable intersection member' => [ |
||||||
|
'function broken(A&callable $value): void {}', |
||||||
|
'Type callable cannot be part of an intersection type', |
||||||
|
]; |
||||||
|
yield 'duplicate resolved intersection member' => [ |
||||||
|
'namespace App; use Vendor\Contract as C; function broken(C&\Vendor\Contract $value): void {}', |
||||||
|
'Duplicate type Vendor\Contract is redundant', |
||||||
|
]; |
||||||
|
yield 'permuted duplicate DNF member' => [ |
||||||
|
'function broken((A&B)|(B&A) $value): void {}', |
||||||
|
'Type B&A is redundant with type A&B', |
||||||
|
]; |
||||||
|
yield 'DNF strict superset after subset' => [ |
||||||
|
'function broken((A&B)|(A&B&C) $value): void {}', |
||||||
|
'Type A&B&C is redundant as it is more restrictive than type A&B', |
||||||
|
]; |
||||||
|
yield 'DNF strict superset before subset' => [ |
||||||
|
'function broken((A&B&C)|(A&B) $value): void {}', |
||||||
|
'Type A&B&C is redundant as it is more restrictive than type A&B', |
||||||
|
]; |
||||||
|
yield 'plain class subsumes DNF member' => [ |
||||||
|
'function broken((A&B)|A $value): void {}', |
||||||
|
'Type A&B is redundant as it is more restrictive than type A', |
||||||
|
]; |
||||||
|
yield 'object subsumes class' => [ |
||||||
|
'function broken(object|A $value): void {}', |
||||||
|
'contains both object and a class type, which is redundant', |
||||||
|
]; |
||||||
|
yield 'object subsumes DNF member' => [ |
||||||
|
'function broken(object|(A&B) $value): void {}', |
||||||
|
'contains both object and a class type, which is redundant', |
||||||
|
]; |
||||||
|
yield 'self in global function' => [ |
||||||
|
'function broken(): self {}', |
||||||
|
'Cannot use "self" when no class scope is active', |
||||||
|
]; |
||||||
|
yield 'self in global function union' => [ |
||||||
|
'function broken(): self|A {}', |
||||||
|
'Cannot use "self" when no class scope is active', |
||||||
|
]; |
||||||
|
yield 'self in global function DNF parameter' => [ |
||||||
|
'function broken((self&A)|B $value): void {}', |
||||||
|
'Cannot use "self" when no class scope is active', |
||||||
|
]; |
||||||
|
yield 'static in global function' => [ |
||||||
|
'function broken(): static {}', |
||||||
|
'Cannot use "static" when no class scope is active', |
||||||
|
]; |
||||||
|
yield 'static in global function union' => [ |
||||||
|
'function broken(): static|A {}', |
||||||
|
'Cannot use "static" when no class scope is active', |
||||||
|
]; |
||||||
|
yield 'parent in global function' => [ |
||||||
|
'function broken(): parent {}', |
||||||
|
'Cannot use "parent" when no class scope is active', |
||||||
|
]; |
||||||
|
yield 'parent method without parent class' => [ |
||||||
|
'class A { public function broken(): parent {} }', |
||||||
|
'Cannot use "parent" when current class scope has no parent', |
||||||
|
]; |
||||||
|
yield 'parent property without parent class' => [ |
||||||
|
'class A { public parent $value; }', |
||||||
|
'Cannot use "parent" when current class scope has no parent', |
||||||
|
]; |
||||||
|
yield 'parent constant without parent class' => [ |
||||||
|
'class A { public const parent VALUE = null; }', |
||||||
|
'Cannot use "parent" when current class scope has no parent', |
||||||
|
]; |
||||||
|
yield 'self in intersection inside class' => [ |
||||||
|
'class A { public function broken(self&B $value): void {} }', |
||||||
|
"Type 'self' cannot be part of an intersection type", |
||||||
|
]; |
||||||
|
yield 'self in DNF promoted property' => [ |
||||||
|
'class A { public function __construct(public (self&B)|C $value) {} }', |
||||||
|
"Type 'self' cannot be part of an intersection type", |
||||||
|
]; |
||||||
|
yield 'static in intersection return type' => [ |
||||||
|
'class A { public function broken(): static&B {} }', |
||||||
|
"Type 'static' cannot be part of an intersection type", |
||||||
|
]; |
||||||
|
yield 'duplicate class implements' => [ |
||||||
|
'interface I {} class A implements I, I {}', |
||||||
|
'Class A cannot implement previously implemented interface I', |
||||||
|
]; |
||||||
|
yield 'duplicate enum implements through alias' => [ |
||||||
|
'namespace App; interface I {} use App\I as Contract; enum E implements I, Contract { case A; }', |
||||||
|
'Enum App\E cannot implement previously implemented interface App\I', |
||||||
|
]; |
||||||
|
} |
||||||
|
|
||||||
|
/** @dataProvider invalidClosureDeclarationProvider */ |
||||||
|
public function testInvalidClosureDeclarationFailsDuringConvert(string $body, string $diagnostic): void |
||||||
|
{ |
||||||
|
[$compiler, $file] = $this->compilerFor("<?php\nfunction main(): void {\n{$body}\n}\n");
|
||||||
|
$compiler->prepareFile($file); |
||||||
|
|
||||||
|
$this->expectException(TestError::class); |
||||||
|
$this->expectExceptionMessage($diagnostic); |
||||||
|
$compiler->convertFile($file); |
||||||
|
} |
||||||
|
|
||||||
|
public static function invalidClosureDeclarationProvider(): iterable |
||||||
|
{ |
||||||
|
yield 'closure duplicate union' => [ |
||||||
|
'$fn = function (int|string|int $value): void {};', |
||||||
|
'Duplicate type int is redundant', |
||||||
|
]; |
||||||
|
yield 'arrow function invalid intersection' => [ |
||||||
|
'$fn = fn (A&int $value): int => 1;', |
||||||
|
'Type int cannot be part of an intersection type', |
||||||
|
]; |
||||||
|
yield 'closure permuted DNF' => [ |
||||||
|
'$fn = function ((A&B)|(B&A) $value): void {};', |
||||||
|
'Type B&A is redundant with type A&B', |
||||||
|
]; |
||||||
|
yield 'global closure self intersection' => [ |
||||||
|
'$fn = function (self&A $value): void {};', |
||||||
|
"Type 'self' cannot be part of an intersection type", |
||||||
|
]; |
||||||
|
} |
||||||
|
|
||||||
|
public function testValidBoundaryDeclarationsCompile(): void |
||||||
|
{ |
||||||
|
[$compiler, $file] = $this->compilerFor(<<<'PHP' |
||||||
|
<?php |
||||||
|
namespace App; |
||||||
|
|
||||||
|
interface A {} |
||||||
|
interface B {} |
||||||
|
interface C {} |
||||||
|
interface Traversable {} |
||||||
|
|
||||||
|
class Base {} |
||||||
|
class Child extends Base |
||||||
|
{ |
||||||
|
public function choose((A&B)|(A&C)|self|parent $value): (A&B)|(A&C)|self|parent|static |
||||||
|
{ |
||||||
|
return $value; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function localNameDoesNotOverlapBuiltin(iterable|Traversable $value): void {} |
||||||
|
function main(): void {} |
||||||
|
PHP); |
||||||
|
$compiler->prepareFile($file); |
||||||
|
$compiler->convertFile($file); |
||||||
|
|
||||||
|
self::assertFileExists($compiler->getCppFile($file)); |
||||||
|
} |
||||||
|
|
||||||
|
public function testGlobalClosuresKeepBindableSelfAndStaticTypes(): void |
||||||
|
{ |
||||||
|
[$compiler, $file] = $this->compilerFor(<<<'PHP' |
||||||
|
<?php |
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
$withSelf = function (self $value): self { return $value; }; |
||||||
|
$withStatic = function (): static { throw new Exception('not invoked'); }; |
||||||
|
} |
||||||
|
PHP); |
||||||
|
$compiler->prepareFile($file); |
||||||
|
$compiler->convertFile($file); |
||||||
|
|
||||||
|
self::assertFileExists($compiler->getCppFile($file)); |
||||||
|
} |
||||||
|
|
||||||
|
/** @return array{CompilerTest, string} */ |
||||||
|
private function compilerFor(string $source): array |
||||||
|
{ |
||||||
|
$file = $this->testRoot . '/program.php'; |
||||||
|
file_put_contents($file, $source); |
||||||
|
|
||||||
|
global $translator; |
||||||
|
$compiler = CompilerTest::create($this->testRoot); |
||||||
|
$translator = $compiler; |
||||||
|
$compiler->addFiles([$file]); |
||||||
|
|
||||||
|
return [$compiler, $file]; |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,271 @@ |
|||||||
|
<?php |
||||||
|
/** |
||||||
|
* This file is part of TypePHP(AOT). |
||||||
|
* |
||||||
|
* @link https://www.swoole.com/aot/ |
||||||
|
* @contact service@swoole.com |
||||||
|
*/ |
||||||
|
|
||||||
|
namespace TypePhp\TypeSystem; |
||||||
|
|
||||||
|
use PhpParser\Node; |
||||||
|
use PhpParser\Node\IntersectionType; |
||||||
|
use PhpParser\Node\NullableType; |
||||||
|
use PhpParser\Node\UnionType; |
||||||
|
use PhpParser\NodeAbstract; |
||||||
|
|
||||||
|
/** |
||||||
|
* Validates the structure of PHP compound type declarations before they are |
||||||
|
* lowered to TypePHP's runtime type checks. |
||||||
|
* |
||||||
|
* This deliberately has no knowledge of the declaration context. Rules for |
||||||
|
* self/parent/static outside a class-like scope remain in the preprocessor so |
||||||
|
* closures, whose scope may be supplied later by Closure::bindTo(), are not |
||||||
|
* incorrectly treated as ordinary global functions. |
||||||
|
*/ |
||||||
|
trait CompoundTypeDeclarationValidationTrait |
||||||
|
{ |
||||||
|
/** @var array<string, true> */ |
||||||
|
private const array PHP_INTERSECTION_FORBIDDEN_TYPES = [ |
||||||
|
'array' => true, |
||||||
|
'bool' => true, |
||||||
|
'callable' => true, |
||||||
|
'false' => true, |
||||||
|
'float' => true, |
||||||
|
'int' => true, |
||||||
|
'iterable' => true, |
||||||
|
'mixed' => true, |
||||||
|
'never' => true, |
||||||
|
'null' => true, |
||||||
|
'object' => true, |
||||||
|
'parent' => true, |
||||||
|
'self' => true, |
||||||
|
'static' => true, |
||||||
|
'string' => true, |
||||||
|
'true' => true, |
||||||
|
'void' => true, |
||||||
|
]; |
||||||
|
|
||||||
|
protected function validateCompoundTypeDeclaration(?NodeAbstract $type): void |
||||||
|
{ |
||||||
|
if ($type instanceof NullableType) { |
||||||
|
$name = strtolower($this->parseIdentifier($type->type)); |
||||||
|
if ($name === 'mixed') { |
||||||
|
$this->fatalError($type, 'Type mixed cannot be marked as nullable since mixed already includes null'); |
||||||
|
} |
||||||
|
if ($name === 'null') { |
||||||
|
$this->fatalError($type, 'null cannot be marked as nullable'); |
||||||
|
} |
||||||
|
if ($name === 'void') { |
||||||
|
$this->fatalError($type, 'Void can only be used as a standalone type'); |
||||||
|
} |
||||||
|
if ($name === 'never') { |
||||||
|
$this->fatalError($type, 'never can only be used as a standalone type'); |
||||||
|
} |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if ($type instanceof IntersectionType) { |
||||||
|
$this->validateIntersectionTypeDeclaration($type); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if ($type instanceof UnionType) { |
||||||
|
$this->validateUnionTypeDeclaration($type); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @return array{members: array<string, true>, display: string} |
||||||
|
*/ |
||||||
|
private function validateIntersectionTypeDeclaration(IntersectionType $type): array |
||||||
|
{ |
||||||
|
$members = []; |
||||||
|
$display = []; |
||||||
|
foreach ($type->types as $member) { |
||||||
|
[$key, $name, $classLike] = $this->getCompoundTypeMemberIdentity($member); |
||||||
|
$lowerName = strtolower($name); |
||||||
|
if (!$classLike || isset(self::PHP_INTERSECTION_FORBIDDEN_TYPES[$lowerName])) { |
||||||
|
$message = in_array($lowerName, ['self', 'parent', 'static'], true) |
||||||
|
? "Type '{$lowerName}' cannot be part of an intersection type" |
||||||
|
: "Type {$name} cannot be part of an intersection type"; |
||||||
|
$this->fatalError($member, $message); |
||||||
|
} |
||||||
|
if (isset($members[$key])) { |
||||||
|
$this->fatalError($member, "Duplicate type {$name} is redundant"); |
||||||
|
} |
||||||
|
$members[$key] = true; |
||||||
|
$display[] = $name; |
||||||
|
} |
||||||
|
|
||||||
|
return ['members' => $members, 'display' => implode('&', $display)]; |
||||||
|
} |
||||||
|
|
||||||
|
private function validateUnionTypeDeclaration(UnionType $type): void |
||||||
|
{ |
||||||
|
/** @var array<string, true> $seen */ |
||||||
|
$seen = []; |
||||||
|
/** @var list<array{members: array<string, true>, display: string}> $classGroups */ |
||||||
|
$classGroups = []; |
||||||
|
$hasIterable = false; |
||||||
|
$hasArray = false; |
||||||
|
$hasTraversable = false; |
||||||
|
$hasBool = false; |
||||||
|
$hasTrue = false; |
||||||
|
$hasFalse = false; |
||||||
|
$hasObject = false; |
||||||
|
$hasClassType = false; |
||||||
|
|
||||||
|
foreach ($type->types as $member) { |
||||||
|
if ($member instanceof IntersectionType) { |
||||||
|
$group = $this->validateIntersectionTypeDeclaration($member); |
||||||
|
$this->assertDnfGroupIsNotRedundant($member, $group, $classGroups); |
||||||
|
$classGroups[] = $group; |
||||||
|
$hasClassType = true; |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
[$key, $name, $classLike] = $this->getCompoundTypeMemberIdentity($member); |
||||||
|
$lowerName = strtolower($name); |
||||||
|
if (in_array($lowerName, ['mixed', 'void', 'never'], true)) { |
||||||
|
$this->fatalError($member, "Type {$name} can only be used as a standalone type"); |
||||||
|
} |
||||||
|
if (isset($seen[$key])) { |
||||||
|
$this->fatalError($member, "Duplicate type {$name} is redundant"); |
||||||
|
} |
||||||
|
|
||||||
|
if ($lowerName === 'iterable') { |
||||||
|
if ($hasArray) { |
||||||
|
$this->fatalError($member, 'Duplicate type array is redundant'); |
||||||
|
} |
||||||
|
if ($hasTraversable) { |
||||||
|
$this->fatalError($member, 'Duplicate type Traversable is redundant'); |
||||||
|
} |
||||||
|
$hasIterable = true; |
||||||
|
} elseif ($lowerName === 'array') { |
||||||
|
if ($hasIterable) { |
||||||
|
$this->fatalError($member, 'Duplicate type array is redundant'); |
||||||
|
} |
||||||
|
$hasArray = true; |
||||||
|
} elseif ($key === 'class:traversable') { |
||||||
|
if ($hasIterable) { |
||||||
|
$this->fatalError($member, 'Duplicate type Traversable is redundant'); |
||||||
|
} |
||||||
|
$hasTraversable = true; |
||||||
|
} |
||||||
|
|
||||||
|
if ($lowerName === 'bool') { |
||||||
|
if ($hasTrue) { |
||||||
|
$this->fatalError($member, 'Duplicate type true is redundant'); |
||||||
|
} |
||||||
|
if ($hasFalse) { |
||||||
|
$this->fatalError($member, 'Duplicate type false is redundant'); |
||||||
|
} |
||||||
|
$hasBool = true; |
||||||
|
} elseif ($lowerName === 'true') { |
||||||
|
if ($hasBool) { |
||||||
|
$this->fatalError($member, 'Duplicate type true is redundant'); |
||||||
|
} |
||||||
|
if ($hasFalse) { |
||||||
|
$this->fatalError($member, 'Type contains both true and false, bool must be used instead'); |
||||||
|
} |
||||||
|
$hasTrue = true; |
||||||
|
} elseif ($lowerName === 'false') { |
||||||
|
if ($hasBool) { |
||||||
|
$this->fatalError($member, 'Duplicate type false is redundant'); |
||||||
|
} |
||||||
|
if ($hasTrue) { |
||||||
|
$this->fatalError($member, 'Type contains both true and false, bool must be used instead'); |
||||||
|
} |
||||||
|
$hasFalse = true; |
||||||
|
} |
||||||
|
|
||||||
|
if ($lowerName === 'object') { |
||||||
|
$hasObject = true; |
||||||
|
} elseif ($classLike) { |
||||||
|
$hasClassType = true; |
||||||
|
$group = ['members' => [$key => true], 'display' => $name]; |
||||||
|
$this->assertDnfGroupIsNotRedundant($member, $group, $classGroups); |
||||||
|
$classGroups[] = $group; |
||||||
|
} |
||||||
|
|
||||||
|
$seen[$key] = true; |
||||||
|
} |
||||||
|
|
||||||
|
if ($hasObject && $hasClassType) { |
||||||
|
$this->fatalError($type, 'Type ' . $this->compoundTypeToString($type) . ' contains both object and a class type, which is redundant'); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @param array{members: array<string, true>, display: string} $group |
||||||
|
* @param list<array{members: array<string, true>, display: string}> $previousGroups |
||||||
|
*/ |
||||||
|
private function assertDnfGroupIsNotRedundant(NodeAbstract $node, array $group, array $previousGroups): void |
||||||
|
{ |
||||||
|
foreach ($previousGroups as $previous) { |
||||||
|
$sameMembers = count($group['members']) === count($previous['members']) |
||||||
|
&& array_diff_key($group['members'], $previous['members']) === []; |
||||||
|
if ($sameMembers) { |
||||||
|
$this->fatalError( |
||||||
|
$node, |
||||||
|
"Type {$group['display']} is redundant with type {$previous['display']}", |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
$groupContainsPrevious = array_diff_key($previous['members'], $group['members']) === []; |
||||||
|
$previousContainsGroup = array_diff_key($group['members'], $previous['members']) === []; |
||||||
|
if ($groupContainsPrevious || $previousContainsGroup) { |
||||||
|
$moreRestrictive = count($group['members']) > count($previous['members']) ? $group : $previous; |
||||||
|
$lessRestrictive = $moreRestrictive === $group ? $previous : $group; |
||||||
|
$this->fatalError( |
||||||
|
$node, |
||||||
|
"Type {$moreRestrictive['display']} is redundant as it is more restrictive than type {$lessRestrictive['display']}", |
||||||
|
); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @return array{string, string, bool} canonical key, display name, class-like |
||||||
|
*/ |
||||||
|
private function getCompoundTypeMemberIdentity(NodeAbstract $member): array |
||||||
|
{ |
||||||
|
$name = $this->parseIdentifier($member); |
||||||
|
$lowerName = strtolower($name); |
||||||
|
if (in_array($lowerName, ['self', 'parent', 'static'], true)) { |
||||||
|
return ['class:' . $lowerName, $lowerName, true]; |
||||||
|
} |
||||||
|
if ($member instanceof Node\Identifier || isset($this->zendTypeMap[$lowerName])) { |
||||||
|
return ['builtin:' . $lowerName, $lowerName, false]; |
||||||
|
} |
||||||
|
|
||||||
|
if ($member instanceof Node\Name) { |
||||||
|
$resolvedName = $member->getAttribute('resolvedName'); |
||||||
|
if ($resolvedName instanceof Node\Name) { |
||||||
|
$name = $resolvedName->toString(); |
||||||
|
} elseif ($member instanceof Node\Name\FullyQualified) { |
||||||
|
$name = $member->toString(); |
||||||
|
} else { |
||||||
|
$name = $this->getNamespacedClassName($name); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return ['class:' . strtolower(ltrim($name, '\\')), ltrim($name, '\\'), true]; |
||||||
|
} |
||||||
|
|
||||||
|
private function compoundTypeToString(NodeAbstract $type): string |
||||||
|
{ |
||||||
|
if ($type instanceof UnionType) { |
||||||
|
return implode('|', array_map(fn (NodeAbstract $member): string => $this->compoundTypeToString($member), $type->types)); |
||||||
|
} |
||||||
|
if ($type instanceof IntersectionType) { |
||||||
|
return implode('&', array_map(fn (NodeAbstract $member): string => $this->compoundTypeToString($member), $type->types)); |
||||||
|
} |
||||||
|
if ($type instanceof NullableType) { |
||||||
|
return '?' . $this->compoundTypeToString($type->type); |
||||||
|
} |
||||||
|
[, $name] = $this->getCompoundTypeMemberIdentity($type); |
||||||
|
return $name; |
||||||
|
} |
||||||
|
} |
||||||
Loading…
Reference in new issue