Completed
Push — php-7.3/continue-in-switch-is-... ( 945a8e )
by Juliette
02:58
created

DiscouragedSwitchContinueSniff   A

Complexity

Total Complexity 29

Size/Duplication

Total Lines 208
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 2

Importance

Changes 0
Metric Value
wmc 29
lcom 1
cbo 2
dl 0
loc 208
rs 10
c 0
b 0
f 0

2 Methods

Rating   Name   Duplication   Size   Complexity  
A register() 0 13 2
F process() 0 132 27
1
<?php
2
/**
3
 * \PHPCompatibility\Sniffs\PHP\DiscouragedSwitchContinue.
4
 *
5
 * PHP version 7.3
6
 *
7
 * @category PHP
8
 * @package  PHPCompatibility
9
 * @author   Juliette Reinders Folmer <[email protected]>
10
 */
11
12
namespace PHPCompatibility\Sniffs\PHP;
13
14
use PHPCompatibility\Sniff;
15
use PHPCompatibility\PHPCSHelper;
16
17
/**
18
 * \PHPCompatibility\Sniffs\PHP\DiscouragedSwitchContinue.
19
 *
20
 * PHP 7.3 will throw a warning when continue is used to target a switch control structure.
21
 *
22
 * PHP version 7.3
23
 *
24
 * @category PHP
25
 * @package  PHPCompatibility
26
 * @author   Juliette Reinders Folmer <[email protected]>
27
 */
28
class DiscouragedSwitchContinueSniff extends Sniff
29
{
30
31
    /**
32
     * Token codes of control structures which can be targeted using continue.
33
     *
34
     * @var array
35
     */
36
    protected $loopStructures = array(
37
        \T_FOR     => \T_FOR,
38
        \T_FOREACH => \T_FOREACH,
39
        \T_WHILE   => \T_WHILE,
40
        \T_DO      => \T_DO,
41
        \T_SWITCH  => \T_SWITCH,
42
    );
43
44
    /**
45
     * Tokens which start a new case within a switch.
46
     *
47
     * @var array
48
     */
49
    protected $caseTokens = array(
50
        \T_CASE    => \T_CASE,
51
        \T_DEFAULT => \T_DEFAULT,
52
    );
53
54
    /**
55
     * Token codes which are accepted to determine the level for the continue.
56
     *
57
     * This array is enriched with the arithmetic operators in the register() method.
58
     *
59
     * @var array
60
     */
61
    protected $acceptedLevelTokens = array(
62
        \T_LNUMBER           => \T_LNUMBER,
63
        \T_OPEN_PARENTHESIS  => \T_OPEN_PARENTHESIS,
64
        \T_CLOSE_PARENTHESIS => \T_CLOSE_PARENTHESIS,
65
    );
66
67
    /**
68
     * PHPCS cross-version compatible version of the Tokens::$emptyTokens array.
69
     *
70
     * @var array
71
     */
72
    private $emptyTokens = array();
73
74
    /**
75
     * Returns an array of tokens this test wants to listen for.
76
     *
77
     * @return array
78
     */
79
    public function register()
80
    {
81
        $arithmeticTokens  = \PHP_CodeSniffer_Tokens::$arithmeticTokens;
82
        $this->emptyTokens = \PHP_CodeSniffer_Tokens::$emptyTokens;
83
        if (version_compare(PHPCSHelper::getVersion(), '2.0', '<')) {
84
            $arithmeticTokens  = array_combine($arithmeticTokens, $arithmeticTokens);
85
            $this->emptyTokens = array_combine($this->emptyTokens, $this->emptyTokens);
86
        }
87
88
        $this->acceptedLevelTokens = $this->acceptedLevelTokens + $arithmeticTokens + $this->emptyTokens;
89
90
        return array(\T_SWITCH);
91
    }
92
93
    /**
94
     * Processes this test, when one of its tokens is encountered.
95
     *
96
     * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
97
     * @param int                   $stackPtr  The position of the current token in the
98
     *                                         stack passed in $tokens.
99
     *
100
     * @return void
101
     */
102
    public function process(\PHP_CodeSniffer_File $phpcsFile, $stackPtr)
103
    {
104
        if ($this->supportsAbove('7.3') === false) {
105
            return;
106
        }
107
108
        $tokens = $phpcsFile->getTokens();
109
110
        if (isset($tokens[$stackPtr]['scope_opener'], $tokens[$stackPtr]['scope_closer']) === false) {
111
            return;
112
        }
113
114
        $switchOpener = $tokens[$stackPtr]['scope_opener'];
115
        $switchCloser = $tokens[$stackPtr]['scope_closer'];
116
117
        // Quick check whether we need to bother with the more complex logic.
118
        $hasContinue = $phpcsFile->findNext(\T_CONTINUE, ($switchOpener + 1), $switchCloser);
119
        if ($hasContinue === false) {
120
            return;
121
        }
122
123
        $caseDefault = $switchOpener;
124
125
        do {
126
            $caseDefault = $phpcsFile->findNext($this->caseTokens, ($caseDefault + 1), $switchCloser);
127
            if ($caseDefault === false) {
128
                break;
129
            }
130
131
            if (isset($tokens[$caseDefault]['scope_opener']) === false) {
132
                // Unknown start of the case, skip.
133
                continue;
134
            }
135
136
            $caseOpener      = $tokens[$caseDefault]['scope_opener'];
137
            $nextCaseDefault = $phpcsFile->findNext($this->caseTokens, ($caseDefault + 1), $switchCloser);
138
            if ($nextCaseDefault === false) {
139
                $caseCloser = $switchCloser;
140
            } else {
141
                $caseCloser = $nextCaseDefault;
142
            }
143
144
            // Check for unscoped control structures within the case.
145
            $controlStructure = $caseOpener;
146
            $doCount          = 0;
147
            while (($controlStructure = $phpcsFile->findNext($this->loopStructures, ($controlStructure + 1), $caseCloser)) !== false) {
148
                if ($tokens[$controlStructure]['code'] === \T_DO) {
149
                    $doCount++;
150
                }
151
152
                if (isset($tokens[$controlStructure]['scope_opener'], $tokens[$controlStructure]['scope_closer']) === false) {
153
                    if ($tokens[$controlStructure]['code'] === \T_WHILE && $doCount > 0) {
154
                        // While in a do-while construct.
155
                        $doCount--;
156
                        continue;
157
                    }
158
159
                    // Control structure without braces found within the case, ignore this case.
160
                    continue 2;
161
                }
162
            }
163
164
            // Examine the contents of the case.
165
            $continue = $caseOpener;
166
167
            do {
168
                $continue = $phpcsFile->findNext(\T_CONTINUE, ($continue + 1), $caseCloser);
169
                if ($continue === false) {
170
                    break;
171
                }
172
173
                $nextSemicolon = $phpcsFile->findNext(array(\T_SEMICOLON, \T_CLOSE_TAG), ($continue + 1), $caseCloser);
174
                $codeString    = '';
175
                for ($i = ($continue + 1); $i < $nextSemicolon; $i++) {
176
                    if (isset($this->acceptedLevelTokens[$tokens[$i]['code']]) === false) {
177
                        // Function call/variable or other token which make numeric level impossible to determine.
178
                        continue 2;
179
                    }
180
181
                    if (isset($this->emptyTokens[$tokens[$i]['code']]) === true) {
182
                        continue;
183
                    }
184
185
                    $codeString .= $tokens[$i]['content'];
186
                }
187
188
                $level = null;
189
                if ($codeString !== '') {
190
                    if (is_numeric($codeString)) {
191
                        $level = (int) $codeString;
192
                    } else {
193
                        // With the above logic, the string can only contain digits and operators, eval!
194
                        $level = eval("return ( $codeString );");
0 ignored issues
show
Coding Style introduced by
It is generally not recommended to use eval unless absolutely required.

On one hand, eval might be exploited by malicious users if they somehow manage to inject dynamic content. On the other hand, with the emergence of faster PHP runtimes like the HHVM, eval prevents some optimization that they perform.

Loading history...
195
                    }
196
                }
197
198
                if (isset($level) === false || $level === 0) {
199
                    $level = 1;
200
                }
201
202
                // Examine which control structure is being targeted by the continue statement.
203
                if (isset($tokens[$continue]['conditions']) === false) {
204
                    continue;
205
                }
206
207
                $conditions = array_reverse($tokens[$continue]['conditions'], true);
208
                // PHPCS adds more structures to the conditions array than we want to take into
209
                // consideration, so clean up the array.
210
                foreach ($conditions as $tokenPtr => $tokenCode) {
211
                    if (isset($this->loopStructures[$tokenCode]) === false) {
212
                        unset($conditions[$tokenPtr]);
213
                    }
214
                }
215
216
                $targetCondition = \array_slice($conditions, ($level - 1), 1, true);
217
                if (empty($targetCondition)) {
218
                    continue;
219
                }
220
221
                $conditionToken = key($targetCondition);
222
                if ($conditionToken === $stackPtr) {
223
                    $phpcsFile->addWarning(
224
                        "Targeting a 'switch' control structure with a 'continue' statement is strongly discouraged and will throw a warning as of PHP 7.3.",
225
                        $continue,
226
                        'Found'
227
                    );
228
                }
229
230
            } while ($continue < $caseCloser);
231
232
        } while ($caseDefault < $switchCloser);
233
    }
234
235
}
236