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..5ffbfbc481b 100644 --- a/tests/PHPStan/Rules/Names/UsedNamesRuleTest.php +++ b/tests/PHPStan/Rules/Names/UsedNamesRuleTest.php @@ -84,6 +84,39 @@ 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 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 @@ +