Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 53 |
|
0.00% |
0 / 4 |
CRAP | |
0.00% |
0 / 1 |
FunctionAnnotationsSniff | |
0.00% |
0 / 53 |
|
0.00% |
0 / 4 |
342 | |
0.00% |
0 / 1 |
register | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
process | |
0.00% |
0 / 24 |
|
0.00% |
0 / 1 |
72 | |||
normalizeAnnotation | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
20 | |||
handleAccessAnnotation | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
30 |
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 | use PHP_CodeSniffer\Util\Tokens; |
26 | |
27 | class FunctionAnnotationsSniff implements Sniff { |
28 | /** |
29 | * Annotations allowed for functions. This includes bad annotations that we check for |
30 | * elsewhere. |
31 | */ |
32 | private const ALLOWED_ANNOTATIONS = [ |
33 | // Allowed all-lowercase tags |
34 | '@after' => true, |
35 | '@author' => true, |
36 | '@before' => true, |
37 | '@code' => true, |
38 | '@cover' => true, |
39 | '@covers' => true, |
40 | '@depends' => true, |
41 | '@deprecated' => true, |
42 | '@endcode' => true, |
43 | '@fixme' => true, |
44 | '@group' => true, |
45 | '@internal' => true, |
46 | '@license' => true, |
47 | '@link' => true, |
48 | '@note' => true, |
49 | '@par' => true, |
50 | '@param' => true, |
51 | '@requires' => true, |
52 | '@return' => true, |
53 | '@see' => true, |
54 | '@since' => true, |
55 | '@throws' => true, |
56 | '@todo' => true, |
57 | '@uses' => true, |
58 | '@warning' => true, |
59 | |
60 | // Automatically replaced |
61 | '@param[in]' => '@param', |
62 | '@param[in,out]' => '@param', |
63 | '@param[out]' => '@param', |
64 | '@params' => '@param', |
65 | '@returns' => '@return', |
66 | '@throw' => '@throws', |
67 | '@exception' => '@throws', |
68 | |
69 | // private and protected is needed when functions stay public |
70 | // for deprecation or backward compatibility reasons |
71 | // @see https://www.mediawiki.org/wiki/Deprecation_policy#Scope |
72 | '@private' => true, |
73 | '@protected' => true, |
74 | |
75 | // Special handling |
76 | '@access' => true, |
77 | |
78 | // Stable interface policy tags |
79 | // @see https://www.mediawiki.org/wiki/Stable_interface_policy |
80 | '@newable' => true, |
81 | '@stable' => true, |
82 | '@unstable' => true, |
83 | |
84 | // phan |
85 | '@phan-param' => true, |
86 | '@phan-return' => true, |
87 | '@phan-suppress-next-line' => true, |
88 | '@phan-var' => true, |
89 | '@phan-assert' => true, |
90 | '@phan-assert-true-condition' => true, |
91 | '@phan-assert-false-condition' => true, |
92 | '@phan-side-effect-free' => true, |
93 | '@suppress' => true, |
94 | '@phan-template' => true, |
95 | '@phan-type' => true, |
96 | // No other consumers for now. |
97 | '@template' => '@phan-template', |
98 | |
99 | // pseudo-tags from phan-taint-check-plugin |
100 | '@param-taint' => true, |
101 | '@return-taint' => true, |
102 | |
103 | // psalm |
104 | '@psalm-template' => true, |
105 | '@psalm-param' => true, |
106 | '@psalm-return' => true, |
107 | |
108 | // T263390 |
109 | '@noinspection' => true, |
110 | |
111 | // phpunit tags that are mixed-case - map lowercase to preferred mixed-case |
112 | // phpunit tags that are already all-lowercase, like @after and @before |
113 | // are listed above |
114 | '@afterclass' => '@afterClass', |
115 | '@beforeclass' => '@beforeClass', |
116 | '@codecoverageignore' => '@codeCoverageIgnore', |
117 | '@covernothing' => '@coverNothing', |
118 | '@coversnothing' => '@coversNothing', |
119 | '@dataprovider' => '@dataProvider', |
120 | '@doesnotperformassertions' => '@doesNotPerformAssertions', |
121 | '@testwith' => '@testWith', |
122 | |
123 | // Other phpunit annotations that we recognize, even if PhpunitAnnotationsSniff |
124 | // complains about them. See T276971 |
125 | '@small' => true, |
126 | '@medium' => true, |
127 | '@large' => true, |
128 | '@test' => true, |
129 | '@testdox' => true, |
130 | '@backupglobals' => '@backupGlobals', |
131 | '@backupstaticattributes' => '@backupStaticAttributes', |
132 | '@runinseparateprocess' => '@runInSeparateProcess', |
133 | '@expectedexception' => '@expectedException', |
134 | '@expectedexceptioncode' => '@expectedExceptionCode', |
135 | '@expectedexceptionmessage' => '@expectedExceptionMessage', |
136 | '@expectedexceptionmessageregexp' => '@expectedExceptionMessageRegExp', |
137 | |
138 | '@inheritdoc' => '@inheritDoc', |
139 | |
140 | // Tags to automatically fix |
141 | '@deprecate' => '@deprecated', |
142 | '@gropu' => '@group', |
143 | '@parma' => '@param', |
144 | '@warn' => '@warning', |
145 | ]; |
146 | |
147 | /** |
148 | * @inheritDoc |
149 | */ |
150 | public function register(): array { |
151 | return [ T_FUNCTION ]; |
152 | } |
153 | |
154 | /** |
155 | * Processes this test, when one of its tokens is encountered. |
156 | * |
157 | * @param File $phpcsFile The file being scanned. |
158 | * @param int $stackPtr The position of the current token in the stack passed in $tokens. |
159 | * |
160 | * @return void |
161 | */ |
162 | public function process( File $phpcsFile, $stackPtr ) { |
163 | $tokens = $phpcsFile->getTokens(); |
164 | |
165 | $tokensToSkip = array_merge( Tokens::$emptyTokens, Tokens::$methodPrefixes ); |
166 | unset( $tokensToSkip[T_DOC_COMMENT_CLOSE_TAG] ); |
167 | |
168 | $commentEnd = $phpcsFile->findPrevious( $tokensToSkip, $stackPtr - 1, null, true ); |
169 | if ( !$commentEnd || $tokens[$commentEnd]['code'] !== T_DOC_COMMENT_CLOSE_TAG ) { |
170 | return; |
171 | } |
172 | |
173 | $commentStart = $tokens[$commentEnd]['comment_opener']; |
174 | |
175 | foreach ( $tokens[$commentStart]['comment_tags'] as $tag ) { |
176 | $tagContent = $tokens[$tag]['content']; |
177 | $annotation = $this->normalizeAnnotation( $tagContent ); |
178 | if ( $annotation === false ) { |
179 | $error = '%s is not a valid function annotation'; |
180 | $phpcsFile->addError( $error, $tag, 'UnrecognizedAnnotation', [ $tagContent ] ); |
181 | } elseif ( $annotation === '@access' ) { |
182 | $this->handleAccessAnnotation( $phpcsFile, $tokens, $tag, $tagContent ); |
183 | } elseif ( $tagContent !== $annotation ) { |
184 | $fix = $phpcsFile->addFixableWarning( |
185 | 'Use %s annotation instead of %s', |
186 | $tag, |
187 | 'NonNormalizedAnnotation', |
188 | [ $annotation, $tagContent ] |
189 | ); |
190 | if ( $fix ) { |
191 | $phpcsFile->fixer->replaceToken( $tag, $annotation ); |
192 | } |
193 | } |
194 | } |
195 | } |
196 | |
197 | /** |
198 | * Normalizes an annotation |
199 | * |
200 | * @param string $anno |
201 | * @return string|false Tag or false if it's not canonical |
202 | */ |
203 | private function normalizeAnnotation( string $anno ) { |
204 | $anno = rtrim( $anno, ':' ); |
205 | $lower = mb_strtolower( $anno ); |
206 | if ( array_key_exists( $lower, self::ALLOWED_ANNOTATIONS ) ) { |
207 | return is_string( self::ALLOWED_ANNOTATIONS[$lower] ) |
208 | ? self::ALLOWED_ANNOTATIONS[$lower] |
209 | : $lower; |
210 | } |
211 | |
212 | if ( preg_match( '/^@code{\W?([a-z]+)}$/', $lower, $matches ) ) { |
213 | return '@code{.' . $matches[1] . '}'; |
214 | } |
215 | |
216 | return false; |
217 | } |
218 | |
219 | /** |
220 | * @param File $phpcsFile |
221 | * @param array[] $tokens |
222 | * @param int $tag Token position of the annotation tag |
223 | * @param string $tagContent Content of the annotation |
224 | */ |
225 | private function handleAccessAnnotation( File $phpcsFile, array $tokens, int $tag, string $tagContent ): void { |
226 | if ( $tokens[$tag + 2]['code'] === T_DOC_COMMENT_STRING ) { |
227 | $text = strtolower( $tokens[$tag + 2]['content'] ); |
228 | if ( $text === 'protected' || $text === 'private' ) { |
229 | $replacement = '@' . $text; |
230 | $fix = $phpcsFile->addFixableWarning( |
231 | 'Use %s annotation instead of "%s"', |
232 | $tag, |
233 | 'AccessAnnotationReplacement', |
234 | [ $replacement, $phpcsFile->getTokensAsString( $tag, 3 ) ] |
235 | ); |
236 | if ( $fix ) { |
237 | $phpcsFile->fixer->beginChangeset(); |
238 | $phpcsFile->fixer->replaceToken( $tag, $replacement ); |
239 | $phpcsFile->fixer->replaceToken( $tag + 1, '' ); |
240 | $phpcsFile->fixer->replaceToken( $tag + 2, '' ); |
241 | $phpcsFile->fixer->endChangeset(); |
242 | } |
243 | return; |
244 | } |
245 | } |
246 | $error = '%s is not a valid function annotation'; |
247 | $phpcsFile->addError( $error, $tag, 'AccessAnnotationInvalid', [ $tagContent ] ); |
248 | } |
249 | } |