Completed
Push — master ( 8ac4b1...9b48a3 )
by Juliette
12s
created

process()   F

Complexity

Conditions 68
Paths > 20000

Size

Total Lines 319

Duplication

Lines 55
Ratio 17.24 %

Importance

Changes 0
Metric Value
dl 55
loc 319
rs 0
c 0
b 0
f 0
cc 68
nc 32063
nop 2

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
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) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
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) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
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) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
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']])
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
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']])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
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']])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
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
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
397
                            || isset($tokens[$afterVar]['bracket_closer']) === false
398
                        ) {
399
                            break;
400
                        }
401
                    }
402
                }
403
404 View Code Duplication
                if ($afterVar !== false
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
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'],
0 ignored issues
show
Bug introduced by
The variable $variableToken does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
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