Skip to content

UsedNamesRule: reset imported names at every namespace declaration instead of keying them by namespace name - #6413

Merged
ondrejmirtes merged 2 commits into
phpstan:2.2.xfrom
phpstan-bot:create-pull-request/patch-69zky9r
Sep 11, 2026
Merged

UsedNamesRule: reset imported names at every namespace declaration instead of keying them by namespace name#6413
ondrejmirtes merged 2 commits into
phpstan:2.2.xfrom
phpstan-bot:create-pull-request/patch-69zky9r

Conversation

@phpstan-bot

Copy link
Copy Markdown
Collaborator

Summary

A namespace declaration starts a brand new import scope in PHP, even when the namespace name was already declared earlier in the same file (3v4l). UsedNamesRule instead accumulated every use alias it had ever seen into one list per namespace name, valid for the whole file. Repeating a namespace declaration therefore produced false positives:

<?php

namespace X;

use stdClass;

namespace X;

use stdClass; // Cannot use stdClass as stdClass because the name is already in use.

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.php
    • processNode() starts a fresh $namespaceScopeNames list for every Namespace_ node (and keeps one $fileScopeNames list for files whose statements are not wrapped in a namespace declaration).
    • The single $usedNames map 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.
    • The ClassLike branch checks against $currentScopeNames or $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 existing multiple-namespaces.php and no-namespace.php fixtures.

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:

Case PHP PHPStan before PHPStan after
use A\Foo;namespace X;use B\Foo; ok ❌ error ✅ ok
group use repeated across declarations ok ❌ error ✅ ok
aliased use ... as Y repeated across declarations ok ❌ error ✅ ok
class Foo {}namespace X;use A\Foo; ok ❌ error ✅ ok
use A\Foo;namespace X;class Foo {} ok ❌ error ✅ ok
use A\Foo; + class Foo {} in the same declaration fatal ✅ error ✅ error
class Foo {} + use A\Foo; in the same declaration fatal ✅ error ✅ error
class Foo {} in two namespace X; declarations fatal ✅ error ✅ error
braced namespace X { } namespace X { } — all of the above ❌ same false positives ✅ matches PHP
namespace X; … namespace Y; … namespace X; ok ❌ error ✅ ok

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 — clears uses/constUses when entering a Namespace_ node, which is exactly the per-declaration semantics.
  • src/Parser/UseAliasVisitor.php — clears explicitAliases when entering a Namespace_ node; verified that an alias in one declaration no longer suppresses the incorrect-case error in a later declaration.
  • php-parser's NameContext (driven by NameResolver in RichParser/SimpleParser/StubParser) already resets aliases per declaration and already reports duplicate imports — including use function and use const, which UsedNamesRule deliberately ignores — so those are correct too.
  • Name resolution of PHPDoc types inside classes, methods, functions and properties was verified to use the right namespace in a repeated 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 @api method FileTypeMapper::getResolvedPhpDoc() to say which namespace declaration a doc block belongs to, which is out of scope here.

Root cause

UsedNamesRule modelled the import scope as "the file, partitioned by namespace name". PHP models it as "one namespace declaration", i.e. the compiler resets FC(imports) and the seen-symbol table every time it compiles a namespace statement, whether or not the name repeats. Because the rule's $usedNames was keyed by strtolower($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 declare X\Foo twice regardless of how its namespace declarations are arranged — PHP fatals at runtime with Cannot redeclare class X\Foo).

Test

  • UsedNamesRuleTest::testRepeatedNamespaces() with data/repeated-namespaces.php — the reported unbraced shape, covering a repeated plain use, a repeated group use, a repeated aliased use, 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() with data/repeated-braced-namespaces.php — the same situations in the braced namespace X { } form, plus a trailing namespace { } block confirming the global namespace gets its own import scope. Before the fix this file reported 3 errors; 1 was a false positive.
  • Both were confirmed to fail for the right reason with the rule change stashed, and the expected errors were cross-checked against what PHP itself accepts and rejects.
  • Existing UsedNamesRuleTest cases (including multiple-namespaces.php, which relies on file-wide class redeclaration detection across braced blocks) still pass unchanged.
  • Full make tests (21347 tests) and make phpstan are green.

Fixes phpstan/phpstan#15215

…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 ondrejmirtes left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
@phpstan-bot

Copy link
Copy Markdown
Collaborator Author

Addressed the review: added the exact reproducer from the issue as a regression test.

tests/PHPStan/Rules/Names/data/bug-15215.php — verbatim from the playground link in the issue (fetched from the playground API, including the leading-backslash use \stdClass; form):

<?php

namespace X;

use \stdClass;

namespace X;

use \stdClass;

UsedNamesRuleTest::testBug15215() — expects no errors.

Verification:

  • With src/Rules/Names/UsedNamesRule.php reverted to its pre-fix version, the test fails with exactly the error from the issue: 09: Cannot use stdClass as stdClass because the name is already in use.
  • With the fix in place, it passes.
  • make tests: 21349 tests, 96484 assertions, 97 skipped, OK.
  • make phpstan: no errors.

Pushed as 72d3a5203 to create-pull-request/patch-69zky9r.

@ondrejmirtes
ondrejmirtes merged commit 1fd74a5 into phpstan:2.2.x Sep 11, 2026
500 of 503 checks passed
@ondrejmirtes
ondrejmirtes deleted the create-pull-request/patch-69zky9r branch September 11, 2026 08:23
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.

2 participants