Skip to content

Commit c3c1c41

Browse files
authored
added RequireOverrideAttributeSniff that checks existence of #[Override] attribute for overriden methods (#3858)
2 parents 92dd676 + c955504 commit c3c1c41

22 files changed

Lines changed: 259 additions & 5 deletions

File tree

ecs.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@
104104
use Shopsys\CodingStandards\Sniffs\ForbiddenExitSniff;
105105
use Shopsys\CodingStandards\Sniffs\ForbiddenSuperGlobalSniff;
106106
use Shopsys\CodingStandards\Sniffs\ObjectIsCreatedByFactorySniff;
107+
use Shopsys\CodingStandards\Sniffs\RequireOverrideAttributeSniff;
107108
use Shopsys\CodingStandards\Sniffs\ValidVariableNameSniff;
108109
use SlevomatCodingStandard\Sniffs\Arrays\TrailingArrayCommaSniff;
109110
use SlevomatCodingStandard\Sniffs\Classes\ClassLengthSniff;
@@ -146,11 +147,6 @@
146147
SetList::DOCBLOCK,
147148
SetList::NAMESPACES,
148149
])
149-
->withRules([
150-
InlineDocCommentDeclarationSniff::class,
151-
NullableTypeForNullDefaultValueSniff::class,
152-
ReturnTypeHintSpacingSniff::class,
153-
])
154150
->withConfiguredRule(
155151
ForbiddenClassesSniff::class, [
156152
'forbiddenClasses' => [
@@ -159,6 +155,9 @@
159155
],
160156
])
161157
->withRules([
158+
InlineDocCommentDeclarationSniff::class,
159+
NullableTypeForNullDefaultValueSniff::class,
160+
ReturnTypeHintSpacingSniff::class,
162161
InheritDocFormatFixer::class,
163162
ForbiddenDumpFixer::class,
164163
MissingButtonTypeFixer::class,
@@ -259,6 +258,7 @@
259258
DisallowEmptySniff::class,
260259
ParentCallSpacingSniff::class,
261260
UselessIfConditionWithReturnSniff::class,
261+
RequireOverrideAttributeSniff::class,
262262
])
263263
->withConfiguredRule(CyclomaticComplexitySniff::class, [
264264
'absoluteComplexity' => CyclomaticComplexitySniffSetting::DEFAULT_ABSOLUTE_COMPLEXITY,
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Shopsys\CodingStandards\Sniffs;
6+
7+
use PHP_CodeSniffer\Files\File;
8+
use PHP_CodeSniffer\Sniffs\Sniff;
9+
use ReflectionClass;
10+
use ReflectionException;
11+
12+
class RequireOverrideAttributeSniff implements Sniff
13+
{
14+
/**
15+
* @return array
16+
*/
17+
public function register(): array
18+
{
19+
return [T_FUNCTION];
20+
}
21+
22+
/**
23+
* @param \PHP_CodeSniffer\Files\File $phpcsFile
24+
* @param mixed $stackPtr
25+
*/
26+
public function process(File $phpcsFile, $stackPtr): void
27+
{
28+
$tokens = $phpcsFile->getTokens();
29+
30+
$classPtr = $phpcsFile->findPrevious([T_CLASS, T_TRAIT], $stackPtr);
31+
32+
if ($classPtr === false) {
33+
return;
34+
}
35+
36+
$classNamePtr = $phpcsFile->findNext(T_STRING, $classPtr);
37+
38+
if ($classNamePtr === false) {
39+
return;
40+
}
41+
$className = $tokens[$classNamePtr]['content'];
42+
43+
$methodNamePtr = $phpcsFile->findNext(T_STRING, $stackPtr);
44+
45+
if ($methodNamePtr === false) {
46+
return;
47+
}
48+
$methodName = $tokens[$methodNamePtr]['content'];
49+
50+
if ($methodName === '__construct') {
51+
return;
52+
}
53+
54+
$parentClassPtr = $phpcsFile->findNext(T_EXTENDS, $classNamePtr);
55+
56+
if ($parentClassPtr === false) {
57+
return;
58+
}
59+
60+
$parentClassNamePtr = $phpcsFile->findNext(T_STRING, $parentClassPtr);
61+
62+
if ($parentClassNamePtr === false) {
63+
return;
64+
}
65+
66+
$parentClassName = $tokens[$parentClassNamePtr]['content'];
67+
$parentClassFqn = $this->getFqnOfClass($phpcsFile, $tokens, $parentClassName);
68+
69+
if ($parentClassFqn === '') {
70+
return;
71+
}
72+
73+
74+
try {
75+
$parentClassReflection = new ReflectionClass($parentClassFqn);
76+
77+
$hasMethod = $this->hasParentClassMethod($parentClassReflection, $methodName);
78+
} catch (ReflectionException $e) {
79+
return;
80+
}
81+
82+
if ($hasMethod === false) {
83+
return;
84+
}
85+
86+
$hasOverride = $this->checkMethodHasOverrideAttribute($phpcsFile, $methodNamePtr, $tokens, $hasOverride);
87+
88+
if ($hasOverride === true) {
89+
return;
90+
}
91+
92+
$error = "Method {$className}::{$methodName} overrides {$parentClassName}::{$methodName} but is missing #[Override] attribute.";
93+
$fix = $phpcsFile->addFixableError($error, $stackPtr, 'MissingOverride');
94+
95+
if (!$fix) {
96+
return;
97+
}
98+
99+
$phpcsFile->fixer->beginChangeset();
100+
101+
$previousLine = $phpcsFile->findPrevious(T_WHITESPACE, $methodNamePtr, value: PHP_EOL);
102+
$indent = $tokens[$previousLine + 1]['content'];
103+
$phpcsFile->fixer->addContentBefore($previousLine + 1, "{$indent}#[Override]" . PHP_EOL);
104+
105+
$overrideFqn = $this->getFqnOfClass($phpcsFile, $tokens, 'Override');
106+
107+
if ($overrideFqn === '') {
108+
$usePtr = $phpcsFile->findNext(T_USE, 0);
109+
$previousLine = $phpcsFile->findPrevious(T_WHITESPACE, $usePtr, value: PHP_EOL);
110+
$phpcsFile->fixer->addContentBefore($previousLine + 1, 'use Override;' . PHP_EOL);
111+
}
112+
113+
$phpcsFile->fixer->endChangeset();
114+
}
115+
116+
/**
117+
* @param \ReflectionClass $parentClass
118+
* @param string $methodName
119+
* @return bool
120+
*/
121+
protected function hasParentClassMethod(ReflectionClass $parentClass, string $methodName): bool
122+
{
123+
if ($parentClass->hasMethod($methodName)) {
124+
return true;
125+
}
126+
127+
$parentClass = $parentClass->getParentClass();
128+
129+
if ($parentClass === false) {
130+
return false;
131+
}
132+
133+
return $parentClass->hasMethod($methodName);
134+
}
135+
136+
/**
137+
* @param \PHP_CodeSniffer\Files\File $phpcsFile
138+
* @param array $tokens
139+
* @param string $parentClassName
140+
* @return string
141+
*/
142+
private function getFqnOfClass(File $phpcsFile, array $tokens, mixed $parentClassName): string
143+
{
144+
$usePtr = $phpcsFile->findNext(T_USE, 0);
145+
146+
$parentClassFqn = '';
147+
148+
while ($usePtr !== false) {
149+
$semicolonPtr = $phpcsFile->findNext(T_SEMICOLON, $usePtr);
150+
$fqnClassNamePtr = $phpcsFile->findPrevious(T_STRING, $semicolonPtr);
151+
152+
$fqnClassName = $tokens[$fqnClassNamePtr]['content'];
153+
154+
if ($fqnClassName !== $parentClassName) {
155+
$usePtr = $phpcsFile->findNext(T_USE, $usePtr + 1);
156+
157+
continue;
158+
}
159+
160+
$fqnCurrentPtr = $usePtr + 2;
161+
162+
while ($tokens[$fqnCurrentPtr]['type'] !== 'T_WHITESPACE' && $tokens[$fqnCurrentPtr]['type'] !== 'T_SEMICOLON') {
163+
$parentClassFqn .= $tokens[$fqnCurrentPtr]['content'];
164+
$fqnCurrentPtr++;
165+
}
166+
167+
break;
168+
}
169+
170+
return $parentClassFqn;
171+
}
172+
173+
/**
174+
* @param \PHP_CodeSniffer\Files\File $phpcsFile
175+
* @param int $methodNamePtr
176+
* @param array $tokens
177+
* @return bool
178+
*/
179+
private function checkMethodHasOverrideAttribute(File $phpcsFile, int $methodNamePtr, array $tokens): bool
180+
{
181+
$hasOverride = false;
182+
$previousLinePtr = $phpcsFile->findPrevious(T_WHITESPACE, $methodNamePtr, value: PHP_EOL);
183+
184+
for ($i = $previousLinePtr - 1; $i >= 0; $i--) {
185+
if ($tokens[$i]['code'] === T_ATTRIBUTE) {
186+
$attributeNamePtr = $phpcsFile->findNext(T_STRING, $i);
187+
188+
if ($attributeNamePtr !== false && $tokens[$attributeNamePtr]['content'] === 'Override') {
189+
$hasOverride = true;
190+
191+
break;
192+
}
193+
}
194+
195+
if ($tokens[$i]['code'] === T_DOC_COMMENT_CLOSE_TAG || $tokens[$i]['code'] === T_FUNCTION) {
196+
break;
197+
}
198+
}
199+
200+
return $hasOverride;
201+
}
202+
}

src/Sniffs/ValidVariableNameSniff.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace Shopsys\CodingStandards\Sniffs;
66

7+
use Override;
78
use PHP_CodeSniffer\Files\File;
89
use PHP_CodeSniffer\Sniffs\AbstractVariableSniff;
910
use PHP_CodeSniffer\Util\Common;
@@ -23,6 +24,7 @@ final class ValidVariableNameSniff extends AbstractVariableSniff
2324
* @param \PHP_CodeSniffer\Files\File $file
2425
* @param int $position
2526
*/
27+
#[Override]
2628
protected function processVariable(File $file, $position): void
2729
{
2830
$errorMessageFormat = 'Variable "$%s" should be camel case';
@@ -33,6 +35,7 @@ protected function processVariable(File $file, $position): void
3335
* @param \PHP_CodeSniffer\Files\File $file
3436
* @param int $position
3537
*/
38+
#[Override]
3639
protected function processVariableInString(File $file, $position): void
3740
{
3841
}
@@ -41,6 +44,7 @@ protected function processVariableInString(File $file, $position): void
4144
* @param \PHP_CodeSniffer\Files\File $file
4245
* @param int $position
4346
*/
47+
#[Override]
4448
protected function processMemberVar(File $file, $position): void
4549
{
4650
$errorMessageFormat = 'Class member variable "$%s" should be camel case';

tests/Unit/CsFixer/Constraint/IsIdenticalString.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace Tests\CodingStandards\Unit\CsFixer\Constraint;
66

7+
use Override;
78
use PHPUnit\Framework\Constraint\Constraint;
89
use PHPUnit\Framework\Constraint\IsIdentical;
910
use PHPUnit\Framework\ExpectationFailedException;
@@ -30,6 +31,7 @@ public function __construct(private readonly mixed $value)
3031
* @param bool $returnResult
3132
* @return bool|null
3233
*/
34+
#[Override]
3335
public function evaluate($other, string $description = '', bool $returnResult = false): ?bool
3436
{
3537
try {
@@ -54,6 +56,7 @@ public function evaluate($other, string $description = '', bool $returnResult =
5456
/**
5557
* @return string
5658
*/
59+
#[Override]
5760
public function toString(): string
5861
{
5962
return $this->isIdentical->toString();
@@ -63,6 +66,7 @@ public function toString(): string
6366
* @param mixed $other
6467
* @return string
6568
*/
69+
#[Override]
6670
protected function additionalFailureDescription($other): string
6771
{
6872
$pattern = '/(\r\n|\n\r|\r)/';

tests/Unit/CsFixer/ForbiddenDumpFixer/ForbiddenDumpFixerTest.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace Tests\CodingStandards\Unit\CsFixer\ForbiddenDumpFixer;
66

7+
use Override;
78
use Shopsys\CodingStandards\CsFixer\ForbiddenDumpFixer;
89
use Tests\CodingStandards\Unit\CsFixer\AbstractFixerTestCase;
910

@@ -12,6 +13,7 @@ final class ForbiddenDumpFixerTest extends AbstractFixerTestCase
1213
/**
1314
* @return \Shopsys\CodingStandards\CsFixer\ForbiddenDumpFixer
1415
*/
16+
#[Override]
1517
protected function createFixerService(): ForbiddenDumpFixer
1618
{
1719
return new ForbiddenDumpFixer();
@@ -20,6 +22,7 @@ protected function createFixerService(): ForbiddenDumpFixer
2022
/**
2123
* {@inheritdoc}
2224
*/
25+
#[Override]
2326
public static function getTestingFiles(): iterable
2427
{
2528
yield [__DIR__ . '/fixed/fixed.html.twig', __DIR__ . '/wrong/wrong.html.twig'];

tests/Unit/CsFixer/ForbiddenPrivateVisibilityFixer/ForbiddenPrivateVisibilityFixerTest.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace Tests\CodingStandards\Unit\CsFixer\ForbiddenPrivateVisibilityFixer;
66

7+
use Override;
78
use Shopsys\CodingStandards\CsFixer\ForbiddenPrivateVisibilityFixer;
89
use Tests\CodingStandards\Unit\CsFixer\AbstractFixerTestCase;
910

@@ -12,6 +13,7 @@ final class ForbiddenPrivateVisibilityFixerTest extends AbstractFixerTestCase
1213
/**
1314
* @return \Shopsys\CodingStandards\CsFixer\ForbiddenPrivateVisibilityFixer
1415
*/
16+
#[Override]
1517
protected function createFixerService(): ForbiddenPrivateVisibilityFixer
1618
{
1719
$fixer = new ForbiddenPrivateVisibilityFixer();
@@ -25,6 +27,7 @@ protected function createFixerService(): ForbiddenPrivateVisibilityFixer
2527
/**
2628
* {@inheritdoc}
2729
*/
30+
#[Override]
2831
public static function getTestingFiles(): iterable
2932
{
3033
yield [__DIR__ . '/fixed/constructor_property_promotion.php', __DIR__ . '/wrong/constructor_property_promotion.php'];

tests/Unit/CsFixer/MissingButtonTypeFixer/MissingButtonTypeFixerTest.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace Tests\CodingStandards\Unit\CsFixer\MissingButtonTypeFixer;
66

7+
use Override;
78
use Shopsys\CodingStandards\CsFixer\MissingButtonTypeFixer;
89
use Tests\CodingStandards\Unit\CsFixer\AbstractFixerTestCase;
910

@@ -12,6 +13,7 @@ final class MissingButtonTypeFixerTest extends AbstractFixerTestCase
1213
/**
1314
* @return \Shopsys\CodingStandards\CsFixer\MissingButtonTypeFixer
1415
*/
16+
#[Override]
1517
protected function createFixerService(): MissingButtonTypeFixer
1618
{
1719
return new MissingButtonTypeFixer();
@@ -20,6 +22,7 @@ protected function createFixerService(): MissingButtonTypeFixer
2022
/**
2123
* {@inheritdoc}
2224
*/
25+
#[Override]
2326
public static function getTestingFiles(): iterable
2427
{
2528
yield [__DIR__ . '/fixed/fixed.html.twig', __DIR__ . '/wrong/wrong.html.twig'];

0 commit comments

Comments
 (0)