diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 8d43a7d613..b7066cf980 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -974,6 +974,7 @@ private function resolveType(string $exprString, Expr $node): Type && !$node instanceof Expr\Closure && !$node instanceof Expr\ArrowFunction && $this->hasExpressionType($node)->yes() + && (!$this->expressionHasNewInChain($node) || $this->expressionTypes[$exprString]->getExpr() === $node) ) { return $this->expressionTypes[$exprString]->getType(); } @@ -990,6 +991,27 @@ private function resolveType(string $exprString, Expr $node): Type return new MixedType(); } + private function expressionHasNewInChain(Expr $expr): bool + { + if ( + $expr instanceof MethodCall || + $expr instanceof Expr\NullsafeMethodCall || + $expr instanceof Expr\ArrayDimFetch || + $expr instanceof PropertyFetch || + $expr instanceof Expr\NullsafePropertyFetch + ) { + return $expr->var instanceof Expr\New_ || $this->expressionHasNewInChain($expr->var); + } + if ( + $expr instanceof Expr\StaticCall + || $expr instanceof Expr\StaticPropertyFetch + ) { + return $expr->class instanceof Expr\New_ || ($expr->class instanceof Expr && $this->expressionHasNewInChain($expr->class)); + } + + return false; + } + /** * @param callable(Type): ?bool $typeCallback */ diff --git a/tests/PHPStan/Analyser/nsrt/bug-8985.php b/tests/PHPStan/Analyser/nsrt/bug-8985.php new file mode 100644 index 0000000000..47e5522a3b --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-8985.php @@ -0,0 +1,111 @@ += 8.0 + +declare(strict_types = 1); + +namespace Bug8985; + +use function PHPStan\Testing\assertType; + +class Entity +{ + public string $value; + + public function __construct(string $value) + { + $this->value = $value; + } + + public function getValue(): string + { + return $this->value; + } +} + +class Repository +{ + /** @return array */ + public function getAll(): array + { + return [new Entity('test')]; + } + + public string $name = 'default'; + + /** @return array */ + public static function staticGetAll(): array + { + return [new Entity('test')]; + } + + public function getEntity(): Entity + { + return new Entity('test'); + } + + public const MY_CONST = 'const_value'; +} + +function testMethodCall(): void { + assert((new Repository())->getAll() === []); + + $all = (new Repository())->getAll(); + assertType('array', $all); + $value = $all[0]->getValue(); +} + +function testNullsafeMethodCall(): void { + assert((new Repository())?->getEntity()?->getValue() === 'specific'); + + assertType('string', (new Repository())?->getEntity()?->getValue()); +} + +function testPropertyFetch(): void { + assert((new Repository())->name === 'foo'); + + assertType('string', (new Repository())->name); +} + +function testNullsafePropertyFetch(): void { + assert((new Repository())?->name === 'foo'); + + assertType('string', (new Repository())?->name); +} + +function testArrayDimFetch(): void { + assert((new Repository())->getAll()[0]->getValue() === 'specific'); + + assertType('string', (new Repository())->getAll()[0]->getValue()); +} + +function testStaticCall(): void { + assert((new Repository())::staticGetAll() === []); + + assertType('array', (new Repository())::staticGetAll()); +} + +function testChainedMethodCalls(): void { + assert((new Repository())->getEntity()->getValue() === 'specific'); + + assertType('string', (new Repository())->getEntity()->getValue()); +} + +function testChainedPropertyOnMethodCall(): void { + assert((new Repository())->getEntity()->value === 'specific'); + + assertType('string', (new Repository())->getEntity()->value); +} + +function testClassConstFetch(): void { + assert((new Repository())::MY_CONST === 'const_value'); + + assertType("'const_value'", (new Repository())::MY_CONST); +} + +function testClassConstFetchOnUnknownClass(string $class, string $anotherClass): void { + assert((new $class())::MY_CONST === 'const_value'); + + assertType("'const_value'", (new $class())::MY_CONST); + + $class = $anotherClass; + assertType("*ERROR*", (new $class())::MY_CONST); +} diff --git a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php index 7cd71f4123..44588063e8 100644 --- a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php @@ -1270,6 +1270,13 @@ public function testBug13773(): void ]); } + public function testBug8985(): void + { + $this->reportPossiblyNonexistentConstantArrayOffset = true; + + $this->analyse([__DIR__ . '/data/bug-8985.php'], []); + } + public function testBug14308(): void { $this->reportPossiblyNonexistentConstantArrayOffset = true; diff --git a/tests/PHPStan/Rules/Arrays/data/bug-8985.php b/tests/PHPStan/Rules/Arrays/data/bug-8985.php new file mode 100644 index 0000000000..f066b4d590 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-8985.php @@ -0,0 +1,31 @@ += 8.0 + +declare(strict_types=1); + +namespace Bug8985c; + +class Entity +{ + public function __construct(private string $value) + { + } + + public function getValue(): string + { + return $this->value; + } +} + +class Repository +{ + /** @return array */ + public function getAll(): array + { + return [new Entity('test')]; + } +} + +assert((new Repository())->getAll() === []); + +$all = (new Repository())->getAll(); +$value = $all[0]->getValue(); diff --git a/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php b/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php index e381f321f0..3d233b8725 100644 --- a/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php +++ b/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php @@ -1326,6 +1326,12 @@ public function testBug10924(): void $this->analyse([__DIR__ . '/data/bug-10924.php'], []); } + public function testBug8985(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-8985.php'], []); + } + public function testBug11430(): void { $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-11430.php'], []); diff --git a/tests/PHPStan/Rules/Methods/data/bug-8985.php b/tests/PHPStan/Rules/Methods/data/bug-8985.php new file mode 100644 index 0000000000..2dd4d713e5 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-8985.php @@ -0,0 +1,38 @@ + + */ + protected function getDefaultFunctions(): array + { + /** @var array $x */ + $x = (new Defaults())->getFunctions(); + return $x; + } +} + +class HelloWorld2 +{ + /** + * @return array + */ + protected function getDefaultFunctions(): array + { + /** @var array */ + return (new Defaults())->getFunctions(); + } +} + +class Defaults +{ + public function getFunctions(): mixed + { + return []; + } +}