Skip to content
Open
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
25 changes: 25 additions & 0 deletions src/Analyser/MutatingScope.php
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
Comment on lines +993 to +1007
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add more tests to cover the relevant expression types

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Here's a summary of the changes:

Bug fix (src/Analyser/MutatingScope.php):

  • expressionHasNewInChain() would crash with a TypeError when $expr->class was a Name node (e.g., Foo::bar()) instead of an Expr node. Added an $expr->class instanceof Expr guard before the recursive call on line 1007.

Tests (tests/PHPStan/Analyser/nsrt/bug-8985.php):
Added test functions covering all expression types handled by expressionHasNewInChain():

  • testMethodCall - (new Foo())->method() (existing, reorganized)
  • testNullsafeMethodCall - (new Foo())?->method()
  • testPropertyFetch - (new Foo())->prop
  • testNullsafePropertyFetch - (new Foo())?->prop
  • testArrayDimFetch - (new Foo())->getArray()[0]->method()
  • testStaticCall - (new Foo())::staticMethod()
  • testChainedMethodCalls - (new Foo())->getEntity()->getValue()
  • testChainedPropertyOnMethodCall - (new Foo())->getEntity()->value

All 11853 tests pass, PHPStan reports no errors.


return false;
}

/**
* @param callable(Type): ?bool $typeCallback
*/
Expand Down Expand Up @@ -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
Expand Down
111 changes: 111 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-8985.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
<?php // lint >= 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<int, Entity> */
public function getAll(): array
{
return [new Entity('test')];
}

public string $name = 'default';

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