Skip to content

Commit

Permalink
UsedNamesRule - level 0
Browse files Browse the repository at this point in the history
  • Loading branch information
lulco authored Jan 5, 2024
1 parent 486cc52 commit 6c92190
Show file tree
Hide file tree
Showing 12 changed files with 348 additions and 0 deletions.
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ lint:
--exclude tests/PHPStan/Analyser/data \
--exclude tests/PHPStan/Rules/Methods/data \
--exclude tests/PHPStan/Rules/Functions/data \
--exclude tests/PHPStan/Rules/Names/data \
--exclude tests/PHPStan/Rules/Operators/data/invalid-inc-dec.php \
--exclude tests/PHPStan/Rules/Arrays/data/offset-access-without-dim-for-reading.php \
--exclude tests/PHPStan/Rules/Classes/data/duplicate-declarations.php \
Expand Down
2 changes: 2 additions & 0 deletions build/collision-detector.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
"../tests/PHPStan/Parser/data/cleaning-1-after.php",
"../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/no-namespace.php",
"../tests/notAutoloaded",
"../tests/PHPStan/Rules/Functions/data/define-bug-3349.php",
"../tests/PHPStan/Levels/data/stubs/function.php"
Expand Down
1 change: 1 addition & 0 deletions conf/config.level0.neon
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ rules:
- PHPStan\Rules\Methods\MissingMethodImplementationRule
- PHPStan\Rules\Methods\MethodAttributesRule
- PHPStan\Rules\Methods\StaticMethodCallableRule
- PHPStan\Rules\Names\UsedNamesRule
- PHPStan\Rules\Operators\InvalidAssignVarRule
- PHPStan\Rules\Properties\AccessPropertiesInAssignRule
- PHPStan\Rules\Properties\AccessStaticPropertiesInAssignRule
Expand Down
148 changes: 148 additions & 0 deletions src/Rules/Names/UsedNamesRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Names;

use PhpParser\Node;
use PhpParser\Node\Stmt\ClassLike;
use PhpParser\Node\Stmt\Enum_;
use PhpParser\Node\Stmt\GroupUse;
use PhpParser\Node\Stmt\Interface_;
use PhpParser\Node\Stmt\Namespace_;
use PhpParser\Node\Stmt\Trait_;
use PhpParser\Node\Stmt\Use_;
use PhpParser\Node\Stmt\UseUse;
use PHPStan\Analyser\Scope;
use PHPStan\Node\FileNode;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleError;
use PHPStan\Rules\RuleErrorBuilder;
use function in_array;
use function sprintf;
use function strtolower;

/**
* @implements Rule<FileNode>
*/
final class UsedNamesRule implements Rule
{

public function getNodeType(): string
{
return FileNode::class;
}

/**
* @param FileNode $node
*/
public function processNode(Node $node, Scope $scope): array
{
$usedNames = [];
$errors = [];
foreach ($node->getNodes() as $oneNode) {
if ($oneNode instanceof Namespace_) {
$namespaceName = $oneNode->name !== null ? $oneNode->name->toString() : '';
foreach ($oneNode->stmts as $stmt) {
foreach ($this->findErrorsForNode($stmt, $namespaceName, $usedNames) as $error) {
$errors[] = $error;
}
}
continue;
}

foreach ($this->findErrorsForNode($oneNode, '', $usedNames) as $error) {
$errors[] = $error;
}
}

return $errors;
}

/**
* @param array<string, string[]> $usedNames
* @return RuleError[]
*/
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);
}

if ($node instanceof GroupUse) {
if ($this->shouldBeIgnored($node)) {
return [];
}
$useGroupPrefix = $node->prefix->toString();
return $this->findErrorsInUses($node->uses, $useGroupPrefix, $lowerNamespace, $usedNames);
}

if ($node instanceof ClassLike) {
if ($node->name === null) {
return [];
}
$type = 'class';
if ($node instanceof Interface_) {
$type = 'interface';
} elseif ($node instanceof Trait_) {
$type = 'trait';
} elseif ($node instanceof Enum_) {
$type = 'enum';
}
$name = $node->name->toLowerString();
if (in_array($name, $usedNames[$lowerNamespace] ?? [], true)) {
return [
RuleErrorBuilder::message(sprintf(
'Cannot declare %s %s because the name is already in use.',
$type,
$namespace !== '' ? $namespace . '\\' . $node->name->toString() : $node->name->toString(),
))
->line($node->getLine())
->nonIgnorable()
->build(),
];
}
$usedNames[$lowerNamespace][] = $name;
return [];
}

return [];
}

/**
* @param UseUse[] $uses
* @param array<string, string[]> $usedNames
* @return RuleError[]
*/
private function findErrorsInUses(array $uses, string $useGroupPrefix, string $lowerNamespace, array &$usedNames): array
{
$errors = [];
foreach ($uses as $use) {
if ($this->shouldBeIgnored($use)) {
continue;
}
$useAlias = $use->getAlias()->toLowerString();
if (in_array($useAlias, $usedNames[$lowerNamespace] ?? [], 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(),
$use->getAlias()->toString(),
))
->line($use->getLine())
->nonIgnorable()
->build();
continue;
}
$usedNames[$lowerNamespace][] = $useAlias;
}
return $errors;
}

private function shouldBeIgnored(Use_|GroupUse|UseUse $use): bool
{
return in_array($use->type, [Use_::TYPE_FUNCTION, Use_::TYPE_CONSTANT], true);
}

}
92 changes: 92 additions & 0 deletions tests/PHPStan/Rules/Names/UsedNamesRuleTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Names;

use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;

/**
* @extends RuleTestCase<UsedNamesRule>
*/
final class UsedNamesRuleTest extends RuleTestCase
{

protected function getRule(): Rule
{
return new UsedNamesRule();
}

public function testSimpleUses(): void
{
$this->analyse([__DIR__ . '/data/simple-uses.php'], [
[
'Cannot declare class SomeNamespace\SimpleUses because the name is already in use.',
7,
],
]);
}

public function testGroupedUses(): void
{
$this->analyse([__DIR__ . '/data/grouped-uses.php'], [
[
'Cannot declare interface SomeNamespace\GroupedUses because the name is already in use.',
10,
],
]);
}

public function testSimpleUsesUnderClass(): void
{
$this->analyse([__DIR__ . '/data/simple-uses-under-class.php'], [
[
'Cannot use SomeOtherNamespace\UsesUnderClass as SimpleUsesUnderClass because the name is already in use.',
9,
],
]);
}

public function testGroupedUsesUnderClass(): void
{
$this->analyse([__DIR__ . '/data/grouped-uses-under-class.php'], [
[
'Cannot use SomeOtherNamespace\FooBar as FooBar because the name is already in use.',
14,
],
[
'Cannot use SomeOtherNamespace\UsesUnderClass as GroupedUsesUnderClass because the name is already in use.',
15,
],
]);
}

public function testNoNamespace(): void
{
$this->analyse([__DIR__ . '/data/no-namespace.php'], [
[
'Cannot declare class NoNamespace because the name is already in use.',
5,
],
[
'Cannot declare class NoNamespace because the name is already in use.',
9,
],
]);
}

public function testMultipleNamespaces(): void
{
$this->analyse([__DIR__ . '/data/multiple-namespaces.php'], [
[
'Cannot declare trait FirstNamespace\MultipleNamespaces because the name is already in use.',
24,
],
]);
}

public function testIgnoreUseFunctionAndConstant(): void
{
$this->analyse([__DIR__ . '/data/ignore-use-function-and-constant.php'], []);
}

}
16 changes: 16 additions & 0 deletions tests/PHPStan/Rules/Names/data/grouped-uses-under-class.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace SomeNamespace;

final class GroupedUsesUnderClass
{
}

final class FooBar
{
}

use SomeOtherNamespace\{
FooBar,
UsesUnderClass as GroupedUsesUnderClass,
};
12 changes: 12 additions & 0 deletions tests/PHPStan/Rules/Names/data/grouped-uses.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

namespace SomeNamespace;

use SomeOtherNamespace\{
SimpleUses as GroupedUses,
UsesUnderClass,
};

interface GroupedUses
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

namespace SomeNamespace;

use function count;
use const CaSeInSeNsItIvE;

final class Count
{
}

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

namespace FirstNamespace
{
use SomeOtherNamespace\SimpleUses;

final class MultipleNamespaces
{
}
}

namespace SecondNamespace
{
use SomeOtherNamespace\SimpleUses;

final class MultipleNamespaces
{
}
}


namespace FirstNamespace
{
trait MultipleNamespaces
{
}
}

namespace {
final class MultipleNamespaces
{
}
}
11 changes: 11 additions & 0 deletions tests/PHPStan/Rules/Names/data/no-namespace.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

use SomeNamespace\NoNamespace;

abstract class NoNamespace
{
}

final class NoNamespace
{
}
9 changes: 9 additions & 0 deletions tests/PHPStan/Rules/Names/data/simple-uses-under-class.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

namespace SomeNamespace;

final class SimpleUsesUnderClass
{
}

use SomeOtherNamespace\UsesUnderClass as SimpleUsesUnderClass;
9 changes: 9 additions & 0 deletions tests/PHPStan/Rules/Names/data/simple-uses.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

namespace SomeNamespace;

use SomeOtherNamespace\SimpleUses;

final class SimpleUses
{
}

0 comments on commit 6c92190

Please sign in to comment.