From c4cc83b5bc43ef48fa6a2e14c2cb30552888b722 Mon Sep 17 00:00:00 2001 From: phpstan-bot <79867460+phpstan-bot@users.noreply.github.com> Date: Fri, 11 Sep 2026 07:03:09 +0000 Subject: [PATCH 1/2] `UsedNamesRule`: reset imported names at every `namespace` declaration 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 --- build/collision-detector.json | 2 + src/Rules/Names/UsedNamesRule.php | 38 ++++++++++++------- .../PHPStan/Rules/Names/UsedNamesRuleTest.php | 28 ++++++++++++++ .../Names/data/repeated-braced-namespaces.php | 35 +++++++++++++++++ .../Rules/Names/data/repeated-namespaces.php | 38 +++++++++++++++++++ 5 files changed, 127 insertions(+), 14 deletions(-) create mode 100644 tests/PHPStan/Rules/Names/data/repeated-braced-namespaces.php create mode 100644 tests/PHPStan/Rules/Names/data/repeated-namespaces.php diff --git a/build/collision-detector.json b/build/collision-detector.json index c5171fcc96a..fbdee3556b9 100644 --- a/build/collision-detector.json +++ b/build/collision-detector.json @@ -10,6 +10,8 @@ "../tests/PHPStan/Rules/Functions/data/duplicate-function.php", "../tests/PHPStan/Rules/Classes/data/duplicate-class.php", "../tests/PHPStan/Rules/Names/data/multiple-namespaces.php", + "../tests/PHPStan/Rules/Names/data/repeated-namespaces.php", + "../tests/PHPStan/Rules/Names/data/repeated-braced-namespaces.php", "../tests/PHPStan/Rules/Names/data/no-namespace.php", "../tests/notAutoloaded", "../tests/PHPStan/Rules/Functions/data/define-bug-3349.php", diff --git a/src/Rules/Names/UsedNamesRule.php b/src/Rules/Names/UsedNamesRule.php index 61dd8f285c5..b59cac6fbbc 100644 --- a/src/Rules/Names/UsedNamesRule.php +++ b/src/Rules/Names/UsedNamesRule.php @@ -37,20 +37,25 @@ public function getNodeType(): string */ public function processNode(Node $node, Scope $scope): array { - $usedNames = []; + $declaredNames = []; + $fileScopeNames = []; $errors = []; foreach ($node->getNodes() as $oneNode) { if ($oneNode instanceof Namespace_) { $namespaceName = $oneNode->name !== null ? $oneNode->name->toString() : ''; + + // A namespace declaration starts a new import scope, even when the namespace + // has already been declared earlier in the same file. + $namespaceScopeNames = []; foreach ($oneNode->stmts as $stmt) { - foreach ($this->findErrorsForNode($stmt, $namespaceName, $usedNames) as $error) { + foreach ($this->findErrorsForNode($stmt, $namespaceName, $declaredNames, $namespaceScopeNames) as $error) { $errors[] = $error; } } continue; } - foreach ($this->findErrorsForNode($oneNode, '', $usedNames) as $error) { + foreach ($this->findErrorsForNode($oneNode, '', $declaredNames, $fileScopeNames) as $error) { $errors[] = $error; } } @@ -59,17 +64,17 @@ public function processNode(Node $node, Scope $scope): array } /** - * @param array $usedNames + * @param array $declaredNames names of classes declared anywhere in the file, by namespace + * @param string[] $currentScopeNames names taken in the current namespace declaration * @return list */ - private function findErrorsForNode(Node $node, string $namespace, array &$usedNames): array + private function findErrorsForNode(Node $node, string $namespace, array &$declaredNames, array &$currentScopeNames): array { - $lowerNamespace = strtolower($namespace); if ($node instanceof Use_) { if ($this->shouldBeIgnored($node)) { return []; } - return $this->findErrorsInUses($node->uses, '', $lowerNamespace, $usedNames); + return $this->findErrorsInUses($node->uses, '', $currentScopeNames); } if ($node instanceof GroupUse) { @@ -77,7 +82,7 @@ private function findErrorsForNode(Node $node, string $namespace, array &$usedNa return []; } $useGroupPrefix = $node->prefix->toString(); - return $this->findErrorsInUses($node->uses, $useGroupPrefix, $lowerNamespace, $usedNames); + return $this->findErrorsInUses($node->uses, $useGroupPrefix, $currentScopeNames); } if ($node instanceof ClassLike) { @@ -92,8 +97,12 @@ private function findErrorsForNode(Node $node, string $namespace, array &$usedNa } elseif ($node instanceof Enum_) { $type = 'enum'; } + $lowerNamespace = strtolower($namespace); $name = $node->name->toLowerString(); - if (in_array($name, $usedNames[$lowerNamespace] ?? [], true)) { + if ( + in_array($name, $currentScopeNames, true) + || in_array($name, $declaredNames[$lowerNamespace] ?? [], true) + ) { return [ RuleErrorBuilder::message(sprintf( 'Cannot declare %s %s because the name is already in use.', @@ -106,7 +115,8 @@ private function findErrorsForNode(Node $node, string $namespace, array &$usedNa ->build(), ]; } - $usedNames[$lowerNamespace][] = $name; + $currentScopeNames[] = $name; + $declaredNames[$lowerNamespace][] = $name; return []; } @@ -115,10 +125,10 @@ private function findErrorsForNode(Node $node, string $namespace, array &$usedNa /** * @param Node\UseItem[] $uses - * @param array $usedNames + * @param string[] $currentScopeNames * @return list */ - private function findErrorsInUses(array $uses, string $useGroupPrefix, string $lowerNamespace, array &$usedNames): array + private function findErrorsInUses(array $uses, string $useGroupPrefix, array &$currentScopeNames): array { $errors = []; foreach ($uses as $use) { @@ -126,7 +136,7 @@ private function findErrorsInUses(array $uses, string $useGroupPrefix, string $l continue; } $useAlias = $use->getAlias()->toLowerString(); - if (in_array($useAlias, $usedNames[$lowerNamespace] ?? [], true)) { + if (in_array($useAlias, $currentScopeNames, true)) { $errors[] = RuleErrorBuilder::message(sprintf( 'Cannot use %s as %s because the name is already in use.', $useGroupPrefix !== '' ? $useGroupPrefix . '\\' . $use->name->toString() : $use->name->toString(), @@ -138,7 +148,7 @@ private function findErrorsInUses(array $uses, string $useGroupPrefix, string $l ->build(); continue; } - $usedNames[$lowerNamespace][] = $useAlias; + $currentScopeNames[] = $useAlias; } return $errors; } diff --git a/tests/PHPStan/Rules/Names/UsedNamesRuleTest.php b/tests/PHPStan/Rules/Names/UsedNamesRuleTest.php index ce58341c06f..0d2e6445964 100644 --- a/tests/PHPStan/Rules/Names/UsedNamesRuleTest.php +++ b/tests/PHPStan/Rules/Names/UsedNamesRuleTest.php @@ -84,6 +84,34 @@ public function testMultipleNamespaces(): void ]); } + public function testRepeatedNamespaces(): void + { + $this->analyse([__DIR__ . '/data/repeated-namespaces.php'], [ + [ + 'Cannot declare class RepeatedNamespaces\RepeatedUses because the name is already in use.', + 23, + ], + [ + 'Cannot declare class RepeatedNamespaces\RepeatedClass because the name is already in use.', + 29, + ], + ]); + } + + public function testRepeatedBracedNamespaces(): void + { + $this->analyse([__DIR__ . '/data/repeated-braced-namespaces.php'], [ + [ + 'Cannot declare class RepeatedBracedNamespaces\RepeatedBracedUses because the name is already in use.', + 16, + ], + [ + 'Cannot declare class RepeatedBracedNamespaces\RepeatedBracedClass because the name is already in use.', + 23, + ], + ]); + } + public function testIgnoreUseFunctionAndConstant(): void { $this->analyse([__DIR__ . '/data/ignore-use-function-and-constant.php'], []); diff --git a/tests/PHPStan/Rules/Names/data/repeated-braced-namespaces.php b/tests/PHPStan/Rules/Names/data/repeated-braced-namespaces.php new file mode 100644 index 00000000000..700f200b9d7 --- /dev/null +++ b/tests/PHPStan/Rules/Names/data/repeated-braced-namespaces.php @@ -0,0 +1,35 @@ + Date: Fri, 11 Sep 2026 08:20:24 +0000 Subject: [PATCH 2/2] Add regression test with the reproducing snippet from the issue Copied verbatim from the playground link in phpstan/phpstan#15215. Co-Authored-By: Claude Opus 5 --- tests/PHPStan/Rules/Names/UsedNamesRuleTest.php | 5 +++++ tests/PHPStan/Rules/Names/data/bug-15215.php | 9 +++++++++ 2 files changed, 14 insertions(+) create mode 100644 tests/PHPStan/Rules/Names/data/bug-15215.php diff --git a/tests/PHPStan/Rules/Names/UsedNamesRuleTest.php b/tests/PHPStan/Rules/Names/UsedNamesRuleTest.php index 0d2e6445964..5ffbfbc481b 100644 --- a/tests/PHPStan/Rules/Names/UsedNamesRuleTest.php +++ b/tests/PHPStan/Rules/Names/UsedNamesRuleTest.php @@ -112,6 +112,11 @@ public function testRepeatedBracedNamespaces(): void ]); } + public function testBug15215(): void + { + $this->analyse([__DIR__ . '/data/bug-15215.php'], []); + } + public function testIgnoreUseFunctionAndConstant(): void { $this->analyse([__DIR__ . '/data/ignore-use-function-and-constant.php'], []); diff --git a/tests/PHPStan/Rules/Names/data/bug-15215.php b/tests/PHPStan/Rules/Names/data/bug-15215.php new file mode 100644 index 00000000000..e7aecb50910 --- /dev/null +++ b/tests/PHPStan/Rules/Names/data/bug-15215.php @@ -0,0 +1,9 @@ +