diff --git a/Makefile b/Makefile index 42ca3f4587..3663878387 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/src/PhpDoc/StubValidator.php b/src/PhpDoc/StubValidator.php index 52c207e7c7..8f1f045004 100644 --- a/src/PhpDoc/StubValidator.php +++ b/src/PhpDoc/StubValidator.php @@ -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; @@ -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), diff --git a/src/Rules/Classes/DuplicateDeclarationHelper.php b/src/Rules/Classes/DuplicateDeclarationHelper.php new file mode 100644 index 0000000000..22e54548ac --- /dev/null +++ b/src/Rules/Classes/DuplicateDeclarationHelper.php @@ -0,0 +1,126 @@ + + */ + 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; + } + +} diff --git a/src/Rules/Classes/DuplicateDeclarationRule.php b/src/Rules/Classes/DuplicateDeclarationRule.php index 6fe86ed9bc..6a8fa386e1 100644 --- a/src/Rules/Classes/DuplicateDeclarationRule.php +++ b/src/Rules/Classes/DuplicateDeclarationRule.php @@ -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; /** @@ -23,6 +16,10 @@ final class DuplicateDeclarationRule implements Rule { + public function __construct(private DuplicateDeclarationHelper $helper) + { + } + public function getNodeType(): string { return InClassNode::class; @@ -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()), + ); } } diff --git a/src/Rules/Classes/DuplicateTraitDeclarationRule.php b/src/Rules/Classes/DuplicateTraitDeclarationRule.php new file mode 100644 index 0000000000..4e0ec0836e --- /dev/null +++ b/src/Rules/Classes/DuplicateTraitDeclarationRule.php @@ -0,0 +1,36 @@ + + */ +#[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', + ); + } + +} diff --git a/tests/PHPStan/Rules/Classes/DuplicateDeclarationRuleTest.php b/tests/PHPStan/Rules/Classes/DuplicateDeclarationRuleTest.php index c0f21a6053..0dab25ac6d 100644 --- a/tests/PHPStan/Rules/Classes/DuplicateDeclarationRuleTest.php +++ b/tests/PHPStan/Rules/Classes/DuplicateDeclarationRuleTest.php @@ -14,7 +14,7 @@ class DuplicateDeclarationRuleTest extends RuleTestCase protected function getRule(): Rule { - return new DuplicateDeclarationRule(); + return new DuplicateDeclarationRule(new DuplicateDeclarationHelper()); } public function testDuplicateDeclarations(): void diff --git a/tests/PHPStan/Rules/Classes/DuplicateTraitDeclarationRuleTest.php b/tests/PHPStan/Rules/Classes/DuplicateTraitDeclarationRuleTest.php new file mode 100644 index 0000000000..6c6ea519eb --- /dev/null +++ b/tests/PHPStan/Rules/Classes/DuplicateTraitDeclarationRuleTest.php @@ -0,0 +1,67 @@ + + */ +class DuplicateTraitDeclarationRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new DuplicateTraitDeclarationRule(new DuplicateDeclarationHelper()); + } + + public function testBug14250(): void + { + $this->analyse([__DIR__ . '/data/bug-14250.php'], [ + [ + 'Cannot redeclare method Bug14250\MyTrait::doSomething().', + 11, + ], + [ + 'Cannot redeclare constant Bug14250\TraitWithDuplicateConstants::CONST1.', + 24, + ], + [ + 'Cannot redeclare constant Bug14250\TraitWithDuplicateConstants::CONST2.', + 26, + ], + [ + 'Cannot redeclare property Bug14250\TraitWithDuplicateProperties::$prop1.', + 41, + ], + [ + 'Cannot redeclare property Bug14250\TraitWithDuplicateProperties::$prop2.', + 44, + ], + [ + 'Cannot redeclare method Bug14250\TraitWithDuplicateMethods::func1().', + 59, + ], + [ + 'Cannot redeclare method Bug14250\TraitWithDuplicateMethods::Func1().', + 69, + ], + ]); + } + + public function testDuplicatePromotedProperty(): void + { + $this->analyse([__DIR__ . '/data/bug-14250-promoted-properties.php'], [ + [ + 'Cannot redeclare property Bug14250PromotedProperties\TraitWithDuplicatePromotedProperties::$foo.', + 10, + ], + [ + 'Cannot redeclare property Bug14250PromotedProperties\TraitWithDuplicatePromotedProperties::$bar.', + 12, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Classes/data/bug-14250-promoted-properties.php b/tests/PHPStan/Rules/Classes/data/bug-14250-promoted-properties.php new file mode 100644 index 0000000000..efff2fb7b9 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-14250-promoted-properties.php @@ -0,0 +1,22 @@ += 8.0 + +namespace Bug14250PromotedProperties; + +trait TraitWithDuplicatePromotedProperties +{ + private $foo; + + public function __construct( + private $foo, + private $bar, + private $bar + ) + { + + } +} + +class Foo +{ + use TraitWithDuplicatePromotedProperties; +} diff --git a/tests/PHPStan/Rules/Classes/data/bug-14250.php b/tests/PHPStan/Rules/Classes/data/bug-14250.php new file mode 100644 index 0000000000..2138c97305 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-14250.php @@ -0,0 +1,94 @@ +