diff --git a/src/Rules/PhpDoc/VarTagTypeRuleHelper.php b/src/Rules/PhpDoc/VarTagTypeRuleHelper.php index 0070554896..0c1d4e1b49 100644 --- a/src/Rules/PhpDoc/VarTagTypeRuleHelper.php +++ b/src/Rules/PhpDoc/VarTagTypeRuleHelper.php @@ -4,12 +4,16 @@ use PhpParser\Node; use PhpParser\Node\Expr; +use PHPStan\Analyser\NameScope; use PHPStan\Analyser\Scope; use PHPStan\Node\Expr\GetOffsetValueTypeExpr; +use PHPStan\PhpDoc\NameScopeAlreadyBeingCreatedException; use PHPStan\PhpDoc\Tag\VarTag; +use PHPStan\PhpDoc\TypeNodeResolver; use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\ArrayType; +use PHPStan\Type\FileTypeMapper; use PHPStan\Type\Generic\GenericObjectType; use PHPStan\Type\MixedType; use PHPStan\Type\ObjectType; @@ -24,7 +28,12 @@ final class VarTagTypeRuleHelper { - public function __construct(private bool $checkTypeAgainstPhpDocType, private bool $strictWideningCheck) + public function __construct( + private TypeNodeResolver $typeNodeResolver, + private FileTypeMapper $fileTypeMapper, + private bool $checkTypeAgainstPhpDocType, + private bool $strictWideningCheck, + ) { } @@ -76,7 +85,7 @@ public function checkExprType(Scope $scope, Node\Expr $expr, Type $varTagType): $errors = []; $exprNativeType = $scope->getNativeType($expr); $containsPhpStanType = $this->containsPhpStanType($varTagType); - if ($this->shouldVarTagTypeBeReported($expr, $exprNativeType, $varTagType)) { + if ($this->shouldVarTagTypeBeReported($scope, $expr, $exprNativeType, $varTagType)) { $verbosity = VerbosityLevel::getRecommendedLevelByType($exprNativeType, $varTagType); $errors[] = RuleErrorBuilder::message(sprintf( 'PHPDoc tag @var with type %s is not subtype of native type %s.', @@ -86,7 +95,7 @@ public function checkExprType(Scope $scope, Node\Expr $expr, Type $varTagType): } else { $exprType = $scope->getType($expr); if ( - $this->shouldVarTagTypeBeReported($expr, $exprType, $varTagType) + $this->shouldVarTagTypeBeReported($scope, $expr, $exprType, $varTagType) && ($this->checkTypeAgainstPhpDocType || $containsPhpStanType) ) { $verbosity = VerbosityLevel::getRecommendedLevelByType($exprType, $varTagType); @@ -127,22 +136,22 @@ private function containsPhpStanType(Type $type): bool return false; } - private function shouldVarTagTypeBeReported(Node\Expr $expr, Type $type, Type $varTagType): bool + private function shouldVarTagTypeBeReported(Scope $scope, Node\Expr $expr, Type $type, Type $varTagType): bool { if ($expr instanceof Expr\Array_) { if ($expr->items === []) { $type = new ArrayType(new MixedType(), new MixedType()); } - return $type->isSuperTypeOf($varTagType)->no(); + return !$this->isAtLeastMaybeSuperTypeOfVarType($scope, $type, $varTagType); } if ($expr instanceof Expr\ConstFetch) { - return $type->isSuperTypeOf($varTagType)->no(); + return !$this->isAtLeastMaybeSuperTypeOfVarType($scope, $type, $varTagType); } if ($expr instanceof Node\Scalar) { - return $type->isSuperTypeOf($varTagType)->no(); + return !$this->isAtLeastMaybeSuperTypeOfVarType($scope, $type, $varTagType); } if ($expr instanceof Expr\New_) { @@ -151,24 +160,24 @@ private function shouldVarTagTypeBeReported(Node\Expr $expr, Type $type, Type $v } } - return $this->checkType($type, $varTagType); + return $this->checkType($scope, $type, $varTagType); } - private function checkType(Type $type, Type $varTagType, int $depth = 0): bool + private function checkType(Scope $scope, Type $type, Type $varTagType, int $depth = 0): bool { if ($this->strictWideningCheck) { - return !$type->isSuperTypeOf($varTagType)->yes(); + return !$this->isSuperTypeOfVarType($scope, $type, $varTagType); } if ($type->isConstantArray()->yes()) { if ($type->isIterableAtLeastOnce()->no()) { $type = new ArrayType(new MixedType(), new MixedType()); - return $type->isSuperTypeOf($varTagType)->no(); + return !$this->isAtLeastMaybeSuperTypeOfVarType($scope, $type, $varTagType); } } if ($type->isIterable()->yes() && $varTagType->isIterable()->yes()) { - if ($type->isSuperTypeOf($varTagType)->no()) { + if (!$this->isAtLeastMaybeSuperTypeOfVarType($scope, $type, $varTagType)) { return true; } @@ -176,17 +185,62 @@ private function checkType(Type $type, Type $varTagType, int $depth = 0): bool $innerVarTagType = $varTagType->getIterableValueType(); if ($type->equals($innerType) || $varTagType->equals($innerVarTagType)) { - return !$innerType->isSuperTypeOf($innerVarTagType)->yes(); + return !$this->isSuperTypeOfVarType($scope, $innerType, $innerVarTagType); } - return $this->checkType($innerType, $innerVarTagType, $depth + 1); + return $this->checkType($scope, $innerType, $innerVarTagType, $depth + 1); } - if ($type->isConstantValue()->yes() && $depth === 0) { - return $type->isSuperTypeOf($varTagType)->no(); + if ($depth === 0 && $type->isConstantValue()->yes()) { + return !$this->isAtLeastMaybeSuperTypeOfVarType($scope, $type, $varTagType); } - return !$type->isSuperTypeOf($varTagType)->yes(); + return !$this->isSuperTypeOfVarType($scope, $type, $varTagType); + } + + private function isSuperTypeOfVarType(Scope $scope, Type $type, Type $varTagType): bool + { + if ($type->isSuperTypeOf($varTagType)->yes()) { + return true; + } + + try { + $type = $this->typeNodeResolver->resolve($type->toPhpDocNode(), $this->createNameScope($scope)); + } catch (NameScopeAlreadyBeingCreatedException) { + return true; + } + + return $type->isSuperTypeOf($varTagType)->yes(); + } + + private function isAtLeastMaybeSuperTypeOfVarType(Scope $scope, Type $type, Type $varTagType): bool + { + if (!$type->isSuperTypeOf($varTagType)->no()) { + return true; + } + + try { + $type = $this->typeNodeResolver->resolve($type->toPhpDocNode(), $this->createNameScope($scope)); + } catch (NameScopeAlreadyBeingCreatedException) { + return true; + } + + return !$type->isSuperTypeOf($varTagType)->no(); + } + + /** + * @throws NameScopeAlreadyBeingCreatedException + */ + private function createNameScope(Scope $scope): NameScope + { + $function = $scope->getFunction(); + + return $this->fileTypeMapper->getNameScope( + $scope->getFile(), + $scope->isInClass() ? $scope->getClassReflection()->getName() : null, + $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null, + $function !== null ? $function->getName() : null, + ); } } diff --git a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php index a4ee70e41f..a73ff8ec72 100644 --- a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php +++ b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php @@ -1198,9 +1198,7 @@ public function testBug5091(): void public function testBug9459(): void { $errors = $this->runAnalyse(__DIR__ . '/data/bug-9459.php'); - $this->assertCount(1, $errors); - $this->assertSame('PHPDoc tag @var with type callable(): array is not subtype of native type Closure(): array{}.', $errors[0]->getMessage()); - $this->assertSame(10, $errors[0]->getLine()); + $this->assertCount(0, $errors); } public function testBug9573(): void diff --git a/tests/PHPStan/Rules/PhpDoc/VarTagChangedExpressionTypeRuleTest.php b/tests/PHPStan/Rules/PhpDoc/VarTagChangedExpressionTypeRuleTest.php index f3ea56d12f..08bb43b2fd 100644 --- a/tests/PHPStan/Rules/PhpDoc/VarTagChangedExpressionTypeRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/VarTagChangedExpressionTypeRuleTest.php @@ -2,8 +2,10 @@ namespace PHPStan\Rules\PhpDoc; +use PHPStan\PhpDoc\TypeNodeResolver; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use PHPStan\Type\FileTypeMapper; /** * @extends RuleTestCase @@ -13,7 +15,12 @@ class VarTagChangedExpressionTypeRuleTest extends RuleTestCase protected function getRule(): Rule { - return new VarTagChangedExpressionTypeRule(new VarTagTypeRuleHelper(true, true)); + return new VarTagChangedExpressionTypeRule(new VarTagTypeRuleHelper( + self::getContainer()->getByType(TypeNodeResolver::class), + self::getContainer()->getByType(FileTypeMapper::class), + true, + true, + )); } public function testRule(): void diff --git a/tests/PHPStan/Rules/PhpDoc/WrongVariableNameInVarTagRuleTest.php b/tests/PHPStan/Rules/PhpDoc/WrongVariableNameInVarTagRuleTest.php index e25e8df924..c4bb1aec92 100644 --- a/tests/PHPStan/Rules/PhpDoc/WrongVariableNameInVarTagRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/WrongVariableNameInVarTagRuleTest.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules\PhpDoc; +use PHPStan\PhpDoc\TypeNodeResolver; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use PHPStan\Type\FileTypeMapper; @@ -23,7 +24,12 @@ protected function getRule(): Rule { return new WrongVariableNameInVarTagRule( self::getContainer()->getByType(FileTypeMapper::class), - new VarTagTypeRuleHelper($this->checkTypeAgainstPhpDocType, $this->strictWideningCheck), + new VarTagTypeRuleHelper( + self::getContainer()->getByType(TypeNodeResolver::class), + self::getContainer()->getByType(FileTypeMapper::class), + $this->checkTypeAgainstPhpDocType, + $this->strictWideningCheck, + ), $this->checkTypeAgainstNativeType, ); } @@ -182,6 +188,15 @@ public function testBug4505(): void $this->analyse([__DIR__ . '/data/bug-4505.php'], []); } + public function testBug12458(): void + { + $this->checkTypeAgainstNativeType = true; + $this->checkTypeAgainstPhpDocType = true; + $this->strictWideningCheck = true; + + $this->analyse([__DIR__ . '/data/bug-12458.php'], []); + } + public function testEnums(): void { if (PHP_VERSION_ID < 80100) { diff --git a/tests/PHPStan/Rules/PhpDoc/data/bug-12458.php b/tests/PHPStan/Rules/PhpDoc/data/bug-12458.php new file mode 100644 index 0000000000..08e7e1c710 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/bug-12458.php @@ -0,0 +1,29 @@ + $a + */ + public function test(array $a): void + { + /** @var \Closure(): list $c */ + $c = function () use ($a): array { + return $a; + }; + } + + /** + * @template T of HelloWorld + * @param list $a + */ + public function testGeneric(array $a): void + { + /** @var \Closure(): list $c */ + $c = function () use ($a): array { + return $a; + }; + } +}