Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 188 |
|
0.00% |
0 / 4 |
CRAP | |
0.00% |
0 / 1 |
DocCommentSniff | |
0.00% |
0 / 188 |
|
0.00% |
0 / 4 |
3306 | |
0.00% |
0 / 1 |
register | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
process | |
0.00% |
0 / 179 |
|
0.00% |
0 / 1 |
2756 | |||
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]['length'] > 0 |
136 | ) { |
137 | $commentStarSpacing = $i + 1; |
138 | $expectedSpaces = 1; |
139 | // ignore * removed by SyntaxMultiDocStar and count spaces after that |
140 | $currentSpaces = strspn( |
141 | $tokens[$commentStarSpacing]['content'], " \t", $initialStarChars |
142 | ); |
143 | $error = null; |
144 | $code = null; |
145 | if ( $isMultiLineDoc && $currentSpaces < $expectedSpaces ) { |
146 | // be relax for multiline docs, because some line breaks in @param can |
147 | // have more than one space after a doc star |
148 | $error = 'Expected at least %s spaces after doc star; %s found'; |
149 | $code = 'SpacingDocStar'; |
150 | } elseif ( !$isMultiLineDoc && $currentSpaces !== $expectedSpaces ) { |
151 | $error = 'Expected %s spaces after doc star on single line; %s found'; |
152 | $code = 'SpacingDocStarSingleLine'; |
153 | } |
154 | if ( $error !== null && $code !== null ) { |
155 | $fix = $phpcsFile->addFixableError( |
156 | $error, |
157 | $commentStarSpacing, |
158 | $code, |
159 | [ $expectedSpaces, $currentSpaces ] |
160 | ); |
161 | if ( $fix ) { |
162 | if ( $currentSpaces > $expectedSpaces ) { |
163 | // Remove whitespace |
164 | $content = $tokens[$commentStarSpacing]['content']; |
165 | $phpcsFile->fixer->replaceToken( |
166 | $commentStarSpacing, |
167 | substr( $content, 0, $expectedSpaces - $currentSpaces ) |
168 | ); |
169 | } else { |
170 | // Add whitespace |
171 | $phpcsFile->fixer->addContent( |
172 | $i, str_repeat( ' ', $expectedSpaces ) |
173 | ); |
174 | } |
175 | } |
176 | } |
177 | } |
178 | |
179 | if ( !$isMultiLineDoc ) { |
180 | continue; |
181 | } |
182 | |
183 | // Ensure one whitespace before @param/@return |
184 | if ( $tokens[$i]['code'] === T_DOC_COMMENT_TAG && |
185 | $tokens[$i]['line'] === $tokens[$i - 1]['line'] |
186 | ) { |
187 | $commentTagSpacing = $i - 1; |
188 | $expectedSpaces = 1; |
189 | $currentSpaces = strspn( strrev( $tokens[$commentTagSpacing]['content'] ), ' ' ); |
190 | // Relax the check for a list of annotations for multi spaces before the annotation |
191 | if ( $currentSpaces > $expectedSpaces && |
192 | !in_array( $tokens[$i]['content'], self::ANNOTATIONS_IGNORE_MULTI_SPACE_BEFORE, true ) |
193 | ) { |
194 | $data = [ |
195 | $expectedSpaces, |
196 | $tokens[$i]['content'], |
197 | $currentSpaces, |
198 | ]; |
199 | $fix = $phpcsFile->addFixableError( |
200 | 'Expected %s spaces before %s; %s found', |
201 | $commentTagSpacing, |
202 | 'SpacingDocTag', |
203 | $data |
204 | ); |
205 | if ( $fix ) { |
206 | // Remove whitespace |
207 | $content = $tokens[$commentTagSpacing]['content']; |
208 | $phpcsFile->fixer->replaceToken( |
209 | $commentTagSpacing, |
210 | substr( $content, 0, $expectedSpaces - $currentSpaces ) |
211 | ); |
212 | } |
213 | } |
214 | |
215 | continue; |
216 | } |
217 | |
218 | // Ensure aligned * or */ for multiline comments |
219 | if ( ( $tokens[$i]['code'] === T_DOC_COMMENT_STAR || |
220 | $tokens[$i]['code'] === T_DOC_COMMENT_CLOSE_TAG ) && |
221 | $tokens[$i]['column'] !== $columnDocStar && |
222 | $tokens[$i]['line'] !== $prevLineDocStar |
223 | ) { |
224 | if ( $tokens[$i]['code'] === T_DOC_COMMENT_STAR ) { |
225 | $error = 'Comment star tag not aligned with open tag'; |
226 | $code = 'SyntaxAlignedDocStar'; |
227 | } else { |
228 | $error = 'Comment close tag not aligned with open tag'; |
229 | $code = 'SyntaxAlignedDocClose'; |
230 | } |
231 | $fix = $phpcsFile->addFixableError( $error, $i, $code ); |
232 | if ( $fix ) { |
233 | $tokenBefore = $i - 1; |
234 | $columnOff = $columnDocStar - $tokens[$i]['column']; |
235 | if ( $columnOff < 0 ) { |
236 | // Ensure to remove only whitespaces |
237 | if ( $tokens[$tokenBefore]['code'] === T_DOC_COMMENT_WHITESPACE ) { |
238 | $columnOff = max( $columnOff, $tokens[$tokenBefore]['length'] * -1 ); |
239 | // remove whitespaces |
240 | $phpcsFile->fixer->replaceToken( |
241 | $tokenBefore, |
242 | substr( $tokens[$tokenBefore]['content'], 0, $columnOff ) |
243 | ); |
244 | } |
245 | } elseif ( $tokens[$tokenBefore]['length'] !== 0 ) { |
246 | // Set correct indent |
247 | $phpcsFile->fixer->replaceToken( $tokenBefore, $indent ); |
248 | } else { |
249 | // Add correct indent |
250 | $phpcsFile->fixer->addContent( $tokenBefore, $indent ); |
251 | } |
252 | } |
253 | $prevLineDocStar = $tokens[$i]['line']; |
254 | |
255 | continue; |
256 | } |
257 | } |
258 | |
259 | // End token should exact */ |
260 | if ( $tokens[$commentEnd]['code'] === T_DOC_COMMENT_CLOSE_TAG && |
261 | $tokens[$commentEnd]['length'] > 0 && |
262 | $tokens[$commentEnd]['content'] !== '*/' |
263 | ) { |
264 | $error = 'Comment close tag must be \'*/\''; |
265 | $fix = $phpcsFile->addFixableError( $error, $commentEnd, 'SyntaxCloseTag' ); |
266 | if ( $fix ) { |
267 | $phpcsFile->fixer->replaceToken( $commentEnd, '*/' ); |
268 | } |
269 | } |
270 | |
271 | // For multi line comments the closing tag must have it own line |
272 | if ( $isMultiLineDoc ) { |
273 | $prev = $commentEnd - 1; |
274 | $prevNonWhitespace = $phpcsFile->findPrevious( |
275 | [ T_DOC_COMMENT_WHITESPACE ], $prev, null, true |
276 | ); |
277 | if ( $tokens[$prevNonWhitespace]['line'] === $tokens[$commentEnd]['line'] ) { |
278 | $error = 'Comment close tag should have own line'; |
279 | $fix = $phpcsFile->addFixableError( $error, $commentEnd, 'CloseTagOwnLine' ); |
280 | if ( $fix ) { |
281 | $phpcsFile->fixer->beginChangeset(); |
282 | $phpcsFile->fixer->addNewline( $prev ); |
283 | $phpcsFile->fixer->addContent( $prev, $indent ); |
284 | $phpcsFile->fixer->endChangeset(); |
285 | } |
286 | } |
287 | } elseif ( $tokens[$commentEnd]['length'] > 0 ) { |
288 | // Ensure a whitespace before the token |
289 | $commentCloseSpacing = $commentEnd - 1; |
290 | $expectedSpaces = 1; |
291 | $currentSpaces = strspn( strrev( $tokens[$commentCloseSpacing]['content'] ), ' ' ); |
292 | if ( $currentSpaces !== $expectedSpaces ) { |
293 | $data = [ |
294 | $expectedSpaces, |
295 | $currentSpaces, |
296 | ]; |
297 | $fix = $phpcsFile->addFixableError( |
298 | 'Expected %s spaces before close comment tag on single line; %s found', |
299 | $commentCloseSpacing, |
300 | 'SpacingSingleLineCloseTag', |
301 | $data |
302 | ); |
303 | if ( $fix ) { |
304 | if ( $currentSpaces > $expectedSpaces ) { |
305 | // Remove whitespace |
306 | $content = $tokens[$commentCloseSpacing]['content']; |
307 | $phpcsFile->fixer->replaceToken( |
308 | $commentCloseSpacing, substr( $content, 0, $expectedSpaces - $currentSpaces ) |
309 | ); |
310 | } else { |
311 | // Add whitespace |
312 | $phpcsFile->fixer->addContentBefore( |
313 | $commentEnd, str_repeat( ' ', $expectedSpaces ) |
314 | ); |
315 | } |
316 | } |
317 | } |
318 | } |
319 | } |
320 | |
321 | /** |
322 | * @param File $phpcsFile |
323 | * @param int $stackPtr |
324 | * @return string |
325 | */ |
326 | private function getCommentIndent( File $phpcsFile, int $stackPtr ): string { |
327 | $firstLineToken = $phpcsFile->findFirstOnLine( [ T_WHITESPACE ], $stackPtr ); |
328 | if ( $firstLineToken === false ) { |
329 | // no indent before the comment, but the doc star has one space indent |
330 | return ' '; |
331 | } |
332 | return $phpcsFile->getTokensAsString( $firstLineToken, $stackPtr - $firstLineToken ) . ' '; |
333 | } |
334 | |
335 | /** |
336 | * @param File $phpcsFile |
337 | * @param int $stackPtr |
338 | * @return int |
339 | */ |
340 | private function getDocStarColumn( File $phpcsFile, int $stackPtr ): int { |
341 | $tokens = $phpcsFile->getTokens(); |
342 | // Handle special case /*****//** to look for the column of the first comment start |
343 | if ( $tokens[$stackPtr - 1]['code'] === T_DOC_COMMENT_CLOSE_TAG ) { |
344 | $stackPtr = $tokens[$stackPtr - 1]['comment_opener']; |
345 | } |
346 | // Calculate the column to align all doc stars. Use column of /**, add 1 to skip char / |
347 | return $tokens[$stackPtr]['column'] + 1; |
348 | } |
349 | } |