process()   F
last analyzed

Complexity

Conditions 68
Paths > 20000

Size

Total Lines 319

Duplication

Lines 47
Ratio 14.73 %

Importance

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