Skip to content

Commit 15b7ed0

Browse files
committed
Fix phpstan/phpstan#14274: "Variable might not be defined" false positive
- Fixed invalidateExpression() dropping all ConditionalExpressionHolders for a variable when only some holders had an invalidated condition guard - Previously, `continue 3` skipped the entire variable's holders when any single holder's condition guard matched the invalidated expression - Now filters holders individually, preserving valid holders (e.g. those guarded by $has_staged_update) when unrelated holders (e.g. those guarded by $messages) are invalidated - New regression test in tests/PHPStan/Rules/Variables/data/bug-14274.php
1 parent b24bd5e commit 15b7ed0

3 files changed

Lines changed: 75 additions & 3 deletions

File tree

src/Analyser/MutatingScope.php

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2893,16 +2893,28 @@ public function invalidateExpression(Expr $expressionToInvalidate, bool $require
28932893
$invalidated = true;
28942894
continue;
28952895
}
2896-
foreach ($holders as $holder) {
2896+
$filteredHolders = [];
2897+
foreach ($holders as $key => $holder) {
2898+
$shouldKeep = true;
28972899
$conditionalTypeHolders = $holder->getConditionExpressionTypeHolders();
28982900
foreach ($conditionalTypeHolders as $conditionalTypeHolderExprString => $conditionalTypeHolder) {
28992901
if ($this->shouldInvalidateExpression($exprStringToInvalidate, $expressionToInvalidate, $conditionalTypeHolder->getExpr(), $conditionalTypeHolderExprString, false, $invalidatingClass)) {
29002902
$invalidated = true;
2901-
continue 3;
2903+
$shouldKeep = false;
2904+
break;
29022905
}
29032906
}
2907+
if (!$shouldKeep) {
2908+
continue;
2909+
}
2910+
2911+
$filteredHolders[$key] = $holder;
2912+
}
2913+
if (count($filteredHolders) <= 0) {
2914+
continue;
29042915
}
2905-
$newConditionalExpressions[$conditionalExprString] = $holders;
2916+
2917+
$newConditionalExpressions[$conditionalExprString] = $filteredHolders;
29062918
}
29072919

29082920
if (!$invalidated) {

tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1403,6 +1403,17 @@ public function testBug14019(): void
14031403
$this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-14019.php'], []);
14041404
}
14051405

1406+
#[RequiresPhp('>= 8.0')]
1407+
public function testBug14274(): void
1408+
{
1409+
$this->cliArgumentsVariablesRegistered = true;
1410+
$this->polluteScopeWithLoopInitialAssignments = false;
1411+
$this->checkMaybeUndefinedVariables = true;
1412+
$this->polluteScopeWithAlwaysIterableForeach = true;
1413+
1414+
$this->analyse([__DIR__ . '/data/bug-14274.php'], []);
1415+
}
1416+
14061417
public function testBug14117(): void
14071418
{
14081419
$this->cliArgumentsVariablesRegistered = true;
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php // lint >= 8.0
2+
3+
declare(strict_types=1);
4+
5+
namespace Bug14274;
6+
7+
class PreApplyEvent {}
8+
9+
final class ComposerPatchesValidator {
10+
/**
11+
* Validates the status of the patcher plugin.
12+
*/
13+
public function validate(mixed $event): void {
14+
$messages = [];
15+
16+
[$plugin_installed_in_active, $is_active_root_requirement, $active_configuration_ok] = $this->computePatcherStatus();
17+
if ($event instanceof PreApplyEvent) {
18+
[$plugin_installed_in_stage, $is_stage_root_requirement, $stage_configuration_ok] = $this->computePatcherStatus();
19+
$has_staged_update = TRUE;
20+
}
21+
else {
22+
// No staged update exists.
23+
$has_staged_update = FALSE;
24+
}
25+
26+
if ($has_staged_update && $plugin_installed_in_active !== $plugin_installed_in_stage) {
27+
$messages[] = 'package-manager-faq-composer-patches-installed-or-removed';
28+
}
29+
30+
// If the patcher is not listed in the runtime or dev dependencies, that's
31+
// an error as well.
32+
if (($plugin_installed_in_active && !$is_active_root_requirement) || ($has_staged_update && $plugin_installed_in_stage && !$is_stage_root_requirement)) {
33+
$messages[] = 'It must be a root dependency.';
34+
}
35+
36+
// If the plugin is misconfigured in either the active or stage directories,
37+
// flag an error.
38+
if (($plugin_installed_in_active && !$active_configuration_ok) || ($has_staged_update && $plugin_installed_in_stage && !$stage_configuration_ok)) {
39+
$messages[] = 'The composer-exit-on-patch-failure key is not set to true.';
40+
}
41+
}
42+
43+
/**
44+
* @return bool[]
45+
*/
46+
private function computePatcherStatus(): array {
47+
return [];
48+
}
49+
}

0 commit comments

Comments
 (0)