UsedNamesRule: reset imported names at every namespace declaration instead of keying them by namespace name - #6413
Merged
ondrejmirtes merged 2 commits intoSep 11, 2026
Conversation
…n instead of keying them by namespace name
* `UsedNamesRule` kept a single `$usedNames` list per namespace *name* for the whole file, so `use` aliases from one `namespace X;` block were still considered taken in a later `namespace X;` block. PHP starts a fresh import scope at every namespace declaration, so this produced false `use.nameInUse` / `class.nameInUse` errors.
* Split the bookkeeping in two: `$currentScopeNames` (imports plus class-likes declared in the current namespace declaration, reset at every `Namespace_` node) and `$declaredNames` (class-like names declared anywhere in the file, grouped by namespace).
* `use` aliases are now only checked against the current namespace declaration; class-like declarations are checked against the current declaration *and* against every class-like declared earlier in the file in the same namespace, so the genuine redeclaration error is preserved.
* Covers plain `use`, group `use`, and aliased `use`, in both the unbraced (`namespace X;`) and braced (`namespace X { }`) forms.
* Probed the other places that track imports while walking a file and found them already correct: `ExportedNameScopeTracker` and `UseAliasVisitor` both clear their imports when *entering* a `Namespace_` node, and php-parser's `NameContext` already reports duplicate class/function/const imports with the right per-declaration scope.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ondrejmirtes
requested changes
Sep 11, 2026
ondrejmirtes
left a comment
Member
There was a problem hiding this comment.
The PR doesn't have the exact regression test for the failing code snippet from the issue.
Copied verbatim from the playground link in phpstan/phpstan#15215. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Collaborator
Author
|
Addressed the review: added the exact reproducer from the issue as a regression test.
<?php
namespace X;
use \stdClass;
namespace X;
use \stdClass;
Verification:
Pushed as |
ondrejmirtes
approved these changes
Sep 11, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A
namespacedeclaration starts a brand new import scope in PHP, even when the namespace name was already declared earlier in the same file (3v4l).UsedNamesRuleinstead accumulated everyusealias it had ever seen into one list per namespace name, valid for the whole file. Repeating a namespace declaration therefore produced false positives:The rule now tracks imports per namespace declaration, while still tracking class-like declarations for the whole file so that genuine redeclarations keep being reported.
Changes
src/Rules/Names/UsedNamesRule.phpprocessNode()starts a fresh$namespaceScopeNameslist for everyNamespace_node (and keeps one$fileScopeNameslist for files whose statements are not wrapped in a namespace declaration).$usedNamesmap keyed by lowercased namespace name is replaced by two structures:$currentScopeNames— names taken in the namespace declaration currently being walked (its imports plus its class-like declarations) — and$declaredNames— class-like names declared anywhere in the file, grouped by namespace.findErrorsInUses()checks an alias only against$currentScopeNames, so imports no longer leak between namespace declarations. It no longer needs the namespace at all.ClassLikebranch checks against$currentScopeNamesor$declaredNames[$namespace], and records the name in both, so a class-like still conflicts with an import in the same declaration and with a class-like declared in any earlier block of the same namespace.tests/PHPStan/Rules/Names/UsedNamesRuleTest.php,tests/PHPStan/Rules/Names/data/repeated-namespaces.php,tests/PHPStan/Rules/Names/data/repeated-braced-namespaces.php— new regression tests.build/collision-detector.json— the two new fixtures intentionally declare the same class twice, like the existingmultiple-namespaces.phpandno-namespace.phpfixtures.Analogous cases
Every construct on the "names imported into a namespace" axis was checked against real PHP behaviour (
php -l/ running the file) and against PHPStan before and after the change:use A\Foo;…namespace X;…use B\Foo;userepeated across declarationsuse ... as Yrepeated across declarationsclass Foo {}…namespace X;…use A\Foo;use A\Foo;…namespace X;…class Foo {}use A\Foo;+class Foo {}in the same declarationclass Foo {}+use A\Foo;in the same declarationclass Foo {}in twonamespace X;declarationsnamespace X { } namespace X { }— all of the abovenamespace X; … namespace Y; … namespace X;The other places in the codebase that accumulate imports while walking a file were probed and are already correct, so no change was needed there:
src/Dependency/ExportedNameScopeTracker.php— clearsuses/constUseswhen entering aNamespace_node, which is exactly the per-declaration semantics.src/Parser/UseAliasVisitor.php— clearsexplicitAliaseswhen entering aNamespace_node; verified that an alias in one declaration no longer suppresses the incorrect-case error in a later declaration.NameContext(driven byNameResolverinRichParser/SimpleParser/StubParser) already resets aliases per declaration and already reports duplicate imports — includinguse functionanduse const, whichUsedNamesRuledeliberately ignores — so those are correct too.namespace X;block.One related pre-existing limitation was found and left alone:
FileTypeMapper::getNameScopeKey()derives a single file-level key (md5($file)) for statements that are neither in a class nor in a function, so a/** @var */or/** @throws */on a top-level statement in the second namespace block of a file gets the first block's name scope. That is not specific to imports — the namespace itself is wrong too, even when the two blocks declare different namespaces — and fixing it needs a way for callers of the@apimethodFileTypeMapper::getResolvedPhpDoc()to say which namespace declaration a doc block belongs to, which is out of scope here.Root cause
UsedNamesRulemodelled the import scope as "the file, partitioned by namespace name". PHP models it as "one namespace declaration", i.e. the compiler resetsFC(imports)and the seen-symbol table every time it compiles anamespacestatement, whether or not the name repeats. Because the rule's$usedNameswas keyed bystrtolower($namespace)and never cleared, both directions of the "name already in use" check leaked across declaration boundaries: an alias imported in one block made a later import or class declaration of the same name look like a conflict, and a class declared in one block made a later import look like a conflict.The fix separates the two lifetimes that were conflated in
$usedNames: imports and same-block declarations live for one namespace declaration, while class-like declarations additionally live for the whole file (a file cannot declareX\Footwice regardless of how its namespace declarations are arranged — PHP fatals at runtime withCannot redeclare class X\Foo).Test
UsedNamesRuleTest::testRepeatedNamespaces()withdata/repeated-namespaces.php— the reported unbraced shape, covering a repeated plainuse, a repeated groupuse, a repeated aliaseduse, importing a name that was declared as a class in an earlier declaration, a class conflicting with an import in its own declaration (error, line 23), and a class conflicting with a class declared in an earlier declaration of the same namespace (error, line 29). Before the fix this file reported 7 errors; 5 of them were false positives.UsedNamesRuleTest::testRepeatedBracedNamespaces()withdata/repeated-braced-namespaces.php— the same situations in the bracednamespace X { }form, plus a trailingnamespace { }block confirming the global namespace gets its own import scope. Before the fix this file reported 3 errors; 1 was a false positive.UsedNamesRuleTestcases (includingmultiple-namespaces.php, which relies on file-wide class redeclaration detection across braced blocks) still pass unchanged.make tests(21347 tests) andmake phpstanare green.Fixes phpstan/phpstan#15215