Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 174
0.00% covered (danger)
0.00%
0 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
MockBoilerplateSniff
0.00% covered (danger)
0.00%
0 / 174
0.00% covered (danger)
0.00%
0 / 5
2652
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 / 17
0.00% covered (danger)
0.00%
0 / 1
72
 handleExactly
0.00% covered (danger)
0.00%
0 / 34
0.00% covered (danger)
0.00%
0 / 1
90
 handleWill
0.00% covered (danger)
0.00%
0 / 42
0.00% covered (danger)
0.00%
0 / 1
240
 handleWith
0.00% covered (danger)
0.00%
0 / 80
0.00% covered (danger)
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
22namespace MediaWiki\Sniffs\PHPUnit;
23
24use PHP_CodeSniffer\Files\File;
25use 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 */
45class 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}