Skip to content

Treat all in-scope variables as used when an unused-parameter body accesses a variable variable ($$name)#6058

Merged
staabm merged 4 commits into
phpstan:2.2.xfrom
phpstan-bot:create-pull-request/patch-ba8483d
Jul 16, 2026
Merged

Treat all in-scope variables as used when an unused-parameter body accesses a variable variable ($$name)#6058
staabm merged 4 commits into
phpstan:2.2.xfrom
phpstan-bot:create-pull-request/patch-ba8483d

Conversation

@phpstan-bot

Copy link
Copy Markdown
Collaborator

Summary

The reported bug — a false closure.unusedUse error for a use variable that is only accessed inside an include/require/eval — was already fixed on 2.2.x (commit 501ac99) by treating all in-scope variables as used when the body contains include/require/eval. Per the maintainer, only a regression test was outstanding.

While adding that test, I probed the parallel "dynamic variable access PHPStan cannot resolve statically" axis and found the same false positive for variable variables ($$name): PHPStan cannot know which variable $$name resolves to, yet it still reported the captured use/constructor parameter as unused. This PR fixes that analogous case and adds regression tests for both the original and the new case.

Changes

  • src/Rules/UnusedFunctionParametersCheck.php: in getUsedVariables(), when a Variable node has a non-string name (a variable variable such as $$name), return $scope->getDefinedVariables() so every in-scope variable is considered used — the same conservative treatment already applied to include/require/eval and func_get_args()/get_defined_vars(). The two Variable branches were merged to keep the narrowing clean and avoid a now-redundant is_string() check.
  • tests/PHPStan/Rules/Functions/data/unused-closure-uses.php: added the issue's verbatim closure-with-require reproducer, plus a closure whose body accesses $$name.
  • tests/PHPStan/Rules/Classes/data/unused-constructor-parameters-include.php: added a constructor whose body accesses $$name.

Both callers of the shared check — UnusedClosureUsesRule and UnusedConstructorParametersRule — are covered by the single fix location.

Root cause

UnusedFunctionParametersCheck walks the function/closure body collecting the names of variables that are "used", then flags any parameter/use variable that never appears. Constructs that can access variables PHPStan cannot enumerate statically must short-circuit this walk and mark every in-scope variable as used. include/require/eval were handled; a Variable with a dynamic (non-string) name — i.e. a variable variable — was not, so it fell through to the recursive descent, marking only the inner name expression ($name) as used and leaving the actually-captured variable ($container) reported as unused.

Test

  • UnusedClosureUsesRuleTest::testUnusedClosureUses — extended with the require reproducer from the issue (no error expected) and a $$name closure body; the latter fails without the fix with Anonymous function has an unused use $container. and passes with it.
  • UnusedConstructorParametersRuleTest::testParameterUsedInIncludedFile — extended with a constructor accessing $$name; fails without the fix with Constructor of class ... has an unused parameter $usedViaVariableVariable. and passes with it.

Fixes phpstan/phpstan#13960

…cesses a variable variable (`$$name`)

- `UnusedFunctionParametersCheck::getUsedVariables()` now returns `$scope->getDefinedVariables()` when it encounters a `Variable` node whose name is not a static string (a variable variable like `$$name`), mirroring the existing handling for `include`/`require`/`eval` and `func_get_args()`/`get_defined_vars()`.
- This covers both callers of the shared check: `UnusedClosureUsesRule` (`closure.unusedUse`) and `UnusedConstructorParametersRule` (`constructor.unusedParameter`), since the accessed variable cannot be statically determined.
- Merged the two `Variable` branches to avoid a redundant `is_string()` check flagged by self-analysis.
- Added regression tests: a closure `use ($container, $name)` body containing `$$name`, and a constructor whose body reads `$$name`; also added the issue's verbatim closure-with-`require` reproducer to `unused-closure-uses.php`.
@VincentLanglet

Copy link
Copy Markdown
Contributor

Should

$name = 'notcontainer';

function () use ($container, $name) {
	echo $$name;
};

still be reported as unused ?

@staabm staabm left a comment

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.

check the comment #6058 (comment)

…very variable used

When an unused-parameter body accesses `$$name` where `$name` resolves to
known constant string(s), the accessed variable can be determined statically,
so mark exactly those as used instead of conservatively treating every
in-scope variable as used. The fall-back to `getDefinedVariables()` now only
applies when the name cannot be resolved to constant strings.

This keeps reporting a genuinely unused `use`/parameter when `$$name`
provably refers to a different variable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@phpstan-bot

Copy link
Copy Markdown
Collaborator Author

I've addressed the review. Here's what the referenced comment (VincentLanglet's) was asking:

Should $name = 'notcontainer'; function () use ($container, $name) { echo $$name; }; still be reported as unused?

Yes — and my original PR was wrong there. The first version conservatively marked every in-scope variable as used whenever the body contained any variable variable, which is a false negative when $$name provably resolves to a different variable.

What I changed

src/Rules/UnusedFunctionParametersCheck.php — for a Variable with a non-string name ($$name), I now resolve the type of the name expression first:

  • If it's isString()->yes() with non-empty getConstantStrings() (i.e. a fully enumerable set of constant string names — TypeCombinator guarantees a general string component would collapse the constants away), mark exactly those resolved variable names as used, and let the normal recursion also mark the inner $name.
  • Otherwise (name can't be resolved statically), fall back to getDefinedVariables() as before.

This mirrors the existing precedent in VariableHandler::resolveType() and the compact() branch in the same method.

Tests — added VincentLanglet's exact scenario to unused-closure-uses.php ($otherName = 'notContainer') with an expected unused use $container error at line 43, since $$otherName provably refers to notContainer, not container.

Verification

  • Confirmed the new test fails without the source change (the $container error is missing) and passes with it.
  • make phpstan — no errors.
  • UnusedClosureUsesRuleTest + UnusedConstructorParametersRuleTest — 9/9 pass.

Committed and pushed as one logical change.

@staabm
staabm requested a review from VincentLanglet July 16, 2026 15:09
@staabm
staabm merged commit 371b67d into phpstan:2.2.x Jul 16, 2026
736 of 739 checks passed
@staabm
staabm deleted the create-pull-request/patch-ba8483d branch July 16, 2026 15:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Invalid closure.unusedUse error when including external files

3 participants