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
9 changes: 9 additions & 0 deletions src/Analyser/MutatingScope.php
Original file line number Diff line number Diff line change
Expand Up @@ -3649,6 +3649,15 @@
) {
continue;
}
if (
!array_key_exists($exprString, $theirExpressionTypes)
&& !$holder->getExpr() instanceof Variable
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.

Why is it restricted for !$holder->getExpr() instanceof Variable ?
Can't the same bug occurs in that case ?

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.

The changes have been pushed. Here's a summary of what was done to address VincentLanglet's review comment:

The reviewer's question: Why is the check restricted to !$holder->getExpr() instanceof Variable? Can't the same bug occur for Variables?

Investigation and answer:

I tested the Variable case empirically:

  1. Pre-defined Variables (defined before the if/elseif, present in both branches' expression types): These are handled correctly by the existing check at lines 3644-3651 which tests array_key_exists($exprString, $theirExpressionTypes). I verified this with a variable-equivalent test case ($bb = $input before the if/elseif) - no false positive occurs.

  2. Newly-defined Variables (only defined in one branch, NOT in $theirExpressionTypes): Removing the Variable exception causes a regression in bug-14411-regression.php where $order must be recognized as always-defined when two conditions ($event instanceof Event and $event instanceof OrderInterface) together cover all cases. Conditional expressions for newly-defined variables are essential for this certainty tracking.

The fundamental difference is that non-Variable expressions are "always queryable" (absence from a branch means "not narrowed"), while Variables have genuine defined/undefined semantics (absence means "not defined").

Changes made:

  • Added a code comment explaining the Variable exception and why it's needed
  • Added variable-equivalent test cases to both the rule test data and NSRT test to demonstrate correctness

&& array_key_exists($guardExprString, $theirExpressionTypes)
&& $theirExpressionTypes[$guardExprString]->getCertainty()->yes()

Check warning on line 3656 in src/Analyser/MutatingScope.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ !array_key_exists($exprString, $theirExpressionTypes) && !$holder->getExpr() instanceof Variable && array_key_exists($guardExprString, $theirExpressionTypes) - && $theirExpressionTypes[$guardExprString]->getCertainty()->yes() + && !$theirExpressionTypes[$guardExprString]->getCertainty()->no() && !$guardHolder->getType()->isSuperTypeOf($theirExpressionTypes[$guardExprString]->getType())->no() ) { continue;

Check warning on line 3656 in src/Analyser/MutatingScope.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ !array_key_exists($exprString, $theirExpressionTypes) && !$holder->getExpr() instanceof Variable && array_key_exists($guardExprString, $theirExpressionTypes) - && $theirExpressionTypes[$guardExprString]->getCertainty()->yes() + && !$theirExpressionTypes[$guardExprString]->getCertainty()->no() && !$guardHolder->getType()->isSuperTypeOf($theirExpressionTypes[$guardExprString]->getType())->no() ) { continue;
&& !$guardHolder->getType()->isSuperTypeOf($theirExpressionTypes[$guardExprString]->getType())->no()

Check warning on line 3657 in src/Analyser/MutatingScope.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "PHPStan\Infection\IsSuperTypeOfCalleeAndArgumentMutator": @@ @@ && !$holder->getExpr() instanceof Variable && array_key_exists($guardExprString, $theirExpressionTypes) && $theirExpressionTypes[$guardExprString]->getCertainty()->yes() - && !$guardHolder->getType()->isSuperTypeOf($theirExpressionTypes[$guardExprString]->getType())->no() + && !$theirExpressionTypes[$guardExprString]->getType()->isSuperTypeOf($guardHolder->getType())->no() ) { continue; }

Check warning on line 3657 in src/Analyser/MutatingScope.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "PHPStan\Infection\IsSuperTypeOfCalleeAndArgumentMutator": @@ @@ && !$holder->getExpr() instanceof Variable && array_key_exists($guardExprString, $theirExpressionTypes) && $theirExpressionTypes[$guardExprString]->getCertainty()->yes() - && !$guardHolder->getType()->isSuperTypeOf($theirExpressionTypes[$guardExprString]->getType())->no() + && !$theirExpressionTypes[$guardExprString]->getType()->isSuperTypeOf($guardHolder->getType())->no() ) { continue; }
) {
continue;
}
$conditionalExpression = new ConditionalExpressionHolder([$guardExprString => $guardHolder], $holder);
$conditionalExpressions[$exprString][$conditionalExpression->getKey()] = $conditionalExpression;
}
Expand Down
19 changes: 19 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-14469.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

namespace Bug14469Nsrt;

use function PHPStan\Testing\assertType;

function t(array $R, bool $var1, object $user): void {
$aa = null;

if ($var1) {
$aa = $user->id === 10 ? 2 : null;
} elseif ($R['aa']) {
$aa = $R['aa'];
}

if ($aa) {
assertType('mixed', $R['aa']);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -236,4 +236,10 @@ public function testBug6702(): void
$this->analyse([__DIR__ . '/data/bug-6702.php'], []);
}

public function testBug14469(): void
{
$this->treatPhpDocTypesAsCertain = true;
$this->analyse([__DIR__ . '/data/bug-14469.php'], []);
}

}
73 changes: 73 additions & 0 deletions tests/PHPStan/Rules/Comparison/data/bug-14469.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?php

namespace Bug14469;

function t(array $R, bool $var1, object $user, bool $is): array {
$aa = null;

if ($var1) {
$aa = $user->id === 10 ? 2 : null;
} elseif ($R['aa']) {
$aa = $R['aa'];
}

if ($aa) {
if (!$R['aa']) {
return [];
}
}
return $R;
}

/** Property fetch variant */
function propertyFetch(object $obj, bool $var1, object $user): void {
$aa = null;

if ($var1) {
$aa = $user->id === 10 ? 2 : null;
} elseif ($obj->prop) {
$aa = $obj->prop;
}

if ($aa) {
if (!$obj->prop) {
return;
}
}
}

/** Nested array fetch variant */
function nestedArrayFetch(array $R, bool $var1, object $user): void {
$aa = null;

if ($var1) {
$aa = $user->id === 10 ? 2 : null;
} elseif ($R['a']['b']) {
$aa = $R['a']['b'];
}

if ($aa) {
if (!$R['a']['b']) {
return;
}
}
}

/** Multiple elseif branches */
function multipleElseif(array $R, bool $var1, bool $var2, object $user): void {
$aa = null;

if ($var1) {
$aa = $user->id === 10 ? 2 : null;
} elseif ($var2) {
$aa = 5;
} elseif ($R['aa']) {
$aa = $R['aa'];
}

if ($aa) {
if (!$R['aa']) {
return;
}
}
}
Loading