Skip to content

Commit a0365f7

Browse files
committed
Detect duplicate declarations for functions and constants
1 parent 013e021 commit a0365f7

File tree

4 files changed

+86
-22
lines changed

4 files changed

+86
-22
lines changed

src/Rules/Names/UsedNamesRule.php

Lines changed: 67 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44

55
use PhpParser\Node;
66
use PhpParser\Node\Stmt\ClassLike;
7+
use PhpParser\Node\Stmt\Const_;
78
use PhpParser\Node\Stmt\Enum_;
9+
use PhpParser\Node\Stmt\Function_;
810
use PhpParser\Node\Stmt\GroupUse;
911
use PhpParser\Node\Stmt\Interface_;
1012
use PhpParser\Node\Stmt\Namespace_;
@@ -16,6 +18,7 @@
1618
use PHPStan\Rules\IdentifierRuleError;
1719
use PHPStan\Rules\Rule;
1820
use PHPStan\Rules\RuleErrorBuilder;
21+
use PHPStan\ShouldNotHappenException;
1922
use function in_array;
2023
use function sprintf;
2124
use function strtolower;
@@ -59,25 +62,19 @@ public function processNode(Node $node, Scope $scope): array
5962
}
6063

6164
/**
62-
* @param array<string, string[]> $usedNames
65+
* @param array<Use_::TYPE_*, array<string, list<string>>> $usedNames
6366
* @return list<IdentifierRuleError>
6467
*/
6568
private function findErrorsForNode(Node $node, string $namespace, array &$usedNames): array
6669
{
6770
$lowerNamespace = strtolower($namespace);
6871
if ($node instanceof Use_) {
69-
if ($this->shouldBeIgnored($node)) {
70-
return [];
71-
}
72-
return $this->findErrorsInUses($node->uses, '', $lowerNamespace, $usedNames);
72+
return $this->findErrorsInUses($node->uses, '', $lowerNamespace, $usedNames, $node->type);
7373
}
7474

7575
if ($node instanceof GroupUse) {
76-
if ($this->shouldBeIgnored($node)) {
77-
return [];
78-
}
7976
$useGroupPrefix = $node->prefix->toString();
80-
return $this->findErrorsInUses($node->uses, $useGroupPrefix, $lowerNamespace, $usedNames);
77+
return $this->findErrorsInUses($node->uses, $useGroupPrefix, $lowerNamespace, $usedNames, $node->type);
8178
}
8279

8380
if ($node instanceof ClassLike) {
@@ -93,7 +90,7 @@ private function findErrorsForNode(Node $node, string $namespace, array &$usedNa
9390
$type = 'enum';
9491
}
9592
$name = $node->name->toLowerString();
96-
if (in_array($name, $usedNames[$lowerNamespace] ?? [], true)) {
93+
if (in_array($name, $usedNames[Use_::TYPE_NORMAL][$lowerNamespace] ?? [], true)) {
9794
return [
9895
RuleErrorBuilder::message(sprintf(
9996
'Cannot declare %s %s because the name is already in use.',
@@ -106,27 +103,64 @@ private function findErrorsForNode(Node $node, string $namespace, array &$usedNa
106103
->build(),
107104
];
108105
}
109-
$usedNames[$lowerNamespace][] = $name;
106+
$usedNames[Use_::TYPE_NORMAL][$lowerNamespace][] = $name;
110107
return [];
111108
}
112109

110+
if ($node instanceof Function_) {
111+
$name = $node->name->toLowerString();
112+
if (in_array($name, $usedNames[Use_::TYPE_FUNCTION][$lowerNamespace] ?? [], true)) {
113+
return [
114+
RuleErrorBuilder::message(sprintf(
115+
'Cannot declare function %s() because the name is already in use.',
116+
$namespace !== '' ? $namespace . '\\' . $node->name->toString() : $node->name->toString(),
117+
))
118+
->identifier('function.nameInUse')
119+
->line($node->getStartLine())
120+
->build(),
121+
];
122+
}
123+
$usedNames[Use_::TYPE_FUNCTION][$lowerNamespace][] = $name;
124+
return [];
125+
}
126+
127+
if ($node instanceof Const_) {
128+
$errors = [];
129+
foreach ($node->consts as $constNode) {
130+
$name = $constNode->name->toLowerString();
131+
if (in_array($name, $usedNames[Use_::TYPE_CONSTANT][$lowerNamespace] ?? [], true)) {
132+
$errors[] = RuleErrorBuilder::message(sprintf(
133+
'Cannot declare constant %s because the name is already in use.',
134+
$namespace !== '' ? $namespace . '\\' . $constNode->name->toString() : $constNode->name->toString(),
135+
))
136+
->identifier('const.nameInUse')
137+
->line($constNode->getStartLine())
138+
->build();
139+
}
140+
$usedNames[Use_::TYPE_CONSTANT][$lowerNamespace][] = $name;
141+
}
142+
return $errors;
143+
}
144+
113145
return [];
114146
}
115147

116148
/**
117149
* @param Node\UseItem[] $uses
118-
* @param array<string, string[]> $usedNames
150+
* @param array<Use_::TYPE_*, array<string, list<string>>> $usedNames
151+
* @param Use_::TYPE_* $useType
119152
* @return list<IdentifierRuleError>
120153
*/
121-
private function findErrorsInUses(array $uses, string $useGroupPrefix, string $lowerNamespace, array &$usedNames): array
154+
private function findErrorsInUses(array $uses, string $useGroupPrefix, string $lowerNamespace, array &$usedNames, int $useType): array
122155
{
123156
$errors = [];
124157
foreach ($uses as $use) {
125-
if ($this->shouldBeIgnored($use)) {
126-
continue;
158+
$realUseType = $this->getRealUseType($use->type, $useType);
159+
if ($realUseType === Use_::TYPE_UNKNOWN) {
160+
throw new ShouldNotHappenException();
127161
}
128162
$useAlias = $use->getAlias()->toLowerString();
129-
if (in_array($useAlias, $usedNames[$lowerNamespace] ?? [], true)) {
163+
if (in_array($useAlias, $usedNames[$realUseType][$lowerNamespace] ?? [], true)) {
130164
$errors[] = RuleErrorBuilder::message(sprintf(
131165
'Cannot use %s as %s because the name is already in use.',
132166
$useGroupPrefix !== '' ? $useGroupPrefix . '\\' . $use->name->toString() : $use->name->toString(),
@@ -138,14 +172,28 @@ private function findErrorsInUses(array $uses, string $useGroupPrefix, string $l
138172
->build();
139173
continue;
140174
}
141-
$usedNames[$lowerNamespace][] = $useAlias;
175+
$usedNames[$realUseType][$lowerNamespace][] = $useAlias;
142176
}
143177
return $errors;
144178
}
145179

146-
private function shouldBeIgnored(Use_|GroupUse|Node\UseItem $use): bool
180+
/**
181+
* @param Use_::TYPE_* $useType
182+
* @param Use_::TYPE_* $parentUseType
183+
* @return Use_::TYPE_*
184+
*/
185+
private function getRealUseType(int $useType, int $parentUseType): int
147186
{
148-
return in_array($use->type, [Use_::TYPE_FUNCTION, Use_::TYPE_CONSTANT], true);
187+
if ($parentUseType === Use_::TYPE_UNKNOWN) {
188+
return $useType;
189+
}
190+
if ($useType === Use_::TYPE_UNKNOWN) {
191+
return $parentUseType;
192+
}
193+
if ($useType === $parentUseType) {
194+
return $useType;
195+
}
196+
return Use_::TYPE_UNKNOWN;
149197
}
150198

151199
}

tests/PHPStan/Rules/Names/UsedNamesRuleTest.php

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,11 @@ public function testSimpleUses(): void
2121
$this->analyse([__DIR__ . '/data/simple-uses.php'], [
2222
[
2323
'Cannot declare class SomeNamespace\SimpleUses because the name is already in use.',
24-
7,
24+
9,
25+
],
26+
[
27+
'Cannot declare function SomeNamespace\SimpleUsesFunc() because the name is already in use.',
28+
13,
2529
],
2630
]);
2731
}
@@ -65,11 +69,15 @@ public function testNoNamespace(): void
6569
$this->analyse([__DIR__ . '/data/no-namespace.php'], [
6670
[
6771
'Cannot declare class NoNamespace because the name is already in use.',
68-
5,
72+
7,
6973
],
7074
[
7175
'Cannot declare class NoNamespace because the name is already in use.',
72-
9,
76+
11,
77+
],
78+
[
79+
'Cannot declare function NoNamespaceFunc() because the name is already in use.',
80+
15,
7381
],
7482
]);
7583
}

tests/PHPStan/Rules/Names/data/no-namespace.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,14 @@
22

33
use SomeNamespace\NoNamespace;
44

5+
use function SomeNamespace\NoNamespaceFunc;
6+
57
abstract class NoNamespace
68
{
79
}
810

911
final class NoNamespace
1012
{
1113
}
14+
15+
function NoNamespaceFunc(): void { }

tests/PHPStan/Rules/Names/data/simple-uses.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44

55
use SomeOtherNamespace\SimpleUses;
66

7+
use function SomeOtherNamespace\SimpleUsesFunc;
8+
79
final class SimpleUses
810
{
911
}
12+
13+
function SimpleUsesFunc(): void { }

0 commit comments

Comments
 (0)