• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

PHPCSStandards / PHP_CodeSniffer / 15719074832

17 Jun 2025 10:10PM UTC coverage: 78.771% (+0.04%) from 78.728%
15719074832

push

github

web-flow
Generic/InlineControlStructure: bail early for control structures without body (#880)

* Generic/InlineControlStructure: bail early for control structures without body

The sniff now consistently handles all supported control structures
without a body by bailing early. Extending the existing behavior for
`while` and `for` to also include `do while`, `else`, `elseif`, `if`,
and `foreach`.

Previously, the sniff would incorrectly flag these empty control
structures as inline control structures that needed curly braces. For
`else`, `elseif`, `if`, and `foreach`, the fixer would remove the
semicolon and add the curly braces. For `do while`, the fixer would add
the curly braces and keep the semicolon in between the braces. In all
the cases, the resulting code was syntactically correct.

Consider the following example:

```
do ; while ($foo < 5);
```

Previously, PHPCS would flag this as an inline control structure and
PHPCBF would fix it to:

```
do { ;
} while ($foo < 5);
```

Now an empty `do while` is ignored by the sniff (no warnings and no
fixes).

Here is a link showing that control structures without a body are
valid in PHP:

https://3v4l.org/slnYL

And here is a link showing that the way that they were being fixed by
PHPCBF was resulting in valid code (`while` and `for` are not included
below as they were already ignored before this commit):

https://3v4l.org/8k1N3

* Generic/InlineControlStructure: removed unnecessary code block

29d0be3e changed the sniff to bail early for all control structures
without body, so the code will never reach the fixer if `$closer + 1` is
`T_SEMICOLON` and thus the removed condition is not necessary anymore.

* Generic/InlineControlStructure: removed another unnecessary code block

The original version of this part of the code that is now being removed
was added in the early days by the commit that enabled this sniff to fix
errors:

https://github.com/squizlabs/PHP_CodeSniffer/commit/a54c619#diff-4b3945c2100b0a92a5650... (continued)

48 of 48 new or added lines in 1 file covered. (100.0%)

1 existing line in 1 file now uncovered.

19762 of 25088 relevant lines covered (78.77%)

88.94 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.03
/src/Standards/Generic/Sniffs/ControlStructures/InlineControlStructureSniff.php
1
<?php
2
/**
3
 * Verifies that inline control statements are not present.
4
 *
5
 * @author    Greg Sherwood <gsherwood@squiz.net>
6
 * @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
7
 * @license   https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
8
 */
9

10
namespace PHP_CodeSniffer\Standards\Generic\Sniffs\ControlStructures;
11

12
use PHP_CodeSniffer\Files\File;
13
use PHP_CodeSniffer\Sniffs\Sniff;
14
use PHP_CodeSniffer\Util\Tokens;
15

16
class InlineControlStructureSniff implements Sniff
17
{
18

19
    /**
20
     * If true, an error will be thrown; otherwise a warning.
21
     *
22
     * @var boolean
23
     */
24
    public $error = true;
25

26

27
    /**
28
     * Returns an array of tokens this test wants to listen for.
29
     *
30
     * @return array<int|string>
31
     */
32
    public function register()
3✔
33
    {
34
        return [
2✔
35
            T_IF,
3✔
36
            T_ELSE,
3✔
37
            T_ELSEIF,
3✔
38
            T_FOREACH,
3✔
39
            T_WHILE,
3✔
40
            T_DO,
3✔
41
            T_FOR,
3✔
42
        ];
2✔
43

44
    }//end register()
45

46

47
    /**
48
     * Processes this test, when one of its tokens is encountered.
49
     *
50
     * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
51
     * @param int                         $stackPtr  The position of the current token in the
52
     *                                               stack passed in $tokens.
53
     *
54
     * @return void|int
55
     */
56
    public function process(File $phpcsFile, $stackPtr)
3✔
57
    {
58
        $tokens = $phpcsFile->getTokens();
3✔
59

60
        if (isset($tokens[$stackPtr]['scope_opener']) === true) {
3✔
61
            $phpcsFile->recordMetric($stackPtr, 'Control structure defined inline', 'no');
3✔
62
            return;
3✔
63
        }
64

65
        // Ignore the ELSE in ELSE IF. We'll process the IF part later.
66
        if ($tokens[$stackPtr]['code'] === T_ELSE) {
3✔
67
            $next = $phpcsFile->findNext(Tokens::EMPTY_TOKENS, ($stackPtr + 1), null, true);
3✔
68
            if ($tokens[$next]['code'] === T_IF) {
3✔
69
                return;
3✔
70
            }
71
        }
72

73
        if (isset($tokens[$stackPtr]['parenthesis_closer']) === true) {
3✔
74
            $nextTokenIndex = ($tokens[$stackPtr]['parenthesis_closer'] + 1);
3✔
75
        } else if (in_array($tokens[$stackPtr]['code'], [T_ELSE, T_DO], true) === true) {
3✔
76
            $nextTokenIndex = ($stackPtr + 1);
3✔
77
        }
78

79
        if (isset($nextTokenIndex) === true) {
3✔
80
            $firstNonEmptyToken = $phpcsFile->findNext(Tokens::EMPTY_TOKENS, $nextTokenIndex, null, true);
3✔
81
            if ($firstNonEmptyToken === false) {
3✔
82
                // Live coding.
83
                return;
3✔
84
            }
85

86
            if ($tokens[$firstNonEmptyToken]['code'] === T_SEMICOLON) {
3✔
87
                // This is a control structure without a body. Bow out.
88
                $phpcsFile->recordMetric($stackPtr, 'Control structure defined inline', 'no');
3✔
89
                return;
3✔
90
            }
91
        }
92

93
        if (isset($tokens[$stackPtr]['parenthesis_opener'], $tokens[$stackPtr]['parenthesis_closer']) === false
3✔
94
            && $tokens[$stackPtr]['code'] !== T_ELSE
3✔
95
        ) {
96
            if ($tokens[$stackPtr]['code'] !== T_DO) {
3✔
97
                // Live coding or parse error.
98
                return;
3✔
99
            }
100

101
            $nextWhile = $phpcsFile->findNext(T_WHILE, ($stackPtr + 1));
3✔
102
            if ($nextWhile !== false
3✔
103
                && isset($tokens[$nextWhile]['parenthesis_opener'], $tokens[$nextWhile]['parenthesis_closer']) === false
3✔
104
            ) {
105
                // Live coding or parse error.
106
                return;
3✔
107
            }
108

109
            unset($nextWhile);
×
110
        }
111

112
        $start = $stackPtr;
3✔
113
        if (isset($tokens[$stackPtr]['parenthesis_closer']) === true) {
3✔
114
            $start = $tokens[$stackPtr]['parenthesis_closer'];
3✔
115
        }
116

117
        $nextNonEmpty = $phpcsFile->findNext(Tokens::EMPTY_TOKENS, ($start + 1), null, true);
3✔
118
        if ($nextNonEmpty === false) {
3✔
119
            // Live coding or parse error.
UNCOV
120
            return;
×
121
        }
122

123
        if ($tokens[$nextNonEmpty]['code'] === T_OPEN_CURLY_BRACKET
3✔
124
            || $tokens[$nextNonEmpty]['code'] === T_COLON
3✔
125
        ) {
126
            // T_CLOSE_CURLY_BRACKET missing, or alternative control structure with
127
            // T_END... missing. Either live coding, parse error or end
128
            // tag in short open tags and scan run with short_open_tag=Off.
129
            // Bow out completely as any further detection will be unreliable
130
            // and create incorrect fixes or cause fixer conflicts.
131
            return $phpcsFile->numTokens;
2✔
132
        }
133

134
        unset($nextNonEmpty, $start);
3✔
135

136
        // This is a control structure without an opening brace,
137
        // so it is an inline statement.
138
        if ($this->error === true) {
3✔
139
            $fix = $phpcsFile->addFixableError('Inline control structures are not allowed', $stackPtr, 'NotAllowed');
3✔
140
        } else {
141
            $fix = $phpcsFile->addFixableWarning('Inline control structures are discouraged', $stackPtr, 'Discouraged');
×
142
        }
143

144
        $phpcsFile->recordMetric($stackPtr, 'Control structure defined inline', 'yes');
3✔
145

146
        // Stop here if we are not fixing the error.
147
        if ($fix !== true) {
3✔
148
            return;
3✔
149
        }
150

151
        $phpcsFile->fixer->beginChangeset();
3✔
152
        if (isset($tokens[$stackPtr]['parenthesis_closer']) === true) {
3✔
153
            $closer = $tokens[$stackPtr]['parenthesis_closer'];
3✔
154
        } else {
155
            $closer = $stackPtr;
3✔
156
        }
157

158
        if ($tokens[($closer + 1)]['code'] === T_WHITESPACE) {
3✔
159
            $phpcsFile->fixer->addContent($closer, ' {');
3✔
160
        } else {
161
            $phpcsFile->fixer->addContent($closer, ' { ');
×
162
        }
163

164
        $fixableScopeOpeners = $this->register();
3✔
165

166
        $lastNonEmpty = $closer;
3✔
167
        for ($end = ($closer + 1); $end < $phpcsFile->numTokens; $end++) {
3✔
168
            if ($tokens[$end]['code'] === T_SEMICOLON) {
3✔
169
                break;
3✔
170
            }
171

172
            if ($tokens[$end]['code'] === T_CLOSE_TAG) {
3✔
173
                $end = $lastNonEmpty;
3✔
174
                break;
3✔
175
            }
176

177
            if (in_array($tokens[$end]['code'], $fixableScopeOpeners, true) === true
3✔
178
                && isset($tokens[$end]['scope_opener']) === false
3✔
179
            ) {
180
                // The best way to fix nested inline scopes is middle-out.
181
                // So skip this one. It will be detected and fixed on a future loop.
182
                $phpcsFile->fixer->rollbackChangeset();
3✔
183
                return;
3✔
184
            }
185

186
            if (isset($tokens[$end]['scope_opener']) === true) {
3✔
187
                $type = $tokens[$end]['code'];
3✔
188
                $end  = $tokens[$end]['scope_closer'];
3✔
189
                if ($type === T_DO
3✔
190
                    || $type === T_IF || $type === T_ELSEIF
3✔
191
                    || $type === T_TRY || $type === T_CATCH || $type === T_FINALLY
3✔
192
                ) {
193
                    $next = $phpcsFile->findNext(Tokens::EMPTY_TOKENS, ($end + 1), null, true);
3✔
194
                    if ($next === false) {
3✔
195
                        break;
×
196
                    }
197

198
                    $nextType = $tokens[$next]['code'];
3✔
199

200
                    // Let additional conditions loop and find their ending.
201
                    if (($type === T_IF
3✔
202
                        || $type === T_ELSEIF)
3✔
203
                        && ($nextType === T_ELSEIF
3✔
204
                        || $nextType === T_ELSE)
3✔
205
                    ) {
206
                        continue;
3✔
207
                    }
208

209
                    // Account for TRY... CATCH/FINALLY statements.
210
                    if (($type === T_TRY
3✔
211
                        || $type === T_CATCH
3✔
212
                        || $type === T_FINALLY)
3✔
213
                        && ($nextType === T_CATCH
3✔
214
                        || $nextType === T_FINALLY)
3✔
215
                    ) {
216
                        continue;
3✔
217
                    }
218

219
                    // Account for DO... WHILE conditions.
220
                    if ($type === T_DO && $nextType === T_WHILE) {
3✔
221
                        $end = $phpcsFile->findNext(T_SEMICOLON, ($next + 1));
3✔
222
                    }
223
                } else if ($type === T_CLOSURE) {
3✔
224
                    // There should be a semicolon after the closing brace.
225
                    $next = $phpcsFile->findNext(Tokens::EMPTY_TOKENS, ($end + 1), null, true);
3✔
226
                    if ($next !== false && $tokens[$next]['code'] === T_SEMICOLON) {
3✔
227
                        $end = $next;
3✔
228
                    }
229
                }//end if
230

231
                if ($tokens[$end]['code'] !== T_END_HEREDOC
3✔
232
                    && $tokens[$end]['code'] !== T_END_NOWDOC
3✔
233
                ) {
234
                    break;
3✔
235
                }
236
            }//end if
237

238
            if (isset($tokens[$end]['parenthesis_closer']) === true) {
3✔
239
                $end          = $tokens[$end]['parenthesis_closer'];
3✔
240
                $lastNonEmpty = $end;
3✔
241
                continue;
3✔
242
            }
243

244
            if ($tokens[$end]['code'] !== T_WHITESPACE) {
3✔
245
                $lastNonEmpty = $end;
3✔
246
            }
247
        }//end for
248

249
        if ($end === $phpcsFile->numTokens) {
3✔
250
            $end = $lastNonEmpty;
×
251
        }
252

253
        $nextContent = $phpcsFile->findNext(Tokens::EMPTY_TOKENS, ($end + 1), null, true);
3✔
254

255
        if ($nextContent === false || $tokens[$nextContent]['line'] !== $tokens[$end]['line']) {
3✔
256
            // Account for a comment on the end of the line.
257
            for ($endLine = $end; $endLine < $phpcsFile->numTokens; $endLine++) {
3✔
258
                if (isset($tokens[($endLine + 1)]) === false
3✔
259
                    || $tokens[$endLine]['line'] !== $tokens[($endLine + 1)]['line']
3✔
260
                ) {
261
                    break;
3✔
262
                }
263
            }
264

265
            if (isset(Tokens::COMMENT_TOKENS[$tokens[$endLine]['code']]) === false
3✔
266
                && ($tokens[$endLine]['code'] !== T_WHITESPACE
3✔
267
                || isset(Tokens::COMMENT_TOKENS[$tokens[($endLine - 1)]['code']]) === false)
3✔
268
            ) {
269
                $endLine = $end;
3✔
270
            }
271
        } else {
272
            $endLine = $end;
3✔
273
        }
274

275
        if ($endLine !== $end) {
3✔
276
            $endToken     = $endLine;
3✔
277
            $addedContent = '';
3✔
278
        } else {
279
            $endToken     = $end;
3✔
280
            $addedContent = $phpcsFile->eolChar;
3✔
281

282
            if ($tokens[$end]['code'] !== T_SEMICOLON
3✔
283
                && $tokens[$end]['code'] !== T_CLOSE_CURLY_BRACKET
3✔
284
            ) {
285
                $phpcsFile->fixer->addContent($end, '; ');
3✔
286
            }
287
        }
288

289
        $next = $phpcsFile->findNext(T_WHITESPACE, ($endToken + 1), null, true);
3✔
290
        if ($next !== false
3✔
291
            && ($tokens[$next]['code'] === T_ELSE
3✔
292
            || $tokens[$next]['code'] === T_ELSEIF)
3✔
293
        ) {
294
            $phpcsFile->fixer->addContentBefore($next, '} ');
3✔
295
        } else {
296
            $indent = '';
3✔
297
            for ($first = $stackPtr; $first > 0; $first--) {
3✔
298
                if ($tokens[$first]['column'] === 1) {
3✔
299
                    break;
3✔
300
                }
301
            }
302

303
            if ($tokens[$first]['code'] === T_WHITESPACE) {
3✔
304
                $indent = $tokens[$first]['content'];
3✔
305
            } else if ($tokens[$first]['code'] === T_INLINE_HTML
3✔
306
                || $tokens[$first]['code'] === T_OPEN_TAG
3✔
307
            ) {
308
                $addedContent = '';
3✔
309
            }
310

311
            $addedContent .= $indent.'}';
3✔
312
            if ($next !== false && $tokens[$endToken]['code'] === T_COMMENT) {
3✔
313
                $addedContent .= $phpcsFile->eolChar;
3✔
314
            }
315

316
            $phpcsFile->fixer->addContent($endToken, $addedContent);
3✔
317
        }//end if
318

319
        $phpcsFile->fixer->endChangeset();
3✔
320

321
    }//end process()
1✔
322

323

324
}//end class
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc