diff --git a/conf/bleedingEdge.neon b/conf/bleedingEdge.neon index 1742270d01..2c4bc61dfc 100644 --- a/conf/bleedingEdge.neon +++ b/conf/bleedingEdge.neon @@ -23,5 +23,6 @@ parameters: neverInGenericReturnType: true validateOverridingMethodsInStubs: true crossCheckInterfaces: true + finalByPhpDocTag: true stubFiles: - ../stubs/arrayFunctions.stub diff --git a/conf/config.level0.neon b/conf/config.level0.neon index 5df0e46ec7..e13224c8d9 100644 --- a/conf/config.level0.neon +++ b/conf/config.level0.neon @@ -42,7 +42,6 @@ rules: - PHPStan\Rules\Classes\DuplicateDeclarationRule - PHPStan\Rules\Classes\ExistingClassesInClassImplementsRule - PHPStan\Rules\Classes\ExistingClassesInInterfaceExtendsRule - - PHPStan\Rules\Classes\ExistingClassInClassExtendsRule - PHPStan\Rules\Classes\ExistingClassInTraitUseRule - PHPStan\Rules\Classes\InstantiationRule - PHPStan\Rules\Classes\InvalidPromotedPropertiesRule @@ -98,6 +97,13 @@ services: - class: PHPStan\Rules\Api\PhpStanNamespaceIn3rdPartyPackageRule + - + class: PHPStan\Rules\Classes\ExistingClassInClassExtendsRule + arguments: + checkFinalByPhpDocTag: %featureToggles.finalByPhpDocTag% + tags: + - phpstan.rules.rule + - class: PHPStan\Rules\Classes\ExistingClassInInstanceOfRule tags: diff --git a/conf/config.neon b/conf/config.neon index c87e525cce..2ffb0dd6be 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -46,6 +46,7 @@ parameters: neverInGenericReturnType: false validateOverridingMethodsInStubs: false crossCheckInterfaces: false + finalByPhpDocTag: false fileExtensions: - php checkAlwaysTrueCheckTypeFunctionCall: false @@ -219,7 +220,8 @@ parametersSchema: deepInspectTypes: bool(), neverInGenericReturnType: bool(), validateOverridingMethodsInStubs: bool(), - crossCheckInterfaces: bool() + crossCheckInterfaces: bool(), + finalByPhpDocTag: bool() ]) fileExtensions: listOf(string()) checkAlwaysTrueCheckTypeFunctionCall: bool() diff --git a/src/Rules/Classes/ExistingClassInClassExtendsRule.php b/src/Rules/Classes/ExistingClassInClassExtendsRule.php index 53e049e9d1..c500cc1ee0 100644 --- a/src/Rules/Classes/ExistingClassInClassExtendsRule.php +++ b/src/Rules/Classes/ExistingClassInClassExtendsRule.php @@ -19,13 +19,17 @@ class ExistingClassInClassExtendsRule implements \PHPStan\Rules\Rule private ReflectionProvider $reflectionProvider; + private bool $checkFinalByPhpDocTag; + public function __construct( ClassCaseSensitivityCheck $classCaseSensitivityCheck, - ReflectionProvider $reflectionProvider + ReflectionProvider $reflectionProvider, + bool $checkFinalByPhpDocTag = false ) { $this->classCaseSensitivityCheck = $classCaseSensitivityCheck; $this->reflectionProvider = $reflectionProvider; + $this->checkFinalByPhpDocTag = $checkFinalByPhpDocTag; } public function getNodeType(): string @@ -72,6 +76,12 @@ public function processNode(Node $node, Scope $scope): array $currentClassName !== null ? sprintf('Class %s', $currentClassName) : 'Anonymous class', $extendedClassName ))->nonIgnorable()->build(); + } elseif ($this->checkFinalByPhpDocTag && $reflection->isFinal()) { + $messages[] = RuleErrorBuilder::message(sprintf( + '%s extends @final class %s.', + $currentClassName !== null ? sprintf('Class %s', $currentClassName) : 'Anonymous class', + $extendedClassName + ))->build(); } } diff --git a/tests/PHPStan/Rules/Classes/ExistingClassInClassExtendsRuleTest.php b/tests/PHPStan/Rules/Classes/ExistingClassInClassExtendsRuleTest.php index 836894513f..43c2b2242d 100644 --- a/tests/PHPStan/Rules/Classes/ExistingClassInClassExtendsRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ExistingClassInClassExtendsRuleTest.php @@ -16,7 +16,8 @@ protected function getRule(): Rule $broker = $this->createReflectionProvider(); return new ExistingClassInClassExtendsRule( new ClassCaseSensitivityCheck($broker), - $broker + $broker, + true ); } @@ -27,6 +28,10 @@ public function testRule(): void 'Class ExtendsImplements\Foo referenced with incorrect case: ExtendsImplements\FOO.', 15, ], + [ + 'Class ExtendsImplements\ExtendsFinalWithAnnotation extends @final class ExtendsImplements\FinalWithAnnotation.', + 43, + ], ]); } @@ -61,4 +66,14 @@ public function testRuleExtendsError(): void ]); } + public function testFinalByTag(): void + { + $this->analyse([__DIR__ . '/data/extends-final-by-tag.php'], [ + [ + 'Class ExtendsFinalByTag\Bar2 extends @final class ExtendsFinalByTag\Bar.', + 21, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Classes/data/extends-final-by-tag.php b/tests/PHPStan/Rules/Classes/data/extends-final-by-tag.php new file mode 100644 index 0000000000..501c6e8f00 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/extends-final-by-tag.php @@ -0,0 +1,24 @@ +