Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 190 |
|
0.00% |
0 / 4 |
CRAP | |
0.00% |
0 / 1 |
DocCommentSniff | |
0.00% |
0 / 190 |
|
0.00% |
0 / 4 |
3540 | |
0.00% |
0 / 1 |
register | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
process | |
0.00% |
0 / 181 |
|
0.00% |
0 / 1 |
2970 | |||
getCommentIndent | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
getDocStarColumn | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | /** |
3 | * This program is free software; you can redistribute it and/or modify |
4 | * it under the terms of the GNU General Public License as published by |
5 | * the Free Software Foundation; either version 2 of the License, or |
6 | * (at your option) any later version. |
7 | * |
8 | * This program is distributed in the hope that it will be useful, |
9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
11 | * GNU General Public License for more details. |
12 | * |
13 | * You should have received a copy of the GNU General Public License along |
14 | * with this program; if not, write to the Free Software Foundation, Inc., |
15 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
16 | * http://www.gnu.org/copyleft/gpl.html |
17 | * |
18 | * @file |
19 | */ |
20 | |
21 | namespace MediaWiki\Sniffs\Commenting; |
22 | |
23 | use PHP_CodeSniffer\Files\File; |
24 | use PHP_CodeSniffer\Sniffs\Sniff; |
25 | |
26 | class DocCommentSniff implements Sniff { |
27 | |
28 | /** Do not report very long asterisks line, there are eye-catchers for structure of the code */ |
29 | private const COMMENT_START_ASTERISKS_MAX_LEN = 10; |
30 | |
31 | /** |
32 | * List of annotations where the spacing before is not checked. |
33 | * For example because this annotations are used inside a long text |
34 | * |
35 | * @var string[] |
36 | */ |
37 | private const ANNOTATIONS_IGNORE_MULTI_SPACE_BEFORE = [ |
38 | '@see', |
39 | '@deprecated', |
40 | ]; |
41 | |
42 | /** |
43 | * @inheritDoc |
44 | */ |
45 | public function register(): array { |
46 | return [ T_DOC_COMMENT_OPEN_TAG ]; |
47 | } |
48 | |
49 | /** |
50 | * Processes this test, when one of its tokens is encountered. |
51 | * |
52 | * @param File $phpcsFile The file being scanned. |
53 | * @param int $stackPtr The position of the current token in the stack passed in $tokens. |
54 | * |
55 | * @return void |
56 | */ |
57 | public function process( File $phpcsFile, $stackPtr ) { |
58 | $tokens = $phpcsFile->getTokens(); |
59 | $commentStart = $stackPtr; |
60 | $commentEnd = $tokens[$stackPtr]['comment_closer']; |
61 | $isMultiLineDoc = ( $tokens[$commentStart]['line'] !== $tokens[$commentEnd]['line'] ); |
62 | |
63 | // Start token should exact /** |
64 | // Self-closing comments are tokenized also as open tag, but ignore them |
65 | if ( $tokens[$commentStart]['code'] === T_DOC_COMMENT_OPEN_TAG && |
66 | $tokens[$commentStart]['content'] !== '/**' && |
67 | $tokens[$commentStart]['length'] < self::COMMENT_START_ASTERISKS_MAX_LEN && |
68 | !str_ends_with( $tokens[$commentStart]['content'], '*/' ) |
69 | ) { |
70 | $error = 'Comment open tag must be \'/**\''; |
71 | $fix = $phpcsFile->addFixableError( $error, $commentStart, 'SyntaxOpenTag' ); |
72 | if ( $fix ) { |
73 | $phpcsFile->fixer->replaceToken( $commentStart, '/**' ); |
74 | } |
75 | } |
76 | $columnDocStar = $this->getDocStarColumn( $phpcsFile, $commentStart ); |
77 | $prevLineDocStar = $tokens[$commentStart]['line']; |
78 | $lastLine = $commentStart; |
79 | $lineWithDocStar = true; |
80 | $indent = $this->getCommentIndent( $phpcsFile, $commentStart ); |
81 | |
82 | for ( $i = $commentStart; $i <= $commentEnd; $i++ ) { |
83 | $initialStarChars = 0; |
84 | |
85 | if ( $tokens[$lastLine]['line'] !== $tokens[$i]['line'] ) { |
86 | if ( !$lineWithDocStar ) { |
87 | $fix = $phpcsFile->addFixableError( |
88 | 'Expected \'*\' on each line', |
89 | $lastLine, |
90 | 'NoDocStar' |
91 | ); |
92 | if ( $fix ) { |
93 | $posNonWhitespace = $phpcsFile->findFirstOnLine( |
94 | [ T_DOC_COMMENT_WHITESPACE ], $i - 1, true |
95 | ); |
96 | if ( $posNonWhitespace === false ) { |
97 | // empty line |
98 | $phpcsFile->fixer->addContentBefore( $lastLine, $indent . '*' ); |
99 | } else { |
100 | $phpcsFile->fixer->beginChangeset(); |
101 | // line with content, first remove old indent |
102 | for ( $j = $lastLine; $j < $posNonWhitespace; $j++ ) { |
103 | $phpcsFile->fixer->replaceToken( $j, '' ); |
104 | } |
105 | // and set a new indent with the doc star and a space |
106 | $phpcsFile->fixer->addContentBefore( $lastLine, $indent . '* ' ); |
107 | $phpcsFile->fixer->endChangeset(); |
108 | } |
109 | } |
110 | } |
111 | $lineWithDocStar = false; |
112 | $lastLine = $i; |
113 | } |
114 | |
115 | // Star token should exact * |
116 | if ( $tokens[$i]['code'] === T_DOC_COMMENT_STAR ) { |
117 | // Multi stars in a line are parsed as a new token |
118 | $initialStarChars = strspn( $tokens[$i + 1]['content'], '*' ); |
119 | if ( $initialStarChars > 0 ) { |
120 | $error = 'Comment star must be a single \'*\''; |
121 | $fix = $phpcsFile->addFixableError( $error, $i, 'SyntaxMultiDocStar' ); |
122 | if ( $fix ) { |
123 | $phpcsFile->fixer->replaceToken( |
124 | $i + 1, |
125 | substr( $tokens[$i + 1]['content'], $initialStarChars ) |
126 | ); |
127 | } |
128 | } |
129 | $lineWithDocStar = true; |
130 | } |
131 | |
132 | // Ensure whitespace or tab after /** or * |
133 | if ( ( $tokens[$i]['code'] === T_DOC_COMMENT_OPEN_TAG || |
134 | $tokens[$i]['code'] === T_DOC_COMMENT_STAR ) && |
135 | $tokens[$i + 1]['code'] !== T_DOC_COMMENT_CLOSE_TAG && |
136 | $tokens[$i + 1]['length'] > 0 |
137 | ) { |
138 | $commentStarSpacing = $i + 1; |
139 | $expectedSpaces = 1; |
140 | // ignore * removed by SyntaxMultiDocStar and count spaces after that |
141 | $currentSpaces = strspn( |
142 | $tokens[$commentStarSpacing]['content'], " \t", $initialStarChars |
143 | ); |
144 | $error = null; |
145 | $code = null; |
146 | if ( $isMultiLineDoc && $currentSpaces < $expectedSpaces ) { |
147 | // be relax for multiline docs, because some line breaks in @param can |
148 | // have more than one space after a doc star |
149 | $error = 'Expected at least %s spaces after doc star; %s found'; |
150 | $code = 'SpacingDocStar'; |
151 | } elseif ( !$isMultiLineDoc && $currentSpaces !== $expectedSpaces ) { |
152 | $error = 'Expected %s spaces after doc star on single line; %s found'; |
153 | $code = 'SpacingDocStarSingleLine'; |
154 | } |
155 | if ( $error !== null && $code !== null ) { |
156 | $fix = $phpcsFile->addFixableError( |
157 | $error, |
158 | $commentStarSpacing, |
159 | $code, |
160 | [ $expectedSpaces, $currentSpaces ] |
161 | ); |
162 | if ( $fix ) { |
163 | if ( $currentSpaces > $expectedSpaces ) { |
164 | // Remove whitespace |
165 | $content = $tokens[$commentStarSpacing]['content']; |
166 | $phpcsFile->fixer->replaceToken( |
167 | $commentStarSpacing, |
168 | substr( $content, 0, $expectedSpaces - $currentSpaces ) |
169 | ); |
170 | } else { |
171 | // Add whitespace |
172 | $phpcsFile->fixer->addContent( |
173 | $i, str_repeat( ' ', $expectedSpaces ) |
174 | ); |
175 | } |
176 | } |
177 | } |
178 | } |
179 | |
180 | if ( !$isMultiLineDoc ) { |
181 | continue; |
182 | } |
183 | |
184 | // Ensure one whitespace before @param/@return |
185 | if ( $tokens[$i]['code'] === T_DOC_COMMENT_TAG && |
186 | $tokens[$i]['line'] === $tokens[$i - 1]['line'] |
187 | ) { |
188 | $commentTagSpacing = $i - 1; |
189 | $expectedSpaces = 1; |
190 | $currentSpaces = strspn( strrev( $tokens[$commentTagSpacing]['content'] ), ' ' ); |
191 | // Relax the check for a list of annotations for multi spaces before the annotation |
192 | if ( $currentSpaces > $expectedSpaces && |
193 | !in_array( $tokens[$i]['content'], self::ANNOTATIONS_IGNORE_MULTI_SPACE_BEFORE, true ) |
194 | ) { |
195 | $data = [ |
196 | $expectedSpaces, |
197 | $tokens[$i]['content'], |
198 | $currentSpaces, |
199 | ]; |
200 | $fix = $phpcsFile->addFixableError( |
201 | 'Expected %s spaces before %s; %s found', |
202 | $commentTagSpacing, |
203 | 'SpacingDocTag', |
204 | $data |
205 | ); |
206 | if ( $fix ) { |
207 | // Remove whitespace |
208 | $content = $tokens[$commentTagSpacing]['content']; |
209 | $phpcsFile->fixer->replaceToken( |
210 | $commentTagSpacing, |
211 | substr( $content, 0, $expectedSpaces - $currentSpaces ) |
212 | ); |
213 | } |
214 | } |
215 | |
216 | continue; |
217 | } |
218 | |
219 | // Ensure aligned * or */ for multiline comments |
220 | if ( ( $tokens[$i]['code'] === T_DOC_COMMENT_STAR || |
221 | $tokens[$i]['code'] === T_DOC_COMMENT_CLOSE_TAG ) && |
222 | $tokens[$i]['column'] !== $columnDocStar && |
223 | $tokens[$i]['line'] !== $prevLineDocStar |
224 | ) { |
225 | if ( $tokens[$i]['code'] === T_DOC_COMMENT_STAR ) { |
226 | $error = 'Comment star tag not aligned with open tag'; |
227 | $code = 'SyntaxAlignedDocStar'; |
228 | } else { |
229 | $error = 'Comment close tag not aligned with open tag'; |
230 | $code = 'SyntaxAlignedDocClose'; |
231 | } |
232 | $fix = $phpcsFile->addFixableError( $error, $i, $code ); |
233 | if ( $fix ) { |
234 | $tokenBefore = $i - 1; |
235 | $columnOff = $columnDocStar - $tokens[$i]['column']; |
236 | if ( $columnOff < 0 ) { |
237 | // Ensure to remove only whitespaces |
238 | if ( $tokens[$tokenBefore]['code'] === T_DOC_COMMENT_WHITESPACE ) { |
239 | $columnOff = max( $columnOff, $tokens[$tokenBefore]['length'] * -1 ); |
240 | // remove whitespaces |
241 | $phpcsFile->fixer->replaceToken( |
242 | $tokenBefore, |
243 | substr( $tokens[$tokenBefore]['content'], 0, $columnOff ) |
244 | ); |
245 | } |
246 | } elseif ( $tokens[$tokenBefore]['length'] !== 0 ) { |
247 | // Set correct indent |
248 | $phpcsFile->fixer->replaceToken( $tokenBefore, $indent ); |
249 | } else { |
250 | // Add correct indent |
251 | $phpcsFile->fixer->addContent( $tokenBefore, $indent ); |
252 | } |
253 | } |
254 | $prevLineDocStar = $tokens[$i]['line']; |
255 | |
256 | continue; |
257 | } |
258 | } |
259 | |
260 | // End token should exact */ |
261 | if ( $tokens[$commentEnd]['code'] === T_DOC_COMMENT_CLOSE_TAG && |
262 | $tokens[$commentEnd]['length'] > 0 && |
263 | $tokens[$commentEnd]['content'] !== '*/' |
264 | ) { |
265 | $error = 'Comment close tag must be \'*/\''; |
266 | $fix = $phpcsFile->addFixableError( $error, $commentEnd, 'SyntaxCloseTag' ); |
267 | if ( $fix ) { |
268 | $phpcsFile->fixer->replaceToken( $commentEnd, '*/' ); |
269 | } |
270 | } |
271 | |
272 | // For multi line comments the closing tag must have it own line |
273 | if ( $isMultiLineDoc ) { |
274 | $prev = $commentEnd - 1; |
275 | $prevNonWhitespace = $phpcsFile->findPrevious( |
276 | [ T_DOC_COMMENT_WHITESPACE ], $prev, null, true |
277 | ); |
278 | if ( $tokens[$prevNonWhitespace]['line'] === $tokens[$commentEnd]['line'] ) { |
279 | $error = 'Comment close tag should have own line'; |
280 | $fix = $phpcsFile->addFixableError( $error, $commentEnd, 'CloseTagOwnLine' ); |
281 | if ( $fix ) { |
282 | $phpcsFile->fixer->beginChangeset(); |
283 | $phpcsFile->fixer->addNewline( $prev ); |
284 | $phpcsFile->fixer->addContent( $prev, $indent ); |
285 | $phpcsFile->fixer->endChangeset(); |
286 | } |
287 | } |
288 | } elseif ( $tokens[$commentEnd]['length'] > 0 && |
289 | $tokens[$commentEnd - 1]['code'] !== T_DOC_COMMENT_OPEN_TAG |
290 | ) { |
291 | // Ensure a whitespace before the token |
292 | $commentCloseSpacing = $commentEnd - 1; |
293 | $expectedSpaces = 1; |
294 | $currentSpaces = strspn( strrev( $tokens[$commentCloseSpacing]['content'] ), ' ' ); |
295 | if ( $currentSpaces !== $expectedSpaces ) { |
296 | $data = [ |
297 | $expectedSpaces, |
298 | $currentSpaces, |
299 | ]; |
300 | $fix = $phpcsFile->addFixableError( |
301 | 'Expected %s spaces before close comment tag on single line; %s found', |
302 | $commentCloseSpacing, |
303 | 'SpacingSingleLineCloseTag', |
304 | $data |
305 | ); |
306 | if ( $fix ) { |
307 | if ( $currentSpaces > $expectedSpaces ) { |
308 | // Remove whitespace |
309 | $content = $tokens[$commentCloseSpacing]['content']; |
310 | $phpcsFile->fixer->replaceToken( |
311 | $commentCloseSpacing, substr( $content, 0, $expectedSpaces - $currentSpaces ) |
312 | ); |
313 | } else { |
314 | // Add whitespace |
315 | $phpcsFile->fixer->addContentBefore( |
316 | $commentEnd, str_repeat( ' ', $expectedSpaces ) |
317 | ); |
318 | } |
319 | } |
320 | } |
321 | } |
322 | } |
323 | |
324 | /** |
325 | * @param File $phpcsFile |
326 | * @param int $stackPtr |
327 | * @return string |
328 | */ |
329 | private function getCommentIndent( File $phpcsFile, int $stackPtr ): string { |
330 | $firstLineToken = $phpcsFile->findFirstOnLine( [ T_WHITESPACE ], $stackPtr ); |
331 | if ( $firstLineToken === false ) { |
332 | // no indent before the comment, but the doc star has one space indent |
333 | return ' '; |
334 | } |
335 | return $phpcsFile->getTokensAsString( $firstLineToken, $stackPtr - $firstLineToken ) . ' '; |
336 | } |
337 | |
338 | /** |
339 | * @param File $phpcsFile |
340 | * @param int $stackPtr |
341 | * @return int |
342 | */ |
343 | private function getDocStarColumn( File $phpcsFile, int $stackPtr ): int { |
344 | $tokens = $phpcsFile->getTokens(); |
345 | // Handle special case /*****//** to look for the column of the first comment start |
346 | if ( $tokens[$stackPtr - 1]['code'] === T_DOC_COMMENT_CLOSE_TAG ) { |
347 | $stackPtr = $tokens[$stackPtr - 1]['comment_opener']; |
348 | } |
349 | // Calculate the column to align all doc stars. Use column of /**, add 1 to skip char / |
350 | return $tokens[$stackPtr]['column'] + 1; |
351 | } |
352 | } |