Skip to content

Commit

Permalink
Fix resolving self and static in @phpstan-closure-this from tra…
Browse files Browse the repository at this point in the history
…it stub file
  • Loading branch information
ondrejmirtes committed May 15, 2024
1 parent 9ff5aaf commit 9340249
Show file tree
Hide file tree
Showing 10 changed files with 211 additions and 63 deletions.
14 changes: 14 additions & 0 deletions src/Analyser/NameScope.php
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,20 @@ public function withTemplateTypeMap(TemplateTypeMap $map): self
);
}

public function withClassName(string $className): self
{
return new self(
$this->namespace,
$this->uses,
$className,
$this->functionName,
$this->templateTypeMap,
$this->typeAliasesMap,
$this->bypassTypeAliases,
$this->constUses,
);
}

public function unsetTemplateType(string $name): self
{
$map = $this->templateTypeMap;
Expand Down
106 changes: 51 additions & 55 deletions src/Analyser/NodeScopeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -2636,67 +2636,63 @@ static function (): void {
$impurePoints = array_merge($impurePoints, $result->getImpurePoints());
$scope = $result->getScope();
} elseif ($expr->class instanceof Name) {
$className = $scope->resolveName($expr->class);
if ($this->reflectionProvider->hasClass($className)) {
$classReflection = $this->reflectionProvider->getClass($className);
$methodName = $expr->name->name;
if ($classReflection->hasMethod($methodName)) {
$methodReflection = $classReflection->getMethod($methodName, $scope);
$parametersAcceptor = ParametersAcceptorSelector::selectFromArgs(
$scope,
$expr->getArgs(),
$methodReflection->getVariants(),
$methodReflection->getNamedArgumentsVariants(),
);
$classType = $scope->resolveTypeByName($expr->class);
$methodName = $expr->name->name;
if ($classType->hasMethod($methodName)->yes()) {
$methodReflection = $classType->getMethod($methodName, $scope);
$parametersAcceptor = ParametersAcceptorSelector::selectFromArgs(
$scope,
$expr->getArgs(),
$methodReflection->getVariants(),
$methodReflection->getNamedArgumentsVariants(),
);

$methodThrowPoint = $this->getStaticMethodThrowPoint($methodReflection, $parametersAcceptor, $expr, $scope);
if ($methodThrowPoint !== null) {
$throwPoints[] = $methodThrowPoint;
}
if (
$classReflection->getName() === 'Closure'
&& strtolower($methodName) === 'bind'
) {
$thisType = null;
$nativeThisType = null;
if (isset($expr->getArgs()[1])) {
$argType = $scope->getType($expr->getArgs()[1]->value);
if ($argType->isNull()->yes()) {
$thisType = null;
} else {
$thisType = $argType;
}
$methodThrowPoint = $this->getStaticMethodThrowPoint($methodReflection, $parametersAcceptor, $expr, $scope);
if ($methodThrowPoint !== null) {
$throwPoints[] = $methodThrowPoint;
}

$nativeArgType = $scope->getNativeType($expr->getArgs()[1]->value);
if ($nativeArgType->isNull()->yes()) {
$nativeThisType = null;
} else {
$nativeThisType = $nativeArgType;
}
$declaringClass = $methodReflection->getDeclaringClass();
if (
$declaringClass->getName() === 'Closure'
&& strtolower($methodName) === 'bind'
) {
$thisType = null;
$nativeThisType = null;
if (isset($expr->getArgs()[1])) {
$argType = $scope->getType($expr->getArgs()[1]->value);
if ($argType->isNull()->yes()) {
$thisType = null;
} else {
$thisType = $argType;
}

$nativeArgType = $scope->getNativeType($expr->getArgs()[1]->value);
if ($nativeArgType->isNull()->yes()) {
$nativeThisType = null;
} else {
$nativeThisType = $nativeArgType;
}
$scopeClasses = ['static'];
if (isset($expr->getArgs()[2])) {
$argValue = $expr->getArgs()[2]->value;
$argValueType = $scope->getType($argValue);

$scopeClasses = [];
$directClassNames = $argValueType->getObjectClassNames();
if (count($directClassNames) > 0) {
$scopeClasses = $directClassNames;
$thisTypes = [];
foreach ($directClassNames as $directClassName) {
$thisTypes[] = new ObjectType($directClassName);
}
$thisType = TypeCombinator::union(...$thisTypes);
} else {
$thisType = $argValueType->getClassStringObjectType();
$scopeClasses = $thisType->getObjectClassNames();
}
$scopeClasses = ['static'];
if (isset($expr->getArgs()[2])) {
$argValue = $expr->getArgs()[2]->value;
$argValueType = $scope->getType($argValue);

$directClassNames = $argValueType->getObjectClassNames();
if (count($directClassNames) > 0) {
$scopeClasses = $directClassNames;
$thisTypes = [];
foreach ($directClassNames as $directClassName) {
$thisTypes[] = new ObjectType($directClassName);
}
$thisType = TypeCombinator::union(...$thisTypes);
} else {
$thisType = $argValueType->getClassStringObjectType();
$scopeClasses = $thisType->getObjectClassNames();
}
$closureBindScope = $scope->enterClosureBind($thisType, $nativeThisType, $scopeClasses);
}
} else {
$throwPoints[] = ThrowPoint::createImplicit($scope, $expr);
$closureBindScope = $scope->enterClosureBind($thisType, $nativeThisType, $scopeClasses);
}
} else {
$throwPoints[] = ThrowPoint::createImplicit($scope, $expr);
Expand Down
2 changes: 1 addition & 1 deletion src/PhpDoc/PhpDocInheritanceResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ private function docBlockToResolvedDocBlock(PhpDocBlock $phpDocBlock, ?string $t
$classReflection = $phpDocBlock->getClassReflection();
if ($functionName !== null && $classReflection->getNativeReflection()->hasMethod($functionName)) {
$methodReflection = $classReflection->getNativeReflection()->getMethod($functionName);
$stub = $this->stubPhpDocProvider->findMethodPhpDoc($classReflection->getName(), $functionName, array_map(static fn (ReflectionParameter $parameter): string => $parameter->getName(), $methodReflection->getParameters()));
$stub = $this->stubPhpDocProvider->findMethodPhpDoc($classReflection->getName(), $classReflection->getName(), $functionName, array_map(static fn (ReflectionParameter $parameter): string => $parameter->getName(), $methodReflection->getParameters()));
if ($stub !== null) {
return $stub;
}
Expand Down
18 changes: 17 additions & 1 deletion src/PhpDoc/ResolvedPhpDocBlock.php
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ public static function create(
ReflectionProvider $reflectionProvider,
): self
{
// new property also needs to be added to createEmpty() and merge()
// new property also needs to be added to withNameScope(), createEmpty() and merge()
$self = new self();
$self->phpDocNode = $phpDocNode;
$self->phpDocNodes = [$phpDocNode];
Expand All @@ -176,6 +176,22 @@ public static function create(
return $self;
}

public function withNameScope(NameScope $nameScope): self
{
$self = new self();
$self->phpDocNode = $this->phpDocNode;
$self->phpDocNodes = $this->phpDocNodes;
$self->phpDocString = $this->phpDocString;
$self->filename = $this->filename;
$self->nameScope = $nameScope;
$self->templateTypeMap = $this->templateTypeMap;
$self->templateTags = $this->templateTags;
$self->phpDocNodeResolver = $this->phpDocNodeResolver;
$self->reflectionProvider = $this->reflectionProvider;

return $self;
}

public static function createEmpty(): self
{
// new property also needs to be added to merge()
Expand Down
13 changes: 12 additions & 1 deletion src/PhpDoc/StubPhpDocProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,12 @@ public function findClassConstantPhpDoc(string $className, string $constantName)
/**
* @param array<int, string> $positionalParameterNames
*/
public function findMethodPhpDoc(string $className, string $methodName, array $positionalParameterNames): ?ResolvedPhpDocBlock
public function findMethodPhpDoc(
string $className,
string $implementingClassName,
string $methodName,
array $positionalParameterNames,
): ?ResolvedPhpDocBlock
{
if (!$this->isKnownClass($className)) {
return null;
Expand All @@ -170,6 +175,12 @@ public function findMethodPhpDoc(string $className, string $methodName, array $p
throw new ShouldNotHappenException();
}

if ($className !== $implementingClassName && $resolvedPhpDoc->getNullableNameScope() !== null) {
$resolvedPhpDoc = $resolvedPhpDoc->withNameScope(
$resolvedPhpDoc->getNullableNameScope()->withClassName($implementingClassName),
);
}

$methodParameterNames = $this->knownMethodsParameterNames[$className][$methodName];
$parameterNameMapping = [];
foreach ($positionalParameterNames as $i => $parameterName) {
Expand Down
16 changes: 11 additions & 5 deletions src/Reflection/Php/PhpClassReflectionExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -502,7 +502,7 @@ private function createMethod(
$stubImmediatelyInvokedCallableParameters = [];
$stubClosureThisParameters = [];
if (count($methodSignatures) === 1) {
$stubPhpDocPair = $this->findMethodPhpDocIncludingAncestors($declaringClass, $methodReflection->getName(), array_map(static fn (ParameterSignature $parameterSignature): string => $parameterSignature->getName(), $methodSignature->getParameters()));
$stubPhpDocPair = $this->findMethodPhpDocIncludingAncestors($declaringClass, $declaringClass, $methodReflection->getName(), array_map(static fn (ParameterSignature $parameterSignature): string => $parameterSignature->getName(), $methodSignature->getParameters()));
if ($stubPhpDocPair !== null) {
[$stubPhpDoc, $stubDeclaringClass] = $stubPhpDocPair;
$templateTypeMap = $stubDeclaringClass->getActiveTemplateTypeMap();
Expand Down Expand Up @@ -637,7 +637,7 @@ private function createMethod(
public function createUserlandMethodReflection(ClassReflection $fileDeclaringClass, ClassReflection $actualDeclaringClass, BuiltinMethodReflection $methodReflection, ?string $declaringTraitName): PhpMethodReflection
{
$resolvedPhpDoc = null;
$stubPhpDocPair = $this->findMethodPhpDocIncludingAncestors($fileDeclaringClass, $methodReflection->getName(), array_map(static fn (ReflectionParameter $parameter): string => $parameter->getName(), $methodReflection->getParameters()));
$stubPhpDocPair = $this->findMethodPhpDocIncludingAncestors($fileDeclaringClass, $fileDeclaringClass, $methodReflection->getName(), array_map(static fn (ReflectionParameter $parameter): string => $parameter->getName(), $methodReflection->getParameters()));
$phpDocBlockClassReflection = $fileDeclaringClass;

if ($methodReflection->getReflection() !== null) {
Expand All @@ -647,6 +647,7 @@ public function createUserlandMethodReflection(ClassReflection $fileDeclaringCla
if (! $methodReflection->getDeclaringClass()->isTrait() || $methodDeclaringClass->getName() !== $methodReflection->getDeclaringClass()->getName()) {
$stubPhpDocPair = $this->findMethodPhpDocIncludingAncestors(
$this->reflectionProviderProvider->getReflectionProvider()->getClass($methodDeclaringClass->getName()),
$this->reflectionProviderProvider->getReflectionProvider()->getClass($methodReflection->getDeclaringClass()->getName()),
$methodReflection->getName(),
array_map(
static fn (ReflectionParameter $parameter): string => $parameter->getName(),
Expand Down Expand Up @@ -1126,10 +1127,15 @@ private function getPhpDocReturnType(ClassReflection $phpDocBlockClassReflection
* @param array<int, string> $positionalParameterNames
* @return array{ResolvedPhpDocBlock, ClassReflection}|null
*/
private function findMethodPhpDocIncludingAncestors(ClassReflection $declaringClass, string $methodName, array $positionalParameterNames): ?array
private function findMethodPhpDocIncludingAncestors(
ClassReflection $declaringClass,
ClassReflection $implementingClass,
string $methodName,
array $positionalParameterNames,
): ?array
{
$declaringClassName = $declaringClass->getName();
$resolved = $this->stubPhpDocProvider->findMethodPhpDoc($declaringClassName, $methodName, $positionalParameterNames);
$resolved = $this->stubPhpDocProvider->findMethodPhpDoc($declaringClassName, $implementingClass->getName(), $methodName, $positionalParameterNames);
if ($resolved !== null) {
return [$resolved, $declaringClass];
}
Expand All @@ -1146,7 +1152,7 @@ private function findMethodPhpDocIncludingAncestors(ClassReflection $declaringCl
continue;
}

$resolved = $this->stubPhpDocProvider->findMethodPhpDoc($ancestor->getName(), $methodName, $positionalParameterNames);
$resolved = $this->stubPhpDocProvider->findMethodPhpDoc($ancestor->getName(), $ancestor->getName(), $methodName, $positionalParameterNames);
if ($resolved === null) {
continue;
}
Expand Down
36 changes: 36 additions & 0 deletions tests/PHPStan/Analyser/Bug11009Test.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php declare(strict_types = 1);

namespace PHPStan\Analyser;

use PHPStan\Testing\TypeInferenceTestCase;

class Bug11009Test extends TypeInferenceTestCase
{

public function dataFileAsserts(): iterable
{
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-11009.php');
}

/**
* @dataProvider dataFileAsserts
* @param mixed ...$args
*/
public function testFileAsserts(
string $assertType,
string $file,
...$args,
): void
{
$this->assertFileAsserts($assertType, $file, ...$args);
}

public static function getAdditionalConfigFiles(): array
{
return [
__DIR__ . '/../../../conf/bleedingEdge.neon',
__DIR__ . '/bug-11009.neon',
];
}

}
3 changes: 3 additions & 0 deletions tests/PHPStan/Analyser/bug-11009.neon
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
parameters:
stubFiles:
- data/bug-11009.stub
45 changes: 45 additions & 0 deletions tests/PHPStan/Analyser/data/bug-11009.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

namespace Bug11009;

use function PHPStan\Testing\assertType;

trait A
{
public static function callbackStatic(callable $cb): void
{
}

public static function callbackSelf(callable $cb): void
{
}

public function returnStatic()
{
return $this;
}

public function returnSelf()
{
return new self;
}
}

class B
{
use A;
}

function (): void {
B::callbackStatic(function (): void {
assertType(B::class, $this);
});

B::callbackSelf(function (): void {
assertType(B::class, $this);
});

$b = new B();
assertType(B::class, $b->returnStatic());
assertType(B::class, $b->returnSelf());
};
21 changes: 21 additions & 0 deletions tests/PHPStan/Analyser/data/bug-11009.stub
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace Bug11009;

trait A {
/**
* @param-closure-this static $cb
*/
public static function callbackStatic(callable $cb): void {}

/**
* @param-closure-this self $cb
*/
public static function callbackSelf(callable $cb): void {}

/** @return static */
public function returnStatic() {}

/** @return self */
public function returnSelf() {}
}

0 comments on commit 9340249

Please sign in to comment.