Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions build/collision-detector.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
38 changes: 24 additions & 14 deletions src/Rules/Names/UsedNamesRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Expand All @@ -59,25 +64,25 @@ public function processNode(Node $node, Scope $scope): array
}

/**
* @param array<string, string[]> $usedNames
* @param array<string, string[]> $declaredNames names of classes declared anywhere in the file, by namespace
* @param string[] $currentScopeNames names taken in the current namespace declaration
* @return list<IdentifierRuleError>
*/
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) {
if ($this->shouldBeIgnored($node)) {
return [];
}
$useGroupPrefix = $node->prefix->toString();
return $this->findErrorsInUses($node->uses, $useGroupPrefix, $lowerNamespace, $usedNames);
return $this->findErrorsInUses($node->uses, $useGroupPrefix, $currentScopeNames);
}

if ($node instanceof ClassLike) {
Expand All @@ -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.',
Expand All @@ -106,7 +115,8 @@ private function findErrorsForNode(Node $node, string $namespace, array &$usedNa
->build(),
];
}
$usedNames[$lowerNamespace][] = $name;
$currentScopeNames[] = $name;
$declaredNames[$lowerNamespace][] = $name;
return [];
}

Expand All @@ -115,18 +125,18 @@ private function findErrorsForNode(Node $node, string $namespace, array &$usedNa

/**
* @param Node\UseItem[] $uses
* @param array<string, string[]> $usedNames
* @param string[] $currentScopeNames
* @return list<IdentifierRuleError>
*/
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) {
if ($this->shouldBeIgnored($use)) {
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(),
Expand All @@ -138,7 +148,7 @@ private function findErrorsInUses(array $uses, string $useGroupPrefix, string $l
->build();
continue;
}
$usedNames[$lowerNamespace][] = $useAlias;
$currentScopeNames[] = $useAlias;
}
return $errors;
}
Expand Down
33 changes: 33 additions & 0 deletions tests/PHPStan/Rules/Names/UsedNamesRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'], []);
Expand Down
9 changes: 9 additions & 0 deletions tests/PHPStan/Rules/Names/data/bug-15215.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

namespace X;

use \stdClass;

namespace X;

use \stdClass;
35 changes: 35 additions & 0 deletions tests/PHPStan/Rules/Names/data/repeated-braced-namespaces.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

namespace RepeatedBracedNamespaces
{
use SomeOtherNamespace\RepeatedBracedUses;

final class RepeatedBracedClass
{
}
}

namespace RepeatedBracedNamespaces
{
use SomeOtherNamespace\RepeatedBracedUses;

final class RepeatedBracedUses
{
}
}

namespace RepeatedBracedNamespaces
{
final class RepeatedBracedClass
{
}
}

namespace
{
use SomeOtherNamespace\RepeatedBracedUses;

final class RepeatedBracedClass
{
}
}
38 changes: 38 additions & 0 deletions tests/PHPStan/Rules/Names/data/repeated-namespaces.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

namespace RepeatedNamespaces;

use SomeOtherNamespace\RepeatedUses;
use SomeOtherNamespace\{
RepeatedGroupUses,
RepeatedOther as RepeatedAliased,
};

final class RepeatedClass
{
}

namespace RepeatedNamespaces;

use SomeOtherNamespace\RepeatedUses;
use SomeOtherNamespace\{
RepeatedGroupUses,
RepeatedOther as RepeatedAliased,
};

final class RepeatedUses
{
}

namespace RepeatedNamespaces;

final class RepeatedClass
{
}

namespace RepeatedNamespaces;

use SomeOtherNamespace\RepeatedClass;
use SomeOtherNamespace\{
RepeatedGroupUses as RepeatedAliased,
};
Loading