Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 138 |
|
0.00% |
0 / 9 |
CRAP | |
0.00% |
0 / 1 |
UnusedUseStatementSniff | |
0.00% |
0 / 138 |
|
0.00% |
0 / 9 |
4032 | |
0.00% |
0 / 1 |
register | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
process | |
0.00% |
0 / 59 |
|
0.00% |
0 / 1 |
702 | |||
extractType | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
72 | |||
findUseStatements | |
0.00% |
0 / 25 |
|
0.00% |
0 / 1 |
56 | |||
findNamespace | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
readNamespace | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
90 | |||
addSameNamespaceWarning | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
6 | |||
markAsUsed | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
removeUseStatement | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
42 |
1 | <?php |
2 | /** |
3 | * Originally from Drupal's coding standard <https://github.com/klausi/coder> |
4 | * |
5 | * This program is free software; you can redistribute it and/or modify |
6 | * it under the terms of the GNU General Public License as published by |
7 | * the Free Software Foundation; either version 2 of the License, or |
8 | * (at your option) any later version. |
9 | * |
10 | * This program is distributed in the hope that it will be useful, |
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
13 | * GNU General Public License for more details. |
14 | * |
15 | * You should have received a copy of the GNU General Public License along |
16 | * with this program; if not, write to the Free Software Foundation, Inc., |
17 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
18 | * http://www.gnu.org/copyleft/gpl.html |
19 | * |
20 | * @file |
21 | */ |
22 | |
23 | namespace MediaWiki\Sniffs\Classes; |
24 | |
25 | use PHP_CodeSniffer\Files\File; |
26 | use PHP_CodeSniffer\Sniffs\Sniff; |
27 | use PHP_CodeSniffer\Util\Tokens; |
28 | |
29 | /** |
30 | * @author Thiemo Kreuz |
31 | */ |
32 | class UnusedUseStatementSniff implements Sniff { |
33 | |
34 | /** |
35 | * Doc tags where a class name is used |
36 | */ |
37 | private const CLASS_TAGS = [ |
38 | '@param' => null, |
39 | '@property' => null, |
40 | '@property-read' => null, |
41 | '@property-write' => null, |
42 | '@return' => null, |
43 | '@see' => null, |
44 | '@throws' => null, |
45 | '@var' => null, |
46 | // phan |
47 | '@phan-param' => null, |
48 | '@phan-property' => null, |
49 | '@phan-return' => null, |
50 | '@phan-var' => null, |
51 | // Deprecated |
52 | '@expectedException' => null, |
53 | '@method' => null, |
54 | '@phan-method' => null, |
55 | '@type' => null, |
56 | ]; |
57 | |
58 | /** |
59 | * @inheritDoc |
60 | */ |
61 | public function register(): array { |
62 | return [ T_USE ]; |
63 | } |
64 | |
65 | /** |
66 | * Processes this test, when one of its tokens is encountered. |
67 | * |
68 | * @param File $phpcsFile The file being scanned. |
69 | * @param int $stackPtr The position of the current token in the stack passed in $tokens. |
70 | * |
71 | * @return int|void |
72 | */ |
73 | public function process( File $phpcsFile, $stackPtr ) { |
74 | $tokens = $phpcsFile->getTokens(); |
75 | |
76 | // In case this is a `use` of a class (or constant or function) within |
77 | // a bracketed namespace rather than in the global scope, update the end |
78 | // accordingly |
79 | $useScopeEnd = $phpcsFile->numTokens; |
80 | |
81 | if ( !empty( $tokens[$stackPtr]['conditions'] ) ) { |
82 | // We only care about use statements in the global scope, or the |
83 | // equivalent for bracketed namespace (use statements in the namespace |
84 | // and not in any class, etc.) |
85 | $scope = array_key_first( $tokens[$stackPtr]['conditions'] ); |
86 | if ( count( $tokens[$stackPtr]['conditions'] ) === 1 |
87 | // @phan-suppress-next-line PhanTypeArraySuspiciousNullable False positive |
88 | && $tokens[$stackPtr]['conditions'][$scope] === T_NAMESPACE |
89 | ) { |
90 | $useScopeEnd = $tokens[$scope]['scope_closer']; |
91 | } else { |
92 | return $tokens[$scope]['scope_closer'] ?? $stackPtr; |
93 | } |
94 | } |
95 | |
96 | $afterUseSection = $stackPtr; |
97 | $shortClassNames = $this->findUseStatements( $phpcsFile, $stackPtr, $afterUseSection ); |
98 | if ( !$shortClassNames ) { |
99 | return; |
100 | } |
101 | |
102 | $classNamesPattern = '{(?<!\\\\)\b(' |
103 | . implode( '|', array_map( 'preg_quote', array_keys( $shortClassNames ) ) ) |
104 | . ')\b}i'; |
105 | |
106 | // Search where the class name is used. PHP treats class names case |
107 | // insensitive, that's why we cannot search for the exact class name string |
108 | // and need to iterate over all T_STRING tokens in the file. |
109 | for ( $i = $afterUseSection; $i < $useScopeEnd; $i++ ) { |
110 | if ( $tokens[$i]['code'] === T_STRING ) { |
111 | if ( !isset( $shortClassNames[ strtolower( $tokens[$i]['content'] ) ] ) ) { |
112 | continue; |
113 | } |
114 | |
115 | // If a backslash is used before the class name then this is some other |
116 | // use statement. |
117 | // T_STRING also used for $this->property or self::function() or "function namedFuncton()" |
118 | $before = $phpcsFile->findPrevious( Tokens::$emptyTokens, $i - 1, null, true ); |
119 | if ( $tokens[$before]['code'] === T_OBJECT_OPERATOR |
120 | || $tokens[$before]['code'] === T_NULLSAFE_OBJECT_OPERATOR |
121 | || $tokens[$before]['code'] === T_DOUBLE_COLON |
122 | || $tokens[$before]['code'] === T_NS_SEPARATOR |
123 | || $tokens[$before]['code'] === T_FUNCTION |
124 | // Trait use statement within a class. |
125 | || ( $tokens[$before]['code'] === T_USE |
126 | && empty( $tokens[$before]['conditions'] ) |
127 | ) |
128 | ) { |
129 | continue; |
130 | } |
131 | |
132 | $className = $tokens[$i]['content']; |
133 | |
134 | } elseif ( $tokens[$i]['code'] === T_DOC_COMMENT_TAG ) { |
135 | // Usage in a doc comment |
136 | if ( !array_key_exists( $tokens[$i]['content'], self::CLASS_TAGS ) |
137 | || $tokens[$i + 2]['code'] !== T_DOC_COMMENT_STRING |
138 | ) { |
139 | continue; |
140 | } |
141 | $docType = $this->extractType( $tokens[$i + 2]['content'] ); |
142 | if ( !preg_match_all( $classNamesPattern, $docType, $matches ) ) { |
143 | continue; |
144 | } |
145 | $className = $matches[1]; |
146 | |
147 | } elseif ( $tokens[$i]['code'] === T_CONSTANT_ENCAPSED_STRING ) { |
148 | if ( $tokens[$i + 1]['code'] !== T_SEMICOLON |
149 | || !preg_match( '/^.@phan-var\S*\s+(.*)/i', $tokens[$i]['content'], $matches ) |
150 | ) { |
151 | continue; |
152 | } |
153 | |
154 | $phanVarType = $this->extractType( $matches[1] ); |
155 | if ( !preg_match_all( $classNamesPattern, $phanVarType, $matches ) ) { |
156 | continue; |
157 | } |
158 | $className = $matches[1]; |
159 | |
160 | } else { |
161 | continue; |
162 | } |
163 | |
164 | $this->markAsUsed( $shortClassNames, $className ); |
165 | if ( $shortClassNames === [] ) { |
166 | break; |
167 | } |
168 | } |
169 | |
170 | foreach ( $shortClassNames as [ $i, $shortClassName ] ) { |
171 | $fix = $phpcsFile->addFixableWarning( |
172 | 'Unused use statement "%s"', |
173 | $i, |
174 | 'UnusedUse', |
175 | [ $shortClassName ] |
176 | ); |
177 | if ( $fix ) { |
178 | $this->removeUseStatement( $phpcsFile, $i ); |
179 | } |
180 | } |
181 | |
182 | return $afterUseSection; |
183 | } |
184 | |
185 | /** |
186 | * Extracts the type from PHPDoc comment strings like "bool[] $var Comment" and |
187 | * "$var bool[] Comment" (wrong order, but that's for another sniff), while respecting types |
188 | * like "array<int, array<string, bool>>". |
189 | * |
190 | * @param string $str |
191 | * |
192 | * @return string |
193 | */ |
194 | private function extractType( string $str ): string { |
195 | $start = 0; |
196 | $brackets = 0; |
197 | $strLen = strlen( $str ); |
198 | for ( $i = 0; $i < $strLen; $i += strcspn( $str, ' <>', $i + 1 ) + 1 ) { |
199 | $char = $str[$i]; |
200 | if ( $char === ' ' && !$brackets ) { |
201 | // If we find the variable name before the type, continue |
202 | if ( $str[$start] !== '$' ) { |
203 | return substr( $str, $start, $i ); |
204 | } |
205 | $start = $i + 1; |
206 | } elseif ( $char === '>' && $brackets ) { |
207 | $brackets--; |
208 | } elseif ( $char === '<' ) { |
209 | $brackets++; |
210 | } |
211 | } |
212 | return substr( $str, $start ); |
213 | } |
214 | |
215 | /** |
216 | * @param File $phpcsFile |
217 | * @param int $stackPtr |
218 | * @param int &$afterUseSection Updated to point to the first token after the found section |
219 | * |
220 | * @return int[] Array mapping short, lowercased class names to stack pointers |
221 | */ |
222 | private function findUseStatements( |
223 | File $phpcsFile, |
224 | int $stackPtr, |
225 | int &$afterUseSection |
226 | ): array { |
227 | $tokens = $phpcsFile->getTokens(); |
228 | $currentUsePtr = $stackPtr; |
229 | |
230 | $namespace = $this->findNamespace( $phpcsFile, $stackPtr ); |
231 | $shortClassNames = []; |
232 | |
233 | // No need to cache this as we won't execute this often |
234 | $namespaceTokenTypes = Tokens::$emptyTokens; |
235 | $namespaceTokenTypes[] = T_NS_SEPARATOR; |
236 | $namespaceTokenTypes[] = T_STRING; |
237 | $useTokenTypes = array_merge( $namespaceTokenTypes, [ T_AS ] ); |
238 | |
239 | while ( $currentUsePtr && $tokens[$currentUsePtr]['code'] === T_USE ) { |
240 | // Seek to the end of the statement and get the string before the semicolon. |
241 | $semicolon = $phpcsFile->findNext( $useTokenTypes, $currentUsePtr + 1, null, true ); |
242 | if ( $tokens[$semicolon]['code'] !== T_SEMICOLON ) { |
243 | break; |
244 | } |
245 | $afterUseSection = $semicolon + 1; |
246 | |
247 | // Find the unprefixed class name or "as" alias, if there is one |
248 | $classNamePtr = $phpcsFile->findPrevious( T_STRING, $semicolon - 1, $currentUsePtr ); |
249 | if ( !$classNamePtr ) { |
250 | // Live coding |
251 | break; |
252 | } |
253 | $shortClassName = $tokens[$classNamePtr]['content']; |
254 | $shortClassNames[strtolower( $shortClassName )] = [ $currentUsePtr, $shortClassName ]; |
255 | |
256 | // Check if the referenced class is in the same namespace as the current |
257 | // file. If it is then the use statement is not necessary. |
258 | $prev = $phpcsFile->findPrevious( $namespaceTokenTypes, $classNamePtr - 1, null, true ); |
259 | // Check if the use statement does aliasing with the "as" keyword. Aliasing |
260 | // is allowed even in the same namespace. |
261 | if ( $tokens[$prev]['code'] !== T_AS ) { |
262 | $useNamespace = $this->readNamespace( $phpcsFile, $prev + 1, $classNamePtr - 2 ); |
263 | if ( $useNamespace === $namespace ) { |
264 | $this->addSameNamespaceWarning( $phpcsFile, $currentUsePtr, $shortClassName ); |
265 | } |
266 | } |
267 | |
268 | // This intentionally stops at non-empty tokens for performance reasons, and might miss |
269 | // later use statements. The sniff will be called another time for these. |
270 | $currentUsePtr = $phpcsFile->findNext( Tokens::$emptyTokens, $semicolon + 1, null, true ); |
271 | } |
272 | |
273 | return $shortClassNames; |
274 | } |
275 | |
276 | /** |
277 | * @param File $phpcsFile |
278 | * @param int $stackPtr |
279 | * |
280 | * @return string |
281 | */ |
282 | private function findNamespace( File $phpcsFile, int $stackPtr ): string { |
283 | $namespacePtr = $phpcsFile->findPrevious( T_NAMESPACE, $stackPtr - 1 ); |
284 | if ( !$namespacePtr ) { |
285 | return ''; |
286 | } |
287 | |
288 | return $this->readNamespace( $phpcsFile, $namespacePtr + 2, $stackPtr - 1 ); |
289 | } |
290 | |
291 | /** |
292 | * @param File $phpcsFile |
293 | * @param int $start |
294 | * @param int $end |
295 | * |
296 | * @return string |
297 | */ |
298 | private function readNamespace( File $phpcsFile, int $start, int $end ): string { |
299 | $tokens = $phpcsFile->getTokens(); |
300 | $content = ''; |
301 | |
302 | for ( $i = $start; $i <= $end; $i++ ) { |
303 | if ( isset( Tokens::$emptyTokens[ $tokens[$i]['code'] ] ) ) { |
304 | continue; |
305 | } |
306 | if ( $tokens[$i]['code'] !== T_STRING && $tokens[$i]['code'] !== T_NS_SEPARATOR ) { |
307 | break; |
308 | } |
309 | |
310 | // This skips leading separators as well as a preceding "const" or "function" |
311 | if ( $content || ( $tokens[$i]['code'] === T_STRING && ( |
312 | strcasecmp( $tokens[$i]['content'], 'const' ) !== 0 && |
313 | strcasecmp( $tokens[$i]['content'], 'function' ) !== 0 |
314 | ) ) ) { |
315 | $content .= $tokens[$i]['content']; |
316 | } |
317 | } |
318 | |
319 | // Something like "Namespace\ Class" might leave a trailing separator |
320 | return rtrim( $content, '\\' ); |
321 | } |
322 | |
323 | /** |
324 | * @param File $phpcsFile |
325 | * @param int $stackPtr |
326 | * @param string $shortClassName |
327 | */ |
328 | private function addSameNamespaceWarning( File $phpcsFile, int $stackPtr, string $shortClassName ): void { |
329 | $fix = $phpcsFile->addFixableWarning( |
330 | 'Unnecessary use statement "%s" in the same namespace', |
331 | $stackPtr, |
332 | 'UnnecessaryUse', |
333 | [ $shortClassName ] |
334 | ); |
335 | if ( $fix ) { |
336 | $this->removeUseStatement( $phpcsFile, $stackPtr ); |
337 | } |
338 | } |
339 | |
340 | /** |
341 | * @param array &$classNames List of class names found in the use section |
342 | * @param string|string[] $usedClassNames Class name(s) to be marked as used |
343 | */ |
344 | private function markAsUsed( array &$classNames, $usedClassNames ): void { |
345 | foreach ( (array)$usedClassNames as $className ) { |
346 | unset( $classNames[ strtolower( $className ) ] ); |
347 | } |
348 | } |
349 | |
350 | /** |
351 | * @param File $phpcsFile |
352 | * @param int $stackPtr |
353 | */ |
354 | private function removeUseStatement( File $phpcsFile, int $stackPtr ): void { |
355 | $tokens = $phpcsFile->getTokens(); |
356 | // Remove the whole use statement line. |
357 | $phpcsFile->fixer->beginChangeset(); |
358 | |
359 | // Removing any whitespace before the use statement, for use statements in bracketed |
360 | // namespaces |
361 | $i = $phpcsFile->findFirstOnLine( [ T_WHITESPACE ], $stackPtr ); |
362 | if ( !$i ) { |
363 | // No whitespace beforehand |
364 | $i = $stackPtr; |
365 | } |
366 | do { |
367 | $phpcsFile->fixer->replaceToken( $i, '' ); |
368 | } while ( $tokens[$i++]['code'] !== T_SEMICOLON && isset( $tokens[$i] ) ); |
369 | |
370 | // Also remove whitespace after the semicolon (new lines). |
371 | while ( isset( $tokens[$i] ) |
372 | && $tokens[$i]['code'] === T_WHITESPACE |
373 | && $tokens[$i]['line'] === $tokens[$i - 1]['line'] |
374 | ) { |
375 | $phpcsFile->fixer->replaceToken( $i, '' ); |
376 | $i++; |
377 | } |
378 | |
379 | $phpcsFile->fixer->endChangeset(); |
380 | } |
381 | |
382 | } |