Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 126
0.00% covered (danger)
0.00%
0 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
PropertyDocumentationSniff
0.00% covered (danger)
0.00%
0 / 126
0.00% covered (danger)
0.00%
0 / 3
1332
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 / 43
0.00% covered (danger)
0.00%
0 / 1
380
 processVar
0.00% covered (danger)
0.00%
0 / 82
0.00% covered (danger)
0.00%
0 / 1
272
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 * @license GPL-2.0-or-later
19 * @file
20 */
21
22namespace MediaWiki\Sniffs\Commenting;
23
24use PHP_CodeSniffer\Files\File;
25use PHP_CodeSniffer\Sniffs\Sniff;
26use PHP_CodeSniffer\Util\Tokens;
27
28class PropertyDocumentationSniff implements Sniff {
29
30    use DocumentationTypeTrait;
31
32    /**
33     * @inheritDoc
34     */
35    public function register(): array {
36        return [ T_VARIABLE ];
37    }
38
39    /**
40     * Processes this test, when one of its tokens is encountered.
41     *
42     * @param File $phpcsFile The file being scanned.
43     * @param int $stackPtr The position of the current token in the stack passed in $tokens.
44     *
45     * @return void
46     */
47    public function process( File $phpcsFile, $stackPtr ) {
48        $tokens = $phpcsFile->getTokens();
49
50        // Only for class properties
51        $scopes = array_keys( $tokens[$stackPtr]['conditions'] );
52        $scope = array_pop( $scopes );
53        if ( isset( $tokens[$stackPtr]['nested_parenthesis'] )
54            || $scope === null
55            || ( $tokens[$scope]['code'] !== T_CLASS && $tokens[$scope]['code'] !== T_TRAIT )
56        ) {
57            return;
58        }
59
60        $find = Tokens::$emptyTokens;
61        $find[] = T_STATIC;
62        $find[] = T_NULLABLE;
63        $find[] = T_STRING;
64        $visibilityPtr = $phpcsFile->findPrevious( $find, $stackPtr - 1, null, true );
65        if ( !$visibilityPtr || ( $tokens[$visibilityPtr]['code'] !== T_VAR &&
66            !isset( Tokens::$scopeModifiers[ $tokens[$visibilityPtr]['code'] ] ) )
67        ) {
68            return;
69        }
70        $commentEnd = $phpcsFile->findPrevious( [ T_WHITESPACE ], $visibilityPtr - 1, null, true );
71        if ( $tokens[$commentEnd]['code'] === T_COMMENT ) {
72            // Inline comments might just be closing comments for
73            // control structures or functions instead of function comments
74            // using the wrong comment type. If there is other code on the line,
75            // assume they relate to that code.
76            $prev = $phpcsFile->findPrevious( $find, $commentEnd - 1, null, true );
77            if ( $prev !== false && $tokens[$prev]['line'] === $tokens[$commentEnd]['line'] ) {
78                $commentEnd = $prev;
79            }
80        }
81        if ( $tokens[$commentEnd]['code'] !== T_DOC_COMMENT_CLOSE_TAG
82            && $tokens[$commentEnd]['code'] !== T_COMMENT
83        ) {
84            $memberProps = $phpcsFile->getMemberProperties( $stackPtr );
85            if ( $memberProps['type'] === '' ) {
86                $phpcsFile->addError(
87                    'Missing class property doc comment',
88                    $stackPtr,
89                    // Messages used: MissingDocumentationPublic, MissingDocumentationProtected,
90                    // MissingDocumentationPrivate
91                    'MissingDocumentation' . ucfirst( $memberProps['scope'] )
92                );
93            }
94            return;
95        }
96        if ( $tokens[$commentEnd]['code'] === T_COMMENT ) {
97            $phpcsFile->addError( 'You must use "/**" style comments for a class property comment',
98            $stackPtr, 'WrongStyle' );
99            return;
100        }
101        if ( $tokens[$commentEnd]['line'] !== $tokens[$visibilityPtr]['line'] - 1 ) {
102            $error = 'There must be no blank lines after the class property comment';
103            $phpcsFile->addError( $error, $commentEnd, 'SpacingAfter' );
104        }
105        $commentStart = $tokens[$commentEnd]['comment_opener'];
106        foreach ( $tokens[$commentStart]['comment_tags'] as $tag ) {
107            $tagText = $tokens[$tag]['content'];
108            if ( strcasecmp( $tagText, '@inheritDoc' ) === 0 || $tagText === '@deprecated' ) {
109                // No need to validate deprecated properties or those that inherit
110                // their documentation
111                return;
112            }
113        }
114
115        $this->processVar( $phpcsFile, $commentStart, $stackPtr );
116    }
117
118    /**
119     * Process the var doc comments.
120     *
121     * @param File $phpcsFile The file being scanned.
122     * @param int $commentStart The position in the stack where the comment started.
123     * @param int $stackPtr The position in the stack where the property itself started (T_VARIABLE)
124     */
125    private function processVar( File $phpcsFile, int $commentStart, int $stackPtr ): void {
126        $tokens = $phpcsFile->getTokens();
127        $var = null;
128        foreach ( $tokens[$commentStart]['comment_tags'] as $ptr ) {
129            $tag = $tokens[$ptr]['content'];
130            if ( $tag !== '@var' ) {
131                continue;
132            }
133            if ( $var ) {
134                $error = 'Only 1 @var tag is allowed in a class property comment';
135                $phpcsFile->addError( $error, $ptr, 'DuplicateVar' );
136                return;
137            }
138            $var = $ptr;
139        }
140        if ( $var !== null ) {
141            $varTypeSpacing = $var + 1;
142            // Check spaces before var
143            if ( $tokens[$varTypeSpacing]['code'] === T_DOC_COMMENT_WHITESPACE ) {
144                $expectedSpaces = 1;
145                $currentSpaces = strlen( $tokens[$varTypeSpacing]['content'] );
146                if ( $currentSpaces !== $expectedSpaces ) {
147                    $fix = $phpcsFile->addFixableWarning(
148                        'Expected %s spaces before var type; %s found',
149                        $varTypeSpacing,
150                        'SpacingBeforeVarType',
151                        [ $expectedSpaces, $currentSpaces ]
152                    );
153                    if ( $fix ) {
154                        $phpcsFile->fixer->replaceToken( $varTypeSpacing, ' ' );
155                    }
156                }
157            }
158            $varType = $var + 2;
159            $content = '';
160            if ( $tokens[$varType]['code'] === T_DOC_COMMENT_STRING ) {
161                $content = $tokens[$varType]['content'];
162            }
163            if ( $content === '' ) {
164                $error = 'Var type missing for @var tag in class property comment';
165                $phpcsFile->addError( $error, $var, 'MissingVarType' );
166                return;
167            }
168            [ $type, $separatorLength, $comment ] = $this->splitTypeAndComment( $content );
169            $fixType = false;
170            // Check for unneeded punctuation
171            $type = $this->fixTrailingPunctuation(
172                $phpcsFile,
173                $varType,
174                $type,
175                $fixType,
176                'var type'
177            );
178            $type = $this->fixWrappedParenthesis(
179                $phpcsFile,
180                $varType,
181                $type,
182                $fixType,
183                'var type'
184            );
185            // Check the type for short types
186            $type = $this->fixShortTypes( $phpcsFile, $varType, $type, $fixType, 'var' );
187            $this->maybeAddObjectTypehintError(
188                $phpcsFile,
189                $varType,
190                $type,
191                'var'
192            );
193            $this->maybeAddTypeTypehintError(
194                $phpcsFile,
195                $varType,
196                $type,
197                'var'
198            );
199            // Check spacing after type
200            if ( $comment !== '' ) {
201                $expectedSpaces = 1;
202                if ( $separatorLength !== $expectedSpaces ) {
203                    $fix = $phpcsFile->addFixableWarning(
204                        'Expected %s spaces after var type; %s found',
205                        $varType,
206                        'SpacingAfterVarType',
207                        [ $expectedSpaces, $separatorLength ]
208                    );
209                    if ( $fix ) {
210                        $fixType = true;
211                        $separatorLength = $expectedSpaces;
212                    }
213                }
214            }
215            if ( $fixType ) {
216                $phpcsFile->fixer->replaceToken(
217                    $varType,
218                    $type . ( $comment !== '' ? str_repeat( ' ', $separatorLength ) . $comment : '' )
219                );
220            }
221        } elseif ( $phpcsFile->getMemberProperties( $stackPtr )['type'] === '' ) {
222            $error = 'Missing type or @var tag in class property comment';
223            $phpcsFile->addError( $error, $tokens[$commentStart]['comment_closer'], 'MissingVar' );
224        }
225    }
226
227}