Skip to content
2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@ lint:
--exclude tests/PHPStan/Rules/Properties/data/property-override-attr-missing.php \
--exclude tests/PHPStan/Rules/Properties/data/override-attr-on-property.php \
--exclude tests/PHPStan/Rules/Properties/data/property-override-attr.php \
--exclude tests/PHPStan/Rules/Classes/data/bug-14250.php \
--exclude tests/PHPStan/Rules/Classes/data/bug-14250-promoted-properties.php \
--exclude tests/PHPStan/Rules/Operators/data/bug-3585.php \
src tests

Expand Down
3 changes: 2 additions & 1 deletion src/PhpDoc/StubValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Reflection\ReflectionProviderStaticAccessor;
use PHPStan\Rules\Classes\DuplicateClassDeclarationRule;
use PHPStan\Rules\Classes\DuplicateDeclarationHelper;
use PHPStan\Rules\Classes\DuplicateDeclarationRule;
use PHPStan\Rules\Classes\ExistingClassesInClassImplementsRule;
use PHPStan\Rules\Classes\ExistingClassesInInterfaceExtendsRule;
Expand Down Expand Up @@ -228,7 +229,7 @@ private function getRuleRegistry(Container $container): RuleRegistry
new MethodPrototypeFinder($phpVersion, $phpClassReflectionExtension),
$container->getParameter('checkMissingOverrideMethodAttribute'),
),
new DuplicateDeclarationRule(),
new DuplicateDeclarationRule(new DuplicateDeclarationHelper()),
new LocalTypeAliasesRule($localTypeAliasesCheck),
new LocalTypeTraitAliasesRule($localTypeAliasesCheck, $reflectionProvider),
new LocalTypeTraitUseAliasesRule($localTypeAliasesCheck),
Expand Down
126 changes: 126 additions & 0 deletions src/Rules/Classes/DuplicateDeclarationHelper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Classes;

use PhpParser\Node;
use PhpParser\Node\Stmt\ClassConst;
use PhpParser\Node\Stmt\ClassLike;
use PhpParser\Node\Stmt\EnumCase;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Rules\IdentifierRuleError;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\ShouldNotHappenException;
use function array_key_exists;
use function is_string;
use function sprintf;
use function strtolower;

#[AutowiredService]
final class DuplicateDeclarationHelper
{

/**
* @param 'class'|'interface'|'trait'|'enum' $identifierType
* @return list<IdentifierRuleError>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method needs @param with string literals as possible identifier parts so that later we're able to generate the identifiers documentation.

Something like this: @param 'class'|'trait' $identifier

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Here's what I changed:

  1. src/Rules/Classes/DuplicateDeclarationHelper.php: Added @param 'class'|'interface'|'trait'|'enum' $identifierType to the checkClassLike() method so identifier documentation can be generated from the string literals.

  2. src/PhpDoc/StubValidator.php: Fixed the DuplicateDeclarationRule instantiation to pass the DuplicateDeclarationHelper dependency (was missing after the refactor to use constructor injection).

Both PHPStan and tests pass.

*/
public function checkClassLike(ClassLike $classLike, string $displayName, string $identifierType): array
{
$errors = [];

$declaredClassConstantsOrEnumCases = [];
foreach ($classLike->stmts as $stmtNode) {
if ($stmtNode instanceof EnumCase) {
if (array_key_exists($stmtNode->name->name, $declaredClassConstantsOrEnumCases)) {
$errors[] = RuleErrorBuilder::message(sprintf(
'Cannot redeclare enum case %s::%s.',
$displayName,
$stmtNode->name->name,
))->identifier(sprintf('%s.duplicateEnumCase', $identifierType))
->line($stmtNode->getStartLine())
->nonIgnorable()
->build();
} else {
$declaredClassConstantsOrEnumCases[$stmtNode->name->name] = true;
}
} elseif ($stmtNode instanceof ClassConst) {
foreach ($stmtNode->consts as $classConstNode) {
if (array_key_exists($classConstNode->name->name, $declaredClassConstantsOrEnumCases)) {
$errors[] = RuleErrorBuilder::message(sprintf(
'Cannot redeclare constant %s::%s.',
$displayName,
$classConstNode->name->name,
))->identifier(sprintf('%s.duplicateConstant', $identifierType))
->line($classConstNode->getStartLine())
->nonIgnorable()
->build();
} else {
$declaredClassConstantsOrEnumCases[$classConstNode->name->name] = true;
}
}
}
}

$declaredProperties = [];
foreach ($classLike->getProperties() as $propertyDecl) {
foreach ($propertyDecl->props as $property) {
if (array_key_exists($property->name->name, $declaredProperties)) {
$errors[] = RuleErrorBuilder::message(sprintf(
'Cannot redeclare property %s::$%s.',
$displayName,
$property->name->name,
))->identifier(sprintf('%s.duplicateProperty', $identifierType))
->line($property->getStartLine())
->nonIgnorable()
->build();
} else {
$declaredProperties[$property->name->name] = true;
}
}
}

$declaredFunctions = [];
foreach ($classLike->getMethods() as $method) {
if ($method->name->toLowerString() === '__construct') {
foreach ($method->params as $param) {
if ($param->flags === 0) {
continue;
}

if (!$param->var instanceof Node\Expr\Variable || !is_string($param->var->name)) {
throw new ShouldNotHappenException();
}

$propertyName = $param->var->name;

if (array_key_exists($propertyName, $declaredProperties)) {
$errors[] = RuleErrorBuilder::message(sprintf(
'Cannot redeclare property %s::$%s.',
$displayName,
$propertyName,
))->identifier(sprintf('%s.duplicateProperty', $identifierType))
->line($param->getStartLine())
->nonIgnorable()
->build();
} else {
$declaredProperties[$propertyName] = true;
}
}
}
if (array_key_exists(strtolower($method->name->name), $declaredFunctions)) {
$errors[] = RuleErrorBuilder::message(sprintf(
'Cannot redeclare method %s::%s().',
$displayName,
$method->name->name,
))->identifier(sprintf('%s.duplicateMethod', $identifierType))
->line($method->getStartLine())
->nonIgnorable()
->build();
} else {
$declaredFunctions[strtolower($method->name->name)] = true;
}
}

return $errors;
}

}
114 changes: 9 additions & 105 deletions src/Rules/Classes/DuplicateDeclarationRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,10 @@
namespace PHPStan\Rules\Classes;

use PhpParser\Node;
use PhpParser\Node\Stmt\ClassConst;
use PhpParser\Node\Stmt\EnumCase;
use PHPStan\Analyser\Scope;
use PHPStan\DependencyInjection\RegisteredRule;
use PHPStan\Node\InClassNode;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\ShouldNotHappenException;
use function array_key_exists;
use function is_string;
use function sprintf;
use function strtolower;

/**
Expand All @@ -23,6 +16,10 @@
final class DuplicateDeclarationRule implements Rule
{

public function __construct(private DuplicateDeclarationHelper $helper)
{
}

public function getNodeType(): string
{
return InClassNode::class;
Expand All @@ -32,104 +29,11 @@ public function processNode(Node $node, Scope $scope): array
{
$classReflection = $node->getClassReflection();

$identifierType = strtolower($classReflection->getClassTypeDescription());

$errors = [];

$declaredClassConstantsOrEnumCases = [];
foreach ($node->getOriginalNode()->stmts as $stmtNode) {
if ($stmtNode instanceof EnumCase) {
if (array_key_exists($stmtNode->name->name, $declaredClassConstantsOrEnumCases)) {
$errors[] = RuleErrorBuilder::message(sprintf(
'Cannot redeclare enum case %s::%s.',
$classReflection->getDisplayName(),
$stmtNode->name->name,
))->identifier(sprintf('%s.duplicateEnumCase', $identifierType))
->line($stmtNode->getStartLine())
->nonIgnorable()
->build();
} else {
$declaredClassConstantsOrEnumCases[$stmtNode->name->name] = true;
}
} elseif ($stmtNode instanceof ClassConst) {
foreach ($stmtNode->consts as $classConstNode) {
if (array_key_exists($classConstNode->name->name, $declaredClassConstantsOrEnumCases)) {
$errors[] = RuleErrorBuilder::message(sprintf(
'Cannot redeclare constant %s::%s.',
$classReflection->getDisplayName(),
$classConstNode->name->name,
))->identifier(sprintf('%s.duplicateConstant', $identifierType))
->line($classConstNode->getStartLine())
->nonIgnorable()
->build();
} else {
$declaredClassConstantsOrEnumCases[$classConstNode->name->name] = true;
}
}
}
}

$declaredProperties = [];
foreach ($node->getOriginalNode()->getProperties() as $propertyDecl) {
foreach ($propertyDecl->props as $property) {
if (array_key_exists($property->name->name, $declaredProperties)) {
$errors[] = RuleErrorBuilder::message(sprintf(
'Cannot redeclare property %s::$%s.',
$classReflection->getDisplayName(),
$property->name->name,
))->identifier(sprintf('%s.duplicateProperty', $identifierType))
->line($property->getStartLine())
->nonIgnorable()
->build();
} else {
$declaredProperties[$property->name->name] = true;
}
}
}

$declaredFunctions = [];
foreach ($node->getOriginalNode()->getMethods() as $method) {
if ($method->name->toLowerString() === '__construct') {
foreach ($method->params as $param) {
if ($param->flags === 0) {
continue;
}

if (!$param->var instanceof Node\Expr\Variable || !is_string($param->var->name)) {
throw new ShouldNotHappenException();
}

$propertyName = $param->var->name;

if (array_key_exists($propertyName, $declaredProperties)) {
$errors[] = RuleErrorBuilder::message(sprintf(
'Cannot redeclare property %s::$%s.',
$classReflection->getDisplayName(),
$propertyName,
))->identifier(sprintf('%s.duplicateProperty', $identifierType))
->line($param->getStartLine())
->nonIgnorable()
->build();
} else {
$declaredProperties[$propertyName] = true;
}
}
}
if (array_key_exists(strtolower($method->name->name), $declaredFunctions)) {
$errors[] = RuleErrorBuilder::message(sprintf(
'Cannot redeclare method %s::%s().',
$classReflection->getDisplayName(),
$method->name->name,
))->identifier(sprintf('%s.duplicateMethod', $identifierType))
->line($method->getStartLine())
->nonIgnorable()
->build();
} else {
$declaredFunctions[strtolower($method->name->name)] = true;
}
}

return $errors;
return $this->helper->checkClassLike(
$node->getOriginalNode(),
$classReflection->getDisplayName(),
strtolower($classReflection->getClassTypeDescription()),
);
}

}
36 changes: 36 additions & 0 deletions src/Rules/Classes/DuplicateTraitDeclarationRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Classes;

use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\DependencyInjection\RegisteredRule;
use PHPStan\Node\InTraitNode;
use PHPStan\Rules\Rule;

/**
* @implements Rule<InTraitNode>
*/
#[RegisteredRule(level: 0)]
final class DuplicateTraitDeclarationRule implements Rule
{

public function __construct(private DuplicateDeclarationHelper $helper)
{
}

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

public function processNode(Node $node, Scope $scope): array
{
return $this->helper->checkClassLike(
$node->getOriginalNode(),
$node->getTraitReflection()->getDisplayName(),
'trait',
);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class DuplicateDeclarationRuleTest extends RuleTestCase

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

public function testDuplicateDeclarations(): void
Expand Down
Loading
Loading