From b5bd5ebdc2d07961fe167dad261ab5e77ed312af Mon Sep 17 00:00:00 2001 From: Valentin Udaltsov Date: Sun, 4 Feb 2024 11:05:06 +0300 Subject: [PATCH] Reflect anonymous classes with their actual names, but don't cache their reflections Resolves #22 --- src/ClassLocator/AnonymousClassLocator.php | 26 ------- src/NameContext/AnonymousClassName.php | 61 ++++------------ .../ContextualPhpParserReflector.php | 73 ++++++------------- .../FindAnonymousClassVisitor.php | 53 ++++++++++++++ src/PhpParserReflector/PhpParserReflector.php | 24 +++++- ...esourceVisitor.php => ResourceVisitor.php} | 6 +- src/ReflectionStorage/ReflectionStorage.php | 3 +- src/Resource.php | 5 -- src/TyphoonReflector.php | 16 +++- tests/unit/ReflectorCompatibilityTest.php | 48 ++++-------- 10 files changed, 145 insertions(+), 170 deletions(-) delete mode 100644 src/ClassLocator/AnonymousClassLocator.php create mode 100644 src/PhpParserReflector/FindAnonymousClassVisitor.php rename src/PhpParserReflector/{ReflectResourceVisitor.php => ResourceVisitor.php} (84%) diff --git a/src/ClassLocator/AnonymousClassLocator.php b/src/ClassLocator/AnonymousClassLocator.php deleted file mode 100644 index bf47e64..0000000 --- a/src/ClassLocator/AnonymousClassLocator.php +++ /dev/null @@ -1,26 +0,0 @@ -file); - } -} diff --git a/src/NameContext/AnonymousClassName.php b/src/NameContext/AnonymousClassName.php index 2672641..529ab25 100644 --- a/src/NameContext/AnonymousClassName.php +++ b/src/NameContext/AnonymousClassName.php @@ -4,42 +4,34 @@ namespace Typhoon\Reflection\NameContext; -use Typhoon\Reflection\ReflectionException; - /** * @internal * @psalm-internal Typhoon\Reflection * @psalm-immutable + * @template TObject of object + * @psalm-suppress PossiblyUnusedProperty */ final class AnonymousClassName { /** + * @param class-string $name * @param non-empty-string $file * @param int<0, max> $line * @param ?class-string $superType - * @param ?int<0, max> $rtdKeyCounter + * @param int<0, max> $rtdKeyCounter */ - public function __construct( + private function __construct( + public readonly string $name, public readonly string $file, public readonly int $line, - public readonly ?string $superType = null, - public readonly ?int $rtdKeyCounter = null, + public readonly ?string $superType, + public readonly int $rtdKeyCounter, ) {} /** - * @psalm-pure - * @template TName of string - * @param TName $name - * @return TName - */ - public static function normalizeName(string $name): string - { - /** @var TName */ - return self::tryFromString($name)?->toStringWithoutRtdKeyCounter() ?? $name; - } - - /** - * @psalm-pure + * @template TNewObject of object + * @param string|class-string $name + * @return ($name is class-string ? null|self : null|self) */ public static function tryFromString(string $name): ?self { @@ -47,7 +39,7 @@ public static function tryFromString(string $name): ?self return null; } - if (preg_match('/^\\\?(.+)@anonymous\x00(.+):(\d+)(?:\$(\w+))?$/', $name, $matches) !== 1) { + if (preg_match('/^\\\?(.+)@anonymous\x00(.+):(\d+)\$(\w+)$/', $name, $matches) !== 1) { return null; } @@ -57,10 +49,11 @@ public static function tryFromString(string $name): ?self $file = $matches[2]; /** @var int<0, max> */ $line = (int) $matches[3]; - /** @var ?int<0, max> */ - $rtdKeyCounter = isset($matches[4]) ? hexdec($matches[4]) : null; + /** @var int<0, max> */ + $rtdKeyCounter = hexdec($matches[4]); - return new self(file: $file, line: $line, superType: $superType, rtdKeyCounter: $rtdKeyCounter); + /** @var class-string $name */ + return new self($name, $file, $line, $superType, $rtdKeyCounter); } /** @@ -90,26 +83,4 @@ public static function findDeclared(?string $file = null, ?int $line = null): ar return $names; } - - /** - * @return class-string this is not actually true, but it's easier to put it that way - */ - public function toStringWithoutRtdKeyCounter(): string - { - /** @var class-string */ - return sprintf("%s@anonymous\x00%s:%d", $this->superType ?? 'class', $this->file, $this->line); - } - - /** - * @return class-string - */ - public function toString(): string - { - if ($this->rtdKeyCounter === null) { - throw new ReflectionException(); - } - - /** @var class-string */ - return $this->toStringWithoutRtdKeyCounter() . '$' . dechex($this->rtdKeyCounter); - } } diff --git a/src/PhpParserReflector/ContextualPhpParserReflector.php b/src/PhpParserReflector/ContextualPhpParserReflector.php index f0f021f..0a61adf 100644 --- a/src/PhpParserReflector/ContextualPhpParserReflector.php +++ b/src/PhpParserReflector/ContextualPhpParserReflector.php @@ -16,14 +16,12 @@ use Typhoon\Reflection\ClassReflection; use Typhoon\Reflection\ClassReflection\ClassReflector; use Typhoon\Reflection\MethodReflection; -use Typhoon\Reflection\NameContext\AnonymousClassName; use Typhoon\Reflection\ParameterReflection; use Typhoon\Reflection\PhpDocParser\ContextualPhpDocTypeReflector; use Typhoon\Reflection\PhpDocParser\PhpDoc; use Typhoon\Reflection\PhpDocParser\PhpDocParser; use Typhoon\Reflection\PropertyReflection; use Typhoon\Reflection\ReflectionException; -use Typhoon\Reflection\Resource; use Typhoon\Reflection\TemplateReflection; use Typhoon\Reflection\TypeContext\TypeContext; use Typhoon\Reflection\TypeReflection; @@ -38,56 +36,45 @@ final class ContextualPhpParserReflector { private ContextualPhpDocTypeReflector $phpDocTypeReflector; + private bool $internal; + + /** + * @param non-empty-string $file + * @param ?non-empty-string $extension + */ public function __construct( private readonly PhpDocParser $phpDocParser, private readonly ClassReflector $classReflector, private TypeContext $typeContext, - private readonly Resource $resource, + private readonly string $file, + private readonly ?string $extension = null, ) { $this->phpDocTypeReflector = new ContextualPhpDocTypeReflector($typeContext); + $this->internal = $extension !== null; } /** * @return class-string */ - public function resolveClassName(Stmt\ClassLike $node): string + public function resolveClassName(Node\Identifier $name): string { - if ($node->name !== null) { - return $this->typeContext->resolveNameAsClass($node->name->toString()); - } - - if (!$node instanceof Stmt\Class_) { - throw new ReflectionException(sprintf('Unexpected %s with null name.', $node::class)); - } - - $line = $node->getLine(); - - if ($line < 0) { - throw new ReflectionException(sprintf('Unexpected non-positive line %d for anonymous class node.', $line)); - } - - $name = new AnonymousClassName( - file: $this->resource->file, - line: $line, - superType: $this->resolveAnonymousClassSuperType($node), - ); - - return $name->toStringWithoutRtdKeyCounter(); + return $this->typeContext->resolveNameAsClass($name->name); } /** - * @param ?class-string $name + * @template TObject of object + * @param class-string $name + * @return ClassReflection */ - public function reflectClass(Stmt\ClassLike $node, ?string $name = null): ClassReflection + public function reflectClass(Stmt\ClassLike $node, string $name): ClassReflection { - $name ??= $this->resolveClassName($node); $phpDoc = $this->parsePhpDoc($node); return $this->executeWithTypes(types::atClass($name), $phpDoc, fn(): ClassReflection => new ClassReflection( name: $name, - internal: $this->resource->isInternal(), - extensionName: $this->resource->extension, - file: $this->resource->file, + internal: $this->internal, + extensionName: $this->extension, + file: $this->file, startLine: $node->getStartLine() > 0 ? $node->getStartLine() : null, endLine: $node->getEndLine() > 0 ? $node->getEndLine() : null, docComment: $this->reflectDocComment($node), @@ -113,22 +100,6 @@ public function __clone() $this->phpDocTypeReflector = new ContextualPhpDocTypeReflector($this->typeContext); } - /** - * @return ?class-string - */ - private function resolveAnonymousClassSuperType(Stmt\Class_ $node): ?string - { - if ($node->extends !== null) { - return $this->typeContext->resolveNameAsClass($node->extends->toCodeString()); - } - - foreach ($node->implements as $interface) { - return $this->typeContext->resolveNameAsClass($interface->toCodeString()); - } - - return null; - } - /** * @param class-string $class */ @@ -397,9 +368,9 @@ class: $class, templates: $this->reflectTemplatesFromContext($phpDoc), modifiers: $this->reflectMethodModifiers($node, $interface), docComment: $this->reflectDocComment($node), - internal: $this->resource->isInternal(), - extensionName: $this->resource->extension, - file: $this->resource->file, + internal: $this->internal, + extensionName: $this->extension, + file: $this->file, startLine: $node->getStartLine() > 0 ? $node->getStartLine() : null, endLine: $node->getEndLine() > 0 ? $node->getEndLine() : null, returnsReference: $node->byRef, @@ -426,7 +397,7 @@ class: $class, */ private function reflectDocComment(Node $node): ?string { - if ($this->resource->isInternal()) { + if ($this->internal) { return null; } diff --git a/src/PhpParserReflector/FindAnonymousClassVisitor.php b/src/PhpParserReflector/FindAnonymousClassVisitor.php new file mode 100644 index 0000000..d741ad8 --- /dev/null +++ b/src/PhpParserReflector/FindAnonymousClassVisitor.php @@ -0,0 +1,53 @@ +node ?? throw new ReflectionException(); + } + + public function enterNode(Node $node): ?int + { + if ($node->getLine() < $this->name->line) { + return null; + } + + if ($node->getLine() > $this->name->line) { + return NodeTraverser::STOP_TRAVERSAL; + } + + if (!$node instanceof Class_ || $node->name !== null) { + return null; + } + + if ($this->node !== null) { + throw new ReflectionException(sprintf('More than 1 anonymous class at %s:%d.', $this->name->file, $this->name->line)); + } + + $this->node = $node; + + return null; + } +} diff --git a/src/PhpParserReflector/PhpParserReflector.php b/src/PhpParserReflector/PhpParserReflector.php index ced5ee4..ef3e244 100644 --- a/src/PhpParserReflector/PhpParserReflector.php +++ b/src/PhpParserReflector/PhpParserReflector.php @@ -7,7 +7,9 @@ use PhpParser\NodeTraverser; use PhpParser\NodeVisitor; use PhpParser\Parser as PhpParser; +use Typhoon\Reflection\ClassReflection; use Typhoon\Reflection\ClassReflection\ClassReflector; +use Typhoon\Reflection\NameContext\AnonymousClassName; use Typhoon\Reflection\NameContext\NameContext; use Typhoon\Reflection\NameContext\NameContextVisitor; use Typhoon\Reflection\PhpDocParser\PhpDocParser; @@ -38,11 +40,12 @@ public function reflectResource(Resource $resource, ReflectionStorage $reflectio phpDocParser: $this->phpDocParser, classReflector: $classReflector, typeContext: $typeContext, - resource: $resource, + file: $resource->file, + extension: $resource->extension, ); $this->parseAndTraverse($contents, [ new NameContextVisitor($nameContext), - new ReflectResourceVisitor( + new ResourceVisitor( reflectionStorage: $reflectionStorage, reflector: $reflector, changeDetector: ChangeDetector::fromFile($resource->file, $contents), @@ -50,6 +53,23 @@ classReflector: $classReflector, ]); } + public function reflectAnonymousClass(AnonymousClassName $name, ClassReflector $classReflector): ClassReflection + { + $contents = exceptionally(static fn(): string|false => file_get_contents($name->file)); + $nameContext = new NameContext(); + $visitor = new FindAnonymousClassVisitor($name); + $this->parseAndTraverse($contents, [new NameContextVisitor($nameContext), $visitor]); + $node = $visitor->node(); + $reflector = new ContextualPhpParserReflector( + phpDocParser: $this->phpDocParser, + classReflector: $classReflector, + typeContext: new TypeContext($nameContext), + file: $name->file, + ); + + return $reflector->reflectClass($node, $name->name); + } + /** * @param list $visitors */ diff --git a/src/PhpParserReflector/ReflectResourceVisitor.php b/src/PhpParserReflector/ResourceVisitor.php similarity index 84% rename from src/PhpParserReflector/ReflectResourceVisitor.php rename to src/PhpParserReflector/ResourceVisitor.php index dc9e894..794c357 100644 --- a/src/PhpParserReflector/ReflectResourceVisitor.php +++ b/src/PhpParserReflector/ResourceVisitor.php @@ -15,7 +15,7 @@ * @internal * @psalm-internal Typhoon\Reflection\PhpParserReflector */ -final class ReflectResourceVisitor extends NodeVisitorAbstract +final class ResourceVisitor extends NodeVisitorAbstract { public function __construct( private readonly ReflectionStorage $reflectionStorage, @@ -25,8 +25,8 @@ public function __construct( public function enterNode(Node $node): ?int { - if ($node instanceof ClassLike) { - $name = $this->reflector->resolveClassName($node); + if ($node instanceof ClassLike && $node->name !== null) { + $name = $this->reflector->resolveClassName($node->name); $reflector = clone $this->reflector; $this->reflectionStorage->setReflector( class: ClassReflection::class, diff --git a/src/ReflectionStorage/ReflectionStorage.php b/src/ReflectionStorage/ReflectionStorage.php index fc62eba..5ab1eeb 100644 --- a/src/ReflectionStorage/ReflectionStorage.php +++ b/src/ReflectionStorage/ReflectionStorage.php @@ -5,7 +5,6 @@ namespace Typhoon\Reflection\ReflectionStorage; use Psr\SimpleCache\CacheInterface; -use Typhoon\Reflection\NameContext\AnonymousClassName; use Typhoon\Reflection\ReflectionException; /** @@ -154,6 +153,6 @@ private function ensureCached(string $key, Reflection $reflection): void */ private function key(string $class, string $name): string { - return hash('xxh128', $class . '#' . AnonymousClassName::normalizeName($name)); + return hash('xxh128', $class . '#' . $name); } } diff --git a/src/Resource.php b/src/Resource.php index 0c5aa05..658f56e 100644 --- a/src/Resource.php +++ b/src/Resource.php @@ -27,9 +27,4 @@ public function __construct(string $file, ?string $extension = null) $this->extension = $extension; $this->file = $file; } - - public function isInternal(): bool - { - return $this->extension !== null; - } } diff --git a/src/TyphoonReflector.php b/src/TyphoonReflector.php index 1997356..6a9c31a 100644 --- a/src/TyphoonReflector.php +++ b/src/TyphoonReflector.php @@ -7,12 +7,12 @@ use PhpParser\Parser as PhpParser; use PhpParser\ParserFactory; use Psr\SimpleCache\CacheInterface; -use Typhoon\Reflection\ClassLocator\AnonymousClassLocator; use Typhoon\Reflection\ClassLocator\ClassLocatorChain; use Typhoon\Reflection\ClassLocator\ComposerClassLocator; use Typhoon\Reflection\ClassLocator\NativeReflectionLocator; use Typhoon\Reflection\ClassLocator\PhpStormStubsClassLocator; use Typhoon\Reflection\ClassReflection\ClassReflector; +use Typhoon\Reflection\NameContext\AnonymousClassName; use Typhoon\Reflection\NativeReflector\NativeReflector; use Typhoon\Reflection\PhpDocParser\PhpDocParser; use Typhoon\Reflection\PhpDocParser\PHPStanOverPsalmOverOthersTagPrioritizer; @@ -74,7 +74,6 @@ public static function defaultClassLocators(): array $classLocators[] = new PhpStormStubsClassLocator(); } - $classLocators[] = new AnonymousClassLocator(); $classLocators[] = new NativeReflectionLocator(); return $classLocators; @@ -93,6 +92,10 @@ public function classExists(string $name): bool return true; } + if (str_contains($name, '@')) { + return false; + } + /** @var non-empty-string $name */ return $this->reflectionStorage->exists( class: ClassReflection::class, @@ -113,6 +116,15 @@ public function reflectClass(string|object $nameOrObject): ClassReflection $name = $nameOrObject; } + $anonymousName = AnonymousClassName::tryFromString($name); + + if ($anonymousName !== null) { + $reflection = $this->phpParserReflector->reflectAnonymousClass($anonymousName, $this); + $reflection->__initialize($this); + + return $reflection; + } + $reflection = $this->reflectionStorage->get( class: ClassReflection::class, name: $name, diff --git a/tests/unit/ReflectorCompatibilityTest.php b/tests/unit/ReflectorCompatibilityTest.php index 856506a..c70e0fa 100644 --- a/tests/unit/ReflectorCompatibilityTest.php +++ b/tests/unit/ReflectorCompatibilityTest.php @@ -8,6 +8,7 @@ use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\RequiresPhp; use PHPUnit\Framework\TestCase; +use Typhoon\Reflection\ClassLocator\NativeReflectionLocator; use Typhoon\Reflection\NameContext\AnonymousClassName; #[CoversNothing] @@ -22,7 +23,7 @@ final class ReflectorCompatibilityTest extends TestCase public static function classLocators(): \Generator { yield 'TyphoonReflector::defaultClassLocators()' => TyphoonReflector::defaultClassLocators(); - yield 'No locators' => []; + yield 'NativeReflectionLocator' => [new NativeReflectionLocator()]; } /** @@ -40,7 +41,7 @@ public static function classes(): \Generator } foreach ($anonymousNames as $anonymousName) { - yield 'anonymous at line ' . $anonymousName->line . ' using ' . $classLocatorsName => [$classLocators, $anonymousName->toString()]; + yield 'anonymous at line ' . $anonymousName->line . ' using ' . $classLocatorsName => [$classLocators, $anonymousName->name]; } } } @@ -95,9 +96,9 @@ public function testItReflectsReadonlyClasses(?array $classLocators, string $cla private function assertClassEquals(\ReflectionClass $native, ClassReflection $typhoon): void { - self::assertSame($this->resolveNativeClassName($native->name), $typhoon->name, 'class.name'); - self::assertSame($this->resolveNativeClassName($native->getName()), $typhoon->getName(), 'class.getName()'); - self::assertSame($this->resolveNativeClassName($native->getShortName()), $typhoon->getShortName(), 'class.getShortName()'); + self::assertSame($native->name, $typhoon->name, 'class.name'); + self::assertSame($native->getName(), $typhoon->getName(), 'class.getName()'); + self::assertSame($native->getShortName(), $typhoon->getShortName(), 'class.getShortName()'); // self::assertSame($native->getAttributes(), $typhoon->getAttributes(), 'class.getAttributes()'); // self::assertSame($native->getConstant(), $typhoon->getConstant(), 'class.getConstant()'); // self::assertSame($native->getConstants(), $typhoon->getConstants(), 'class.getConstants()'); @@ -134,7 +135,7 @@ private function assertClassEquals(\ReflectionClass $native, ClassReflection $ty self::assertSame($native->isTrait(), $typhoon->isTrait(), 'class.isTrait()'); self::assertSame($native->isUserDefined(), $typhoon->isUserDefined(), 'class.isUserDefined()'); - if ($native->isInstantiable() && !$native->isAnonymous()) { + if ($native->isInstantiable()) { // self::assertEquals($native->newInstance(), $typhoon->newInstance(), 'class.newInstance()'); // self::assertEquals($native->newInstanceArgs(), $typhoon->newInstanceArgs(), 'class.newInstanceArgs()'); self::assertEquals($native->newInstanceWithoutConstructor(), $typhoon->newInstanceWithoutConstructor(), 'class.newInstanceWithoutConstructor()'); @@ -174,16 +175,11 @@ private function assertClassEquals(\ReflectionClass $native, ClassReflection $ty private function assertPropertyEquals(\ReflectionProperty $native, PropertyReflection $typhoon, string $messagePrefix): void { - self::assertSame($this->resolveNativeClassName($native->class), $typhoon->class, $messagePrefix . '.class'); - - if (!$native->getDeclaringClass()->isAnonymous()) { - // TODO: cannot create native reflection for anonymous with customized name - self::assertSame($native->getDefaultValue(), $typhoon->getDefaultValue(), $messagePrefix . '.getDefaultValue()'); - } - + self::assertSame($native->class, $typhoon->class, $messagePrefix . '.class'); + self::assertSame($native->getDefaultValue(), $typhoon->getDefaultValue(), $messagePrefix . '.getDefaultValue()'); self::assertSame($native->name, $typhoon->name, $messagePrefix . '.name'); // self::assertSame($native->getAttributes(), $typhoon->getAttributes(), $messagePrefix . '.getAttributes()'); - self::assertSame($this->resolveNativeClassName($native->getDeclaringClass()->name), $typhoon->getDeclaringClass()->name, $messagePrefix . '.getDeclaringClass()'); + self::assertSame($native->getDeclaringClass()->name, $typhoon->getDeclaringClass()->name, $messagePrefix . '.getDeclaringClass()'); self::assertSame($native->getDocComment() ?: null, $typhoon->getDocComment(), $messagePrefix . '.getDocComment()'); self::assertSame($native->getModifiers(), $typhoon->getModifiers(), $messagePrefix . '.getModifiers()'); self::assertSame($native->getName(), $typhoon->getName(), $messagePrefix . '.getName()'); @@ -202,7 +198,7 @@ private function assertPropertyEquals(\ReflectionProperty $native, PropertyRefle private function assertMethodEquals(\ReflectionMethod $native, MethodReflection $typhoon, string $messagePrefix): void { - self::assertSame($this->resolveNativeClassName($native->class), $typhoon->class, $messagePrefix . '.class'); + self::assertSame($native->class, $typhoon->class, $messagePrefix . '.class'); self::assertSame($native->name, $typhoon->name, $messagePrefix . '.name'); // self::assertSame($native->getAttributes(), $typhoon->getAttributes(), $messagePrefix . '.getAttributes()'); // self::assertSame($native->getClosure(), $typhoon->getClosure(), $messagePrefix . '.getClosure()'); @@ -210,7 +206,7 @@ private function assertMethodEquals(\ReflectionMethod $native, MethodReflection // self::assertSame($native->getClosureScopeClass(), $typhoon->getClosureScopeClass(), $messagePrefix . '.getClosureScopeClass()'); // self::assertSame($native->getClosureThis(), $typhoon->getClosureThis(), $messagePrefix . '.getClosureThis()'); // self::assertSame($native->getClosureUsedVariables(), $typhoon->getClosureUsedVariables(), $messagePrefix . '.getClosureUsedVariables()'); - self::assertSame($this->resolveNativeClassName($native->getDeclaringClass()->name), $typhoon->getDeclaringClass()->name, $messagePrefix . '.getDeclaringClass()'); + self::assertSame($native->getDeclaringClass()->name, $typhoon->getDeclaringClass()->name, $messagePrefix . '.getDeclaringClass()'); self::assertSame($native->getDocComment() ?: null, $typhoon->getDocComment(), $messagePrefix . '.getDocComment()'); // self::assertSame($native->getEndLine(), $typhoon->getEndLine(), $messagePrefix . '.getEndLine()'); // self::assertSame($native->getExtension(), $typhoon->getExtension(), $messagePrefix . '.getExtension()'); @@ -261,7 +257,7 @@ private function assertParameterEquals(\ReflectionParameter $native, ParameterRe self::assertSame($native->name, $typhoon->name, $messagePrefix . '.name'); self::assertSame($native->canBePassedByValue(), $typhoon->canBePassedByValue(), $messagePrefix . '.canBePassedByValue()'); // self::assertSame($native->getAttributes(), $typhoon->getAttributes(), $messagePrefix . '.getAttributes()'); - self::assertSame($this->resolveNativeClassName($native->getDeclaringClass()?->name), $typhoon->getDeclaringClass()?->name, $messagePrefix . '.getDeclaringClass()'); + self::assertSame($native->getDeclaringClass()?->name, $typhoon->getDeclaringClass()?->name, $messagePrefix . '.getDeclaringClass()'); self::assertSame($native->getDeclaringFunction()->name, $typhoon->getDeclaringFunction()->name, $messagePrefix . '.getDeclaringFunction()'); // self::assertSame($native->getDefaultValueConstantName(), $typhoon->getDefaultValueConstantName(), $messagePrefix . '.getDefaultValueConstantName()'); self::assertSame($native->getName(), $typhoon->getName(), $messagePrefix . '.getName()'); @@ -283,22 +279,6 @@ private function assertParameterEquals(\ReflectionParameter $native, ParameterRe */ private function assertSameNames(array $nativeReflections, array $typhoonReflections, string $message): void { - self::assertSame( - array_map($this->resolveNativeClassName(...), array_column($nativeReflections, 'name')), - array_column($typhoonReflections, 'name'), - $message, - ); - } - - /** - * @return ($name is null ? null : string) - */ - private function resolveNativeClassName(?string $name): ?string - { - if ($name === null) { - return null; - } - - return AnonymousClassName::normalizeName($name); + self::assertSame(array_column($nativeReflections, 'name'), array_column($typhoonReflections, 'name'), $message); } }