Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
3 changes: 3 additions & 0 deletions src/Analyser/ExprHandler/MethodCallHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,9 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
if ($methodReflection->getName() === '__construct' || $methodReflection->hasSideEffects()->yes()) {
$nodeScopeResolver->callNodeCallback($nodeCallback, new InvalidateExprNode($normalizedExpr->var), $scope, $storage);
$scope = $scope->invalidateExpression($normalizedExpr->var, true, $methodReflection->getDeclaringClass());
if (!($normalizedExpr->var instanceof Expr\Variable && $normalizedExpr->var->name === 'this')) {
$scope = $scope->invalidateAllMaybeImpureFunctionReturnValues();
}
} elseif ($this->rememberPossiblyImpureFunctionValues && $methodReflection->hasSideEffects()->maybe() && !$methodReflection->getDeclaringClass()->isBuiltin()) {
$scope = $scope->assignExpression(
new PossiblyImpureCallExpr($normalizedExpr, $normalizedExpr->var, sprintf('%s::%s()', $methodReflection->getDeclaringClass()->getDisplayName(), $methodReflection->getName())),
Expand Down
12 changes: 12 additions & 0 deletions src/Analyser/ExprHandler/StaticCallHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,18 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
&& $scope->getClassReflection()->is($methodReflection->getDeclaringClass()->getName())
) {
$scope = $scope->invalidateExpression(new Variable('this'), true, $methodReflection->getDeclaringClass());
$scope = $scope->invalidateAllMaybeImpureFunctionReturnValues();
} elseif (
$methodReflection !== null
&& (
(
!$methodReflection->isStatic()
&& $methodReflection->getName() === '__construct'
)
|| $methodReflection->hasSideEffects()->yes()
)
) {
$scope = $scope->invalidateAllMaybeImpureFunctionReturnValues();
} elseif (
$expr->class instanceof Name
&& $methodReflection !== null
Expand Down
95 changes: 95 additions & 0 deletions src/Analyser/MutatingScope.php
Original file line number Diff line number Diff line change
Expand Up @@ -2885,6 +2885,101 @@
return $this->assignExpression(new PropertyInitializationExpr($propertyName), new MixedType(), new MixedType());
}

public function invalidateAllMaybeImpureFunctionReturnValues(): self
{
$expressionTypes = $this->expressionTypes;
$nativeExpressionTypes = $this->nativeExpressionTypes;
$invalidated = false;

foreach ($expressionTypes as $exprString => $exprTypeHolder) {
$expr = $exprTypeHolder->getExpr();
if (!$this->expressionContainsMaybeImpureCall($expr)) {
continue;
}

unset($expressionTypes[$exprString]);
unset($nativeExpressionTypes[$exprString]);
$invalidated = true;
}

if (!$invalidated) {
return $this;
}

return $this->scopeFactory->create(
$this->context,
$this->isDeclareStrictTypes(),
$this->getFunction(),
$this->getNamespace(),
$expressionTypes,
$nativeExpressionTypes,
$this->conditionalExpressions,
$this->inClosureBindScopeClasses,
$this->anonymousFunctionReflection,
$this->inFirstLevelStatement,
$this->currentlyAssignedExpressions,
$this->currentlyAllowedUndefinedExpressions,
[],
$this->afterExtractCall,
$this->parentScope,
$this->nativeTypesPromoted,
);
}

private function expressionContainsMaybeImpureCall(Expr $expr): bool
{
$nodeFinder = new NodeFinder();
$found = $nodeFinder->findFirst([$expr], function (Node $node): bool {
if (!$node instanceof Expr) {
return false;
}

if ($node instanceof FuncCall) {
if ($node->name instanceof Name) {
if (!$this->reflectionProvider->hasFunction($node->name, $this)) {
return true;
}
return !$this->reflectionProvider->getFunction($node->name, $this)->hasSideEffects()->no();
}
return true;
}

if ($node instanceof MethodCall) {
if ($node->name instanceof Identifier) {
$calledOnType = $this->getType($node->var);

Check failure on line 2949 in src/Analyser/MutatingScope.php

View workflow job for this annotation

GitHub Actions / PHPStan (7.4, ubuntu-latest)

Instanceof between PhpParser\Node\Expr and PhpParser\Node\Expr will always evaluate to true.

Check failure on line 2949 in src/Analyser/MutatingScope.php

View workflow job for this annotation

GitHub Actions / PHPStan (7.4, windows-latest)

Instanceof between PhpParser\Node\Expr and PhpParser\Node\Expr will always evaluate to true.
$methodReflection = $this->getMethodReflection($calledOnType, $node->name->name);
if ($methodReflection === null) {
return true;
}
return !$methodReflection->hasSideEffects()->no();
}
return true;
}

if ($node instanceof Expr\StaticCall) {
if ($node->name instanceof Identifier) {
if ($node->class instanceof Name) {
$calledOnType = $this->resolveTypeByName($node->class);
} elseif ($node->class instanceof Expr) {

Check failure on line 2963 in src/Analyser/MutatingScope.php

View workflow job for this annotation

GitHub Actions / PHPStan with result cache (8.3)

Instanceof between PhpParser\Node\Expr and PhpParser\Node\Expr will always evaluate to true.

Check failure on line 2963 in src/Analyser/MutatingScope.php

View workflow job for this annotation

GitHub Actions / PHPStan with result cache (8.4)

Instanceof between PhpParser\Node\Expr and PhpParser\Node\Expr will always evaluate to true.

Check failure on line 2963 in src/Analyser/MutatingScope.php

View workflow job for this annotation

GitHub Actions / PHPStan with result cache (8.5)

Instanceof between PhpParser\Node\Expr and PhpParser\Node\Expr will always evaluate to true.

Check failure on line 2963 in src/Analyser/MutatingScope.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.5, ubuntu-latest)

Instanceof between PhpParser\Node\Expr and PhpParser\Node\Expr will always evaluate to true.

Check failure on line 2963 in src/Analyser/MutatingScope.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.4, ubuntu-latest)

Instanceof between PhpParser\Node\Expr and PhpParser\Node\Expr will always evaluate to true.

Check failure on line 2963 in src/Analyser/MutatingScope.php

View workflow job for this annotation

GitHub Actions / PHPStan with result cache (8.2)

Instanceof between PhpParser\Node\Expr and PhpParser\Node\Expr will always evaluate to true.

Check failure on line 2963 in src/Analyser/MutatingScope.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.2, ubuntu-latest)

Instanceof between PhpParser\Node\Expr and PhpParser\Node\Expr will always evaluate to true.

Check failure on line 2963 in src/Analyser/MutatingScope.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.3, ubuntu-latest)

Instanceof between PhpParser\Node\Expr and PhpParser\Node\Expr will always evaluate to true.

Check failure on line 2963 in src/Analyser/MutatingScope.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.0, ubuntu-latest)

Instanceof between PhpParser\Node\Expr and PhpParser\Node\Expr will always evaluate to true.

Check failure on line 2963 in src/Analyser/MutatingScope.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.1, ubuntu-latest)

Instanceof between PhpParser\Node\Expr and PhpParser\Node\Expr will always evaluate to true.

Check failure on line 2963 in src/Analyser/MutatingScope.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.3, windows-latest)

Instanceof between PhpParser\Node\Expr and PhpParser\Node\Expr will always evaluate to true.

Check failure on line 2963 in src/Analyser/MutatingScope.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.5, windows-latest)

Instanceof between PhpParser\Node\Expr and PhpParser\Node\Expr will always evaluate to true.

Check failure on line 2963 in src/Analyser/MutatingScope.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.2, windows-latest)

Instanceof between PhpParser\Node\Expr and PhpParser\Node\Expr will always evaluate to true.

Check failure on line 2963 in src/Analyser/MutatingScope.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.4, windows-latest)

Instanceof between PhpParser\Node\Expr and PhpParser\Node\Expr will always evaluate to true.

Check failure on line 2963 in src/Analyser/MutatingScope.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.1, windows-latest)

Instanceof between PhpParser\Node\Expr and PhpParser\Node\Expr will always evaluate to true.

Check failure on line 2963 in src/Analyser/MutatingScope.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.0, windows-latest)

Instanceof between PhpParser\Node\Expr and PhpParser\Node\Expr will always evaluate to true.

Check failure on line 2963 in src/Analyser/MutatingScope.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.3, ubuntu-latest)

Instanceof bet

Check failure on line 2963 in src/Analyser/MutatingScope.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Instanceof bet
$calledOnType = $this->getType($node->class);
} else {
return true;
}
$methodReflection = $this->getMethodReflection($calledOnType, $node->name->name);
if ($methodReflection === null) {
return true;
}
return !$methodReflection->hasSideEffects()->no();
}
return true;
}

return false;
});

return $found !== null;
}

public function invalidateExpression(Expr $expressionToInvalidate, bool $requireMoreCharacters = false, ?ClassReflection $invalidatingClass = null): self
{
$expressionTypes = $this->expressionTypes;
Expand Down
113 changes: 113 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-13416.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
<?php

declare(strict_types = 1);

namespace Bug13416;

use function PHPStan\Testing\assertType;

class MyRecord
{
/** @return list<self> */
public static function find(): array
{
return [];
}

public function insert(): void
{
}

/** @return non-empty-string */
public function getName(): string
{
return 'test';
}
}

class Repository
{
/** @return list<MyRecord> */
public function findAll(): array
{
return [];
}

public function save(MyRecord $record): void
{
}
}

function testStaticCallInvalidatedByMethodCall(): void
{
assert(count(MyRecord::find()) === 1);
assertType('1', count(MyRecord::find()));

$msg2 = new MyRecord();
$msg2->insert();

assertType('int<0, max>', count(MyRecord::find()));
}

function testMethodCallInvalidatedByMethodCall(): void
{
$repo = new Repository();

assert(count($repo->findAll()) === 1);
assertType('1', count($repo->findAll()));

$msg2 = new MyRecord();
$msg2->insert();

assertType('int<0, max>', count($repo->findAll()));
}

function testStrlenOfImpureCall(): void
{
$record = new MyRecord();

assert(strlen($record->getName()) === 3);
assertType('3', strlen($record->getName()));

$msg2 = new MyRecord();
$msg2->insert();

assertType('int<1, max>', strlen($record->getName()));
}

function testCountNotInvalidatedByPureFunction(): void
{
assert(count(MyRecord::find()) === 1);
assertType('1', count(MyRecord::find()));

$x = rand(0, 10);

assertType('1', count(MyRecord::find()));
}

class ServiceWithImpureCall
{
public function testMethodCallInvalidation(): void
{
$repo = new Repository();

assert(count($repo->findAll()) === 1);
assertType('1', count($repo->findAll()));

$msg2 = new MyRecord();
$msg2->insert();

assertType('int<0, max>', count($repo->findAll()));
}

public function testStaticCallInvalidation(): void
{
assert(count(MyRecord::find()) === 1);
assertType('1', count(MyRecord::find()));

$msg2 = new MyRecord();
$msg2->insert();

assertType('int<0, max>', count(MyRecord::find()));
}
}
Loading