Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 81
0.00% covered (danger)
0.00%
0 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
AssertCountSniff
0.00% covered (danger)
0.00%
0 / 81
0.00% covered (danger)
0.00%
0 / 4
992
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 / 57
0.00% covered (danger)
0.00%
0 / 1
506
 parseCount
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
30
 replaceCountContent
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2
3namespace MediaWiki\Sniffs\PHPUnit;
4
5use PHP_CodeSniffer\Files\File;
6use PHP_CodeSniffer\Sniffs\Sniff;
7
8/**
9 * Replace assertEquals and assertSame where the actual value is count( anything ) with
10 * the more specific assertCount. Based on AssertEqualsSniff sniff
11 *
12 * @author DannyS712
13 * @license GPL-2.0-or-later
14 */
15class AssertCountSniff implements Sniff {
16    use PHPUnitTestTrait;
17
18    private const ASSERTIONS = [
19        'assertEquals' => true,
20        'assertSame' => true,
21        'assertCount' => true,
22    ];
23
24    /**
25     * @inheritDoc
26     */
27    public function register(): array {
28        return [ T_STRING ];
29    }
30
31    /**
32     * @param File $phpcsFile
33     * @param int $stackPtr
34     *
35     * @return void|int
36     */
37    public function process( File $phpcsFile, $stackPtr ) {
38        if ( !$this->isTestFile( $phpcsFile, $stackPtr ) ) {
39            return $phpcsFile->numTokens;
40        }
41
42        $tokens = $phpcsFile->getTokens();
43        $assertion = $tokens[$stackPtr]['content'];
44
45        // We don't care about stuff that's not in a method in a class
46        if ( $tokens[$stackPtr]['level'] < 2 || !isset( self::ASSERTIONS[$assertion] ) ) {
47            return;
48        }
49
50        $opener = $phpcsFile->findNext( T_WHITESPACE, $stackPtr + 1, null, true );
51        if ( !isset( $tokens[$opener]['parenthesis_closer'] ) ) {
52            // Looks like this string is not a method call
53            return $opener;
54        }
55        $end = $tokens[$opener]['parenthesis_closer'];
56
57        $firstCount = $this->parseCount( $phpcsFile, $opener );
58        if ( !$firstCount && $assertion === 'assertCount' ) {
59            return $end;
60        }
61
62        // Jump over the expected parameter, whatever it is
63        $searchTokens = [
64            T_OPEN_CURLY_BRACKET,
65            T_OPEN_SQUARE_BRACKET,
66            T_OPEN_PARENTHESIS,
67            T_OPEN_SHORT_ARRAY,
68            T_COMMA
69        ];
70        $commaToken = false;
71        $next = $phpcsFile->findNext( $searchTokens, $opener + 1, $end );
72        while ( $commaToken === false ) {
73            if ( $next === false ) {
74                // No token
75                return;
76            }
77            switch ( $tokens[$next]['code'] ) {
78                case T_OPEN_CURLY_BRACKET:
79                case T_OPEN_SQUARE_BRACKET:
80                case T_OPEN_PARENTHESIS:
81                case T_OPEN_SHORT_ARRAY:
82                    if ( isset( $tokens[$next]['parenthesis_closer'] ) ) {
83                        // jump to closing parenthesis to ignore commas between opener and closer
84                        $next = $tokens[$next]['parenthesis_closer'];
85                    } elseif ( isset( $tokens[$next]['bracket_closer'] ) ) {
86                        // jump to closing bracket
87                        $next = $tokens[$next]['bracket_closer'];
88                    }
89                    break;
90                case T_COMMA:
91                    $commaToken = $next;
92                    break;
93            }
94            $next = $phpcsFile->findNext( $searchTokens, $next + 1, $end );
95        }
96
97        $secondCount = $this->parseCount( $phpcsFile, $commaToken );
98        if ( !( $secondCount xor $assertion === 'assertCount' ) ) {
99            return $end;
100        }
101
102        // T330008: Prefer assertSameSize when both part of comparison are count()
103        $newAssert = $firstCount ? 'assertSameSize' : 'assertCount';
104        $fix = $phpcsFile->addFixableWarning(
105            '%s can be used instead of manually using %s with the result of count()',
106            $stackPtr,
107            $newAssert === 'assertSameSize' ? 'AssertSameSize' : 'NotUsed',
108            [ $newAssert, $assertion ]
109        );
110        if ( !$fix ) {
111            return;
112        }
113
114        $phpcsFile->fixer->replaceToken( $stackPtr, $newAssert );
115        if ( $firstCount ) {
116            $this->replaceCountContent( $phpcsFile, $firstCount );
117        }
118        if ( $secondCount ) {
119            $this->replaceCountContent( $phpcsFile, $secondCount );
120        }
121
122        // There is no way the next assertEquals() or assertSame() can be closer than this
123        return $tokens[$opener]['parenthesis_closer'] + 4;
124    }
125
126    /**
127     * @param File $phpcsFile
128     * @param int $stackPtr
129     * @return array|void
130     */
131    private function parseCount( File $phpcsFile, int $stackPtr ) {
132        $tokens = $phpcsFile->getTokens();
133        $countToken = $phpcsFile->findNext( T_WHITESPACE, $stackPtr + 1, null, true );
134        if ( $tokens[$countToken]['code'] !== T_STRING ||
135            $tokens[$countToken]['content'] !== 'count'
136        ) {
137            // Not `count`
138            return;
139        }
140
141        $countOpen = $phpcsFile->findNext( T_WHITESPACE, $countToken + 1, null, true );
142        if ( !isset( $tokens[$countOpen]['parenthesis_closer'] ) ) {
143            // Not a function
144            return;
145        }
146
147        $countClose = $tokens[$countOpen]['parenthesis_closer'];
148        $afterCount = $phpcsFile->findNext( T_WHITESPACE, $countClose + 1, null, true );
149        if ( !in_array( $tokens[$afterCount]['code'], [ T_COMMA, T_CLOSE_PARENTHESIS ] ) ) {
150            // Not followed by a comma and a third parameter, or a closing parenthesis
151            // something more complex is going on
152            return;
153        }
154
155        return [ $countToken, $countOpen, $countClose ];
156    }
157
158    /**
159     * @param File $phpcsFile
160     * @param int[] $parsed
161     * @return void
162     */
163    private function replaceCountContent( File $phpcsFile, array $parsed ) {
164        [ $countToken, $countOpen, $countClose ] = $parsed;
165        $countContentStart = $phpcsFile->findNext( T_WHITESPACE, $countOpen + 1, null, true );
166        $countContentEnd = $phpcsFile->findPrevious( T_WHITESPACE, $countClose - 1, null, true );
167
168        $phpcsFile->fixer->replaceToken( $countToken, '' );
169        $phpcsFile->fixer->replaceToken( $countOpen, '' );
170        for ( $i = $countOpen + 1; $i < $countContentStart; $i++ ) {
171            // Whitespace between count( and the content
172            $phpcsFile->fixer->replaceToken( $i, '' );
173        }
174        for ( $i = $countContentEnd + 1; $i < $countClose; $i++ ) {
175            // Whitespace between content and )
176            $phpcsFile->fixer->replaceToken( $i, '' );
177        }
178        $phpcsFile->fixer->replaceToken( $countClose, '' );
179    }
180
181}