Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 149 |
|
0.00% |
0 / 3 |
CRAP | |
0.00% |
0 / 1 |
AssertionOrderSniff | |
0.00% |
0 / 149 |
|
0.00% |
0 / 3 |
2162 | |
0.00% |
0 / 1 |
register | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
process | |
0.00% |
0 / 55 |
|
0.00% |
0 / 1 |
90 | |||
getFixInfo | |
0.00% |
0 / 93 |
|
0.00% |
0 / 1 |
1332 |
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 | * Fix uses of assertEquals/assertNotEquals or assertSame/assertNotSame with the actual value before the expected |
29 | * Currently, only catches assertions where the actual value is a variable, or at least |
30 | * starts with a variable token, and the expected is a literal value or a variable in the form |
31 | * $expected*, or an array of such values (including nested arrays). |
32 | * |
33 | * @author DannyS712 |
34 | */ |
35 | class AssertionOrderSniff implements Sniff { |
36 | use PHPUnitTestTrait; |
37 | |
38 | private const ASSERTIONS = [ |
39 | 'assertEquals' => true, |
40 | 'assertSame' => true, |
41 | 'assertNotEquals' => true, |
42 | 'assertNotSame' => true, |
43 | ]; |
44 | |
45 | private const LITERALS = [ |
46 | T_NULL => T_NULL, |
47 | T_FALSE => T_FALSE, |
48 | T_TRUE => T_TRUE, |
49 | T_LNUMBER => T_LNUMBER, |
50 | T_DNUMBER => T_DNUMBER, |
51 | T_CONSTANT_ENCAPSED_STRING => T_CONSTANT_ENCAPSED_STRING, |
52 | ]; |
53 | |
54 | /** |
55 | * @inheritDoc |
56 | */ |
57 | public function register(): array { |
58 | return [ T_STRING ]; |
59 | } |
60 | |
61 | /** |
62 | * @param File $phpcsFile |
63 | * @param int $stackPtr |
64 | * |
65 | * @return void|int |
66 | */ |
67 | public function process( File $phpcsFile, $stackPtr ) { |
68 | if ( !$this->isTestFile( $phpcsFile, $stackPtr ) ) { |
69 | return $phpcsFile->numTokens; |
70 | } |
71 | |
72 | $tokens = $phpcsFile->getTokens(); |
73 | if ( $tokens[$stackPtr]['level'] < 2 ) { |
74 | // Needs to be in a method in a class |
75 | return; |
76 | } |
77 | |
78 | $assertion = $tokens[$stackPtr]['content']; |
79 | if ( !isset( self::ASSERTIONS[$assertion] ) ) { |
80 | // Don't care about this string |
81 | return; |
82 | } |
83 | |
84 | $opener = $phpcsFile->findNext( T_WHITESPACE, $stackPtr + 1, null, true ); |
85 | if ( !isset( $tokens[$opener]['parenthesis_closer'] ) ) { |
86 | // Needs to be a method call |
87 | return $opener; |
88 | } |
89 | |
90 | $fixInfo = $this->getFixInfo( $phpcsFile, $opener ); |
91 | if ( !$fixInfo ) { |
92 | // No warning |
93 | return; |
94 | } |
95 | $end = $tokens[$opener]['parenthesis_closer']; |
96 | |
97 | $fix = $phpcsFile->addFixableWarning( |
98 | 'The expected value goes before the actual value in assertions', |
99 | $stackPtr, |
100 | 'WrongOrder' |
101 | ); |
102 | if ( !$fix ) { |
103 | // There is no way the next assertion can be closer than this |
104 | return $end + 4; |
105 | } |
106 | |
107 | [ $firstParamStart, $firstComma, $afterSecondParam ] = $fixInfo; |
108 | // The first parameter currently goes from $firstParamStart until $firstComma, and the |
109 | // second parameter goes from after $firstComma until before $afterSecondParam |
110 | $actualParamEnd = $phpcsFile->findPrevious( T_WHITESPACE, $firstComma - 1, null, true ); |
111 | $actualParamContent = $phpcsFile->getTokensAsString( |
112 | $firstParamStart, |
113 | $actualParamEnd - $firstParamStart + 1, |
114 | // keep tabs on multiline statements |
115 | true |
116 | ); |
117 | |
118 | $expectedParamStart = $phpcsFile->findNext( |
119 | T_WHITESPACE, |
120 | $firstComma + 1, |
121 | $end, |
122 | true |
123 | ); |
124 | $expectedParamEnd = $phpcsFile->findPrevious( |
125 | T_WHITESPACE, |
126 | $afterSecondParam - 1, |
127 | null, |
128 | true |
129 | ); |
130 | $expectedParamContent = $phpcsFile->getTokensAsString( |
131 | $expectedParamStart, |
132 | $expectedParamEnd - $expectedParamStart + 1, |
133 | // keep tabs on multiline statements |
134 | true |
135 | ); |
136 | |
137 | $phpcsFile->fixer->beginChangeset(); |
138 | |
139 | // Remove the first parameter that previously held the actual value, |
140 | // and replace with the expected |
141 | $phpcsFile->fixer->replaceToken( $firstParamStart, $expectedParamContent ); |
142 | for ( $i = $firstParamStart + 1; $i <= $actualParamEnd; $i++ ) { |
143 | $phpcsFile->fixer->replaceToken( $i, '' ); |
144 | } |
145 | |
146 | // Remove the second parameter that previously held the expeced value, |
147 | // and replace with the actual |
148 | $phpcsFile->fixer->replaceToken( $expectedParamStart, $actualParamContent ); |
149 | for ( $i = $expectedParamStart + 1; $i <= $expectedParamEnd; $i++ ) { |
150 | $phpcsFile->fixer->replaceToken( $i, '' ); |
151 | } |
152 | |
153 | $phpcsFile->fixer->endChangeset(); |
154 | |
155 | // There is no way the next assertion can be closer than this |
156 | return $end + 4; |
157 | } |
158 | |
159 | /** |
160 | * @param File $phpcsFile |
161 | * @param int $opener |
162 | * @return array|false Array with info for fixing, or false for no change |
163 | */ |
164 | private function getFixInfo( |
165 | File $phpcsFile, |
166 | int $opener |
167 | ) { |
168 | $tokens = $phpcsFile->getTokens(); |
169 | $end = $tokens[$opener]['parenthesis_closer']; |
170 | |
171 | // Optimize for the most common case: the first parameter is a single token |
172 | // that is a literal, and then there is a comma. |
173 | $firstParam = $phpcsFile->findNext( T_WHITESPACE, $opener + 1, $end, true ); |
174 | if ( !$firstParam ) { |
175 | // Assertion is invalid (no parameters) but thats not our problem |
176 | return false; |
177 | } |
178 | if ( isset( self::LITERALS[ $tokens[$firstParam]['code'] ] ) |
179 | && isset( $tokens[$firstParam + 1] ) |
180 | && $tokens[$firstParam + 1]['code'] === T_COMMA |
181 | ) { |
182 | return false; |
183 | } |
184 | |
185 | // Analyze the assertion call |
186 | $currentParam = 1; |
187 | // Whether or not the first parameter has variables or method calls that might |
188 | // make sense to put as the actual parameter instead |
189 | $firstParamVariable = false; |
190 | $firstComma = false; |
191 | $secondComma = false; |
192 | $searchTokens = [ |
193 | T_DOUBLE_QUOTED_STRING, |
194 | T_HEREDOC, |
195 | T_START_HEREDOC, |
196 | T_OPEN_CURLY_BRACKET, |
197 | T_OPEN_SQUARE_BRACKET, |
198 | T_OPEN_PARENTHESIS, |
199 | T_OPEN_SHORT_ARRAY, |
200 | T_CLOSE_SHORT_ARRAY, |
201 | T_STRING, |
202 | T_VARIABLE, |
203 | T_COMMA, |
204 | ]; |
205 | $next = $firstParam; |
206 | // For ignoring commas within a literal array in the actual (but should be expected) |
207 | // parameter, including nested arrays, keep track of the closing of the outermost |
208 | // current array |
209 | $arrayEndIndex = -1; |
210 | while ( $secondComma === false ) { |
211 | if ( $next === false ) { |
212 | // If we are in the first parameter and there is no comma, |
213 | // likely live coding. If we are in the second, then it just |
214 | // means that there is third parameter (message) |
215 | if ( $currentParam === 1 ) { |
216 | return false; |
217 | } |
218 | // Second parameter ended |
219 | break; |
220 | } |
221 | switch ( $tokens[$next]['code'] ) { |
222 | // Some things we just don't handle |
223 | case T_DOUBLE_QUOTED_STRING: |
224 | case T_HEREDOC: |
225 | case T_START_HEREDOC: |
226 | return false; |
227 | |
228 | case T_OPEN_SHORT_ARRAY: |
229 | // Commas within an array in the second parameter should |
230 | // not be treated as separating parameters to the assertion, |
231 | // the start of a nested array does not change the end of |
232 | // the outer array |
233 | if ( $currentParam === 2 |
234 | && $arrayEndIndex === -1 |
235 | && isset( $tokens[$next]['bracket_closer'] ) |
236 | ) { |
237 | $arrayEndIndex = $tokens[$next]['bracket_closer']; |
238 | break; |
239 | } |
240 | // Intentional fall through for handling first parameter |
241 | |
242 | case T_OPEN_CURLY_BRACKET: |
243 | case T_OPEN_SQUARE_BRACKET: |
244 | case T_OPEN_PARENTHESIS: |
245 | // Only skipping to the end of these in the first parameter, |
246 | // need to count them in the second one |
247 | if ( $currentParam === 1 ) { |
248 | if ( isset( $tokens[$next]['parenthesis_closer'] ) ) { |
249 | // jump to closing parenthesis to ignore commas between opener and closer |
250 | $next = $tokens[$next]['parenthesis_closer']; |
251 | } elseif ( isset( $tokens[$next]['bracket_closer'] ) ) { |
252 | // jump to closing bracket |
253 | $next = $tokens[$next]['bracket_closer']; |
254 | } |
255 | } |
256 | break; |
257 | |
258 | case T_CLOSE_SHORT_ARRAY: |
259 | // If we reached the end of the correct array in the |
260 | // second parameter, further commas should be treated as |
261 | // separating parameters to the assertion |
262 | if ( $next === $arrayEndIndex ) { |
263 | $arrayEndIndex = -1; |
264 | } |
265 | break; |
266 | |
267 | case T_VARIABLE: |
268 | if ( $currentParam === 2 ) { |
269 | // We are looking at the second parameter, which |
270 | // should be the actual value. Since the actual value |
271 | // includes a variable or function call, its probably correct, |
272 | // unless the variable is named $expected*, in which |
273 | // case we can assume that it was meant to be the |
274 | // expected value, not the actual value |
275 | $expectedVarName = $tokens[$next]['content']; |
276 | // optimize for common case - full name is $expected |
277 | if ( $expectedVarName !== '$expected' |
278 | // but also handle $expectedRes and similar |
279 | && !str_starts_with( $expectedVarName, '$expected' ) |
280 | ) { |
281 | return false; |
282 | } |
283 | // Don't set $firstParamVariable if this is the |
284 | // second param |
285 | break; |
286 | } |
287 | $firstParamVariable = true; |
288 | break; |
289 | |
290 | case T_STRING: |
291 | // Check if its a function call |
292 | $functionOpener = $phpcsFile->findNext( |
293 | T_WHITESPACE, |
294 | $next + 1, |
295 | $end, |
296 | true |
297 | ); |
298 | if ( $functionOpener && |
299 | isset( $tokens[$functionOpener]['parenthesis_closer'] ) |
300 | ) { |
301 | // Function call, similar to T_VARIABLE handling |
302 | if ( $currentParam === 2 ) { |
303 | return false; |
304 | } |
305 | $firstParamVariable = true; |
306 | // Jump over the function call, no need to wait until |
307 | // the next iteration triggers with T_OPEN_PARENTHESIS |
308 | $next = $tokens[$functionOpener]['parenthesis_closer']; |
309 | } |
310 | break; |
311 | |
312 | case T_COMMA: |
313 | // Ignore commas within arrays |
314 | if ( $arrayEndIndex !== -1 ) { |
315 | break; |
316 | } |
317 | if ( $currentParam === 1 ) { |
318 | if ( $firstParamVariable === false ) { |
319 | // No need to check the second parameter, |
320 | // the first one had no variables or |
321 | // method calls that would make sense as |
322 | // the actual parameter |
323 | return false; |
324 | } |
325 | $firstComma = $next; |
326 | $currentParam = 2; |
327 | } else { |
328 | // Triggers the end of the while loop |
329 | $secondComma = $next; |
330 | } |
331 | break; |
332 | } |
333 | $next = $phpcsFile->findNext( $searchTokens, $next + 1, $end ); |
334 | } |
335 | |
336 | // If we got here, then there were no variables or methods in the second parameter, |
337 | // which should have been the actual value. Should switch with the first parameter. |
338 | $afterSecondParam = ( $secondComma ?: $end ); |
339 | return [ $firstParam, $firstComma, $afterSecondParam ]; |
340 | } |
341 | |
342 | } |