Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 174 |
|
0.00% |
0 / 5 |
CRAP | |
0.00% |
0 / 1 |
MockBoilerplateSniff | |
0.00% |
0 / 174 |
|
0.00% |
0 / 5 |
2652 | |
0.00% |
0 / 1 |
register | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
process | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
72 | |||
handleExactly | |
0.00% |
0 / 34 |
|
0.00% |
0 / 1 |
90 | |||
handleWill | |
0.00% |
0 / 42 |
|
0.00% |
0 / 1 |
240 | |||
handleWith | |
0.00% |
0 / 80 |
|
0.00% |
0 / 1 |
342 |
1 | <?php |
2 | |
3 | /** |
4 | * This program is free software; you can redistribute it and/or modify |
5 | * it under the terms of the GNU General Public License as published by |
6 | * the Free Software Foundation; either version 2 of the License, or |
7 | * (at your option) any later version. |
8 | * |
9 | * This program is distributed in the hope that it will be useful, |
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
12 | * GNU General Public License for more details. |
13 | * |
14 | * You should have received a copy of the GNU General Public License along |
15 | * with this program; if not, write to the Free Software Foundation, Inc., |
16 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
17 | * http://www.gnu.org/copyleft/gpl.html |
18 | * |
19 | * @file |
20 | */ |
21 | |
22 | namespace MediaWiki\Sniffs\PHPUnit; |
23 | |
24 | use PHP_CodeSniffer\Files\File; |
25 | use PHP_CodeSniffer\Sniffs\Sniff; |
26 | |
27 | /** |
28 | * Simplify set up of mocks in PHPUnit test cases: |
29 | * ->will( $this->returnValue( ... ) ) becomes ->willReturn( ... ) |
30 | * as well as other ->will() shortcuts, see PHPUnit docs table 8.1 |
31 | * ->with( $this->equalTo( ... ) ) becomes ->with( ... ), for any number of parameters provided, |
32 | * since equalTo() is the default constraint checked if a value is provided (as long as the |
33 | * equalTo() call only had a single parameter) |
34 | * ->exactly( 1 ) becomes ->once() |
35 | * ->exactly( 0 ) becomes ->never() |
36 | * |
37 | * Potential future improvements include |
38 | * - replace unneeded $this->any() calls, i.e. |
39 | * ->expects( $this->any() )->method( ... ) becomes ->method( ... ) |
40 | * |
41 | * - apply the with() replacements to withConsecutive() as well |
42 | * |
43 | * @author DannyS712 |
44 | */ |
45 | class MockBoilerplateSniff implements Sniff { |
46 | use PHPUnitTestTrait; |
47 | |
48 | /** @var array */ |
49 | private const RELEVANT_METHODS = [ |
50 | 'exactly' => 'exactly', |
51 | 'will' => 'will', |
52 | 'with' => 'with', |
53 | ]; |
54 | |
55 | /** @var array */ |
56 | private const WILL_REPLACEMENTS = [ |
57 | 'returnValue' => 'willReturn', |
58 | 'returnArgument' => 'willReturnArgument', |
59 | 'returnCallback' => 'willReturnCallback', |
60 | 'returnValueMap' => 'willReturnMap', |
61 | 'onConsecutiveCalls' => 'willReturnOnConsecutiveCalls', |
62 | 'returnSelf' => 'willReturnSelf', |
63 | 'throwException' => 'willThrowException', |
64 | ]; |
65 | |
66 | /** |
67 | * @inheritDoc |
68 | */ |
69 | public function register(): array { |
70 | return [ T_STRING ]; |
71 | } |
72 | |
73 | /** |
74 | * @param File $phpcsFile |
75 | * @param int $stackPtr |
76 | * |
77 | * @return void|int |
78 | */ |
79 | public function process( File $phpcsFile, $stackPtr ) { |
80 | if ( !$this->isTestFile( $phpcsFile, $stackPtr ) ) { |
81 | return $phpcsFile->numTokens; |
82 | } |
83 | |
84 | $tokens = $phpcsFile->getTokens(); |
85 | if ( $tokens[$stackPtr]['level'] < 2 ) { |
86 | // Needs to be in a method in a class |
87 | return; |
88 | } |
89 | |
90 | $methodName = $tokens[$stackPtr]['content']; |
91 | if ( !isset( self::RELEVANT_METHODS[ $methodName ] ) ) { |
92 | // Not a method we care about |
93 | return; |
94 | } |
95 | |
96 | $methodOpener = $phpcsFile->findNext( T_WHITESPACE, $stackPtr + 1, null, true ); |
97 | if ( !isset( $tokens[$methodOpener]['parenthesis_closer'] ) ) { |
98 | // Needs to be a method call |
99 | return $methodOpener + 1; |
100 | } |
101 | |
102 | switch ( $methodName ) { |
103 | case 'exactly': |
104 | return $this->handleExactly( $phpcsFile, $stackPtr, $methodOpener ); |
105 | case 'will': |
106 | return $this->handleWill( $phpcsFile, $stackPtr, $methodOpener ); |
107 | case 'with': |
108 | return $this->handleWith( $phpcsFile, $methodOpener ); |
109 | } |
110 | } |
111 | |
112 | /** |
113 | * @param File $phpcsFile |
114 | * @param int $stackPtr |
115 | * @param int $exactlyOpener |
116 | * |
117 | * @return void|int |
118 | */ |
119 | public function handleExactly( File $phpcsFile, $stackPtr, $exactlyOpener ) { |
120 | $tokens = $phpcsFile->getTokens(); |
121 | |
122 | $exactlyCloser = $tokens[$exactlyOpener]['parenthesis_closer']; |
123 | |
124 | $exactlyNumPtr = $phpcsFile->findNext( |
125 | T_WHITESPACE, |
126 | $exactlyOpener + 1, |
127 | $exactlyCloser, |
128 | true |
129 | ); |
130 | if ( !$exactlyNumPtr |
131 | || $tokens[$exactlyNumPtr]['code'] !== T_LNUMBER |
132 | ) { |
133 | // Not going to be ->exactly( 0 ) or ->exactly( 1 ) |
134 | return; |
135 | } |
136 | |
137 | // Figure out if it is indeed 0 or 1 |
138 | if ( $tokens[$exactlyNumPtr]['content'] === '0' ) { |
139 | $exactlyShortcut = 'never'; |
140 | } elseif ( $tokens[$exactlyNumPtr]['content'] === '1' ) { |
141 | $exactlyShortcut = 'once'; |
142 | } else { |
143 | // no shortcut |
144 | return; |
145 | } |
146 | |
147 | // Make sure it is only the 0 or 1, not something like ->exactly( 1 + $num ) |
148 | $afterNum = $phpcsFile->findNext( T_WHITESPACE, $exactlyNumPtr + 1, $exactlyCloser, true ); |
149 | if ( $afterNum ) { |
150 | return; |
151 | } |
152 | |
153 | // For reference, here are the different pointers we have stored |
154 | // |
155 | // $exactlyNumPtr |
156 | // $stackPtr | $exactlyCloser |
157 | // \ | / |
158 | // ->exactly( 0 ) |
159 | // | |
160 | // $exactlyOpener |
161 | // |
162 | // ($exactlyNumPtr could point to a 1 instead of a 0) |
163 | // and we want to replace from $stackPtr until $exactlyCloser with the shortcut |
164 | // plus a () |
165 | |
166 | $warningName = ( $exactlyShortcut === 'never' ? 'ExactlyNever' : 'ExactlyOnce' ); |
167 | $fix = $phpcsFile->addFixableWarning( |
168 | 'Matcher ->exactly( %s ) should be replaced with shortcut ->%s()', |
169 | $stackPtr, |
170 | $warningName, |
171 | [ $tokens[$exactlyNumPtr]['content'], $exactlyShortcut ] |
172 | ); |
173 | |
174 | if ( !$fix ) { |
175 | // There is no way the next issue can be closer than this |
176 | return $exactlyCloser; |
177 | } |
178 | |
179 | $phpcsFile->fixer->beginChangeset(); |
180 | |
181 | // Remove from after $stackPtr up to and including $exactlyCloser, so that if |
182 | // they are split over multiple lines we don't leave an ugly mess |
183 | for ( $i = $stackPtr + 1; $i <= $exactlyCloser; $i++ ) { |
184 | $phpcsFile->fixer->replaceToken( $i, '' ); |
185 | } |
186 | // Replace $stackPtr's exactly with the shortcut |
187 | $phpcsFile->fixer->replaceToken( $stackPtr, $exactlyShortcut . '()' ); |
188 | |
189 | $phpcsFile->fixer->endChangeset(); |
190 | |
191 | // There is no way the next issue can be closer that this |
192 | return $exactlyCloser; |
193 | } |
194 | |
195 | /** |
196 | * @param File $phpcsFile |
197 | * @param int $stackPtr |
198 | * @param int $willOpener |
199 | * |
200 | * @return void|int |
201 | */ |
202 | public function handleWill( File $phpcsFile, $stackPtr, $willOpener ) { |
203 | $tokens = $phpcsFile->getTokens(); |
204 | |
205 | $willCloser = $tokens[$willOpener]['parenthesis_closer']; |
206 | |
207 | $thisPtr = $phpcsFile->findNext( T_WHITESPACE, $willOpener + 1, $willCloser, true ); |
208 | if ( !$thisPtr |
209 | || $tokens[$thisPtr]['code'] !== T_VARIABLE |
210 | || $tokens[$thisPtr]['content'] !== '$this' |
211 | ) { |
212 | // Not going to be $this-> |
213 | return; |
214 | } |
215 | |
216 | $objectOperatorPtr = $phpcsFile->findNext( T_WHITESPACE, $thisPtr + 1, $willCloser, true ); |
217 | if ( !$objectOperatorPtr |
218 | || $tokens[$objectOperatorPtr]['code'] !== T_OBJECT_OPERATOR |
219 | ) { |
220 | // Not $this-> |
221 | return; |
222 | } |
223 | |
224 | $methodStubPtr = $phpcsFile->findNext( T_WHITESPACE, $objectOperatorPtr + 1, $willCloser, true ); |
225 | if ( !$methodStubPtr |
226 | || $tokens[$methodStubPtr]['code'] !== T_STRING |
227 | || !isset( self::WILL_REPLACEMENTS[ $tokens[$methodStubPtr]['content'] ] ) |
228 | ) { |
229 | // Not $this-> followed by a method name we care about |
230 | return; |
231 | } |
232 | |
233 | $stubOpener = $phpcsFile->findNext( T_WHITESPACE, $methodStubPtr + 1, $willCloser, true ); |
234 | if ( !$stubOpener |
235 | || !isset( $tokens[$stubOpener]['parenthesis_closer'] ) |
236 | ) { |
237 | // String is not a method name |
238 | return; |
239 | } |
240 | $stubCloser = $tokens[$stubOpener]['parenthesis_closer']; |
241 | |
242 | // Okay, so we found something that might be worth replacing, in the form |
243 | // ->will( $this->returnValue( ... ) ) |
244 | // or similar. Make sure there is nothing between the end of the stub and the parenthesis |
245 | // closer for the ->will() call |
246 | $afterStub = $phpcsFile->findNext( T_WHITESPACE, $stubCloser + 1, $willCloser, true ); |
247 | if ( $afterStub ) { |
248 | return; |
249 | } |
250 | |
251 | // For reference, here are the different pointers we have stored |
252 | // |
253 | // $willOpener $methodStubPtr |
254 | // \ | $willCloser |
255 | // $stackPtr | $thisPtr | $stubOpener / |
256 | // | | | | | | |
257 | // ->will ( $this -> returnValue( ... ) ) |
258 | // | | |
259 | // $objectOperatorPtr $stubCloser |
260 | // |
261 | // What we want to do is to remove the inner stub, i.e. replace |
262 | // $this->returnValue( ... ) |
263 | // with just the |
264 | // ... |
265 | // and then update the outer ->will( ... ) to use the shortcut |
266 | // ->willReturnValue( ... ) |
267 | $stubMethod = $tokens[$methodStubPtr]['content']; |
268 | $willReplacement = self::WILL_REPLACEMENTS[ $stubMethod ]; |
269 | |
270 | $fix = $phpcsFile->addFixableWarning( |
271 | 'Use the shortcut %s() rather that manually stubbing a method with %s()', |
272 | $stackPtr, |
273 | $stubMethod, |
274 | [ $willReplacement, $stubMethod ] |
275 | ); |
276 | |
277 | if ( !$fix ) { |
278 | // There is no way the next issue can be closer than this |
279 | return $willCloser; |
280 | } |
281 | |
282 | $phpcsFile->fixer->beginChangeset(); |
283 | |
284 | // To be consistent with whitespace around the parenthesis, we will keep |
285 | // the original parenthesis from $stubOpener and $stubCloser and the whitespace |
286 | // within them. |
287 | // Step 1: remove everything from after $stubCloser up to and including $willCloser |
288 | for ( $i = $stubCloser + 1; $i <= $willCloser; $i++ ) { |
289 | $phpcsFile->fixer->replaceToken( $i, '' ); |
290 | } |
291 | |
292 | // Step 2: remove everything from $willOpener up to, but not including, $stubOpener |
293 | for ( $i = $willOpener; $i < $stubOpener; $i++ ) { |
294 | $phpcsFile->fixer->replaceToken( $i, '' ); |
295 | } |
296 | |
297 | // Step 3: replace 'will' with the correct shortcut method |
298 | $phpcsFile->fixer->replaceToken( $stackPtr, $willReplacement ); |
299 | |
300 | $phpcsFile->fixer->endChangeset(); |
301 | |
302 | // There is no way the next issue can be closer that this |
303 | return $willCloser; |
304 | } |
305 | |
306 | /** |
307 | * @param File $phpcsFile |
308 | * @param int $withOpener |
309 | * |
310 | * @return int |
311 | */ |
312 | public function handleWith( File $phpcsFile, $withOpener ) { |
313 | $tokens = $phpcsFile->getTokens(); |
314 | |
315 | $withCloser = $tokens[$withOpener]['parenthesis_closer']; |
316 | |
317 | // For every use of `$this->equalTo( ... )` between $withOpener and $withCloser, |
318 | // add a warning, and if fixing, replace with just the inner contents |
319 | |
320 | // Use a for loop so that we can call findNext() after each continue |
321 | // phpcs:ignore Generic.CodeAnalysis.JumbledIncrementer.Found |
322 | for ( |
323 | $thisPtr = $phpcsFile->findNext( T_VARIABLE, $withOpener + 1, $withCloser ); |
324 | $thisPtr; |
325 | $thisPtr = $phpcsFile->findNext( T_VARIABLE, $thisPtr + 1, $withCloser ) |
326 | ) { |
327 | // Needs to be $this |
328 | if ( $tokens[$thisPtr]['content'] !== '$this' ) { |
329 | continue; |
330 | } |
331 | // Needs to be $this-> |
332 | $objectOperatorPtr = $phpcsFile->findNext( |
333 | T_WHITESPACE, |
334 | $thisPtr + 1, |
335 | $withCloser, |
336 | true |
337 | ); |
338 | if ( !$objectOperatorPtr |
339 | || $tokens[$objectOperatorPtr]['code'] !== T_OBJECT_OPERATOR |
340 | ) { |
341 | continue; |
342 | } |
343 | |
344 | // Needs to be $this->equalTo |
345 | $methodPtr = $phpcsFile->findNext( |
346 | T_WHITESPACE, |
347 | $objectOperatorPtr + 1, |
348 | $withCloser, |
349 | true |
350 | ); |
351 | // if its $this->logicalNot() or similar we want to skip past the closing |
352 | // parenthesis, just make sure its a function call here |
353 | if ( !$methodPtr |
354 | || $tokens[$methodPtr]['code'] !== T_STRING |
355 | ) { |
356 | continue; |
357 | } |
358 | // Needs to be $this->equalTo( ... ) |
359 | $methodOpener = $phpcsFile->findNext( |
360 | T_WHITESPACE, |
361 | $methodPtr + 1, |
362 | $withCloser, |
363 | true |
364 | ); |
365 | if ( !$methodOpener |
366 | || !isset( $tokens[$methodOpener]['parenthesis_closer'] ) |
367 | ) { |
368 | // String is not a method name |
369 | continue; |
370 | } |
371 | $methodCloser = $tokens[$methodOpener]['parenthesis_closer']; |
372 | if ( $tokens[$methodPtr]['content'] !== 'equalTo' ) { |
373 | $thisPtr = $methodCloser; |
374 | continue; |
375 | } |
376 | |
377 | // Check for equalTo() with a second parameter, which we cannot fix |
378 | $shouldSkip = false; |
379 | $searchFor = [ T_COMMA, T_OPEN_PARENTHESIS ]; |
380 | for ( |
381 | $checkIndex = $phpcsFile->findNext( $searchFor, $methodOpener + 1, $methodCloser ); |
382 | $checkIndex; |
383 | $checkIndex = $phpcsFile->findNext( $searchFor, $checkIndex + 1, $methodCloser ) |
384 | ) { |
385 | if ( $tokens[$checkIndex]['code'] === T_OPEN_PARENTHESIS |
386 | && isset( $tokens[$checkIndex]['parenthesis_closer'] ) |
387 | ) { |
388 | // Jump past any parentheses in a function call within |
389 | // the equalTo(), eg $this->equalTo( add( 2, 3 ) ) |
390 | $checkIndex = $tokens[$checkIndex]['parenthesis_closer']; |
391 | } elseif ( $tokens[$checkIndex]['code'] === T_COMMA ) { |
392 | // equalTo() with multiple parameters, should not be removed |
393 | $shouldSkip = true; |
394 | break; |
395 | } |
396 | } |
397 | if ( $shouldSkip ) { |
398 | // Next $this->equalTo() cannot be until after the current one |
399 | $thisPtr = $methodCloser; |
400 | continue; |
401 | } |
402 | |
403 | // Add a warning and maybe fix |
404 | $fix = $phpcsFile->addFixableWarning( |
405 | 'Default constraint equalTo() is unneeded and should be removed', |
406 | $methodPtr, |
407 | 'ConstraintEqualTo' |
408 | ); |
409 | if ( !$fix ) { |
410 | // Next $this->equalTo() cannot be until after the current one |
411 | $thisPtr = $methodCloser; |
412 | continue; |
413 | } |
414 | // For reference, here are the different pointers we have stored |
415 | // |
416 | // $objectOperatorPtr |
417 | // \ |
418 | // $thisPtr | $methodOpener $methodCloser |
419 | // \ | | | |
420 | // $this -> equalTo ( ... ) |
421 | // / |
422 | // $methodPtr |
423 | // Find the first and last non-whitespace parts of the ... and only keep |
424 | // those |
425 | $equalContentStart = $phpcsFile->findNext( |
426 | T_WHITESPACE, |
427 | $methodOpener + 1, |
428 | $methodCloser, |
429 | true |
430 | ); |
431 | $equalContentEnd = $phpcsFile->findPrevious( |
432 | T_WHITESPACE, |
433 | $methodCloser - 1, |
434 | $methodOpener, |
435 | true |
436 | ); |
437 | $phpcsFile->fixer->beginChangeset(); |
438 | |
439 | // Step 1: remove from after $equalContentEnd up to and including $methodCloser |
440 | for ( $i = $equalContentEnd + 1; $i <= $methodCloser; $i++ ) { |
441 | $phpcsFile->fixer->replaceToken( $i, '' ); |
442 | } |
443 | |
444 | // Step 2: remove from $thisPtr up to, but not including, $equalContentStart |
445 | for ( $i = $thisPtr; $i < $equalContentStart; $i++ ) { |
446 | $phpcsFile->fixer->replaceToken( $i, '' ); |
447 | } |
448 | |
449 | $phpcsFile->fixer->endChangeset(); |
450 | |
451 | // There is no way the next issue can be closer that this |
452 | $thisPtr = $methodCloser; |
453 | } |
454 | return $withCloser; |
455 | } |
456 | |
457 | } |