Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 188
0.00% covered (danger)
0.00%
0 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
DocCommentSniff
0.00% covered (danger)
0.00%
0 / 188
0.00% covered (danger)
0.00%
0 / 4
3306
0.00% covered (danger)
0.00%
0 / 1
 register
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 process
0.00% covered (danger)
0.00%
0 / 179
0.00% covered (danger)
0.00%
0 / 1
2756
 getCommentIndent
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getDocStarColumn
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
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
21namespace MediaWiki\Sniffs\Commenting;
22
23use PHP_CodeSniffer\Files\File;
24use PHP_CodeSniffer\Sniffs\Sniff;
25
26class 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}