diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index c5ef1f4f3f..79ee919046 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -547,7 +547,21 @@ public function getVariableType(string $variableName): Type return new MixedType(); } - return TypeUtils::resolveLateResolvableTypes($this->expressionTypes[$varExprString]->getType()); + $result = TypeUtils::resolveLateResolvableTypes($this->expressionTypes[$varExprString]->getType()); + + if ($variableName !== 'this') { + return $result; + } + + $staticClassNode = new Expr\ClassConstFetch(new Name('static'), 'class'); + + if ($this->hasExpressionType($staticClassNode)->yes()) { + $staticType = $this->expressionTypes[$this->getNodeKey($staticClassNode)]->getType(); + + return TypeCombinator::intersect($result, $staticType->getClassStringObjectType()); + } + + return $result; } /** @@ -1166,15 +1180,11 @@ private function resolveType(string $exprString, Expr $node): Type } if ($node instanceof Expr\StaticCall) { - if (!$node->class instanceof Name) { - return new ObjectType(Closure::class); - } - if (!$node->name instanceof Node\Identifier) { return new ObjectType(Closure::class); } - $classType = $this->resolveTypeByName($node->class); + $classType = $this->getType(new Expr\ClassConstFetch($node->class, 'class'))->getClassStringObjectType(); $methodName = $node->name->toString(); if (!$classType->hasMethod($methodName)->yes()) { return new ObjectType(Closure::class); @@ -1782,6 +1792,9 @@ static function (Node $node, Scope $scope) use ($arrowScope, &$arrowFunctionImpu if ($this->hasExpressionType($node)->yes()) { return $this->expressionTypes[$exprString]->getType(); } + if ($node->name->toLowerString() === 'class' && $node->class instanceof Name && $node->class->toLowerString() === 'static') { + return new GenericClassStringType($this->resolveTypeByNameWithoutClassName($node->class)); + } return $this->initializerExprTypeResolver->getClassConstFetchTypeByReflection( $node->class, $node->name->name, @@ -1897,7 +1910,7 @@ static function (Node $node, Scope $scope) use ($arrowScope, &$arrowFunctionImpu if ($this->nativeTypesPromoted) { $typeCallback = function () use ($node): Type { if ($node->class instanceof Name) { - $staticMethodCalledOnType = $this->resolveTypeByName($node->class); + $staticMethodCalledOnType = $this->resolveTypeByNameWithoutClassName($node->class); } else { $staticMethodCalledOnType = $this->getNativeType($node->class); } @@ -1922,7 +1935,7 @@ static function (Node $node, Scope $scope) use ($arrowScope, &$arrowFunctionImpu $typeCallback = function () use ($node): Type { if ($node->class instanceof Name) { - $staticMethodCalledOnType = $this->resolveTypeByName($node->class); + $staticMethodCalledOnType = $this->resolveTypeByNameWithoutClassName($node->class); } else { $staticMethodCalledOnType = TypeCombinator::removeNull($this->getType($node->class))->getObjectTypeOrClassStringObjectType(); } @@ -2014,7 +2027,7 @@ static function (Node $node, Scope $scope) use ($arrowScope, &$arrowFunctionImpu $typeCallback = function () use ($node): Type { if ($node->class instanceof Name) { - $staticPropertyFetchedOnType = $this->resolveTypeByName($node->class); + $staticPropertyFetchedOnType = $this->resolveTypeByNameWithoutClassName($node->class); } else { $staticPropertyFetchedOnType = TypeCombinator::removeNull($this->getType($node->class))->getObjectTypeOrClassStringObjectType(); } @@ -2566,6 +2579,45 @@ public function resolveTypeByName(Name $name): TypeWithClassName return new ObjectType($originalClass); } + private function resolveTypeByNameWithoutClassName(Name $name): Type + { + $typeWithClassName = $this->resolveTypeByName($name); + + if ($name->toLowerString() !== 'static' || ! $this->isInClass()) { + return $typeWithClassName; + } + + $typesToIntersect = [$typeWithClassName]; + $thisNode = new Variable('this'); + + if ($this->hasExpressionType($thisNode)->yes()) { + $thisType = $this->expressionTypes[$this->getNodeKey($thisNode)]->getType(); + $typesToIntersect[] = TypeTraverser::map( + $thisType, + static function (Type $type, callable $traverse): Type { + if ($type instanceof UnionType || $type instanceof IntersectionType) { + return $traverse($type); + } + + if ($type instanceof ThisType) { + return new StaticType($type->getClassReflection()); + } + + return $type; + }, + ); + } + + $staticClassNode = new Expr\ClassConstFetch(new Name('static'), 'class'); + + if ($this->hasExpressionType($staticClassNode)->yes()) { + $typesToIntersect[] = $this->expressionTypes[$this->getNodeKey($staticClassNode)]->getType() + ->getClassStringObjectType(); + } + + return TypeCombinator::intersect(...$typesToIntersect); + } + /** * @api * @param mixed $value @@ -3033,12 +3085,17 @@ public function enterClosureBind(?Type $thisType, ?Type $nativeThisType, array $ unset($nativeExpressionTypes['$this']); } + $context = $this->context; + if ($scopeClasses === ['static'] && $this->isInClass()) { $scopeClasses = [$this->getClassReflection()->getName()]; + } elseif (count($scopeClasses) === 1 && $this->reflectionProvider->hasClass($scopeClasses[0])) { + $context = ScopeContext::create($this->context->getFile()); + $context = $context->enterClass($this->reflectionProvider->getClass($scopeClasses[0])); } return $this->scopeFactory->create( - $this->context, + $context, $this->isDeclareStrictTypes(), $this->getFunction(), $this->getNamespace(), @@ -3067,7 +3124,7 @@ public function restoreOriginalScopeAfterClosureBind(self $originalScope): self } return $this->scopeFactory->create( - $this->context, + $originalScope->context, $this->isDeclareStrictTypes(), $this->getFunction(), $this->getNamespace(), diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index b0ab0ae213..e950185598 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -2051,7 +2051,7 @@ private function findEarlyTerminatingExpr(Expr $expr, Scope $scope): ?Expr $methodCalledOnType = $scope->getType($expr->var); } else { if ($expr->class instanceof Name) { - $methodCalledOnType = $scope->resolveTypeByName($expr->class); + $methodCalledOnType = $scope->getType(new Expr\ClassConstFetch($expr->class, 'class'))->getClassStringObjectType(); } else { $methodCalledOnType = $scope->getType($expr->class); } @@ -2678,19 +2678,12 @@ static function (): void { 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(); - } + $scopeObjectType = $argValueType->getObjectTypeOrClassStringObjectType(); + $thisType = $thisType !== null + // $thisType could be mixed, error, ... + ? TypeCombinator::intersect($thisType, $scopeObjectType) + : $scopeObjectType; + $scopeClasses = $scopeObjectType->getObjectClassNames(); } $closureBindScope = $scope->enterClosureBind($thisType, $nativeThisType, $scopeClasses); } @@ -4878,7 +4871,7 @@ static function (): void { } elseif ($var instanceof Expr\StaticPropertyFetch) { if ($var->class instanceof Node\Name) { - $propertyHolderType = $scope->resolveTypeByName($var->class); + $propertyHolderType = $scope->getType(new Expr\ClassConstFetch($var->class, 'class'))->getClassStringObjectType(); } else { $this->processExprNode($stmt, $var->class, $scope, $nodeCallback, $context); $propertyHolderType = $scope->getType($var->class); @@ -5128,9 +5121,10 @@ private function processStmtVarAnnotation(MutatingScope $scope, Node\Stmt $stmt, $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( $scope->getFile(), - $scope->isInClass() ? $scope->getClassReflection()->getName() : null, - $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null, - $function !== null ? $function->getName() : null, + // Closure bind can be in different class which can prevent phpdoc resolving. + $scope->isInClass() && ! $scope->isInClosureBind() ? $scope->getClassReflection()->getName() : null, + $scope->isInTrait() && ! $scope->isInClosureBind() ? $scope->getTraitReflection()->getName() : null, + $function !== null && ! $scope->isInClosureBind() ? $function->getName() : null, $comment->getText(), ); @@ -5159,7 +5153,7 @@ private function processStmtVarAnnotation(MutatingScope $scope, Node\Stmt $stmt, continue; } - if ($scope->isInClass() && $scope->getFunction() === null) { + if ($scope->isInClass() && $scope->getFunction() === null && ! $scope->isInClosureBind()) { continue; } diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 71a2693874..edc933bfca 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -485,7 +485,7 @@ public function specifyTypesInCondition( return $this->handleDefaultTruthyOrFalseyContext($context, $rootExpr, $expr, $scope); } elseif ($expr instanceof StaticCall && $expr->name instanceof Node\Identifier) { if ($expr->class instanceof Name) { - $calleeType = $scope->resolveTypeByName($expr->class); + $calleeType = $scope->getType(new ClassConstFetch($expr->class, 'class'))->getClassStringObjectType(); } else { $calleeType = $scope->getType($expr->class); } @@ -1649,7 +1649,7 @@ private function createForExpr( ) { $methodName = $expr->name->toString(); if ($expr->class instanceof Name) { - $calledOnType = $scope->resolveTypeByName($expr->class); + $calledOnType = $scope->getType(new ClassConstFetch($expr->class, 'class'))->getClassStringObjectType(); } else { $calledOnType = $scope->getType($expr->class); } diff --git a/src/Reflection/InitializerExprTypeResolver.php b/src/Reflection/InitializerExprTypeResolver.php index b3fcce1c6a..55388a8024 100644 --- a/src/Reflection/InitializerExprTypeResolver.php +++ b/src/Reflection/InitializerExprTypeResolver.php @@ -1770,8 +1770,10 @@ private function integerRangeMath(Type $range, BinaryOp $node, Type $operand): T public function getClassConstFetchTypeByReflection(Name|Expr $class, string $constantName, ?ClassReflection $classReflection, callable $getTypeCallback): Type { $isObject = false; + $loweredConstantName = strtolower($constantName); if ($class instanceof Name) { $constantClass = (string) $class; + $loweredConstantClass = strtolower($constantClass); $constantClassType = new ObjectType($constantClass); $namesToResolve = [ 'self', @@ -1780,8 +1782,8 @@ public function getClassConstFetchTypeByReflection(Name|Expr $class, string $con if ($classReflection !== null) { if ($classReflection->isFinal()) { $namesToResolve[] = 'static'; - } elseif (strtolower($constantClass) === 'static') { - if (strtolower($constantName) === 'class') { + } elseif ($loweredConstantClass === 'static') { + if ($loweredConstantName === 'class') { return new GenericClassStringType(new StaticType($classReflection)); } @@ -1789,25 +1791,31 @@ public function getClassConstFetchTypeByReflection(Name|Expr $class, string $con $isObject = true; } } - if (in_array(strtolower($constantClass), $namesToResolve, true)) { - $resolvedName = $this->resolveName($class, $classReflection); - if (strtolower($resolvedName) === 'parent' && strtolower($constantName) === 'class') { - return new ClassStringType(); + + // Exclude ::class to prevent infinite cycle. + if ($loweredConstantClass === 'static' && $loweredConstantName !== 'class') { + $constantClassType = $getTypeCallback(new ClassConstFetch(new Name('static'), 'class'))->getClassStringObjectType(); + } else { + if (in_array($loweredConstantClass, $namesToResolve, true)) { + $resolvedName = $this->resolveName($class, $classReflection); + if (strtolower($resolvedName) === 'parent' && $loweredConstantName === 'class') { + return new ClassStringType(); + } + $constantClassType = $this->resolveTypeByName($class, $classReflection); } - $constantClassType = $this->resolveTypeByName($class, $classReflection); - } - if (strtolower($constantName) === 'class') { - return new ConstantStringType($constantClassType->getClassName(), true); + if ($loweredConstantName === 'class') { + return new ConstantStringType($constantClassType->getClassName(), true); + } } - } elseif ($class instanceof String_ && strtolower($constantName) === 'class') { + } elseif ($class instanceof String_ && $loweredConstantName === 'class') { return new ConstantStringType($class->value, true); } else { $constantClassType = $getTypeCallback($class); $isObject = true; } - if (strtolower($constantName) === 'class') { + if ($loweredConstantName === 'class') { return TypeTraverser::map( $constantClassType, function (Type $type, callable $traverse): Type { diff --git a/src/Rules/Classes/ClassConstantRule.php b/src/Rules/Classes/ClassConstantRule.php index f8425ff295..0dbf42adea 100644 --- a/src/Rules/Classes/ClassConstantRule.php +++ b/src/Rules/Classes/ClassConstantRule.php @@ -66,7 +66,9 @@ public function processNode(Node $node, Scope $scope): array ]; } - $classType = $scope->resolveTypeByName($class); + $classType = $lowercasedClassName === 'static' + ? $scope->getType(new ClassConstFetch(new Node\Name('static'), 'class'))->getClassStringObjectType() + : $scope->resolveTypeByName($class); } elseif ($lowercasedClassName === 'parent') { if (!$scope->isInClass()) { return [ diff --git a/src/Rules/Comparison/ImpossibleCheckTypeHelper.php b/src/Rules/Comparison/ImpossibleCheckTypeHelper.php index 9463072340..e0e9a9de7e 100644 --- a/src/Rules/Comparison/ImpossibleCheckTypeHelper.php +++ b/src/Rules/Comparison/ImpossibleCheckTypeHelper.php @@ -387,7 +387,7 @@ private function determineContext(Scope $scope, Expr $node): TypeSpecifierContex } } elseif ($node instanceof StaticCall && $node->name instanceof Node\Identifier) { if ($node->class instanceof Node\Name) { - $calleeType = $scope->resolveTypeByName($node->class); + $calleeType = $scope->getType(new Expr\ClassConstFetch($node->class, 'class'))->getClassStringObjectType(); } else { $calleeType = $scope->getType($node->class); } diff --git a/src/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRule.php b/src/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRule.php index 5d35d1be1a..98d41a630c 100644 --- a/src/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRule.php +++ b/src/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRule.php @@ -103,7 +103,7 @@ private function getMethod( ): MethodReflection { if ($class instanceof Node\Name) { - $calledOnType = $scope->resolveTypeByName($class); + $calledOnType = $scope->getType(new Expr\ClassConstFetch($class, 'class'))->getClassStringObjectType(); } else { $calledOnType = $scope->getType($class); } diff --git a/src/Rules/Methods/CallPrivateMethodThroughStaticRule.php b/src/Rules/Methods/CallPrivateMethodThroughStaticRule.php index 0a37e48657..b8c311d702 100644 --- a/src/Rules/Methods/CallPrivateMethodThroughStaticRule.php +++ b/src/Rules/Methods/CallPrivateMethodThroughStaticRule.php @@ -36,7 +36,7 @@ public function processNode(Node $node, Scope $scope): array return []; } - $classType = $scope->resolveTypeByName($className); + $classType = $scope->getType(new Node\Expr\ClassConstFetch(new Name('static'), 'class'))->getClassStringObjectType(); if (!$classType->hasMethod($methodName)->yes()) { return []; } diff --git a/src/Rules/Methods/StaticMethodCallCheck.php b/src/Rules/Methods/StaticMethodCallCheck.php index d50f8dfd32..acbd2a1a68 100644 --- a/src/Rules/Methods/StaticMethodCallCheck.php +++ b/src/Rules/Methods/StaticMethodCallCheck.php @@ -146,6 +146,10 @@ public function check( } } } + + if ($lowercasedClassName === 'static') { + $classType = $scope->getType(new Expr\ClassConstFetch(new Name('static'), 'class'))->getClassStringObjectType(); + } } else { $classTypeResult = $this->ruleLevelHelper->findTypeToCheck( $scope, diff --git a/src/Rules/Properties/AccessStaticPropertiesRule.php b/src/Rules/Properties/AccessStaticPropertiesRule.php index 9d929c39a2..96abea2cd1 100644 --- a/src/Rules/Properties/AccessStaticPropertiesRule.php +++ b/src/Rules/Properties/AccessStaticPropertiesRule.php @@ -85,7 +85,9 @@ private function processSingleProperty(Scope $scope, StaticPropertyFetch $node, ))->identifier(sprintf('outOfClass.%s', $lowercasedClass))->build(), ]; } - $classType = $scope->resolveTypeByName($node->class); + $classType = $lowercasedClass === 'static' + ? $scope->getType(new Node\Expr\ClassConstFetch(new Name('static'), 'class'))->getClassStringObjectType() + : $scope->resolveTypeByName($node->class); } elseif ($lowercasedClass === 'parent') { if (!$scope->isInClass()) { return [ diff --git a/src/Rules/Properties/PropertyReflectionFinder.php b/src/Rules/Properties/PropertyReflectionFinder.php index 3f6cc8b000..a742c6f36d 100644 --- a/src/Rules/Properties/PropertyReflectionFinder.php +++ b/src/Rules/Properties/PropertyReflectionFinder.php @@ -49,7 +49,7 @@ public function findPropertyReflectionsFromNode($propertyFetch, Scope $scope): a } if ($propertyFetch->class instanceof Node\Name) { - $propertyHolderType = $scope->resolveTypeByName($propertyFetch->class); + $propertyHolderType = $scope->getType(new Expr\ClassConstFetch($propertyFetch->class, 'class'))->getClassStringObjectType(); } else { $propertyHolderType = $scope->getType($propertyFetch->class); } @@ -98,7 +98,9 @@ public function findPropertyReflectionFromNode($propertyFetch, Scope $scope): ?F } if ($propertyFetch->class instanceof Node\Name) { - $propertyHolderType = $scope->resolveTypeByName($propertyFetch->class); + $propertyHolderType = $propertyFetch->class->toLowerString() === 'static' + ? $scope->getType(new Expr\ClassConstFetch($propertyFetch->class, 'class'))->getClassStringObjectType() + : $scope->resolveTypeByName($propertyFetch->class); } else { $propertyHolderType = $scope->getType($propertyFetch->class); } diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index dabbc6dca5..648804995f 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -1364,6 +1364,7 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8486.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-9000.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/enum-from.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/bug-instanceof-static-vs-this-1st-class-callable.php'); } yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8956.php'); @@ -1473,6 +1474,11 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/PhpDoc/data/bug-10594.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/set-type-type-specifying.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/mysqli_fetch_object.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/bug-instanceof-static-vs-this.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-instanceof-static-vs-this-type-specifier.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-instanceof-static-vs-this-early-termination.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-instanceof-static-vs-this-prop-assign-scope.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5987.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10468.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6613.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10187.php'); @@ -1504,4 +1510,11 @@ public static function getAdditionalConfigFiles(): array ]; } + protected static function getEarlyTerminatingMethodCalls(): array + { + return [ + 'BugInstanceofStaticVsThisEarlyTermination\\FooChild' => ['terminate'], + ]; + } + } diff --git a/tests/PHPStan/Analyser/data/bug-5987.php b/tests/PHPStan/Analyser/data/bug-5987.php new file mode 100644 index 0000000000..5e16aee055 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-5987.php @@ -0,0 +1,46 @@ +', static::class); + \PHPStan\Testing\assertType("'c'", static::getScope()); + \PHPStan\Testing\assertType("'c'", (static::class)::getScope()); + + \PHPStan\Testing\assertType("'Bug5987\\\\A'", parent::class); + \PHPStan\Testing\assertType("'a'", parent::getScope()); + \PHPStan\Testing\assertType("'a'", (parent::class)::getScope()); + + \PHPStan\Testing\assertType("'c'", $this->getScope()); + \PHPStan\Testing\assertType("'b'", $this->getScopePriv()); + }, $this, B::class)(); + } +} + +$c = new C(); +$c->test(); diff --git a/tests/PHPStan/Analyser/data/bug-instanceof-static-vs-this-early-termination.php b/tests/PHPStan/Analyser/data/bug-instanceof-static-vs-this-early-termination.php new file mode 100644 index 0000000000..22d9493082 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-instanceof-static-vs-this-early-termination.php @@ -0,0 +1,59 @@ +terminate(); + } + + if (rand(0, 1)) { + $foo = 'b'; + $this::terminate(); + } + + if (rand(0, 1)) { + $foo = 'c'; + static::terminate(); + } + + \PHPStan\Testing\assertType('1', $foo); + } + + if (is_a(static::class, FooChild::class, true)) { + $foo = 1; + + if (rand(0, 1)) { + $foo = 'a'; + $this->terminate(); + } + + if (rand(0, 1)) { + $foo = 'b'; + $this::terminate(); + } + + if (rand(0, 1)) { + $foo = 'c'; + static::terminate(); + } + + \PHPStan\Testing\assertType('1', $foo); + } + } +} + +class FooChild extends FooBase +{ + // registered as earlyTerminatingMethodCalls in NodeScopeResolverTest + public static function terminate() + { + } +} diff --git a/tests/PHPStan/Analyser/data/bug-instanceof-static-vs-this-prop-assign-scope.php b/tests/PHPStan/Analyser/data/bug-instanceof-static-vs-this-prop-assign-scope.php new file mode 100644 index 0000000000..a8f7c265dd --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-instanceof-static-vs-this-prop-assign-scope.php @@ -0,0 +1,30 @@ +isNull($v)) \PHPStan\Testing\assertType('null', $v); + if ($this::isNull($v)) \PHPStan\Testing\assertType('null', $v); + if (static::isNull($v)) \PHPStan\Testing\assertType('null', $v); + + if ($this::foo() === 5) \PHPStan\Testing\assertType('5', $this::foo()); + if ($this->foo() === 5) \PHPStan\Testing\assertType('5', $this->foo()); + if (static::foo() === 5) \PHPStan\Testing\assertType('5', static::foo()); + } + + if (is_a(static::class, FooInterface::class, true)) { + if ($this->isNull($v)) \PHPStan\Testing\assertType('null', $v); + if ($this::isNull($v)) \PHPStan\Testing\assertType('null', $v); + if (static::isNull($v)) \PHPStan\Testing\assertType('null', $v); + + if ($this::foo() === 5) \PHPStan\Testing\assertType('5', $this::foo()); + if ($this->foo() === 5) \PHPStan\Testing\assertType('5', $this->foo()); + if (static::foo() === 5) \PHPStan\Testing\assertType('5', static::foo()); + } + } +} diff --git a/tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php b/tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php index 42378de1f2..ffe9f75ec1 100644 --- a/tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php @@ -420,4 +420,10 @@ public function testPhpstanInternalClass(): void ]); } + public function testBugInstanceofStaticVsThis(): void + { + $this->phpVersion = PHP_VERSION_ID; + $this->analyse([__DIR__ . '/../Methods/data/bug-instanceof-static-vs-this.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php index 361aa84bbb..90e9ebea28 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php @@ -181,30 +181,30 @@ public function testImpossibleCheckTypeFunctionCall(): void 631, ], [ - 'Call to function method_exists() with \'CheckTypeFunctionCall\\\\MethodExistsWithTrait\' and \'method\' will always evaluate to true.', + 'Call to function method_exists() with class-string and \'method\' will always evaluate to true.', 634, 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', ], [ - 'Call to function method_exists() with \'CheckTypeFunctionCall\\\\MethodExistsWithTrait\' and \'someAnother\' will always evaluate to true.', + 'Call to function method_exists() with class-string and \'someAnother\' will always evaluate to true.', 637, 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', ], [ - 'Call to function method_exists() with \'CheckTypeFunctionCall\\\\MethodExistsWithTrait\' and \'unknown\' will always evaluate to false.', + 'Call to function method_exists() with class-string and \'unknown\' will always evaluate to false.', 640, 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', ], [ - 'Call to function method_exists() with \'CheckTypeFunctionCall\\\\MethodExistsWithTrait\' and \'method\' will always evaluate to true.', + 'Call to function method_exists() with class-string and \'method\' will always evaluate to true.', 643, ], [ - 'Call to function method_exists() with \'CheckTypeFunctionCall\\\\MethodExistsWithTrait\' and \'someAnother\' will always evaluate to true.', + 'Call to function method_exists() with class-string and \'someAnother\' will always evaluate to true.', 646, ], [ - 'Call to function method_exists() with \'CheckTypeFunctionCall\\\\MethodExistsWithTrait\' and \'unknown\' will always evaluate to false.', + 'Call to function method_exists() with class-string and \'unknown\' will always evaluate to false.', 649, ], [ @@ -349,12 +349,12 @@ public function testImpossibleCheckTypeFunctionCallWithoutAlwaysTrue(): void 631, ], [ - 'Call to function method_exists() with \'CheckTypeFunctionCall\\\\MethodExistsWithTrait\' and \'unknown\' will always evaluate to false.', + 'Call to function method_exists() with class-string and \'unknown\' will always evaluate to false.', 640, 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', ], [ - 'Call to function method_exists() with \'CheckTypeFunctionCall\\\\MethodExistsWithTrait\' and \'unknown\' will always evaluate to false.', + 'Call to function method_exists() with class-string and \'unknown\' will always evaluate to false.', 649, ], [ diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleTest.php index 7a697e6ffa..0ae949e720 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleTest.php @@ -248,6 +248,25 @@ public function testReportAlwaysTrueInLastCondition(bool $reportAlwaysTrueInLast $this->analyse([__DIR__ . '/data/impossible-method-report-always-true-last-condition.php'], $expectedErrors); } + public function testBugInstanceofStaticVsThis(): void + { + $message = 'Call to method BugInstanceofStaticVsThisImpossibleCheck\FooInterface::isNull() with int will always evaluate to false.'; + $tip = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-instanceof-static-vs-this-impossible-check.php'], [ + [ + $message, + 17, + $tip, + ], + [ + $message, + 23, + $tip, + ], + ]); + } + public static function getAdditionalConfigFiles(): array { return [ diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRuleTest.php index a93147ab83..910f910a5b 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRuleTest.php @@ -145,6 +145,35 @@ public function testReportAlwaysTrueInLastCondition(bool $reportAlwaysTrueInLast $this->analyse([__DIR__ . '/data/impossible-static-method-report-always-true-last-condition.php'], $expectedErrors); } + public function testBugInstanceofStaticVsThis(): void + { + $message = 'Call to static method BugInstanceofStaticVsThisImpossibleCheck\FooInterface::isNull() with int will always evaluate to false.'; + $tip = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-instanceof-static-vs-this-impossible-check.php'], [ + [ + $message, + 18, + $tip, + ], + [ + $message, + 19, + $tip, + ], + [ + $message, + 24, + $tip, + ], + [ + $message, + 25, + $tip, + ], + ]); + } + public static function getAdditionalConfigFiles(): array { return [ diff --git a/tests/PHPStan/Rules/Comparison/data/bug-instanceof-static-vs-this-impossible-check.php b/tests/PHPStan/Rules/Comparison/data/bug-instanceof-static-vs-this-impossible-check.php new file mode 100644 index 0000000000..0fbc77b916 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-instanceof-static-vs-this-impossible-check.php @@ -0,0 +1,28 @@ +isNull($v)) echo 'a'; + if ($this::isNull($v)) echo 'a'; + if (static::isNull($v)) echo 'a'; + } + + if (is_a(static::class, FooInterface::class, true)) { + if ($this->isNull($v)) echo 'a'; + if ($this::isNull($v)) echo 'a'; + if (static::isNull($v)) echo 'a'; + } + } +} diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index 5d1e0c3e39..854c3291ee 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -1646,6 +1646,40 @@ public function testArgon2PasswordHash(): void $this->analyse([__DIR__ . '/data/argon2id-password-hash.php'], []); } + public function testBugInstanceofStaticVsThis(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/../Properties/data/bug-instanceof-static-vs-this-property-assign.php'], [ + [ + 'Parameter #1 $var is passed by reference so it does not accept readonly property BugInstanceofStaticVsThisPropertyAssign\FooChild::$nativeReadonlyProp.', + 17, + ], + [ + 'Parameter #1 $var of function BugInstanceofStaticVsThisPropertyAssign\set expects int, string given.', + 24, + ], + [ + 'Parameter #1 $var of function BugInstanceofStaticVsThisPropertyAssign\set expects int, string given.', + 25, + ], + [ + 'Parameter #1 $var is passed by reference so it does not accept readonly property BugInstanceofStaticVsThisPropertyAssign\FooChild::$nativeReadonlyProp.', + 31, + ], + [ + 'Parameter #1 $var of function BugInstanceofStaticVsThisPropertyAssign\set expects int, string given.', + 38, + ], + [ + 'Parameter #1 $var of function BugInstanceofStaticVsThisPropertyAssign\set expects int, string given.', + 39, + ], + ]); + } + public function testParamClosureThis(): void { if (PHP_VERSION_ID < 70400) { diff --git a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php index 9196f847c4..18389496d0 100644 --- a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php @@ -3255,6 +3255,15 @@ public function testBuSplObjectStorageRemove(): void ]); } + public function testBugInstanceofStaticVsThis(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-instanceof-static-vs-this.php'], []); + } + public function testClosureBindToParamClosureThis(): void { $this->checkThisOnly = false; diff --git a/tests/PHPStan/Rules/Methods/CallPrivateMethodThroughStaticRuleTest.php b/tests/PHPStan/Rules/Methods/CallPrivateMethodThroughStaticRuleTest.php index 25cd02b49b..bc3997f873 100644 --- a/tests/PHPStan/Rules/Methods/CallPrivateMethodThroughStaticRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallPrivateMethodThroughStaticRuleTest.php @@ -26,4 +26,14 @@ public function testRule(): void ]); } + public function testInstanceof(): void + { + $this->analyse([__DIR__ . '/data/call-private-method-static-instanceof.php'], [ + [ + 'Unsafe call to private method CallPrivateMethodStaticInstanceof\FooBase::fooPrivate() through static::.', + 27, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php index f60d015e6c..8cce993439 100644 --- a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php @@ -817,6 +817,20 @@ public function testConditionalParam(): void ]); } + public function testBugInstanceofStaticVsThis(): void + { + $this->checkThisOnly = false; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-instanceof-static-vs-this.php'], []); + } + + public function testBug9465(): void + { + $this->checkThisOnly = false; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-9465.php'], []); + } + public function testClosureBindParamClosureThis(): void { $this->checkThisOnly = false; diff --git a/tests/PHPStan/Rules/Methods/StaticMethodCallableRuleTest.php b/tests/PHPStan/Rules/Methods/StaticMethodCallableRuleTest.php index 8b026d8abf..ccfba5b4c3 100644 --- a/tests/PHPStan/Rules/Methods/StaticMethodCallableRuleTest.php +++ b/tests/PHPStan/Rules/Methods/StaticMethodCallableRuleTest.php @@ -115,4 +115,13 @@ public function testCallsOnGenericClassString(): void $this->analyse([__DIR__ . '/../Comparison/data/impossible-method-exists-on-generic-class-string.php'], []); } + public function testBugInstanceofStaticVsThis1stClassCallable(): void + { + if (PHP_VERSION_ID < 80100) { + self::markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-instanceof-static-vs-this-1st-class-callable.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Methods/data/bug-9465.php b/tests/PHPStan/Rules/Methods/data/bug-9465.php new file mode 100644 index 0000000000..73261fecd0 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-9465.php @@ -0,0 +1,49 @@ +getStuff(); + } + return null; + } +} + +/** + * @method static void unavailableStatic() + */ +class Application extends Component implements ISingleton +{ + protected static Application $_app; + + public function __construct() + { + static::$_app = $this; + } + + public static function singleton(): ?static + { + return static::$_app; //<- Method Application::singleton() should return static(Application)|null but returns Application. + } +} + +Application::unavailableStatic(); diff --git a/tests/PHPStan/Rules/Methods/data/bug-instanceof-static-vs-this-1st-class-callable.php b/tests/PHPStan/Rules/Methods/data/bug-instanceof-static-vs-this-1st-class-callable.php new file mode 100644 index 0000000000..a6e5da37bc --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-instanceof-static-vs-this-1st-class-callable.php @@ -0,0 +1,28 @@ += 8.1 + +namespace BugInstanceofStaticVsThis1stClassCallable; + +interface FooInterface +{ + public static function foo(): int; +} + +class FooBase +{ + use FooTrait; + + public function bar(): void + { + if ($this instanceof FooInterface) { + \PHPStan\Testing\assertType('int', (static::foo(...))()); + \PHPStan\Testing\assertType('int', ($this::foo(...))()); + \PHPStan\Testing\assertType('int', ($this->foo(...))()); + } + + if (is_a(static::class, FooInterface::class, true)) { + \PHPStan\Testing\assertType('int', (static::foo(...))()); + \PHPStan\Testing\assertType('int', ($this::foo(...))()); + \PHPStan\Testing\assertType('int', ($this->foo(...))()); + } + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-instanceof-static-vs-this.php b/tests/PHPStan/Rules/Methods/data/bug-instanceof-static-vs-this.php new file mode 100644 index 0000000000..651e20de83 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-instanceof-static-vs-this.php @@ -0,0 +1,93 @@ += 7.4 + +namespace BugInstanceofStaticVsThis; + +interface FooInterface +{ + public static function foo(): int; +} + +class FooBase +{ + use FooTrait; + + public function bar(): void + { + if ($this instanceof FooInterface) { + \PHPStan\Testing\assertType('$this(BugInstanceofStaticVsThis\FooBase)&BugInstanceofStaticVsThis\FooInterface', $this); + \PHPStan\Testing\assertType('class-string', static::class); + \PHPStan\Testing\assertType('int', $this::foo()); + \PHPStan\Testing\assertType('int', $this->foo()); + \PHPStan\Testing\assertType('int', static::foo()); + + \PHPStan\Testing\assertNativeType('$this(BugInstanceofStaticVsThis\FooBase)&BugInstanceofStaticVsThis\FooInterface', $this); + \PHPStan\Testing\assertNativeType('class-string', static::class); + \PHPStan\Testing\assertNativeType('int', $this::foo()); + \PHPStan\Testing\assertNativeType('int', $this->foo()); + \PHPStan\Testing\assertNativeType('int', static::foo()); + } + + if (is_a(static::class, FooInterface::class, true)) { + \PHPStan\Testing\assertType('$this(BugInstanceofStaticVsThis\FooBase)&BugInstanceofStaticVsThis\FooInterface', $this); + \PHPStan\Testing\assertType('class-string', static::class); + \PHPStan\Testing\assertType('int', $this::foo()); + \PHPStan\Testing\assertType('int', $this->foo()); + \PHPStan\Testing\assertType('int', static::foo()); + + \PHPStan\Testing\assertNativeType('$this(BugInstanceofStaticVsThis\FooBase)&BugInstanceofStaticVsThis\FooInterface', $this); + \PHPStan\Testing\assertNativeType('class-string', static::class); + \PHPStan\Testing\assertNativeType('int', $this::foo()); + \PHPStan\Testing\assertNativeType('int', $this->foo()); + \PHPStan\Testing\assertNativeType('int', static::foo()); + } + } +} + +final class FooChild extends FooBase +{ + public const CONSTANT = 'a'; + public static int $staticProp = 5; + public string $prop = 'b'; +} + +trait FooTrait +{ + public function baz(): void + { + if ($this instanceof FooChild) { + \PHPStan\Testing\assertType("'a'", static::CONSTANT); + \PHPStan\Testing\assertType("'a'", $this::CONSTANT); + + \PHPStan\Testing\assertType("int", static::$staticProp); + \PHPStan\Testing\assertType("int", $this::$staticProp); + + \PHPStan\Testing\assertType("string", $this->prop); + + \PHPStan\Testing\assertNativeType("'a'", static::CONSTANT); + \PHPStan\Testing\assertNativeType("'a'", $this::CONSTANT); + + \PHPStan\Testing\assertNativeType("int", static::$staticProp); + \PHPStan\Testing\assertNativeType("int", $this::$staticProp); + + \PHPStan\Testing\assertNativeType("string", $this->prop); + } + + if (is_a(static::class, FooChild::class, true)) { + \PHPStan\Testing\assertType("'a'", static::CONSTANT); + \PHPStan\Testing\assertType("'a'", $this::CONSTANT); + + \PHPStan\Testing\assertType("int", static::$staticProp); + \PHPStan\Testing\assertType("int", $this::$staticProp); + + \PHPStan\Testing\assertType("string", $this->prop); + + \PHPStan\Testing\assertNativeType("'a'", static::CONSTANT); + \PHPStan\Testing\assertNativeType("'a'", $this::CONSTANT); + + \PHPStan\Testing\assertNativeType("int", static::$staticProp); + \PHPStan\Testing\assertNativeType("int", $this::$staticProp); + + \PHPStan\Testing\assertNativeType("string", $this->prop); + } + } +} diff --git a/tests/PHPStan/Rules/Methods/data/call-private-method-static-instanceof.php b/tests/PHPStan/Rules/Methods/data/call-private-method-static-instanceof.php new file mode 100644 index 0000000000..ae1490ed44 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/call-private-method-static-instanceof.php @@ -0,0 +1,29 @@ +analyse([__DIR__ . '/data/bug-8629.php'], []); } + public function testBugInstanceofStaticVsThis(): void + { + $this->checkThisOnly = false; + $this->checkUnionTypes = true; + $this->checkDynamicProperties = true; + $this->analyse([__DIR__ . '/../Methods/data/bug-instanceof-static-vs-this.php'], []); + } + public function testBug9694(): void { if (PHP_VERSION_ID < 80000) { diff --git a/tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php index 14e4779f22..a4f9c514a5 100644 --- a/tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php @@ -463,4 +463,9 @@ public function testBug8333(): void ]); } + public function testBugInstanceofStaticVsThis(): void + { + $this->analyse([__DIR__ . '/../Methods/data/bug-instanceof-static-vs-this.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRefRuleTest.php b/tests/PHPStan/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRefRuleTest.php index d000d4f3f3..b4e3d1d621 100644 --- a/tests/PHPStan/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRefRuleTest.php +++ b/tests/PHPStan/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRefRuleTest.php @@ -68,4 +68,22 @@ public function testRuleIgnoresNativeReadonly(): void $this->analyse([__DIR__ . '/data/readonly-assign-ref-phpdoc-and-native.php'], []); } + public function testBugInstanceofStaticVsThis(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-instanceof-static-vs-this-property-assign.php'], [ + [ + '@readonly property BugInstanceofStaticVsThisPropertyAssign\FooChild::$phpdocReadonlyProp is assigned by reference.', + 20, + ], + [ + '@readonly property BugInstanceofStaticVsThisPropertyAssign\FooChild::$phpdocReadonlyProp is assigned by reference.', + 34, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRuleTest.php b/tests/PHPStan/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRuleTest.php index e5496fc646..8a8b086c02 100644 --- a/tests/PHPStan/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRuleTest.php +++ b/tests/PHPStan/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRuleTest.php @@ -160,4 +160,22 @@ public function testFeature7648(): void $this->analyse([__DIR__ . '/data/feature-7648.php'], []); } + public function testBugInstanceofStaticVsThis(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-instanceof-static-vs-this-property-assign.php'], [ + [ + '@readonly property BugInstanceofStaticVsThisPropertyAssign\FooChild::$phpdocReadonlyProp is assigned outside of its declaring class.', + 16, + ], + [ + '@readonly property BugInstanceofStaticVsThisPropertyAssign\FooChild::$phpdocReadonlyProp is assigned outside of its declaring class.', + 30, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Properties/ReadOnlyPropertyAssignRefRuleTest.php b/tests/PHPStan/Rules/Properties/ReadOnlyPropertyAssignRefRuleTest.php index c0dae45d97..63180ec8a4 100644 --- a/tests/PHPStan/Rules/Properties/ReadOnlyPropertyAssignRefRuleTest.php +++ b/tests/PHPStan/Rules/Properties/ReadOnlyPropertyAssignRefRuleTest.php @@ -39,4 +39,22 @@ public function testRule(): void ]); } + public function testBugInstanceofStaticVsThis(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-instanceof-static-vs-this-property-assign.php'], [ + [ + 'Readonly property BugInstanceofStaticVsThisPropertyAssign\FooChild::$nativeReadonlyProp is assigned by reference.', + 19, + ], + [ + 'Readonly property BugInstanceofStaticVsThisPropertyAssign\FooChild::$nativeReadonlyProp is assigned by reference.', + 33, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Properties/ReadOnlyPropertyAssignRuleTest.php b/tests/PHPStan/Rules/Properties/ReadOnlyPropertyAssignRuleTest.php index 90da9c44ec..18b4dd4c87 100644 --- a/tests/PHPStan/Rules/Properties/ReadOnlyPropertyAssignRuleTest.php +++ b/tests/PHPStan/Rules/Properties/ReadOnlyPropertyAssignRuleTest.php @@ -154,4 +154,22 @@ public function testBug6773(): void ]); } + public function testBugInstanceofStaticVsThis(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-instanceof-static-vs-this-property-assign.php'], [ + [ + 'Readonly property BugInstanceofStaticVsThisPropertyAssign\FooChild::$nativeReadonlyProp is assigned outside of its declaring class.', + 15, + ], + [ + 'Readonly property BugInstanceofStaticVsThisPropertyAssign\FooChild::$nativeReadonlyProp is assigned outside of its declaring class.', + 29, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php index 7a788aa3cc..2719b958ab 100644 --- a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php @@ -608,4 +608,32 @@ public function testUnset(): void ]); } + public function testBugInstanceofStaticVsThis(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->checkExplicitMixed = true; + $message = 'Static property BugInstanceofStaticVsThisPropertyAssign\FooChild::$staticStringProp (string) does not accept int.'; + $this->analyse([__DIR__ . '/data/bug-instanceof-static-vs-this-property-assign.php'], [ + [ + $message, + 22, + ], + [ + $message, + 23, + ], + [ + $message, + 36, + ], + [ + $message, + 37, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Properties/data/bug-instanceof-static-vs-this-property-assign.php b/tests/PHPStan/Rules/Properties/data/bug-instanceof-static-vs-this-property-assign.php new file mode 100644 index 0000000000..52abd18681 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-instanceof-static-vs-this-property-assign.php @@ -0,0 +1,58 @@ += 8.1 + +namespace BugInstanceofStaticVsThisPropertyAssign; + +function set(int &$var): void +{ + $var = 5; +} + +class FooBase +{ + public function run(): void + { + if ($this instanceof FooChild) { + $this->nativeReadonlyProp = 5; + $this->phpdocReadonlyProp = 5; + set($this->nativeReadonlyProp); + set($this->phpdocReadonlyProp); + $a = &$this->nativeReadonlyProp; + $b = &$this->phpdocReadonlyProp; + + if (rand()) $this::$staticStringProp = 5; + if (rand()) static::$staticStringProp = 5; + set($this::$staticStringProp); + set(static::$staticStringProp); + } + + if (is_a(static::class, FooChild::class, true)) { + $this->nativeReadonlyProp = 5; + $this->phpdocReadonlyProp = 5; + set($this->nativeReadonlyProp); + set($this->phpdocReadonlyProp); + $a = &$this->nativeReadonlyProp; + $b = &$this->phpdocReadonlyProp; + + if (rand()) $this::$staticStringProp = 5; + if (rand()) static::$staticStringProp = 5; + set($this::$staticStringProp); + set(static::$staticStringProp); + } + } +} + +class FooChild extends FooBase +{ + public readonly int $nativeReadonlyProp; + + /** @readonly */ + public int $phpdocReadonlyProp; + + public static string $staticStringProp; + + public function __construct() + { + $this->nativeReadonlyProp = 5; + $this->phpdocReadonlyProp = 5; + } +}