From 7a9e198481f405fa2ced017e82de5fde6567f61e Mon Sep 17 00:00:00 2001 From: phpstan-bot <79867460+phpstan-bot@users.noreply.github.com> Date: Sat, 24 Oct 2026 15:33:47 +0000 Subject: [PATCH 1/2] Propagate variable flow and end statements through `declare() {}` blocks in `DeclareHandler` - DeclareHandler rebuilt the InternalStatementResult of a declare block without the variableFlow of its body, so variable reads/writes inside `declare(ticks=1) { ... }` were invisible to VariableLivenessResolver. This caused false "Variable $x is never read." (variable.unused) and false "has an unused parameter" errors, and missed overwrites inside the block. The body's variable flow is now passed through as-is (the block always executes, unlike BlockHandler's optional choice). - The body's endStatements are forwarded as well, like BlockHandler does. - Ported the same change to the turbo-ext mirror (DeclareHandler.cpp). - Probed the other statement handlers that do not forward variable flow (namespace, use, const, class-like, label, inline HTML, ...): none of them can contain variable accesses, so DeclareHandler was the only one affected. --- src/Analyser/StmtHandler/DeclareHandler.php | 6 ++- .../Rules/DeadCode/UnusedVariableRuleTest.php | 14 ++++++ .../PHPStan/Rules/DeadCode/data/bug-15394.php | 47 +++++++++++++++++++ .../UnusedFunctionParametersRuleTest.php | 10 ++++ .../unused-function-parameters-declare.php | 17 +++++++ turbo-ext/src/DeclareHandler.cpp | 8 +++- 6 files changed, 100 insertions(+), 2 deletions(-) create mode 100644 tests/PHPStan/Rules/DeadCode/data/bug-15394.php create mode 100644 tests/PHPStan/Rules/Functions/data/unused-function-parameters-declare.php diff --git a/src/Analyser/StmtHandler/DeclareHandler.php b/src/Analyser/StmtHandler/DeclareHandler.php index 25518f529e8..0e4f0aa37b7 100644 --- a/src/Analyser/StmtHandler/DeclareHandler.php +++ b/src/Analyser/StmtHandler/DeclareHandler.php @@ -44,6 +44,8 @@ public function processStmt( $impurePoints = []; $alwaysTerminating = false; $exitPoints = []; + $endStatements = []; + $variableFlow = null; foreach ($stmt->declares as $declare) { $nodeScopeResolver->callNodeCallback($nodeCallback, $declare, $scope, $storage); // the value is a constant scalar - process it so its result is stored @@ -69,9 +71,11 @@ public function processStmt( $impurePoints = $result->getImpurePoints(); $alwaysTerminating = $result->isAlwaysTerminating(); $exitPoints = $result->getExitPoints(); + $endStatements = $result->getEndStatements(); + $variableFlow = $result->getVariableFlow(); } - return new InternalStatementResult($scope, hasYield: $hasYield, isAlwaysTerminating: $alwaysTerminating, exitPoints: $exitPoints, throwPoints: $throwPoints, impurePoints: $impurePoints, dependencies: Dependencies::merge(...$dependencies)); + return new InternalStatementResult($scope, hasYield: $hasYield, isAlwaysTerminating: $alwaysTerminating, exitPoints: $exitPoints, throwPoints: $throwPoints, impurePoints: $impurePoints, endStatements: $endStatements, variableFlow: $variableFlow, dependencies: Dependencies::merge(...$dependencies)); } } diff --git a/tests/PHPStan/Rules/DeadCode/UnusedVariableRuleTest.php b/tests/PHPStan/Rules/DeadCode/UnusedVariableRuleTest.php index 2ceca6991c7..6257d611750 100644 --- a/tests/PHPStan/Rules/DeadCode/UnusedVariableRuleTest.php +++ b/tests/PHPStan/Rules/DeadCode/UnusedVariableRuleTest.php @@ -18,6 +18,20 @@ protected function getRule(): Rule return new UnusedVariableRule(self::getContainer()->getByType(ExprPrinter::class)); } + public function testBug15394(): void + { + $this->analyse([__DIR__ . '/data/bug-15394.php'], [ + [ + 'Value assigned to variable $a is never read before being overwritten.', + 24, + ], + [ + 'Variable $a is never read.', + 35, + ], + ]); + } + public function testBug15411(): void { $this->analyse([__DIR__ . '/data/bug-15411.php'], []); diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-15394.php b/tests/PHPStan/Rules/DeadCode/data/bug-15394.php new file mode 100644 index 00000000000..f342e76a2fd --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-15394.php @@ -0,0 +1,47 @@ +getByType(UnusedParametersCheck::class)); } + public function testDeclareBlock(): void + { + $this->analyse([__DIR__ . '/data/unused-function-parameters-declare.php'], [ + [ + 'Function UnusedFunctionParametersDeclare\\unusedWithDeclare() has an unused parameter $x.', + 12, + ], + ]); + } + public function testRule(): void { $this->analyse([__DIR__ . '/data/unused-function-parameters.php'], [ diff --git a/tests/PHPStan/Rules/Functions/data/unused-function-parameters-declare.php b/tests/PHPStan/Rules/Functions/data/unused-function-parameters-declare.php new file mode 100644 index 00000000000..3a3a7855c2d --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/unused-function-parameters-declare.php @@ -0,0 +1,17 @@ + Date: Sun, 25 Oct 2026 09:07:46 +0000 Subject: [PATCH 2/2] Do not forward end statements of the declare() body in `DeclareHandler` The body is processed as a statement list, whose result never carries end statements, so forwarding them was a no-op. Co-Authored-By: Claude Opus 5.5 --- src/Analyser/StmtHandler/DeclareHandler.php | 4 +--- turbo-ext/src/DeclareHandler.cpp | 5 +---- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/Analyser/StmtHandler/DeclareHandler.php b/src/Analyser/StmtHandler/DeclareHandler.php index 0e4f0aa37b7..b9fba510f09 100644 --- a/src/Analyser/StmtHandler/DeclareHandler.php +++ b/src/Analyser/StmtHandler/DeclareHandler.php @@ -44,7 +44,6 @@ public function processStmt( $impurePoints = []; $alwaysTerminating = false; $exitPoints = []; - $endStatements = []; $variableFlow = null; foreach ($stmt->declares as $declare) { $nodeScopeResolver->callNodeCallback($nodeCallback, $declare, $scope, $storage); @@ -71,11 +70,10 @@ public function processStmt( $impurePoints = $result->getImpurePoints(); $alwaysTerminating = $result->isAlwaysTerminating(); $exitPoints = $result->getExitPoints(); - $endStatements = $result->getEndStatements(); $variableFlow = $result->getVariableFlow(); } - return new InternalStatementResult($scope, hasYield: $hasYield, isAlwaysTerminating: $alwaysTerminating, exitPoints: $exitPoints, throwPoints: $throwPoints, impurePoints: $impurePoints, endStatements: $endStatements, variableFlow: $variableFlow, dependencies: Dependencies::merge(...$dependencies)); + return new InternalStatementResult($scope, hasYield: $hasYield, isAlwaysTerminating: $alwaysTerminating, exitPoints: $exitPoints, throwPoints: $throwPoints, impurePoints: $impurePoints, variableFlow: $variableFlow, dependencies: Dependencies::merge(...$dependencies)); } } diff --git a/turbo-ext/src/DeclareHandler.cpp b/turbo-ext/src/DeclareHandler.cpp index 7c1e36f8cc8..617f68f34ba 100644 --- a/turbo-ext/src/DeclareHandler.cpp +++ b/turbo-ext/src/DeclareHandler.cpp @@ -122,9 +122,6 @@ class DeclareHandler zv::Val exitPointsHold; zval *exitPoints = pt_internal_statement_result_exit_points(result.raw(), exitPointsHold); if (UNEXPECTED(exitPoints == NULL)) return zv::Val(); - zv::Val endStatementsHold; - zval *endStatements = pt_internal_statement_result_end_statements(result.raw(), endStatementsHold); - if (UNEXPECTED(endStatements == NULL)) return zv::Val(); zv::Val variableFlowHold; zval *variableFlow = pt_internal_statement_result_variable_flow(result.raw(), variableFlowHold); if (UNEXPECTED(variableFlow == NULL)) return zv::Val(); @@ -136,7 +133,7 @@ class DeclareHandler } zv::Val mergedDependencies = pt_dependencies_merge_list(dependencies.table()); if (UNEXPECTED(mergedDependencies.isUndef())) return zv::Val(); - return pt_internal_statement_result_new(resultScope, hasYield, alwaysTerminating, exitPoints, throwPoints, impurePoints, endStatements, Z_TYPE_P(variableFlow) == IS_NULL ? NULL : variableFlow, -1, mergedDependencies.raw()); + return pt_internal_statement_result_new(resultScope, hasYield, alwaysTerminating, exitPoints, throwPoints, impurePoints, NULL, Z_TYPE_P(variableFlow) == IS_NULL ? NULL : variableFlow, -1, mergedDependencies.raw()); } /* the statement-handler entry (Engine.h) */