Skip to content

Commit 43d5d8c

Browse files
committed
SlevomatCodingStandard.Commenting.AnnotationName: New sniff to check incorrect annotation names
1 parent dfe226c commit 43d5d8c

10 files changed

+565
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ Slevomat Coding Standard for [PHP_CodeSniffer](https://github.com/squizlabs/PHP_
7272
- [SlevomatCodingStandard.Classes.TraitUseDeclaration](doc/classes.md#slevomatcodingstandardclassestraitusedeclaration-) 🔧
7373
- [SlevomatCodingStandard.Classes.TraitUseSpacing](doc/classes.md#slevomatcodingstandardclassestraitusespacing-) 🔧
7474
- [SlevomatCodingStandard.Classes.UselessLateStaticBinding](doc/classes.md#slevomatcodingstandardclassesuselesslatestaticbinding-) 🔧
75+
- [SlevomatCodingStandard.Commenting.AnnotationName](doc/commenting.md#slevomatcodingstandardcommentingannotationname-)
7576
- [SlevomatCodingStandard.Commenting.DeprecatedAnnotationDeclaration](doc/commenting.md#slevomatcodingstandardcommentingdeprecatedannotationdeclaration)
7677
- [SlevomatCodingStandard.Commenting.DisallowCommentAfterCode](doc/commenting.md#slevomatcodingstandardcommentingdisallowcommentaftercode-) 🔧
7778
- [SlevomatCodingStandard.Commenting.DisallowOneLinePropertyDocComment](doc/commenting.md#slevomatcodingstandardcommentingdisallowonelinepropertydoccomment-) 🔧
Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace SlevomatCodingStandard\Sniffs\Commenting;
4+
5+
use PHP_CodeSniffer\Files\File;
6+
use PHP_CodeSniffer\Sniffs\Sniff;
7+
use SlevomatCodingStandard\Helpers\AnnotationHelper;
8+
use SlevomatCodingStandard\Helpers\FixerHelper;
9+
use SlevomatCodingStandard\Helpers\SniffSettingsHelper;
10+
use SlevomatCodingStandard\Helpers\TokenHelper;
11+
use function array_combine;
12+
use function array_key_exists;
13+
use function array_map;
14+
use function array_merge;
15+
use function array_unique;
16+
use function ltrim;
17+
use function preg_match_all;
18+
use function sprintf;
19+
use function strlen;
20+
use function strpos;
21+
use function strtolower;
22+
use function substr;
23+
use const PREG_OFFSET_CAPTURE;
24+
use const T_DOC_COMMENT_OPEN_TAG;
25+
26+
class AnnotationNameSniff implements Sniff
27+
{
28+
29+
public const CODE_ANNOTATION_NAME_INCORRECT = 'AnnotationNameIncorrect';
30+
31+
private const STANDARD_ANNOTATIONS = [
32+
'api',
33+
'author',
34+
'category',
35+
'copyright',
36+
'deprecated',
37+
'example',
38+
'filesource',
39+
'global',
40+
'ignore',
41+
'inheritDoc',
42+
'internal',
43+
'license',
44+
'link',
45+
'method',
46+
'package',
47+
'param',
48+
'property',
49+
'property-read',
50+
'property-write',
51+
'return',
52+
'see',
53+
'since',
54+
'source',
55+
'subpackage',
56+
'throws',
57+
'todo',
58+
'uses',
59+
'used-by',
60+
'var',
61+
'version',
62+
];
63+
64+
private const STATIC_ANALYSIS_ANNOTATIONS = [
65+
'api',
66+
'allow-private-mutation',
67+
'assert',
68+
'assert-if-true',
69+
'assert-if-false',
70+
'consistent-constructor',
71+
'consistent-templates',
72+
'extends',
73+
'external-mutation-free',
74+
'implements',
75+
'mixin',
76+
'ignore-falsable-return',
77+
'ignore-nullable-return',
78+
'ignore-var',
79+
'ignore-variable-method',
80+
'ignore-variable-property',
81+
'immutable',
82+
'import-type',
83+
'internal',
84+
'method',
85+
'mutation-free',
86+
'no-named-arguments',
87+
'param',
88+
'param-out',
89+
'property',
90+
'property-read',
91+
'property-write',
92+
'psalm-check-type',
93+
'psalm-check-type-exact',
94+
'psalm-suppress',
95+
'psalm-trace',
96+
'pure',
97+
'readonly',
98+
'readonly-allow-private-mutation',
99+
'require-extends',
100+
'require-implements',
101+
'return',
102+
'seal-properties',
103+
'self-out',
104+
'template',
105+
'template-covariant',
106+
'template-extends',
107+
'template-implements',
108+
'template-use',
109+
'this-out',
110+
'type',
111+
'var',
112+
'yield',
113+
];
114+
115+
private const PHPUNIT_ANNOTATIONS = [
116+
'author',
117+
'after',
118+
'afterClass',
119+
'backupGlobals',
120+
'backupStaticAttributes',
121+
'before',
122+
'beforeClass',
123+
'codeCoverageIgnore',
124+
'codeCoverageIgnoreStart',
125+
'codeCoverageIgnoreEnd',
126+
'covers',
127+
'coversDefaultClass',
128+
'coversNothing',
129+
'dataProvider',
130+
'depends',
131+
'doesNotPerformAssertions',
132+
'group',
133+
'large',
134+
'medium',
135+
'preserveGlobalState',
136+
'requires',
137+
'runTestsInSeparateProcesses',
138+
'runInSeparateProcess',
139+
'small',
140+
'test',
141+
'testdox',
142+
'testWith',
143+
'ticket',
144+
'uses',
145+
];
146+
147+
/** @var list<string>|null */
148+
public $annotations;
149+
150+
/** @var array<string, string>|null */
151+
private $normalizedAnnotations;
152+
153+
/**
154+
* @return array<int, (int|string)>
155+
*/
156+
public function register(): array
157+
{
158+
return [
159+
T_DOC_COMMENT_OPEN_TAG,
160+
];
161+
}
162+
163+
/**
164+
* @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint
165+
* @param int $docCommentOpenPointer
166+
*/
167+
public function process(File $phpcsFile, $docCommentOpenPointer): void
168+
{
169+
$annotations = AnnotationHelper::getAnnotations($phpcsFile, $docCommentOpenPointer);
170+
$correctAnnotationNames = $this->getNormalizedAnnotationNames();
171+
172+
foreach ($annotations as $annotationName => $annotationsByName) {
173+
$lowerCasedAnnotationName = strtolower($annotationName);
174+
175+
if (!array_key_exists($lowerCasedAnnotationName, $correctAnnotationNames)) {
176+
continue;
177+
}
178+
179+
$correctAnnotationName = $correctAnnotationNames[$lowerCasedAnnotationName];
180+
181+
if ($correctAnnotationName === $annotationName) {
182+
continue;
183+
}
184+
185+
foreach ($annotationsByName as $annotation) {
186+
$fix = $phpcsFile->addFixableError(
187+
sprintf('Annotation name is incorrect. Expected %s, found %s.', $correctAnnotationName, $annotationName),
188+
$annotation->getStartPointer(),
189+
self::CODE_ANNOTATION_NAME_INCORRECT
190+
);
191+
if (!$fix) {
192+
continue;
193+
}
194+
195+
$phpcsFile->fixer->beginChangeset();
196+
197+
$phpcsFile->fixer->replaceToken($annotation->getStartPointer(), $correctAnnotationName);
198+
199+
$phpcsFile->fixer->endChangeset();
200+
}
201+
}
202+
203+
$tokens = $phpcsFile->getTokens();
204+
205+
$docCommentContent = TokenHelper::getContent($phpcsFile, $docCommentOpenPointer, $tokens[$docCommentOpenPointer]['comment_closer']);
206+
207+
foreach ($correctAnnotationNames as $correctAnnotationName) {
208+
if (preg_match_all('~\{(' . $correctAnnotationName . ')\}~i', $docCommentContent, $matches, PREG_OFFSET_CAPTURE) === 0) {
209+
continue;
210+
}
211+
212+
foreach ($matches[1] as $match) {
213+
if ($match[0] === $correctAnnotationName) {
214+
continue;
215+
}
216+
217+
$fix = $phpcsFile->addFixableError(
218+
sprintf('Annotation name is incorrect. Expected %s, found %s.', $correctAnnotationName, $match[0]),
219+
$docCommentOpenPointer,
220+
self::CODE_ANNOTATION_NAME_INCORRECT
221+
);
222+
if (!$fix) {
223+
continue;
224+
}
225+
226+
$phpcsFile->fixer->beginChangeset();
227+
228+
$fixedDocCommentContent = substr($docCommentContent, 0, $match[1]) . $correctAnnotationName . substr(
229+
$docCommentContent,
230+
$match[1] + strlen($match[0])
231+
);
232+
233+
$phpcsFile->fixer->replaceToken($docCommentOpenPointer, $fixedDocCommentContent);
234+
FixerHelper::removeBetweenIncluding(
235+
$phpcsFile,
236+
$docCommentOpenPointer + 1,
237+
$tokens[$docCommentOpenPointer]['comment_closer']
238+
);
239+
240+
$phpcsFile->fixer->endChangeset();
241+
}
242+
}
243+
}
244+
245+
/**
246+
* @return array<string, string>
247+
*/
248+
private function getNormalizedAnnotationNames(): array
249+
{
250+
if ($this->normalizedAnnotations !== null) {
251+
return $this->normalizedAnnotations;
252+
}
253+
254+
if ($this->annotations !== null) {
255+
$annotationNames = array_map(static function (string $annotationName): string {
256+
return ltrim($annotationName, '@');
257+
}, SniffSettingsHelper::normalizeArray($this->annotations));
258+
} else {
259+
$annotationNames = array_merge(self::STANDARD_ANNOTATIONS, self::PHPUNIT_ANNOTATIONS, self::STATIC_ANALYSIS_ANNOTATIONS);
260+
261+
foreach (self::STATIC_ANALYSIS_ANNOTATIONS as $annotationName) {
262+
if (strpos($annotationName, 'psalm') === 0) {
263+
continue;
264+
}
265+
266+
foreach (AnnotationHelper::STATIC_ANALYSIS_PREFIXES as $prefix) {
267+
$annotationNames[] = sprintf('%s-%s', $prefix, $annotationName);
268+
}
269+
}
270+
}
271+
272+
$annotationNames = array_map(static function (string $annotationName): string {
273+
return '@' . $annotationName;
274+
}, array_unique($annotationNames));
275+
276+
$this->normalizedAnnotations = array_combine(array_map(static function (string $annotationName): string {
277+
return strtolower($annotationName);
278+
}, $annotationNames), $annotationNames);
279+
280+
return $this->normalizedAnnotations;
281+
}
282+
283+
}

doc/commenting.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
## Commenting
22

3+
#### SlevomatCodingStandard.Commenting.AnnotationName 🔧
4+
5+
Reports incorrect annotation name. It reports standard annotation names used by phpDocumentor, PHPUnit, PHPStan and Psalm by default.
6+
Unknown annotation names are ignored.
7+
8+
Sniff provides the following settings:
9+
10+
* `annotations`: allows to configure which annotations are checked and how.
11+
312
#### SlevomatCodingStandard.Commenting.DeprecatedAnnotationDeclaration
413

514
Reports `@deprecated` annotations without description.

0 commit comments

Comments
 (0)