Skip to content

Commit

Permalink
Added type aliases support
Browse files Browse the repository at this point in the history
  • Loading branch information
vudaltsov committed Feb 2, 2024
1 parent c739832 commit e16fd20
Show file tree
Hide file tree
Showing 11 changed files with 295 additions and 40 deletions.
2 changes: 2 additions & 0 deletions psalm.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@
<file name="stubs/PHPStan/PhpDocParser/Ast/ConstExpr/ConstFetchNode.phpstub"/>
<file name="stubs/PHPStan/PhpDocParser/Ast/PhpDoc/PhpDocTagNode.phpstub"/>
<file name="stubs/PHPStan/PhpDocParser/Ast/PhpDoc/TemplateTagValueNode.phpstub"/>
<file name="stubs/PHPStan/PhpDocParser/Ast/PhpDoc/TypeAliasImportTagValueNode.phpstub"/>
<file name="stubs/PHPStan/PhpDocParser/Ast/PhpDoc/TypeAliasTagValueNode.phpstub"/>
<file name="stubs/PHPStan/PhpDocParser/Ast/Type/CallableTypeNode.phpstub"/>
<file name="stubs/PHPStan/PhpDocParser/Ast/Type/GenericTypeNode.phpstub"/>
<file name="stubs/PHPStan/PhpDocParser/Ast/Type/IdentifierTypeNode.phpstub"/>
Expand Down
7 changes: 7 additions & 0 deletions src/ClassReflection.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ final class ClassReflection extends ClassReflectorAwareReflection implements Roo
* @param ?positive-int $endLine
* @param ?non-empty-string $docComment
* @param list<AttributeReflection> $attributes
* @param array<non-empty-string, Type\Type> $typeAliases
* @param list<TemplateReflection> $templates
* @param int-mask-of<self::IS_*> $modifiers
* @param list<Type\NamedObjectType> $ownInterfaceTypes
Expand All @@ -59,6 +60,7 @@ public function __construct(
private readonly ?int $endLine,
private readonly ?string $docComment,
private readonly array $attributes,
private readonly array $typeAliases,
private readonly array $templates,
private readonly bool $interface,
private readonly bool $enum,
Expand Down Expand Up @@ -175,6 +177,11 @@ public function getAttributes(): array
return $this->attributes;
}

public function getTypeAlias(string $name): Type\Type
{
return $this->typeAliases[$name] ?? throw new ReflectionException();
}

/**
* @return list<TemplateReflection>
*/
Expand Down
68 changes: 68 additions & 0 deletions src/PhpDocParser/PhpDoc.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\TypeAliasImportTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\TypeAliasTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\VarTagValueNode;
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
Expand Down Expand Up @@ -51,6 +53,16 @@ final class PhpDoc
*/
private ?array $implementedTypes = null;

/**
* @var ?list<TypeAliasTagValueNode>
*/
private ?array $typeAliases = null;

/**
* @var ?list<TypeAliasImportTagValueNode>
*/
private ?array $typeAliasImports = null;

/**
* @internal
* @psalm-internal Typhoon\Reflection\PhpDocParser
Expand Down Expand Up @@ -294,6 +306,62 @@ public function implementedTypes(): array
);
}

/**
* @return list<TypeAliasTagValueNode>
*/
public function typeAliases(): array
{
if ($this->typeAliases !== null) {
return $this->typeAliases;
}

$typeAliasesByAlias = [];

foreach ($this->tags as $key => $tag) {
if (!$tag->value instanceof TypeAliasTagValueNode) {
continue;
}

/** @var PhpDocTagNode<TypeAliasTagValueNode> $tag */
if ($this->shouldReplaceTag($typeAliasesByAlias[$tag->value->alias] ?? null, $tag)) {
$typeAliasesByAlias[$tag->value->alias] = $tag;
}

unset($this->tags[$key]);
}

return $this->typeAliases = array_column($typeAliasesByAlias, 'value');
}

/**
* @return list<TypeAliasImportTagValueNode>
*/
public function typeAliasImports(): array
{
if ($this->typeAliasImports !== null) {
return $this->typeAliasImports;
}

$typeAliasImportsByAlias = [];

foreach ($this->tags as $key => $tag) {
if (!$tag->value instanceof TypeAliasImportTagValueNode) {
continue;
}

/** @var PhpDocTagNode<TypeAliasImportTagValueNode> $tag */
$alias = $tag->value->importedAs ?? $tag->value->importedAlias;

if ($this->shouldReplaceTag($typeAliasImportsByAlias[$alias] ?? null, $tag)) {
$typeAliasImportsByAlias[$alias] = $tag;
}

unset($this->tags[$key]);
}

return $this->typeAliasImports = array_column($typeAliasImportsByAlias, 'value');
}

/**
* @template TCurrentValueNode of PhpDocTagValueNode
* @template TNewValueNode of PhpDocTagValueNode
Expand Down
5 changes: 4 additions & 1 deletion src/Reflector/Context.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ public function parseFile(string $file, ?string $extension = null): void
new PhpDocParsingVisitor($this->phpDocParser),
new NameContextVisitor($nameContext),
new DiscoveringVisitor(
classReflector: $this,
parsingContext: $this,
typeContext: new TypeContext($nameContext, $this),
resource: $resource,
Expand Down Expand Up @@ -98,7 +99,9 @@ public function classExists(string $name): bool
}

/** @var non-empty-string $name */
return $this->classLoader->loadClass($this, $name) && isset($this->reflections[ClassReflection::class][$name]);
return $this->classLoader->loadClass($this, $name)
&& isset($this->reflections[ClassReflection::class][$name])
&& $this->reflections[ClassReflection::class][$name] !== false;
}

/**
Expand Down
2 changes: 2 additions & 0 deletions src/Reflector/DiscoveringVisitor.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
final class DiscoveringVisitor extends NodeVisitorAbstract
{
public function __construct(
private readonly ClassReflector $classReflector,
private readonly ParsingContext $parsingContext,
private readonly TypeContext $typeContext,
private readonly Resource $resource,
Expand All @@ -33,6 +34,7 @@ public function enterNode(Node $node): ?int
$this->parsingContext->registerClassReflector(
name: $name,
reflector: fn(): ClassReflection => PhpParserReflector::reflectClass(
classReflector: $this->classReflector,
typeContext: $typeContext,
resource: $this->resource,
node: $node,
Expand Down
1 change: 1 addition & 0 deletions src/Reflector/NativeReflectionReflector.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ public function reflectClass(\ReflectionClass $class): ClassReflection
endLine: $class->getEndLine() ?: null,
docComment: $class->getDocComment() ?: null,
attributes: $this->reflectAttributes($class->getAttributes(), [$class->name]),
typeAliases: [],
templates: [],
interface: $class->isInterface(),
enum: $class->isEnum(),
Expand Down
85 changes: 68 additions & 17 deletions src/Reflector/PhpParserReflector.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
use PhpParser\Node\Stmt;
use PhpParser\NodeTraverser;
use PhpParser\NodeVisitorAbstract;
use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use Typhoon\Reflection\AttributeReflection;
use Typhoon\Reflection\ClassReflection;
Expand All @@ -33,56 +32,107 @@
final class PhpParserReflector
{
private function __construct(
private readonly ClassReflector $classReflector,
private readonly TypeContext $typeContext,
private readonly Resource $resource,
) {}

/**
* @param class-string $name
*/
public static function reflectClass(TypeContext $typeContext, Resource $resource, Stmt\ClassLike $node, string $name): ClassReflection
{
return (new self($typeContext, $resource))->doReflectClass($node, $name);
public static function reflectClass(
ClassReflector $classReflector,
TypeContext $typeContext,
Resource $resource,
Stmt\ClassLike $node,
string $name,
): ClassReflection {
return (new self($classReflector, $typeContext, $resource))->doReflectClass($node, $name);
}

/**
* @param list<TemplateTagValueNode> $nodes
* @return list<TemplateReflection>
*/
private function reflectTemplates(array $nodes): array
private function reflectTemplatesFromContext(PhpDoc $phpDoc): array
{
$reflections = [];

foreach ($nodes as $position => $node) {
foreach ($phpDoc->templates() as $position => $node) {
$templateType = $this->typeContext->resolveNameAsType($node->name);
\assert($templateType instanceof Type\TemplateType);
$reflections[] = new TemplateReflection(
position: $position,
name: $node->name,
constraint: $this->safelyReflectPhpDocType($node->bound) ?? types::mixed,
constraint: $templateType->constraint,
variance: PhpDoc::templateTagVariance($node),
);
}

return $reflections;
}

/**
* @return array<non-empty-string, Type\Type>
*/
private function reflectTypeAliasesFromContext(PhpDoc $phpDoc): array
{
$typeAliases = [];

foreach ($phpDoc->typeAliases() as $typeAlias) {
$typeAliases[$typeAlias->alias] = $this->typeContext->resolveNameAsType($typeAlias->alias);
}

foreach ($phpDoc->typeAliasImports() as $typeImport) {
$alias = $typeImport->importedAs ?? $typeImport->importedAlias;
$typeAliases[$alias] = $this->typeContext->resolveNameAsType($alias);
}

return $typeAliases;
}

/**
* @template TReturn
* @param \Closure(): TReturn $do
* @param \Closure(): TReturn $action
* @return TReturn
*/
private function inContextOfTemplates(Type\AtClass|Type\AtMethod $declaredAt, PhpDoc $phpDoc, \Closure $do): mixed
private function executeWithTypes(Type\AtClass|Type\AtMethod $declaredAt, PhpDoc $phpDoc, \Closure $action): mixed
{
$templateTypes = [];
$class = match (true) {
$declaredAt instanceof Type\AtClass => $declaredAt->name,
$declaredAt instanceof Type\AtMethod => $declaredAt->class,
default => null,
};
$types = [];

foreach ($phpDoc->typeAliases() as $typeAlias) {
$types[$typeAlias->alias] = fn(): Type\Type => $this->safelyReflectPhpDocType($typeAlias->type) ?? types::mixed;
}

foreach ($phpDoc->typeAliasImports() as $typeImport) {
$alias = $typeImport->importedAs ?? $typeImport->importedAlias;
$types[$alias] = function () use ($class, $typeImport): Type\Type {
$fromClass = $this->typeContext->resolveNameAsClass($typeImport->importedFrom->name);

if ($fromClass === $class) {
return $this->typeContext->resolveNameAsType($typeImport->importedAlias);
}

return $this
->classReflector
->reflectClass($fromClass)
->getTypeAlias($typeImport->importedAlias);
};
}

foreach ($phpDoc->templates() as $template) {
$templateTypes[$template->name] = fn(): Type\TemplateType => types::template(
$types[$template->name] = fn(): Type\TemplateType => types::template(
name: $template->name,
declaredAt: $declaredAt,
constraint: $this->safelyReflectPhpDocType($template->bound) ?? types::mixed,
);
}

return $this->typeContext->inContextOfTemplates($templateTypes, $do);
return $this->typeContext->executeWithTypes($action, $types);
}

/**
Expand All @@ -92,7 +142,7 @@ private function doReflectClass(Stmt\ClassLike $node, string $name): ClassReflec
{
$phpDoc = PhpDocParsingVisitor::fromNode($node);

return $this->inContextOfTemplates(types::atClass($name), $phpDoc, fn(): ClassReflection => new ClassReflection(
return $this->executeWithTypes(types::atClass($name), $phpDoc, fn(): ClassReflection => new ClassReflection(
name: $name,
changeDetector: $this->resource->changeDetector,
internal: $this->resource->isInternal(),
Expand All @@ -102,7 +152,8 @@ private function doReflectClass(Stmt\ClassLike $node, string $name): ClassReflec
endLine: $node->getEndLine() > 0 ? $node->getEndLine() : null,
docComment: $this->reflectDocComment($node),
attributes: $this->reflectAttributes($node, [$name]),
templates: $this->reflectTemplates($phpDoc->templates()),
typeAliases: $this->reflectTypeAliasesFromContext($phpDoc),
templates: $this->reflectTemplatesFromContext($phpDoc),
interface: $node instanceof Stmt\Interface_,
enum: $node instanceof Stmt\Enum_,
trait: $node instanceof Stmt\Trait_,
Expand Down Expand Up @@ -378,10 +429,10 @@ private function reflectOwnMethods(string $class, Stmt\ClassLike $classNode): ar
$name = $node->name->name;
$phpDoc = PhpDocParsingVisitor::fromNode($node);
$declaredAt = types::atMethod($class, $name);
$methods[] = $this->inContextOfTemplates($declaredAt, $phpDoc, fn(): MethodReflection => new MethodReflection(
$methods[] = $this->executeWithTypes($declaredAt, $phpDoc, fn(): MethodReflection => new MethodReflection(
class: $class,
name: $name,
templates: $this->reflectTemplates($phpDoc->templates()),
templates: $this->reflectTemplatesFromContext($phpDoc),
modifiers: $this->reflectMethodModifiers($node, $interface),
docComment: $this->reflectDocComment($node),
internal: $this->resource->isInternal(),
Expand Down
Loading

0 comments on commit e16fd20

Please sign in to comment.