diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 8d43a7d613..3657fd4ad7 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -973,6 +973,7 @@ private function resolveType(string $exprString, Expr $node): Type !$node instanceof Variable && !$node instanceof Expr\Closure && !$node instanceof Expr\ArrowFunction + && !$this->isExpressionBasedOnNew($node) && $this->hasExpressionType($node)->yes() ) { return $this->expressionTypes[$exprString]->getType(); @@ -2915,6 +2916,36 @@ public function invalidateExpression(Expr $expressionToInvalidate, bool $require ); } + private function isExpressionBasedOnNew(Expr $expr): bool + { + if ( + $expr instanceof MethodCall + || $expr instanceof Expr\NullsafeMethodCall + || $expr instanceof PropertyFetch + || $expr instanceof Expr\NullsafePropertyFetch + || $expr instanceof Expr\ArrayDimFetch + ) { + $var = $expr->var; + if ($var instanceof Expr\New_ || $var instanceof Expr\Clone_) { + return true; + } + return $this->isExpressionBasedOnNew($var); + } + + if ( + ($expr instanceof Expr\StaticCall || $expr instanceof Expr\StaticPropertyFetch || $expr instanceof Expr\ClassConstFetch) + && $expr->class instanceof Expr + ) { + $class = $expr->class; + if ($class instanceof Expr\New_ || $class instanceof Expr\Clone_) { + return true; + } + return $this->isExpressionBasedOnNew($class); + } + + return false; + } + private function getIntertwinedRefRootVariableName(Expr $expr): ?string { if ($expr instanceof Variable && is_string($expr->name)) { diff --git a/tests/PHPStan/Analyser/nsrt/bug-8985.php b/tests/PHPStan/Analyser/nsrt/bug-8985.php new file mode 100644 index 0000000000..cdbcb51179 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-8985.php @@ -0,0 +1,116 @@ +value; + } +} + +class Repository +{ + /** @var array */ + public array $all = []; + + /** @return array */ + public function getAll(): array + { + return [new Entity('test')]; + } + + public function getFirst(): ?Entity + { + return $this->all[0] ?? null; + } +} + +// Method call on new - original bug report +function testAssertOnNewThenNew(): void +{ + assert((new Repository())->getAll() === []); + + $all = (new Repository())->getAll(); + assertType('array', $all); +} + +function testAssignAssertThenNew(): void +{ + $all = (new Repository())->getAll(); + assert($all === []); + assertType('array{}', $all); + + $all = (new Repository())->getAll(); + assertType('array', $all); +} + +// Property access on new +function testPropertyAccessOnNew(): void +{ + assert((new Repository())->all === []); + + $all = (new Repository())->all; + assertType('array', $all); +} + +// Nullsafe method call on new +function testNullsafeMethodOnNew(): void +{ + assert((new Repository())->getFirst()?->getValue() === null); + + $value = (new Repository())->getFirst()?->getValue(); + assertType('string|null', $value); +} + +// Chained method call on new +class Builder +{ + /** @return array */ + public function build(): array + { + return ['a', 'b']; + } +} + +class BuilderFactory +{ + public function create(): Builder + { + return new Builder(); + } +} + +function testChainedMethodOnNew(): void +{ + assert((new BuilderFactory())->create()->build() === []); + + $result = (new BuilderFactory())->create()->build(); + assertType('array', $result); +} + +// Array dim fetch on method call on new +function testArrayDimFetchOnNew(): void +{ + $items = (new Repository())->getAll(); + assert(count($items) === 0); + + $items2 = (new Repository())->getAll(); + assertType('array', $items2); +} + +// Clone expression - also creates a fresh object +function testCloneExpression(Repository $repo): void +{ + assert((clone $repo)->getAll() === []); + + $all = (clone $repo)->getAll(); + assertType('array', $all); +}