From 31012d53609b3510160c31b16381a200dc7b5ee5 Mon Sep 17 00:00:00 2001 From: romalytvynenko Date: Sat, 8 Feb 2025 12:16:03 +0200 Subject: [PATCH] Static and new calls analysis on literal string class name types coming from expressions (#711) * wip * added support for dynamic new call class analysis * wip * Fix styling * removed unused stub --------- Co-authored-by: romalytvynenko --- src/Infer/Scope/Scope.php | 11 +++-- src/Infer/Services/ReferenceTypeResolver.php | 42 +++++++++++++++---- src/Support/RouteInfo.php | 4 +- src/Support/RouteResponseTypeRetriever.php | 2 +- .../Type/Reference/NewCallReferenceType.php | 22 +++++++--- .../StaticMethodCallReferenceType.php | 20 ++++++--- tests/Infer/Analyzer/ClassAnalyzerTest.php | 40 ++++++++++++++++++ 7 files changed, 115 insertions(+), 26 deletions(-) diff --git a/src/Infer/Scope/Scope.php b/src/Infer/Scope/Scope.php index 87ca74ae..36e343e3 100644 --- a/src/Infer/Scope/Scope.php +++ b/src/Infer/Scope/Scope.php @@ -97,7 +97,10 @@ public function getType(Node $node): Type if ($node instanceof Node\Expr\New_) { if (! $node->class instanceof Node\Name) { - return $type; + return $this->setType( + $node, + new NewCallReferenceType($this->getType($node->class), $this->getArgsTypes($node->args)), + ); } return $this->setType( @@ -157,9 +160,11 @@ public function getType(Node $node): Type return $type; } - // Only string class names support. if (! $node->class instanceof Node\Name) { - return $type; + return $this->setType( + $node, + new StaticMethodCallReferenceType($this->getType($node->class), $node->name->name, $this->getArgsTypes($node->args)), + ); } if ( diff --git a/src/Infer/Services/ReferenceTypeResolver.php b/src/Infer/Services/ReferenceTypeResolver.php index dca57ce2..31b291b4 100644 --- a/src/Infer/Services/ReferenceTypeResolver.php +++ b/src/Infer/Services/ReferenceTypeResolver.php @@ -17,6 +17,7 @@ use Dedoc\Scramble\Support\Type\CallableStringType; use Dedoc\Scramble\Support\Type\FunctionType; use Dedoc\Scramble\Support\Type\Generic; +use Dedoc\Scramble\Support\Type\Literal\LiteralStringType; use Dedoc\Scramble\Support\Type\MixedType; use Dedoc\Scramble\Support\Type\ObjectType; use Dedoc\Scramble\Support\Type\Reference\AbstractReferenceType; @@ -321,14 +322,27 @@ private function resolveStaticMethodCallReferenceType(Scope $scope, StaticMethod ); $calleeName = $type->callee; - $contextualClassName = $this->resolveClassName($scope, $type->callee); + + if ($calleeName instanceof Type) { + $calleeType = $this->resolve($scope, $type->callee); + + if ($calleeType instanceof LiteralStringType) { + $calleeName = $calleeType->value; + } + + if (! is_string($calleeName)) { + return new UnknownType; + } + } + + $contextualClassName = $this->resolveClassName($scope, $calleeName); if (! $contextualClassName) { return new UnknownType; } - $type->callee = $contextualClassName; + $calleeName = $contextualClassName; - $isStaticCall = ! in_array($calleeName, StaticReference::KEYWORDS) - || (in_array($calleeName, StaticReference::KEYWORDS) && $scope->context->functionDefinition?->isStatic); + $isStaticCall = ! in_array($type->callee, StaticReference::KEYWORDS) + || (in_array($type->callee, StaticReference::KEYWORDS) && $scope->context->functionDefinition?->isStatic); // Assuming callee here can be only string of known name. Reality is more complex than // that, but it is fine for now. @@ -336,11 +350,11 @@ private function resolveStaticMethodCallReferenceType(Scope $scope, StaticMethod /* * Doing a deep dive into the dependent class, if it has not been analyzed. */ - $this->resolveUnknownClass($type->callee); + $this->resolveUnknownClass($calleeName); // Attempting extensions broker before potentially giving up on type inference if ($isStaticCall && $returnType = Context::getInstance()->extensionsBroker->getStaticMethodReturnType(new StaticMethodCallEvent( - callee: $type->callee, + callee: $calleeName, name: $type->methodName, scope: $scope, arguments: $type->arguments, @@ -367,15 +381,15 @@ private function resolveStaticMethodCallReferenceType(Scope $scope, StaticMethod } } - if (! array_key_exists($type->callee, $this->index->classesDefinitions)) { + if (! array_key_exists($calleeName, $this->index->classesDefinitions)) { return new UnknownType; } /** @var ClassDefinition $calleeDefinition */ - $calleeDefinition = $this->index->getClassDefinition($type->callee); + $calleeDefinition = $this->index->getClassDefinition($calleeName); if (! $methodDefinition = $calleeDefinition->getMethodDefinition($type->methodName, $scope)) { - return new UnknownType("Cannot get a method type [$type->methodName] on type [$type->callee]"); + return new UnknownType("Cannot get a method type [$type->methodName] on type [$calleeName]"); } return $this->getFunctionCallResult($methodDefinition, $type->arguments); @@ -460,6 +474,16 @@ private function resolveNewCallReferenceType(Scope $scope, NewCallReferenceType $type->arguments, ); + if ($type->name instanceof Type) { + $resolvedNameType = $this->resolve($scope, $type->name); + + if ($resolvedNameType instanceof LiteralStringType) { + $type->name = $resolvedNameType->value; + } else { + return new UnknownType; + } + } + $contextualClassName = $this->resolveClassName($scope, $type->name); if (! $contextualClassName) { return new UnknownType; diff --git a/src/Support/RouteInfo.php b/src/Support/RouteInfo.php index 3ee3d86b..b6f07834 100644 --- a/src/Support/RouteInfo.php +++ b/src/Support/RouteInfo.php @@ -43,7 +43,7 @@ public function isClassBased(): bool public function className(): ?string { return $this->isClassBased() - ? explode('@', $this->route->getAction('uses'))[0] + ? ltrim(explode('@', $this->route->getAction('uses'))[0], '\\') : null; } @@ -108,7 +108,7 @@ public function getMethodType(): ?FunctionType } if (! $this->methodType) { - $def = $this->infer->analyzeClass($this->reflectionMethod()->getDeclaringClass()->getName()); + $def = $this->infer->analyzeClass($this->className()); /* * Here the final resolution of the method types may happen. diff --git a/src/Support/RouteResponseTypeRetriever.php b/src/Support/RouteResponseTypeRetriever.php index a3e72e7a..4599a23a 100644 --- a/src/Support/RouteResponseTypeRetriever.php +++ b/src/Support/RouteResponseTypeRetriever.php @@ -80,7 +80,7 @@ private function getInferredType() return null; } - return (new ObjectType($this->routeInfo->reflectionMethod()->getDeclaringClass()->getName())) + return (new ObjectType($this->routeInfo->className())) ->getMethodReturnType($this->routeInfo->methodName()); } diff --git a/src/Support/Type/Reference/NewCallReferenceType.php b/src/Support/Type/Reference/NewCallReferenceType.php index fd590364..5eb31c12 100644 --- a/src/Support/Type/Reference/NewCallReferenceType.php +++ b/src/Support/Type/Reference/NewCallReferenceType.php @@ -8,7 +8,7 @@ class NewCallReferenceType extends AbstractReferenceType { public function __construct( - public string $name, + public string|Type $name, /** @var Type[] $arguments */ public array $arguments, ) {} @@ -20,7 +20,7 @@ public function nodes(): array public function isInstanceOf(string $className) { - return is_a($this->name, $className, true); + return is_string($this->name) && is_a($this->name, $className, true); } public function toString(): string @@ -30,13 +30,23 @@ public function toString(): string array_map(fn ($t) => $t->toString(), $this->arguments), ); - return "(new {$this->name})($argsTypes)"; + $name = is_string($this->name) ? $this->name : $this->name->toString(); + + return "(new {$name})($argsTypes)"; } public function dependencies(): array { - return [ - new ClassDependency($this->name), - ]; + if ($this->name instanceof AbstractReferenceType) { + return $this->name->dependencies(); + } + + if (is_string($this->name)) { + return [ + new ClassDependency($this->name), + ]; + } + + return []; } } diff --git a/src/Support/Type/Reference/StaticMethodCallReferenceType.php b/src/Support/Type/Reference/StaticMethodCallReferenceType.php index 4acc60d2..2dd29370 100644 --- a/src/Support/Type/Reference/StaticMethodCallReferenceType.php +++ b/src/Support/Type/Reference/StaticMethodCallReferenceType.php @@ -8,7 +8,7 @@ class StaticMethodCallReferenceType extends AbstractReferenceType { public function __construct( - public string $callee, + public string|Type $callee, public string $methodName, /** @var Type[] $arguments */ public array $arguments, @@ -26,13 +26,23 @@ public function toString(): string array_map(fn ($t) => $t->toString(), $this->arguments), ); - return "(#{$this->callee})::{$this->methodName}($argsTypes)"; + $calleeType = is_string($this->callee) ? $this->callee : $this->callee->toString(); + + return "(#{$calleeType})::{$this->methodName}($argsTypes)"; } public function dependencies(): array { - return [ - new MethodDependency($this->callee, $this->methodName), - ]; + if ($this->callee instanceof AbstractReferenceType) { + return $this->callee->dependencies(); + } + + if (is_string($this->callee)) { + return [ + new MethodDependency($this->callee, $this->methodName), + ]; + } + + return []; } } diff --git a/tests/Infer/Analyzer/ClassAnalyzerTest.php b/tests/Infer/Analyzer/ClassAnalyzerTest.php index 8c82df35..abbe87fa 100644 --- a/tests/Infer/Analyzer/ClassAnalyzerTest.php +++ b/tests/Infer/Analyzer/ClassAnalyzerTest.php @@ -104,3 +104,43 @@ expect($type->toString())->toBe('Dedoc\Scramble\Tests\Infer\stubs\ChildParentSetterCalls'); }); + +it('analyzes static method call on class constants', function () { + $this->classAnalyzer->analyze(ConstFetchStaticCallChild_ClassAnalyzerTest::class); + + $type = getStatementType('(new ConstFetchStaticCallChild_ClassAnalyzerTest)->staticMethodCall()'); + + expect($type->toString())->toBe('int(42)'); +}); + +it('analyzes new call on class constants', function () { + $this->classAnalyzer->analyze(ConstFetchStaticCallChild_ClassAnalyzerTest::class); + + $type = getStatementType('(new ConstFetchStaticCallChild_ClassAnalyzerTest)->newCall()'); + + expect($type->toString())->toBe('ConstFetchStaticCallFoo_ClassAnalyzerTest'); +}); + +class ConstFetchStaticCallParent_ClassAnalyzerTest +{ + public function staticMethodCall() + { + return (static::FOO_CLASS)::foo(); + } + + public function newCall() + { + return new (static::FOO_CLASS); + } +} +class ConstFetchStaticCallChild_ClassAnalyzerTest extends ConstFetchStaticCallParent_ClassAnalyzerTest +{ + public const FOO_CLASS = ConstFetchStaticCallFoo_ClassAnalyzerTest::class; +} +class ConstFetchStaticCallFoo_ClassAnalyzerTest +{ + public static function foo() + { + return 42; + } +}