Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 190
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 / 190
0.00% covered (danger)
0.00%
0 / 4
3540
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 / 181
0.00% covered (danger)
0.00%
0 / 1
2970
 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]['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}