Skip to content

Commit a47b7ab

Browse files
VincentLangletphpstan-bot
authored andcommitted
Fix phpstan/phpstan#3831: false positive on comparison after dynamic method call
- Dynamic method calls like $this->{'compileSection'}() did not invalidate tracked expression types on the called object, causing properties set before the call (e.g. $this->footer = []) to retain their narrowed types - Added invalidateAllOnExpression() to MutatingScope that clears tracked expression types (property fetches, method call results) without clearing conditional type narrowing - New regression test in tests/PHPStan/Rules/Comparison/data/bug-3831.php
1 parent a3fb8e9 commit a47b7ab

4 files changed

Lines changed: 77 additions & 0 deletions

File tree

src/Analyser/ExprHandler/MethodCallHandler.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,9 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
174174
}
175175

176176
} else {
177+
if ($expr->name instanceof Expr) {
178+
$scope = $scope->invalidateAllOnExpression($normalizedExpr->var);
179+
}
177180
$throwPoints[] = InternalThrowPoint::createImplicit($scope, $expr);
178181
}
179182
$hasYield = $hasYield || $result->hasYield();

src/Analyser/MutatingScope.php

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3350,6 +3350,48 @@ public function invalidateExpression(Expr $expressionToInvalidate, bool $require
33503350
);
33513351
}
33523352

3353+
public function invalidateAllOnExpression(Expr $expressionToInvalidate): self
3354+
{
3355+
$expressionTypes = $this->expressionTypes;
3356+
$nativeExpressionTypes = $this->nativeExpressionTypes;
3357+
$invalidated = false;
3358+
$exprStringToInvalidate = $this->getNodeKey($expressionToInvalidate);
3359+
3360+
foreach ($expressionTypes as $exprString => $exprTypeHolder) {
3361+
$exprExpr = $exprTypeHolder->getExpr();
3362+
if (!$this->shouldInvalidateExpression($exprStringToInvalidate, $expressionToInvalidate, $exprExpr, $exprString, true)) {
3363+
continue;
3364+
}
3365+
3366+
unset($expressionTypes[$exprString]);
3367+
unset($nativeExpressionTypes[$exprString]);
3368+
$invalidated = true;
3369+
}
3370+
3371+
if (!$invalidated) {
3372+
return $this;
3373+
}
3374+
3375+
return $this->scopeFactory->create(
3376+
$this->context,
3377+
$this->isDeclareStrictTypes(),
3378+
$this->getFunction(),
3379+
$this->getNamespace(),
3380+
$expressionTypes,
3381+
$nativeExpressionTypes,
3382+
$this->conditionalExpressions,
3383+
$this->inClosureBindScopeClasses,
3384+
$this->anonymousFunctionReflection,
3385+
$this->inFirstLevelStatement,
3386+
$this->currentlyAssignedExpressions,
3387+
$this->currentlyAllowedUndefinedExpressions,
3388+
[],
3389+
$this->afterExtractCall,
3390+
$this->parentScope,
3391+
$this->nativeTypesPromoted,
3392+
);
3393+
}
3394+
33533395
private function shouldInvalidateExpression(string $exprStringToInvalidate, Expr $exprToInvalidate, Expr $expr, string $exprString, bool $requireMoreCharacters = false, ?ClassReflection $invalidatingClass = null): bool
33543396
{
33553397
if ($requireMoreCharacters && $exprStringToInvalidate === $exprString) {

tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,4 +288,9 @@ public function testBug13874(): void
288288
$this->analyse([__DIR__ . '/data/bug-13874.php'], []);
289289
}
290290

291+
public function testBug3831(): void
292+
{
293+
$this->analyse([__DIR__ . '/data/bug-3831.php'], []);
294+
}
295+
291296
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug3831;
4+
5+
class Template {
6+
7+
/** @var list<string> */
8+
public $footer = [];
9+
10+
public function render() : string {
11+
$content = '';
12+
$this->footer = [];
13+
14+
// dynamic method call - could modify $this->footer
15+
$this->{'compileSection'}();
16+
17+
if (count($this->footer) > 0) {
18+
$content = str_replace('some', 'thing', $content);
19+
}
20+
return $content;
21+
}
22+
23+
private function compileSection(): void {
24+
$this->footer[] = 'section-name';
25+
}
26+
27+
}

0 commit comments

Comments
 (0)