1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* PHPCompatibility, an external standard for PHP_CodeSniffer. |
4
|
|
|
* |
5
|
|
|
* @package PHPCompatibility |
6
|
|
|
* @copyright 2012-2018 PHPCompatibility Contributors |
7
|
|
|
* @license https://opensource.org/licenses/LGPL-3.0 LGPL3 |
8
|
|
|
* @link https://github.com/PHPCompatibility/PHPCompatibility |
9
|
|
|
*/ |
10
|
|
|
|
11
|
|
|
namespace PHPCompatibility\Sniffs\FunctionUse; |
12
|
|
|
|
13
|
|
|
use PHPCompatibility\Sniff; |
14
|
|
|
use PHPCompatibility\PHPCSHelper; |
15
|
|
|
use PHP_CodeSniffer_File as File; |
16
|
|
|
use PHP_CodeSniffer_Tokens as Tokens; |
17
|
|
|
|
18
|
|
|
/** |
19
|
|
|
* Functions inspecting function arguments report the current parameter value |
20
|
|
|
* instead of the original since PHP 7.0. |
21
|
|
|
* |
22
|
|
|
* `func_get_arg()`, `func_get_args()`, `debug_backtrace()` and exception backtraces |
23
|
|
|
* will no longer report the original value that was passed to a parameter, but will |
24
|
|
|
* instead provide the current value (which might have been modified). |
25
|
|
|
* |
26
|
|
|
* PHP version 7.0 |
27
|
|
|
* |
28
|
|
|
* @since 9.1.0 |
29
|
|
|
*/ |
30
|
|
|
class ArgumentFunctionsReportCurrentValueSniff extends Sniff |
31
|
|
|
{ |
32
|
|
|
|
33
|
|
|
/** |
34
|
|
|
* A list of functions that, when called, can behave differently in PHP 7 |
35
|
|
|
* when dealing with parameters of the function they're called in. |
36
|
|
|
* |
37
|
|
|
* @var array |
38
|
|
|
*/ |
39
|
|
|
protected $changedFunctions = array( |
40
|
|
|
'func_get_arg' => true, |
41
|
|
|
'func_get_args' => true, |
42
|
|
|
'debug_backtrace' => true, |
43
|
|
|
'debug_print_backtrace' => true, |
44
|
|
|
); |
45
|
|
|
|
46
|
|
|
/** |
47
|
|
|
* Tokens to look out for to allow us to skip past nested scoped structures. |
48
|
|
|
* |
49
|
|
|
* @var array |
50
|
|
|
*/ |
51
|
|
|
private $skipPastNested = array( |
52
|
|
|
'T_CLASS' => true, |
53
|
|
|
'T_ANON_CLASS' => true, |
54
|
|
|
'T_INTERFACE' => true, |
55
|
|
|
'T_TRAIT' => true, |
56
|
|
|
'T_FUNCTION' => true, |
57
|
|
|
'T_CLOSURE' => true, |
58
|
|
|
); |
59
|
|
|
|
60
|
|
|
/** |
61
|
|
|
* List of tokens which when they preceed a T_STRING *within a function* indicate |
62
|
|
|
* this is not a call to a PHP native function. |
63
|
|
|
* |
64
|
|
|
* This list already takes into account that nested scoped structures are being |
65
|
|
|
* skipped over, so doesn't check for those again. |
66
|
|
|
* Similarly, as constants won't have parentheses, those don't need to be checked |
67
|
|
|
* for either. |
68
|
|
|
* |
69
|
|
|
* @var array |
70
|
|
|
*/ |
71
|
|
|
private $noneFunctionCallIndicators = array( |
72
|
|
|
T_DOUBLE_COLON => true, |
73
|
|
|
T_OBJECT_OPERATOR => true, |
74
|
|
|
); |
75
|
|
|
|
76
|
|
|
/** |
77
|
|
|
* The tokens for variable incrementing/decrementing. |
78
|
|
|
* |
79
|
|
|
* @var array |
80
|
|
|
*/ |
81
|
|
|
private $plusPlusMinusMinus = array( |
82
|
|
|
T_DEC => true, |
83
|
|
|
T_INC => true, |
84
|
|
|
); |
85
|
|
|
|
86
|
|
|
/** |
87
|
|
|
* Tokens to ignore when determining the start of a statement. |
88
|
|
|
* |
89
|
|
|
* @var array |
90
|
|
|
*/ |
91
|
|
|
private $ignoreForStartOfStatement = array( |
92
|
|
|
T_COMMA, |
93
|
|
|
T_DOUBLE_ARROW, |
94
|
|
|
T_OPEN_SQUARE_BRACKET, |
95
|
|
|
T_OPEN_PARENTHESIS, |
96
|
|
|
); |
97
|
|
|
|
98
|
|
|
/** |
99
|
|
|
* Returns an array of tokens this test wants to listen for. |
100
|
|
|
* |
101
|
|
|
* @return array |
102
|
|
|
*/ |
103
|
|
|
public function register() |
104
|
|
|
{ |
105
|
|
|
return array( |
106
|
|
|
T_FUNCTION, |
107
|
|
|
T_CLOSURE, |
108
|
|
|
); |
109
|
|
|
} |
110
|
|
|
|
111
|
|
|
/** |
112
|
|
|
* Processes this test, when one of its tokens is encountered. |
113
|
|
|
* |
114
|
|
|
* @param \PHP_CodeSniffer_File $phpcsFile The file being scanned. |
115
|
|
|
* @param int $stackPtr The position of the current token |
116
|
|
|
* in the stack passed in $tokens. |
117
|
|
|
* |
118
|
|
|
* @return void |
119
|
|
|
*/ |
120
|
|
|
public function process(File $phpcsFile, $stackPtr) |
121
|
|
|
{ |
122
|
|
|
if ($this->supportsAbove('7.0') === false) { |
123
|
|
|
return; |
124
|
|
|
} |
125
|
|
|
|
126
|
|
|
$tokens = $phpcsFile->getTokens(); |
127
|
|
|
|
128
|
|
|
if (isset($tokens[$stackPtr]['scope_opener'], $tokens[$stackPtr]['scope_closer']) === false) { |
129
|
|
|
// Abstract function, interface function, live coding or parse error. |
130
|
|
|
return; |
131
|
|
|
} |
132
|
|
|
|
133
|
|
|
$scopeOpener = $tokens[$stackPtr]['scope_opener']; |
134
|
|
|
$scopeCloser = $tokens[$stackPtr]['scope_closer']; |
135
|
|
|
|
136
|
|
|
// Does the function declaration have parameters ? |
137
|
|
|
$params = PHPCSHelper::getMethodParameters($phpcsFile, $stackPtr); |
138
|
|
|
if (empty($params)) { |
139
|
|
|
// No named arguments found, so no risk of them being changed. |
140
|
|
|
return; |
141
|
|
|
} |
142
|
|
|
|
143
|
|
|
$paramNames = array(); |
144
|
|
|
foreach ($params as $param) { |
145
|
|
|
$paramNames[] = $param['name']; |
146
|
|
|
} |
147
|
|
|
|
148
|
|
|
for ($i = ($scopeOpener + 1); $i < $scopeCloser; $i++) { |
149
|
|
View Code Duplication |
if (isset($this->skipPastNested[$tokens[$i]['type']]) && isset($tokens[$i]['scope_closer'])) { |
|
|
|
|
150
|
|
|
// Skip past nested structures. |
151
|
|
|
$i = $tokens[$i]['scope_closer']; |
152
|
|
|
continue; |
153
|
|
|
} |
154
|
|
|
|
155
|
|
|
if ($tokens[$i]['code'] !== T_STRING) { |
156
|
|
|
continue; |
157
|
|
|
} |
158
|
|
|
|
159
|
|
|
$foundFunctionName = strtolower($tokens[$i]['content']); |
160
|
|
|
|
161
|
|
|
if (isset($this->changedFunctions[$foundFunctionName]) === false) { |
162
|
|
|
// Not one of the target functions. |
163
|
|
|
continue; |
164
|
|
|
} |
165
|
|
|
|
166
|
|
|
/* |
167
|
|
|
* Ok, so is this really a function call to one of the PHP native functions ? |
168
|
|
|
*/ |
169
|
|
|
$next = $phpcsFile->findNext(Tokens::$emptyTokens, ($i + 1), null, true); |
170
|
|
|
if ($next === false || $tokens[$next]['code'] !== T_OPEN_PARENTHESIS) { |
171
|
|
|
// Live coding, parse error or not a function call. |
172
|
|
|
continue; |
173
|
|
|
} |
174
|
|
|
|
175
|
|
|
$prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($i - 1), null, true); |
176
|
|
|
if ($prev !== false) { |
177
|
|
|
if (isset($this->noneFunctionCallIndicators[$tokens[$prev]['code']])) { |
178
|
|
|
continue; |
179
|
|
|
} |
180
|
|
|
|
181
|
|
|
// Check for namespaced functions, ie: \foo\bar() not \bar(). |
182
|
|
View Code Duplication |
if ($tokens[ $prev ]['code'] === T_NS_SEPARATOR) { |
|
|
|
|
183
|
|
|
$pprev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($prev - 1), null, true); |
184
|
|
|
if ($pprev !== false && $tokens[ $pprev ]['code'] === T_STRING) { |
185
|
|
|
continue; |
186
|
|
|
} |
187
|
|
|
} |
188
|
|
|
} |
189
|
|
|
|
190
|
|
|
/* |
191
|
|
|
* Address some special cases. |
192
|
|
|
*/ |
193
|
|
|
if ($foundFunctionName !== 'func_get_args') { |
194
|
|
|
$paramOne = $this->getFunctionCallParameter($phpcsFile, $i, 1); |
195
|
|
|
if ($paramOne !== false) { |
196
|
|
|
switch ($foundFunctionName) { |
197
|
|
|
/* |
198
|
|
|
* Check if `debug_(print_)backtrace()` is called with the |
199
|
|
|
* `DEBUG_BACKTRACE_IGNORE_ARGS` option. |
200
|
|
|
*/ |
201
|
|
|
case 'debug_backtrace': |
202
|
|
|
case 'debug_print_backtrace': |
203
|
|
|
$hasIgnoreArgs = $phpcsFile->findNext( |
204
|
|
|
T_STRING, |
205
|
|
|
$paramOne['start'], |
206
|
|
|
($paramOne['end'] + 1), |
207
|
|
|
false, |
208
|
|
|
'DEBUG_BACKTRACE_IGNORE_ARGS' |
209
|
|
|
); |
210
|
|
|
|
211
|
|
|
if ($hasIgnoreArgs !== false) { |
212
|
|
|
// Debug_backtrace() called with ignore args option. |
213
|
|
|
continue 2; |
214
|
|
|
} |
215
|
|
|
break; |
216
|
|
|
|
217
|
|
|
/* |
218
|
|
|
* Collect the necessary information to only throw a notice if the argument |
219
|
|
|
* touched/changed is in line with the passed $arg_num. |
220
|
|
|
* |
221
|
|
|
* Also, we can ignore `func_get_arg()` if the argument offset passed is |
222
|
|
|
* higher than the number of named parameters. |
223
|
|
|
* |
224
|
|
|
* {@internal Note: This does not take calculations into account! |
225
|
|
|
* Should be exceptionally rare and can - if needs be - be addressed at a later stage.}} |
226
|
|
|
*/ |
227
|
|
|
case 'func_get_arg': |
228
|
|
|
$number = $phpcsFile->findNext(T_LNUMBER, $paramOne['start'], ($paramOne['end'] + 1)); |
229
|
|
View Code Duplication |
if ($number !== false) { |
|
|
|
|
230
|
|
|
$argNumber = $tokens[$number]['content']; |
231
|
|
|
|
232
|
|
|
if (isset($paramNames[$argNumber]) === false) { |
233
|
|
|
// Requesting a non-named additional parameter. Ignore. |
234
|
|
|
continue 2; |
235
|
|
|
} |
236
|
|
|
} |
237
|
|
|
break; |
238
|
|
|
} |
239
|
|
|
} |
240
|
|
|
} else { |
241
|
|
|
/* |
242
|
|
|
* Check if the call to func_get_args() happens to be in an array_slice() or |
243
|
|
|
* array_splice() with an $offset higher than the number of named parameters. |
244
|
|
|
* In that case, we can ignore it. |
245
|
|
|
* |
246
|
|
|
* {@internal Note: This does not take offset calculations into account! |
247
|
|
|
* Should be exceptionally rare and can - if needs be - be addressed at a later stage.}} |
248
|
|
|
*/ |
249
|
|
|
if ($prev !== false && $tokens[$prev]['code'] === T_OPEN_PARENTHESIS) { |
250
|
|
|
|
251
|
|
|
$maybeFunctionCall = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($prev - 1), null, true); |
252
|
|
|
if ($maybeFunctionCall !== false |
253
|
|
|
&& $tokens[$maybeFunctionCall]['code'] === T_STRING |
254
|
|
|
&& ($tokens[$maybeFunctionCall]['content'] === 'array_slice' |
255
|
|
|
|| $tokens[$maybeFunctionCall]['content'] === 'array_splice') |
256
|
|
|
) { |
257
|
|
|
$parentFuncParamTwo = $this->getFunctionCallParameter($phpcsFile, $maybeFunctionCall, 2); |
258
|
|
|
$number = $phpcsFile->findNext( |
259
|
|
|
T_LNUMBER, |
260
|
|
|
$parentFuncParamTwo['start'], |
261
|
|
|
($parentFuncParamTwo['end'] + 1) |
262
|
|
|
); |
263
|
|
|
|
264
|
|
View Code Duplication |
if ($number !== false && isset($paramNames[$tokens[$number]['content']]) === false) { |
|
|
|
|
265
|
|
|
// Requesting non-named additional parameters. Ignore. |
266
|
|
|
continue ; |
267
|
|
|
} |
268
|
|
|
|
269
|
|
|
// Slice starts at a named argument, but we know which params are being accessed. |
270
|
|
|
$paramNamesSubset = array_slice($paramNames, $tokens[$number]['content']); |
271
|
|
|
} |
272
|
|
|
} |
273
|
|
|
} |
274
|
|
|
|
275
|
|
|
/* |
276
|
|
|
* For debug_backtrace(), check if the result is being dereferenced and if so, |
277
|
|
|
* whether the `args` index is used. |
278
|
|
|
* I.e. whether `$index` in `debug_backtrace()[$stackFrame][$index]` is a string |
279
|
|
|
* with the content `args`. |
280
|
|
|
* |
281
|
|
|
* Note: We already know that $next is the open parenthesis of the function call. |
282
|
|
|
*/ |
283
|
|
|
if ($foundFunctionName === 'debug_backtrace' && isset($tokens[$next]['parenthesis_closer'])) { |
284
|
|
|
$afterParenthesis = $phpcsFile->findNext( |
285
|
|
|
Tokens::$emptyTokens, |
286
|
|
|
($tokens[$next]['parenthesis_closer'] + 1), |
287
|
|
|
null, |
288
|
|
|
true |
289
|
|
|
); |
290
|
|
|
|
291
|
|
|
if ($tokens[$afterParenthesis]['code'] === T_OPEN_SQUARE_BRACKET |
292
|
|
|
&& isset($tokens[$afterParenthesis]['bracket_closer']) |
293
|
|
|
) { |
294
|
|
|
$afterStackFrame = $phpcsFile->findNext( |
295
|
|
|
Tokens::$emptyTokens, |
296
|
|
|
($tokens[$afterParenthesis]['bracket_closer'] + 1), |
297
|
|
|
null, |
298
|
|
|
true |
299
|
|
|
); |
300
|
|
|
|
301
|
|
|
if ($tokens[$afterStackFrame]['code'] === T_OPEN_SQUARE_BRACKET |
302
|
|
|
&& isset($tokens[$afterStackFrame]['bracket_closer']) |
303
|
|
|
) { |
304
|
|
|
$arrayIndex = $phpcsFile->findNext( |
305
|
|
|
T_CONSTANT_ENCAPSED_STRING, |
306
|
|
|
($afterStackFrame + 1), |
307
|
|
|
$tokens[$afterStackFrame]['bracket_closer'] |
308
|
|
|
); |
309
|
|
|
|
310
|
|
|
if ($arrayIndex !== false && $this->stripQuotes($tokens[$arrayIndex]['content']) !== 'args') { |
311
|
|
|
continue; |
312
|
|
|
} |
313
|
|
|
} |
314
|
|
|
} |
315
|
|
|
} |
316
|
|
|
|
317
|
|
|
/* |
318
|
|
|
* Only check for variables before the start of the statement to |
319
|
|
|
* prevent false positives on the return value of the function call |
320
|
|
|
* being assigned to one of the parameters, i.e.: |
321
|
|
|
* `$param = func_get_args();`. |
322
|
|
|
*/ |
323
|
|
|
$startOfStatement = PHPCSHelper::findStartOfStatement($phpcsFile, $i, $this->ignoreForStartOfStatement); |
324
|
|
|
|
325
|
|
|
/* |
326
|
|
|
* Ok, so we've found one of the target functions in the right scope. |
327
|
|
|
* Now, let's check if any of the passed parameters were touched. |
328
|
|
|
*/ |
329
|
|
|
$scanResult = 'clean'; |
330
|
|
|
for ($j = ($scopeOpener + 1); $j < $startOfStatement; $j++) { |
331
|
|
View Code Duplication |
if (isset($this->skipPastNested[$tokens[$j]['type']]) |
|
|
|
|
332
|
|
|
&& isset($tokens[$j]['scope_closer']) |
333
|
|
|
) { |
334
|
|
|
// Skip past nested structures. |
335
|
|
|
$j = $tokens[$j]['scope_closer']; |
336
|
|
|
continue; |
337
|
|
|
} |
338
|
|
|
|
339
|
|
|
if ($tokens[$j]['code'] !== T_VARIABLE) { |
340
|
|
|
continue; |
341
|
|
|
} |
342
|
|
|
|
343
|
|
|
if ($foundFunctionName === 'func_get_arg' && isset($argNumber)) { |
344
|
|
|
if (isset($paramNames[$argNumber]) |
345
|
|
|
&& $tokens[$j]['content'] !== $paramNames[$argNumber] |
346
|
|
|
) { |
347
|
|
|
// Different param than the one requested by func_get_arg(). |
348
|
|
|
continue; |
349
|
|
|
} |
350
|
|
|
} elseif ($foundFunctionName === 'func_get_args' && isset($paramNamesSubset)) { |
351
|
|
|
if (in_array($tokens[$j]['content'], $paramNamesSubset, true) === false) { |
352
|
|
|
// Different param than the ones requested by func_get_args(). |
353
|
|
|
continue; |
354
|
|
|
} |
355
|
|
|
} elseif (in_array($tokens[$j]['content'], $paramNames, true) === false) { |
356
|
|
|
// Variable is not one of the function parameters. |
357
|
|
|
continue; |
358
|
|
|
} |
359
|
|
|
|
360
|
|
|
/* |
361
|
|
|
* Ok, so we've found a variable which was passed as one of the parameters. |
362
|
|
|
* Now, is this variable being changed, i.e. incremented, decremented or |
363
|
|
|
* assigned something ? |
364
|
|
|
*/ |
365
|
|
|
$scanResult = 'warning'; |
366
|
|
|
if (isset($variableToken) === false) { |
367
|
|
|
$variableToken = $j; |
368
|
|
|
} |
369
|
|
|
|
370
|
|
|
$beforeVar = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($j - 1), null, true); |
371
|
|
View Code Duplication |
if ($beforeVar !== false && isset($this->plusPlusMinusMinus[$tokens[$beforeVar]['code']])) { |
|
|
|
|
372
|
|
|
// Variable is being (pre-)incremented/decremented. |
373
|
|
|
$scanResult = 'error'; |
374
|
|
|
$variableToken = $j; |
375
|
|
|
break; |
376
|
|
|
} |
377
|
|
|
|
378
|
|
|
$afterVar = $phpcsFile->findNext(Tokens::$emptyTokens, ($j + 1), null, true); |
379
|
|
|
if ($afterVar === false) { |
380
|
|
|
// Shouldn't be possible, but just in case. |
381
|
|
|
continue; |
382
|
|
|
} |
383
|
|
|
|
384
|
|
View Code Duplication |
if (isset($this->plusPlusMinusMinus[$tokens[$afterVar]['code']])) { |
|
|
|
|
385
|
|
|
// Variable is being (post-)incremented/decremented. |
386
|
|
|
$scanResult = 'error'; |
387
|
|
|
$variableToken = $j; |
388
|
|
|
break; |
389
|
|
|
} |
390
|
|
|
|
391
|
|
|
if ($tokens[$afterVar]['code'] === T_OPEN_SQUARE_BRACKET |
392
|
|
|
&& isset($tokens[$afterVar]['bracket_closer']) |
393
|
|
|
) { |
394
|
|
|
// Skip past array access on the variable. |
395
|
|
|
while (($afterVar = $phpcsFile->findNext(Tokens::$emptyTokens, ($tokens[$afterVar]['bracket_closer'] + 1), null, true)) !== false) { |
396
|
|
View Code Duplication |
if ($tokens[$afterVar]['code'] !== T_OPEN_SQUARE_BRACKET |
|
|
|
|
397
|
|
|
|| isset($tokens[$afterVar]['bracket_closer']) === false |
398
|
|
|
) { |
399
|
|
|
break; |
400
|
|
|
} |
401
|
|
|
} |
402
|
|
|
} |
403
|
|
|
|
404
|
|
View Code Duplication |
if ($afterVar !== false |
|
|
|
|
405
|
|
|
&& isset(Tokens::$assignmentTokens[$tokens[$afterVar]['code']]) |
406
|
|
|
) { |
407
|
|
|
// Variable is being assigned something. |
408
|
|
|
$scanResult = 'error'; |
409
|
|
|
$variableToken = $j; |
410
|
|
|
break; |
411
|
|
|
} |
412
|
|
|
} |
413
|
|
|
|
414
|
|
|
unset($argNumber, $paramNamesSubset); |
415
|
|
|
|
416
|
|
|
if ($scanResult === 'clean') { |
417
|
|
|
continue; |
418
|
|
|
} |
419
|
|
|
|
420
|
|
|
$error = 'Since PHP 7.0, functions inspecting arguments, like %1$s(), no longer report the original value as passed to a parameter, but will instead provide the current value. The parameter "%2$s" was %4$s on line %3$s.'; |
421
|
|
|
$data = array( |
422
|
|
|
$foundFunctionName, |
423
|
|
|
$tokens[$variableToken]['content'], |
|
|
|
|
424
|
|
|
$tokens[$variableToken]['line'], |
425
|
|
|
); |
426
|
|
|
|
427
|
|
|
if ($scanResult === 'error') { |
428
|
|
|
$data[] = 'changed'; |
429
|
|
|
$phpcsFile->addError($error, $i, 'Changed', $data); |
430
|
|
|
|
431
|
|
|
} elseif ($scanResult === 'warning') { |
432
|
|
|
$data[] = 'used, and possibly changed (by reference),'; |
433
|
|
|
$phpcsFile->addWarning($error, $i, 'NeedsInspection', $data); |
434
|
|
|
} |
435
|
|
|
|
436
|
|
|
unset($variableToken); |
437
|
|
|
} |
438
|
|
|
} |
439
|
|
|
} |
440
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.