diff --git a/src/Rules/Names/UsedNamesRule.php b/src/Rules/Names/UsedNamesRule.php index 61dd8f285c..22f0d7a6b5 100644 --- a/src/Rules/Names/UsedNamesRule.php +++ b/src/Rules/Names/UsedNamesRule.php @@ -4,7 +4,9 @@ use PhpParser\Node; use PhpParser\Node\Stmt\ClassLike; +use PhpParser\Node\Stmt\Const_; use PhpParser\Node\Stmt\Enum_; +use PhpParser\Node\Stmt\Function_; use PhpParser\Node\Stmt\GroupUse; use PhpParser\Node\Stmt\Interface_; use PhpParser\Node\Stmt\Namespace_; @@ -16,6 +18,7 @@ use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\ShouldNotHappenException; use function in_array; use function sprintf; use function strtolower; @@ -59,25 +62,19 @@ public function processNode(Node $node, Scope $scope): array } /** - * @param array $usedNames + * @param array>> $usedNames * @return list */ private function findErrorsForNode(Node $node, string $namespace, array &$usedNames): 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, '', $lowerNamespace, $usedNames, $node->type); } if ($node instanceof GroupUse) { - if ($this->shouldBeIgnored($node)) { - return []; - } $useGroupPrefix = $node->prefix->toString(); - return $this->findErrorsInUses($node->uses, $useGroupPrefix, $lowerNamespace, $usedNames); + return $this->findErrorsInUses($node->uses, $useGroupPrefix, $lowerNamespace, $usedNames, $node->type); } if ($node instanceof ClassLike) { @@ -93,7 +90,7 @@ private function findErrorsForNode(Node $node, string $namespace, array &$usedNa $type = 'enum'; } $name = $node->name->toLowerString(); - if (in_array($name, $usedNames[$lowerNamespace] ?? [], true)) { + if (in_array($name, $usedNames[Use_::TYPE_NORMAL][$lowerNamespace] ?? [], true)) { return [ RuleErrorBuilder::message(sprintf( 'Cannot declare %s %s because the name is already in use.', @@ -106,46 +103,110 @@ private function findErrorsForNode(Node $node, string $namespace, array &$usedNa ->build(), ]; } - $usedNames[$lowerNamespace][] = $name; + $usedNames[Use_::TYPE_NORMAL][$lowerNamespace][] = $name; + return []; + } + + if ($node instanceof Function_) { + $name = $node->name->toLowerString(); + if (in_array($name, $usedNames[Use_::TYPE_FUNCTION][$lowerNamespace] ?? [], true)) { + return [ + RuleErrorBuilder::message(sprintf( + 'Cannot declare function %s() because the name is already in use.', + $namespace !== '' ? $namespace . '\\' . $node->name->toString() : $node->name->toString(), + )) + ->identifier('function.nameInUse') + ->line($node->getStartLine()) + ->nonIgnorable() + ->build(), + ]; + } + $usedNames[Use_::TYPE_FUNCTION][$lowerNamespace][] = $name; return []; } + if ($node instanceof Const_) { + $errors = []; + foreach ($node->consts as $constNode) { + $name = $constNode->name->toLowerString(); + if (in_array($name, $usedNames[Use_::TYPE_CONSTANT][$lowerNamespace] ?? [], true)) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Cannot declare constant %s because the name is already in use.', + $namespace !== '' ? $namespace . '\\' . $constNode->name->toString() : $constNode->name->toString(), + )) + ->identifier('const.nameInUse') + ->line($constNode->getStartLine()) + ->nonIgnorable() + ->build(); + } + $usedNames[Use_::TYPE_CONSTANT][$lowerNamespace][] = $name; + } + return $errors; + } + return []; } /** * @param Node\UseItem[] $uses - * @param array $usedNames + * @param array>> $usedNames + * @param Use_::TYPE_* $useType * @return list */ - private function findErrorsInUses(array $uses, string $useGroupPrefix, string $lowerNamespace, array &$usedNames): array + private function findErrorsInUses(array $uses, string $useGroupPrefix, string $lowerNamespace, array &$usedNames, int $useType): array { $errors = []; foreach ($uses as $use) { - if ($this->shouldBeIgnored($use)) { - continue; + $realUseType = $this->getRealUseType($use->type, $useType); + if ($realUseType === Use_::TYPE_UNKNOWN) { + throw new ShouldNotHappenException(); } $useAlias = $use->getAlias()->toLowerString(); - if (in_array($useAlias, $usedNames[$lowerNamespace] ?? [], true)) { + if (in_array($useAlias, $usedNames[$realUseType][$lowerNamespace] ?? [], true)) { + if ($realUseType === Use_::TYPE_FUNCTION) { + $displayedUseType = 'use function'; + $identifierUseType = 'useFunction'; + } elseif ($realUseType === Use_::TYPE_CONSTANT) { + $displayedUseType = 'use const'; + $identifierUseType = 'useConst'; + } else { + $displayedUseType = 'use'; + $identifierUseType = 'use'; + } $errors[] = RuleErrorBuilder::message(sprintf( - 'Cannot use %s as %s because the name is already in use.', + 'Cannot %s %s as %s because the name is already in use.', + $displayedUseType, $useGroupPrefix !== '' ? $useGroupPrefix . '\\' . $use->name->toString() : $use->name->toString(), $use->getAlias()->toString(), )) - ->identifier('use.nameInUse') + ->identifier(sprintf('%s.nameInUse', $identifierUseType)) ->line($use->getStartLine()) ->nonIgnorable() ->build(); continue; } - $usedNames[$lowerNamespace][] = $useAlias; + $usedNames[$realUseType][$lowerNamespace][] = $useAlias; } return $errors; } - private function shouldBeIgnored(Use_|GroupUse|Node\UseItem $use): bool + /** + * @param Use_::TYPE_* $useType + * @param Use_::TYPE_* $parentUseType + * @return Use_::TYPE_* + */ + private function getRealUseType(int $useType, int $parentUseType): int { - return in_array($use->type, [Use_::TYPE_FUNCTION, Use_::TYPE_CONSTANT], true); + if ($parentUseType === Use_::TYPE_UNKNOWN) { + return $useType; + } + if ($useType === Use_::TYPE_UNKNOWN) { + return $parentUseType; + } + if ($useType === $parentUseType) { + return $useType; + } + return Use_::TYPE_UNKNOWN; } } diff --git a/tests/PHPStan/Rules/Names/UsedNamesRuleTest.php b/tests/PHPStan/Rules/Names/UsedNamesRuleTest.php index ce58341c06..3fa2fc07a1 100644 --- a/tests/PHPStan/Rules/Names/UsedNamesRuleTest.php +++ b/tests/PHPStan/Rules/Names/UsedNamesRuleTest.php @@ -21,7 +21,11 @@ public function testSimpleUses(): void $this->analyse([__DIR__ . '/data/simple-uses.php'], [ [ 'Cannot declare class SomeNamespace\SimpleUses because the name is already in use.', - 7, + 9, + ], + [ + 'Cannot declare function SomeNamespace\SimpleUsesFunc() because the name is already in use.', + 13, ], ]); } @@ -65,11 +69,15 @@ public function testNoNamespace(): void $this->analyse([__DIR__ . '/data/no-namespace.php'], [ [ 'Cannot declare class NoNamespace because the name is already in use.', - 5, + 7, ], [ 'Cannot declare class NoNamespace because the name is already in use.', - 9, + 11, + ], + [ + 'Cannot declare function NoNamespaceFunc() because the name is already in use.', + 15, ], ]); } diff --git a/tests/PHPStan/Rules/Names/data/no-namespace.php b/tests/PHPStan/Rules/Names/data/no-namespace.php index 92ffc23d0a..d793d2d242 100644 --- a/tests/PHPStan/Rules/Names/data/no-namespace.php +++ b/tests/PHPStan/Rules/Names/data/no-namespace.php @@ -2,6 +2,8 @@ use SomeNamespace\NoNamespace; +use function SomeNamespace\NoNamespaceFunc; + abstract class NoNamespace { } @@ -9,3 +11,5 @@ abstract class NoNamespace final class NoNamespace { } + +function NoNamespaceFunc(): void { } diff --git a/tests/PHPStan/Rules/Names/data/simple-uses.php b/tests/PHPStan/Rules/Names/data/simple-uses.php index a271e11366..d3d5342640 100644 --- a/tests/PHPStan/Rules/Names/data/simple-uses.php +++ b/tests/PHPStan/Rules/Names/data/simple-uses.php @@ -4,6 +4,10 @@ use SomeOtherNamespace\SimpleUses; +use function SomeOtherNamespace\SimpleUsesFunc; + final class SimpleUses { } + +function SimpleUsesFunc(): void { }