ForbiddenThisUseContextsSniff::process()   F
last analyzed

Complexity

Conditions 32
Paths 21

Size

Total Lines 163

Duplication

Lines 18
Ratio 11.04 %

Importance

Changes 0
Metric Value
dl 18
loc 163
rs 3.3333
c 0
b 0
f 0
cc 32
nc 21
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\Variables;
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
 * Detect using `$this` in incompatible contexts.
20
 *
21
 * "Whilst `$this` is considered a special variable in PHP, it lacked proper checks
22
 *  to ensure it wasn't used as a variable name or reassigned. This has now been
23
 *  rectified to ensure that `$this` cannot be a user-defined variable, reassigned
24
 *  to a different value, or be globalised."
25
 *
26
 * This sniff only addresses those situations which did *not* throw an error prior
27
 * to PHP 7.1, either at all or only in PHP 7.0.
28
 * In other words, the following situation, while mentioned in the RFC, will NOT
29
 * be sniffed for:
30
 * - Using $this as static variable. (error _message_ change only).
31
 *
32
 * Also, the changes with relation to assigning `$this` dynamically can not be
33
 * sniffed for reliably, so are not covered by this sniff.
34
 * - Disable ability to re-assign `$this` indirectly through `$$`.
35
 * - Disable ability to re-assign `$this` indirectly through reference.
36
 * - Disable ability to re-assign `$this` indirectly through `extract()` and `parse_str()`.
37
 *
38
 * Other changes not (yet) covered:
39
 * - `get_defined_vars()` always doesn't show value of variable `$this`.
40
 * - Always show true `$this` value in magic method `__call()`.
41
 *   {@internal This could possibly be covered. Similar logic as "outside object context",
42
 *   but with function name check and supportsBelow('7.0').}
43
 *
44
 * PHP version 7.1
45
 *
46
 * @link https://www.php.net/manual/en/migration71.other-changes.php#migration71.other-changes.inconsistency-fixes-to-this
47
 * @link https://wiki.php.net/rfc/this_var
48
 *
49
 * @since 9.1.0
50
 */
51
class ForbiddenThisUseContextsSniff extends Sniff
52
{
53
54
    /**
55
     * OO scope tokens.
56
     *
57
     * Duplicate of Tokens::$ooScopeTokens array in PHPCS which was added in 3.1.0.
58
     *
59
     * @since 9.1.0
60
     *
61
     * @var array
62
     */
63
    private $ooScopeTokens = array(
64
        'T_CLASS'     => \T_CLASS,
65
        'T_INTERFACE' => \T_INTERFACE,
66
        'T_TRAIT'     => \T_TRAIT,
67
    );
68
69
    /**
70
     * Scopes to skip over when examining the contents of functions.
71
     *
72
     * @since 9.1.0
73
     *
74
     * @var array
75
     */
76
    private $skipOverScopes = array(
77
        'T_FUNCTION' => true,
78
        'T_CLOSURE'  => true,
79
    );
80
81
    /**
82
     * Valid uses of $this in plain functions or methods outside object context.
83
     *
84
     * @since 9.1.0
85
     *
86
     * @var array
87
     */
88
    private $validUseOutsideObject = array(
89
        \T_ISSET => true,
90
        \T_EMPTY => true,
91
    );
92
93
    /**
94
     * Returns an array of tokens this test wants to listen for.
95
     *
96
     * @since 9.1.0
97
     *
98
     * @return array
99
     */
100
    public function register()
101
    {
102
        if (\defined('T_ANON_CLASS')) {
103
            $this->ooScopeTokens['T_ANON_CLASS'] = \T_ANON_CLASS;
104
        }
105
106
        $this->skipOverScopes += $this->ooScopeTokens;
107
108
        return array(
109
            \T_FUNCTION,
110
            \T_CLOSURE,
111
            \T_GLOBAL,
112
            \T_CATCH,
113
            \T_FOREACH,
114
            \T_UNSET,
115
        );
116
    }
117
118
    /**
119
     * Processes this test, when one of its tokens is encountered.
120
     *
121
     * @since 9.1.0
122
     *
123
     * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
124
     * @param int                   $stackPtr  The position of the current token in
125
     *                                         the stack passed in $tokens.
126
     *
127
     * @return void
128
     */
129
    public function process(File $phpcsFile, $stackPtr)
130
    {
131
        if ($this->supportsAbove('7.1') === false) {
132
            return;
133
        }
134
135
        $tokens = $phpcsFile->getTokens();
136
137
        switch ($tokens[$stackPtr]['code']) {
138
            case \T_FUNCTION:
139
                $this->isThisUsedAsParameter($phpcsFile, $stackPtr);
140
                $this->isThisUsedOutsideObjectContext($phpcsFile, $stackPtr);
141
                break;
142
143
            case \T_CLOSURE:
144
                $this->isThisUsedAsParameter($phpcsFile, $stackPtr);
145
                break;
146
147
            case \T_GLOBAL:
148
                /*
149
                 * $this can no longer be imported using the `global` keyword.
150
                 * This worked in PHP 7.0, though in PHP 5.x, it would throw a
151
                 * fatal "Cannot re-assign $this" error.
152
                 */
153
                $endOfStatement = $phpcsFile->findNext(array(\T_SEMICOLON, \T_CLOSE_TAG), ($stackPtr + 1));
154
                if ($endOfStatement === false) {
155
                    // No semi-colon - live coding.
156
                    return;
157
                }
158
159
                for ($i = ($stackPtr + 1); $i < $endOfStatement; $i++) {
160 View Code Duplication
                    if ($tokens[$i]['code'] !== \T_VARIABLE || $tokens[$i]['content'] !== '$this') {
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...
161
                        continue;
162
                    }
163
164
                    $phpcsFile->addError(
165
                        '"$this" can no longer be used with the "global" keyword since PHP 7.1.',
166
                        $i,
167
                        'Global'
168
                    );
169
                }
170
171
                break;
172
173
            case \T_CATCH:
174
                /*
175
                 * $this can no longer be used as a catch variable.
176
                 */
177 View Code Duplication
                if (isset($tokens[$stackPtr]['parenthesis_opener'], $tokens[$stackPtr]['parenthesis_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...
178
                    return;
179
                }
180
181
                $varPtr = $phpcsFile->findNext(
182
                    \T_VARIABLE,
183
                    ($tokens[$stackPtr]['parenthesis_opener'] + 1),
184
                    $tokens[$stackPtr]['parenthesis_closer']
185
                );
186
187
                if ($varPtr === false || $tokens[$varPtr]['content'] !== '$this') {
188
                    return;
189
                }
190
191
                $phpcsFile->addError(
192
                    '"$this" can no longer be used as a catch variable since PHP 7.1.',
193
                    $varPtr,
194
                    'Catch'
195
                );
196
197
                break;
198
199
            case \T_FOREACH:
200
                /*
201
                 * $this can no longer be used as a foreach *value* variable.
202
                 * This worked in PHP 7.0, though in PHP 5.x, it would throw a
203
                 * fatal "Cannot re-assign $this" error.
204
                 */
205 View Code Duplication
                if (isset($tokens[$stackPtr]['parenthesis_opener'], $tokens[$stackPtr]['parenthesis_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...
206
                    return;
207
                }
208
209
                $stopPtr = $phpcsFile->findPrevious(
210
                    array(\T_AS, \T_DOUBLE_ARROW),
211
                    ($tokens[$stackPtr]['parenthesis_closer'] - 1),
212
                    $tokens[$stackPtr]['parenthesis_opener']
213
                );
214
                if ($stopPtr === false) {
215
                    return;
216
                }
217
218
                $valueVarPtr = $phpcsFile->findNext(
219
                    \T_VARIABLE,
220
                    ($stopPtr + 1),
221
                    $tokens[$stackPtr]['parenthesis_closer']
222
                );
223
                if ($valueVarPtr === false || $tokens[$valueVarPtr]['content'] !== '$this') {
224
                    return;
225
                }
226
227
                $afterThis = $phpcsFile->findNext(
228
                    Tokens::$emptyTokens,
229
                    ($valueVarPtr + 1),
230
                    $tokens[$stackPtr]['parenthesis_closer'],
231
                    true
232
                );
233
234 View Code Duplication
                if ($afterThis !== 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...
235
                    && ($tokens[$afterThis]['code'] === \T_OBJECT_OPERATOR
236
                        || $tokens[$afterThis]['code'] === \T_DOUBLE_COLON)
237
                ) {
238
                    return;
239
                }
240
241
                $phpcsFile->addError(
242
                    '"$this" can no longer be used as value variable in a foreach control structure since PHP 7.1.',
243
                    $valueVarPtr,
244
                    'ForeachValueVar'
245
                );
246
247
                break;
248
249
            case \T_UNSET:
250
                /*
251
                 * $this can no longer be unset.
252
                 */
253
                $openParenthesis = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true);
254
                if ($openParenthesis === false
255
                    || $tokens[$openParenthesis]['code'] !== \T_OPEN_PARENTHESIS
256
                    || isset($tokens[$openParenthesis]['parenthesis_closer']) === false
257
                ) {
258
                    return;
259
                }
260
261
                for ($i = ($openParenthesis + 1); $i < $tokens[$openParenthesis]['parenthesis_closer']; $i++) {
262 View Code Duplication
                    if ($tokens[$i]['code'] !== \T_VARIABLE || $tokens[$i]['content'] !== '$this') {
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...
263
                        continue;
264
                    }
265
266
                    $afterThis = $phpcsFile->findNext(
267
                        Tokens::$emptyTokens,
268
                        ($i + 1),
269
                        $tokens[$openParenthesis]['parenthesis_closer'],
270
                        true
271
                    );
272
273
                    if ($afterThis !== false
274
                        && ($tokens[$afterThis]['code'] === \T_OBJECT_OPERATOR
275
                            || $tokens[$afterThis]['code'] === \T_DOUBLE_COLON
276
                            || $tokens[$afterThis]['code'] === \T_OPEN_SQUARE_BRACKET)
277
                    ) {
278
                        $i = $afterThis;
279
                        continue;
280
                    }
281
282
                    $phpcsFile->addError(
283
                        '"$this" can no longer be unset since PHP 7.1.',
284
                        $i,
285
                        'Unset'
286
                    );
287
                }
288
289
                break;
290
        }
291
    }
292
293
    /**
294
     * Check if $this is used as a parameter in a function declaration.
295
     *
296
     * $this can no longer be used as a parameter in a *global* function.
297
     * Use as a parameter in a method was already an error prior to PHP 7.1.
298
     *
299
     * @since 9.1.0
300
     *
301
     * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
302
     * @param int                   $stackPtr  The position of the current token in
303
     *                                         the stack passed in $tokens.
304
     *
305
     * @return void
306
     */
307
    protected function isThisUsedAsParameter(File $phpcsFile, $stackPtr)
308
    {
309
        if ($this->validDirectScope($phpcsFile, $stackPtr, $this->ooScopeTokens) !== false) {
310
            return;
311
        }
312
313
        $params = PHPCSHelper::getMethodParameters($phpcsFile, $stackPtr);
314
        if (empty($params)) {
315
            return;
316
        }
317
318
        $tokens = $phpcsFile->getTokens();
319
320
        foreach ($params as $param) {
321
            if ($param['name'] !== '$this') {
322
                continue;
323
            }
324
325
            if ($tokens[$stackPtr]['code'] === \T_FUNCTION) {
326
                $phpcsFile->addError(
327
                    '"$this" can no longer be used as a parameter since PHP 7.1.',
328
                    $param['token'],
329
                    'FunctionParam'
330
                );
331
            } else {
332
                $phpcsFile->addError(
333
                    '"$this" can no longer be used as a closure parameter since PHP 7.0.7.',
334
                    $param['token'],
335
                    'ClosureParam'
336
                );
337
            }
338
        }
339
    }
340
341
    /**
342
     * Check if $this is used in a plain function or method.
343
     *
344
     * Prior to PHP 7.1, this would result in an "undefined variable" notice
345
     * and execution would continue with $this regarded as `null`.
346
     * As of PHP 7.1, this throws an exception.
347
     *
348
     * Note: use within isset() and empty() to check object context is still allowed.
349
     * Note: $this can still be used within a closure.
350
     *
351
     * @since 9.1.0
352
     *
353
     * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
354
     * @param int                   $stackPtr  The position of the current token in
355
     *                                         the stack passed in $tokens.
356
     *
357
     * @return void
358
     */
359
    protected function isThisUsedOutsideObjectContext(File $phpcsFile, $stackPtr)
360
    {
361
        $tokens = $phpcsFile->getTokens();
362
363 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...
364
            return;
365
        }
366
367
        if ($this->validDirectScope($phpcsFile, $stackPtr, $this->ooScopeTokens) !== false) {
368
            $methodProps = $phpcsFile->getMethodProperties($stackPtr);
369
            if ($methodProps['is_static'] === false) {
370
                return;
371
            } else {
372
                $methodName = $phpcsFile->getDeclarationName($stackPtr);
373
                if ($methodName === '__call') {
374
                    /*
375
                     * This is an exception.
376
                     * @link https://wiki.php.net/rfc/this_var#always_show_true_this_value_in_magic_method_call
377
                     */
378
                    return;
379
                }
380
            }
381
        }
382
383
        for ($i = ($tokens[$stackPtr]['scope_opener'] + 1); $i < $tokens[$stackPtr]['scope_closer']; $i++) {
384
            if (isset($this->skipOverScopes[$tokens[$i]['type']])) {
385
                if (isset($tokens[$i]['scope_closer']) === false) {
386
                    // Live coding or parse error, will only lead to inaccurate results.
387
                    return;
388
                }
389
390
                // Skip over nested structures.
391
                $i = $tokens[$i]['scope_closer'];
392
                continue;
393
            }
394
395 View Code Duplication
            if ($tokens[$i]['code'] !== \T_VARIABLE || $tokens[$i]['content'] !== '$this') {
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...
396
                continue;
397
            }
398
399
            if (isset($tokens[$i]['nested_parenthesis']) === true) {
400
                $nestedParenthesis     = $tokens[$i]['nested_parenthesis'];
401
                $nestedOpenParenthesis = array_keys($nestedParenthesis);
402
                $lastOpenParenthesis   = array_pop($nestedOpenParenthesis);
403
404
                $previousNonEmpty = $phpcsFile->findPrevious(
405
                    Tokens::$emptyTokens,
406
                    ($lastOpenParenthesis - 1),
407
                    null,
408
                    true,
409
                    null,
410
                    true
411
                );
412
413
                if (isset($this->validUseOutsideObject[$tokens[$previousNonEmpty]['code']])) {
414
                    continue;
415
                }
416
            }
417
418
            $phpcsFile->addError(
419
                '"$this" can no longer be used in a plain function or method since PHP 7.1.',
420
                $i,
421
                'OutsideObjectContext'
422
            );
423
        }
424
    }
425
}
426