Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions src/Analyser/MutatingScope.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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)) {
Expand Down
116 changes: 116 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-8985.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
<?php declare(strict_types = 1);

namespace Bug8985;

use function PHPStan\Testing\assertType;

class Entity
{
public function __construct(private string $value)
{
}

public function getValue(): string
{
return $this->value;
}
}

class Repository
{
/** @var array<int, Entity> */
public array $all = [];

/** @return array<int, Entity> */
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<int, Bug8985\Entity>', $all);
}

function testAssignAssertThenNew(): void
{
$all = (new Repository())->getAll();
assert($all === []);
assertType('array{}', $all);

$all = (new Repository())->getAll();
assertType('array<int, Bug8985\Entity>', $all);
}

// Property access on new
function testPropertyAccessOnNew(): void
{
assert((new Repository())->all === []);

$all = (new Repository())->all;
assertType('array<int, Bug8985\Entity>', $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<int, string> */
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<int, string>', $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<int, Bug8985\Entity>', $items2);
}

// Clone expression - also creates a fresh object
function testCloneExpression(Repository $repo): void
{
assert((clone $repo)->getAll() === []);

$all = (clone $repo)->getAll();
assertType('array<int, Bug8985\Entity>', $all);
}
Loading