Skip to content

Commit

Permalink
[Feature] AST-preserving node name resolution, implements #136
Browse files Browse the repository at this point in the history
  • Loading branch information
lisachenko committed May 4, 2024
1 parent fe2b2b0 commit bd0ed24
Show file tree
Hide file tree
Showing 8 changed files with 94 additions and 93 deletions.
7 changes: 6 additions & 1 deletion src/ReflectionAttribute.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,12 @@ public function getNode(): Node\Attribute
$nodeExpressionResolver = new NodeExpressionResolver($this);
foreach ($node->attrGroups as $attrGroup) {
foreach ($attrGroup->attrs as $attr) {
if ($attr->name->toString() !== $this->attributeName) {
$attributeNodeName = $attr->name;
// Unpack fully-resolved class name from attribute if we have it
if ($attributeNodeName->hasAttribute('resolvedName')) {
$attributeNodeName = $attributeNodeName->getAttribute('resolvedName');
}
if ($attributeNodeName->toString() !== $this->attributeName) {
continue;
}

Expand Down
9 changes: 5 additions & 4 deletions src/ReflectionClass.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use Go\ParserReflection\Traits\AttributeResolverTrait;
use Go\ParserReflection\Traits\InternalPropertiesEmulationTrait;
use Go\ParserReflection\Traits\ReflectionClassLikeTrait;
use PhpParser\Node\Name;
use PhpParser\Node\Name\FullyQualified;
use PhpParser\Node\Stmt\ClassLike;
use PhpParser\Node\Stmt\Enum_;
Expand Down Expand Up @@ -68,8 +69,8 @@ public static function collectInterfacesFromClassNode(ClassLike $classLikeNode):

if (count($implementsList) > 0) {
foreach ($implementsList as $implementNode) {
if ($implementNode instanceof FullyQualified) {
$implementName = $implementNode->toString();
if ($implementNode instanceof Name && $implementNode->getAttribute('resolvedName') instanceof FullyQualified) {
$implementName = $implementNode->getAttribute('resolvedName')->toString();
$interface = interface_exists($implementName, false)
? new parent($implementName)
: new static($implementName);
Expand Down Expand Up @@ -108,8 +109,8 @@ public static function collectTraitsFromClassNode(ClassLike $classLikeNode, arra
foreach ($classLikeNode->stmts as $classLevelNode) {
if ($classLevelNode instanceof TraitUse) {
foreach ($classLevelNode->traits as $classTraitName) {
if ($classTraitName instanceof FullyQualified) {
$traitName = $classTraitName->toString();
if ($classTraitName instanceof Name && $classTraitName->getAttribute('resolvedName') instanceof FullyQualified) {
$traitName = $classTraitName->getAttribute('resolvedName')->toString();
$trait = trait_exists($traitName, false)
? new parent($traitName)
: new static($traitName);
Expand Down
8 changes: 7 additions & 1 deletion src/ReflectionEngine.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,13 @@ public static function init(LocatorInterface $locator): void
self::$parser = (new ParserFactory())->createForHostVersion();

self::$traverser = $traverser = new NodeTraverser();
$traverser->addVisitor(new NameResolver());
$traverser->addVisitor(new NameResolver(
null,
[
'preserveOriginalNames' => true,
'replaceNodes' => false,
]
));
$traverser->addVisitor(new RootNamespaceNormalizer());

self::$locator = $locator;
Expand Down
5 changes: 5 additions & 0 deletions src/ReflectionParameter.php
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,11 @@ public function getClass(): ?\ReflectionClass
$parameterType = new Name\FullyQualified(\Traversable::class);
}
if ($parameterType instanceof Name) {
// If we have resolved type name, we should use it instead
if ($parameterType->hasAttribute('resolvedName')) {
$parameterType = $parameterType->getAttribute('resolvedName');
}

if (!$parameterType instanceof Name\FullyQualified) {
$parameterTypeName = $parameterType->toString();

Expand Down
61 changes: 48 additions & 13 deletions src/Resolver/NodeExpressionResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use Go\ParserReflection\ReflectionClass;
use Go\ParserReflection\ReflectionException;
use Go\ParserReflection\ReflectionFileNamespace;
use Go\ParserReflection\ReflectionNamedType;
use PhpParser\Node;
use PhpParser\Node\Const_;
use PhpParser\Node\Expr;
Expand Down Expand Up @@ -112,12 +113,17 @@ public function getConstExpression(): ?string
// Clone node to avoid possible side-effects
$node = clone $this->nodeStack[$this->nodeLevel];
if ($node instanceof Expr\ConstFetch) {
if ($node->name->isFullyQualified()) {
$constantNodeName = $node->name;
// Unpack fully-resolved name if we have it inside attribute
if ($constantNodeName->hasAttribute('resolvedName')) {
$constantNodeName = $constantNodeName->getAttribute('resolvedName');
}
if ($constantNodeName->isFullyQualified()) {
// For full-qualified names we would like to remove leading "\"
$node->name = new Name(ltrim($node->name->toString(), '\\'));
$node->name = new Name(ltrim($constantNodeName->toString(), '\\'));
} else {
// For relative names we would like to add namespace prefix
$node->name = new Name($this->resolveScalarMagicConstNamespace() . '\\' . $node->name->toString());
$node->name = new Name($this->resolveScalarMagicConstNamespace() . '\\' . $constantNodeName->toString());
}
}
// All long array nodes are pretty-printed by PHP in short format
Expand Down Expand Up @@ -185,6 +191,15 @@ protected function resolveNameFullyQualified(Name\FullyQualified $node): string
return $node->toString();
}

private function resolveName(Name $node): string
{
if ($node->hasAttribute('resolvedName')) {
return $node->getAttribute('resolvedName')->toString();
}

return $node->toString();
}

protected function resolveIdentifier(Node\Identifier $node): string
{
return $node->toString();
Expand Down Expand Up @@ -332,8 +347,13 @@ protected function resolveExprConstFetch(Expr\ConstFetch $node)
$constantValue = null;
$isResolved = false;

$isFQNConstant = $node->name instanceof Node\Name\FullyQualified;
$constantName = $node->name->toString();
$nodeConstantName = $node->name;
// If we have resolved type name
if ($nodeConstantName->hasAttribute('resolvedName')) {
$nodeConstantName = $nodeConstantName->getAttribute('resolvedName');
}
$isFQNConstant = $nodeConstantName instanceof Node\Name\FullyQualified;
$constantName = $nodeConstantName->toString();

if (!$isFQNConstant && method_exists($this->context, 'getFileName')) {
$fileName = $this->context->getFileName();
Expand Down Expand Up @@ -365,18 +385,29 @@ protected function resolveExprConstFetch(Expr\ConstFetch $node)

protected function resolveExprClassConstFetch(Expr\ClassConstFetch $node)
{
$classToReflect = $node->class;
if (!($classToReflect instanceof Node\Name)) {
$classToReflect = $this->resolve($classToReflect);
if (!is_string($classToReflect)) {
$classToReflectNodeName = $node->class;
if (!($classToReflectNodeName instanceof Node\Name)) {
$classToReflectNodeName = $this->resolve($classToReflectNodeName);
if (!is_string($classToReflectNodeName)) {
throw new ReflectionException("Unable to resolve class constant.");
}
// Strings evaluated as class names are always treated as fully
// qualified.
$classToReflect = new Node\Name\FullyQualified(ltrim($classToReflect, '\\'));
$classToReflectNodeName = new Node\Name\FullyQualified(ltrim($classToReflectNodeName, '\\'));
}
// Unwrap resolved class name if we have it inside attributes
if ($classToReflectNodeName->hasAttribute('resolvedName')) {
$classToReflectNodeName = $classToReflectNodeName->getAttribute('resolvedName');
}
$refClass = $this->fetchReflectionClass($classToReflectNodeName);
if (($node->name instanceof Expr\Error)) {
$constantName = '';
} else {
$constantName = match (true) {
$node->name->hasAttribute('resolvedName') => $node->name->getAttribute('resolvedName')->toString(),
default => $node->name->toString(),
};
}
$refClass = $this->fetchReflectionClass($classToReflect);
$constantName = ($node->name instanceof Expr\Error) ? '' : $node->name->toString();

// special handling of ::class constants
if ('class' === $constantName) {
Expand All @@ -385,7 +416,7 @@ protected function resolveExprClassConstFetch(Expr\ClassConstFetch $node)

$this->isConstant = true;
$this->isConstExpr = true;
$this->constantName = $classToReflect . '::' . $constantName;
$this->constantName = $classToReflectNodeName . '::' . $constantName;

return $refClass->getConstant($constantName);
}
Expand Down Expand Up @@ -581,6 +612,10 @@ private function getDispatchMethodFor(Node $node): string
*/
private function fetchReflectionClass(Node\Name $node)
{
// If we have already resolved node name, we should use it instead
if ($node->hasAttribute('resolvedName')) {
$node = $node->getAttribute('resolvedName');
}
$className = $node->toString();
$isFQNClass = $node instanceof Node\Name\FullyQualified;
if ($isFQNClass) {
Expand Down
70 changes: 4 additions & 66 deletions src/Resolver/TypeExpressionResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,10 @@ private function resolveIdentifier(Node\Identifier $node): ReflectionNamedType

private function resolveName(Name $node): ReflectionNamedType
{
if ($node->hasAttribute('resolvedName')) {
$node = $node->getAttribute('resolvedName');
}

return new ReflectionNamedType($node->toString(), $this->hasDefaultNull, false);
}

Expand All @@ -157,70 +161,4 @@ private function getDispatchMethodFor(Node $node): string

return 'resolve' . str_replace('_', '', $nodeType);
}

/**
* Utility method to fetch reflection class instance by name
*
* Supports:
* 'self' keyword
* 'parent' keyword
* not-FQN class names
*
* @param Node\Name $node Class name node
*
* @return bool|\ReflectionClass
*
* @throws ReflectionException
*/
private function fetchReflectionClass(Node\Name $node)
{
$className = $node->toString();
$isFQNClass = $node instanceof Node\Name\FullyQualified;
if ($isFQNClass) {
// check to see if the class is already loaded and is safe to use
// PHP's ReflectionClass to determine if the class is user defined
if (class_exists($className, false)) {
$refClass = new \ReflectionClass($className);
if (!$refClass->isUserDefined()) {
return $refClass;
}
}

return new ReflectionClass($className);
}

if ('self' === $className) {
if ($this->context instanceof \ReflectionClass) {
return $this->context;
}

if (method_exists($this->context, 'getDeclaringClass')) {
return $this->context->getDeclaringClass();
}
}

if ('parent' === $className) {
if ($this->context instanceof \ReflectionClass) {
return $this->context->getParentClass();
}

if (method_exists($this->context, 'getDeclaringClass')) {
return $this->context->getDeclaringClass()
->getParentClass()
;
}
}

if (method_exists($this->context, 'getFileName')) {
/** @var ReflectionFileNamespace|null $fileNamespace */
$fileName = $this->context->getFileName();
$namespaceName = $this->resolveScalarMagicConstNamespace();

$fileNamespace = new ReflectionFileNamespace($fileName, $namespaceName);

return $fileNamespace->getClass($className);
}

throw new ReflectionException("Can not resolve class $className");
}
}
17 changes: 14 additions & 3 deletions src/Traits/AttributeResolverTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,18 @@ public function getAttributes(?string $name = null, int $flags = 0): array
$arguments[] = $nodeExpressionResolver->getValue();
}

$attributeNameNode = $attr->name;
// If we have resoled node name, then we should use it instead
if ($attributeNameNode->hasAttribute('resolvedName')) {
$attributeNameNode = $attributeNameNode->getAttribute('resolvedName');
}
if ($name === null) {
$attributes[] = new ReflectionAttribute($attr->name->toString(), $this, $arguments, $this->isAttributeRepeated($attr->name->toString(), $node->attrGroups));
$attributes[] = new ReflectionAttribute($attributeNameNode->toString(), $this, $arguments, $this->isAttributeRepeated($attributeNameNode->toString(), $node->attrGroups));

continue;
}

if ($name !== $attr->name->toString()) {
if ($name !== $attributeNameNode->toString()) {
continue;
}

Expand All @@ -63,7 +68,13 @@ private function isAttributeRepeated(string $attributeName, array $attrGroups):

foreach ($attrGroups as $attrGroup) {
foreach ($attrGroup->attrs as $attr) {
if ($attr->name->toString() === $attributeName) {
$attributeNameNode = $attr->name;
// If we have resoled node name, then we should use it instead
if ($attributeNameNode->hasAttribute('resolvedName')) {
$attributeNameNode = $attributeNameNode->getAttribute('resolvedName');
}

if ($attributeNameNode->toString() === $attributeName) {
++$count;
}
}
Expand Down
10 changes: 5 additions & 5 deletions src/Traits/ReflectionClassLikeTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
use Go\ParserReflection\ReflectionMethod;
use Go\ParserReflection\ReflectionProperty;
use Go\ParserReflection\Resolver\NodeExpressionResolver;
use PhpParser\Node\Name;
use PhpParser\Node\Name\FullyQualified;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassConst;
Expand Down Expand Up @@ -450,13 +451,12 @@ public function getNamespaceName(): string
public function getParentClass(): \ReflectionClass|false
{
if (!isset($this->parentClass)) {
static $extendsField = 'extends';

$parentClass = false;
$hasExtends = in_array($extendsField, $this->classLikeNode->getSubNodeNames(), true);
$extendsNode = $hasExtends ? $this->classLikeNode->$extendsField : null;
if ($extendsNode instanceof FullyQualified) {
$extendsName = $extendsNode->toString();
$extendsNode = $this->classLikeNode->extends ?? null;

if ($extendsNode instanceof Name && $extendsNode->getAttribute('resolvedName') instanceof FullyQualified) {
$extendsName = $extendsNode->getAttribute('resolvedName')->toString();
$parentClass = $this->createReflectionForClass($extendsName);
}
$this->parentClass = $parentClass;
Expand Down

0 comments on commit bd0ed24

Please sign in to comment.