diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 122340cc27..cb8491e219 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -988,6 +988,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 */ @@ -2719,6 +2740,10 @@ public function specifyExpressionType(Expr $expr, Type $type, Type $nativeType, } } + if ($this->expressionHasNewInChain($expr)) { + return $this; + } + $scope = $this; if ( $expr instanceof Expr\ArrayDimFetch 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); +}