Skip to content

Commit

Permalink
Reflect anonymous classes with their actual names, but don't cache th…
Browse files Browse the repository at this point in the history
…eir reflections

Resolves #22
  • Loading branch information
vudaltsov committed Feb 5, 2024
1 parent b046a83 commit b5bd5eb
Show file tree
Hide file tree
Showing 10 changed files with 145 additions and 170 deletions.
26 changes: 0 additions & 26 deletions src/ClassLocator/AnonymousClassLocator.php

This file was deleted.

61 changes: 16 additions & 45 deletions src/NameContext/AnonymousClassName.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,50 +4,42 @@

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<TObject> $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<TNewObject> $name
* @return ($name is class-string ? null|self<TNewObject> : null|self)
*/
public static function tryFromString(string $name): ?self
{
if (!str_contains($name, '@')) {
return null;
}

if (preg_match('/^\\\?(.+)@anonymous\x00(.+):(\d+)(?:\$(\w+))?$/', $name, $matches) !== 1) {
if (preg_match('/^\\\?(.+)@anonymous\x00(.+):(\d+)\$(\w+)$/', $name, $matches) !== 1) {
return null;
}

Expand All @@ -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);
}

/**
Expand Down Expand Up @@ -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);
}
}
73 changes: 22 additions & 51 deletions src/PhpParserReflector/ContextualPhpParserReflector.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<TObject> $name
* @return ClassReflection<TObject>
*/
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),
Expand All @@ -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
*/
Expand Down Expand Up @@ -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,
Expand All @@ -426,7 +397,7 @@ class: $class,
*/
private function reflectDocComment(Node $node): ?string
{
if ($this->resource->isInternal()) {
if ($this->internal) {
return null;
}

Expand Down
53 changes: 53 additions & 0 deletions src/PhpParserReflector/FindAnonymousClassVisitor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

declare(strict_types=1);

namespace Typhoon\Reflection\PhpParserReflector;

use PhpParser\Node;
use PhpParser\Node\Stmt\Class_;
use PhpParser\NodeTraverser;
use PhpParser\NodeVisitorAbstract;
use Typhoon\Reflection\NameContext\AnonymousClassName;
use Typhoon\Reflection\ReflectionException;

/**
* @internal
* @psalm-internal Typhoon\Reflection\PhpParserReflector
*/
final class FindAnonymousClassVisitor extends NodeVisitorAbstract
{
private ?Class_ $node = null;

public function __construct(
private readonly AnonymousClassName $name,
) {}

public function node(): Class_
{
return $this->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;
}
}
24 changes: 22 additions & 2 deletions src/PhpParserReflector/PhpParserReflector.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -38,18 +40,36 @@ 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),
),
]);
}

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<NodeVisitor> $visitors
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down
Loading

0 comments on commit b5bd5eb

Please sign in to comment.