Skip to content

Commit

Permalink
Static and new calls analysis on literal string class name types comi…
Browse files Browse the repository at this point in the history
…ng from expressions (#711)

* wip

* added support for dynamic new call class analysis

* wip

* Fix styling

* removed unused stub

---------

Co-authored-by: romalytvynenko <[email protected]>
  • Loading branch information
romalytvynenko and romalytvynenko authored Feb 8, 2025
1 parent de05558 commit 31012d5
Show file tree
Hide file tree
Showing 7 changed files with 115 additions and 26 deletions.
11 changes: 8 additions & 3 deletions src/Infer/Scope/Scope.php
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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 (
Expand Down
42 changes: 33 additions & 9 deletions src/Infer/Services/ReferenceTypeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -321,26 +322,39 @@ 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.

/*
* 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,
Expand All @@ -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);
Expand Down Expand Up @@ -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;
Expand Down
4 changes: 2 additions & 2 deletions src/Support/RouteInfo.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion src/Support/RouteResponseTypeRetriever.php
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}

Expand Down
22 changes: 16 additions & 6 deletions src/Support/Type/Reference/NewCallReferenceType.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
class NewCallReferenceType extends AbstractReferenceType
{
public function __construct(
public string $name,
public string|Type $name,
/** @var Type[] $arguments */
public array $arguments,
) {}
Expand All @@ -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
Expand All @@ -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 [];
}
}
20 changes: 15 additions & 5 deletions src/Support/Type/Reference/StaticMethodCallReferenceType.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 [];
}
}
40 changes: 40 additions & 0 deletions tests/Infer/Analyzer/ClassAnalyzerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,43 @@

expect($type->toString())->toBe('Dedoc\Scramble\Tests\Infer\stubs\ChildParentSetterCalls<string(from ChildParentSetterCalls constructor), string(from ChildParentSetterCalls wow), string(some)>');
});

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;
}
}

0 comments on commit 31012d5

Please sign in to comment.