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

PHPCompatibility / PHPCompatibility / 19804455605

30 Nov 2025 08:31PM UTC coverage: 98.306% (-0.04%) from 98.346%
19804455605

push

github

web-flow
Merge pull request #2014 from PHPCompatibility/php-8.5/new-removedterminatingcasewithsemicolon-sniff

PHP 8.5 | ✨ New `PHPCompatibility.ControlStructures.RemovedTerminatingCaseWithSemicolon` sniff (RFC)

30 of 34 new or added lines in 1 file covered. (88.24%)

21 existing lines in 15 files now uncovered.

8355 of 8499 relevant lines covered (98.31%)

20.51 hits per line

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

98.85
/PHPCompatibility/Sniffs/ParameterValues/NewExitAsFunctionCallSniff.php
1
<?php
2
/**
3
 * PHPCompatibility, an external standard for PHP_CodeSniffer.
4
 *
5
 * @package   PHPCompatibility
6
 * @copyright 2012-2020 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\ParameterValues;
12

13
use PHPCompatibility\AbstractFunctionCallParameterSniff;
14
use PHPCompatibility\Helpers\MiscHelper;
15
use PHPCompatibility\Helpers\ScannedCode;
16
use PHP_CodeSniffer\Files\File;
17
use PHP_CodeSniffer\Util\Tokens;
18
use PHPCSUtils\Utils\GetTokensAsString;
19
use PHPCSUtils\Utils\PassedParameters;
20

21
/**
22
 * Detects the use of exit as a function call, as allowed since PHP 8.4.
23
 *
24
 * Prior to PHP 8.4, exit/die was a language construct.
25
 * Since PHP 8.4, it is a proper function, with internal handling of its use as a constant
26
 * (by changing this to a function call at compile time).
27
 *
28
 * As a consequence of this, exit/die can now:
29
 * - be called with a named argument (handled in the `NewNamedParameters` sniff);
30
 * - passed to functions as a callable (can't be reliably detected);
31
 * - passed to functions as a first class callable (handled in the `NewFirstClassCallables` sniff);
32
 * - have a trailing comma after its parameter (handled in the `NewFunctionCallTrailingComma` sniff);
33
 * - respect strict_types and follow type juggling semantics (handled in this sniff).
34
 *
35
 * Passing as a callable can only reliable be detected when passed as a first class callable as
36
 * when `exit`/`die` is passed as a text string, the chance of flagging a false positive is too high.
37
 *
38
 * As for the type juggling part: this can only be detected when the parameter is passed hard-coded, but
39
 * in that case, this sniff can detect it with high precision.
40
 *
41
 * PHP version 8.4
42
 *
43
 * @link https://wiki.php.net/rfc/exit-as-function
44
 *
45
 * @since 10.0.0
46
 */
47
final class NewExitAsFunctionCallSniff extends AbstractFunctionCallParameterSniff
48
{
49

50
    /**
51
     * Functions to check for.
52
     *
53
     * @since 10.0.0
54
     *
55
     * @var array<string, true>
56
     */
57
    protected $targetFunctions = [
58
        'exit' => true,
59
        'die'  => true,
60
    ];
61

62
    /**
63
     * All constants natively declared by PHP.
64
     *
65
     * @since 10.0.0
66
     *
67
     * @var array<string, mixed>
68
     */
69
    private $phpNativeConstants = [];
70

71
    /**
72
     * Current file being scanned.
73
     *
74
     * @since 10.0.0
75
     *
76
     * @var string
77
     */
78
    private $currentFile = '';
79

80
    /**
81
     * Whether strict types are in effect in the current file.
82
     *
83
     * @since 10.0.0
84
     *
85
     * @var bool
86
     */
87
    private $strictTypes = false;
88

89
    /**
90
     * Returns an array of tokens this test wants to listen for.
91
     *
92
     * @since 10.0.0
93
     *
94
     * @return array<int|string>
95
     */
96
    public function register()
8✔
97
    {
98
        // Get the PHP natively defined constants only once.
99
        $constants = \get_defined_constants(true);
8✔
100
        unset($constants['user']);
8✔
101

102
        $this->phpNativeConstants = [];
8✔
103
        foreach ($constants as $group) {
8✔
104
            $this->phpNativeConstants += $group;
8✔
105
        }
106

107
        // Call the parent method to set up some properties for the abstract.
108
        parent::register();
8✔
109

110
        // ... but register our own target tokens.
111
        return [
4✔
112
            \T_DECLARE,
8✔
113
            \T_EXIT,
4✔
114
        ];
4✔
115
    }
116

117
    /**
118
     * Processes this test, when one of its tokens is encountered.
119
     *
120
     * @since 10.0.0
121
     *
122
     * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
123
     * @param int                         $stackPtr  The position of the current token in
124
     *                                               the stack passed in $tokens.
125
     *
126
     * @return int|void Integer stack pointer to skip forward or void to continue
127
     *                  normal file processing.
128
     */
129
    public function process(File $phpcsFile, $stackPtr)
16✔
130
    {
131
        if ($this->bowOutEarly() === true) {
16✔
132
            return;
8✔
133
        }
134

135
        $fileName = $phpcsFile->getFilename();
8✔
136
        if ($this->currentFile !== $fileName) {
8✔
137
            // Reset the declare statement related properties for each new file.
138
            $this->currentFile = $fileName;
8✔
139
            $this->strictTypes = false;
8✔
140
        }
141

142
        /*
143
         * Check for strict types declarations.
144
         *
145
         * Ignore any invalid/incomplete declare statements.
146
         */
147
        $tokens = $phpcsFile->getTokens();
8✔
148
        if ($tokens[$stackPtr]['code'] === \T_DECLARE) {
8✔
149
            if (isset($tokens[$stackPtr]['parenthesis_opener'], $tokens[$stackPtr]['parenthesis_closer']) === false) {
8✔
150
                // Live coding or parse error.
151
                return;
4✔
152
            }
153

154
            $declarations = GetTokensAsString::noEmpties(
4✔
155
                $phpcsFile,
4✔
156
                ($tokens[$stackPtr]['parenthesis_opener'] + 1),
4✔
157
                ($tokens[$stackPtr]['parenthesis_closer'] - 1)
4✔
158
            );
2✔
159

160
            if (\preg_match('`\bstrict_types=([01])`i', $declarations, $matches) === 1) {
4✔
161
                if ($matches[1] === '1') {
4✔
162
                    $this->strictTypes = true;
4✔
163
                } else {
164
                    $this->strictTypes = false;
4✔
165
                }
166
            }
167

168
            return;
4✔
169
        }
170

171
        // Check if this is exit/die used as a fully qualified function call.
172
        if ($tokens[$stackPtr]['content'][0] === '\\') {
4✔
173
            $phpcsFile->addError(
4✔
174
                'Using "%s" as a fully qualified function call is not allowed in PHP 8.3 or earlier.',
4✔
175
                $stackPtr,
4✔
176
                'FullyQualified',
4✔
177
                [\ltrim($tokens[$stackPtr]['content'], '\\')]
4✔
178
            );
2✔
179
        }
180

181
        return parent::process($phpcsFile, $stackPtr);
4✔
182
    }
183

184
    /**
185
     * Do a version check to determine if this sniff needs to run at all.
186
     *
187
     * @since 10.0.0
188
     *
189
     * @return bool
190
     */
191
    protected function bowOutEarly()
16✔
192
    {
193
        return (ScannedCode::shouldRunOnOrAbove('8.4') === false);
16✔
194
    }
195

196
    /**
197
     * Process the parameters of a matched function.
198
     *
199
     * @since 10.0.0
200
     *
201
     * @param \PHP_CodeSniffer\Files\File                  $phpcsFile    The file being scanned.
202
     * @param int                                          $stackPtr     The position of the current token in the stack.
203
     * @param string                                       $functionName The token content (function name) which was matched.
204
     * @param array<int|string, array<string, int|string>> $parameters   Array with information about the parameters.
205
     *
206
     * @return void
207
     */
208
    public function processParameters(File $phpcsFile, $stackPtr, $functionName, $parameters)
4✔
209
    {
210
        $targetParam = PassedParameters::getParameterFromStack($parameters, 1, 'status');
4✔
211
        if ($targetParam === false) {
4✔
212
            return;
4✔
213
        }
214

215
        $tokens = $phpcsFile->getTokens();
4✔
216
        $data   = [$functionName, $targetParam['clean']];
4✔
217

218
        $integer = 0;
4✔
219
        $string  = 0;
4✔
220
        $boolean = 0;
4✔
221
        $float   = 0;
4✔
222
        $null    = 0;
4✔
223
        $concat  = 0;
4✔
224
        $arithm  = 0;
4✔
225
        $total   = 0;
4✔
226

227
        for ($i = $targetParam['start']; $i <= $targetParam['end']; $i++) {
4✔
228
            if (isset(Tokens::EMPTY_TOKENS[$tokens[$i]['code']])) {
4✔
229
                continue;
4✔
230
            }
231

232
            if (($tokens[$i]['code'] === \T_INT_CAST || $tokens[$i]['code'] === \T_STRING_CAST)
4✔
233
                && $total === 0
4✔
234
            ) {
235
                // Assume the cast is for the whole parameter, in which case, we're good.
236
                return;
4✔
237
            }
238

239
            if ($tokens[$i]['code'] === \T_NEW) {
4✔
240
                // For objects, there is no change in behaviour. This was already a type error,
241
                // or, in case of a stingable object, was okay and is still okay.
242
                return;
4✔
243
            }
244

245
            // Check for use of PHP native global constants for which we know the type.
246
            $trimmedContent = \ltrim($tokens[$i]['content'], '\\');
4✔
247
            if (($tokens[$i]['code'] === \T_STRING
4✔
248
                || $tokens[$i]['code'] === \T_NAME_FULLY_QUALIFIED)
4✔
249
                && isset($this->phpNativeConstants[$trimmedContent]) === true
4✔
250
                && MiscHelper::isUseOfGlobalConstant($phpcsFile, $i) === true
4✔
251
            ) {
252
                $type = \gettype($this->phpNativeConstants[$trimmedContent]);
4✔
253
                switch ($type) {
2✔
254
                    case 'integer':
4✔
255
                        ++$integer;
4✔
256
                        break;
4✔
257

258
                    case 'string':
4✔
259
                        ++$string;
4✔
260
                        break;
4✔
261

262
                    case 'double':
4✔
UNCOV
263
                        ++$float;
×
264
                        break;
×
265

266
                    case 'boolean':
4✔
267
                        ++$boolean;
2✔
268
                        break;
2✔
269

270
                    // At this time, PHP doesn't have any native constants of type null.
271
                    // @codeCoverageIgnoreStart
272
                    case 'null':
273
                        ++$null;
274
                        break;
275
                        // @codeCoverageIgnoreEnd
276

277
                    default:
278
                        $this->flagTypeError($phpcsFile, $i, $data, $type);
4✔
279
                        return;
4✔
280
                }
281

282
                ++$total;
4✔
283
                continue;
4✔
284
            }
285

286
            if (isset(Tokens::NAME_TOKENS[$tokens[$i]['code']]) === true
4✔
287
                || $tokens[$i]['code'] === \T_VARIABLE
4✔
288
            ) {
289
                // Variable, non-PHP-native constant, function call. Ignore as undetermined.
290
                return;
4✔
291
            }
292

293
            if (($tokens[$i]['code'] === \T_ARRAY
4✔
294
                || $tokens[$i]['code'] === \T_LIST
4✔
295
                || $tokens[$i]['code'] === \T_OPEN_SHORT_ARRAY)
4✔
296
                && $total === 0 // Only flag when the parameter starts with one of these tokens.
4✔
297
            ) {
298
                $this->flagTypeError($phpcsFile, $i, $data, 'array');
4✔
299
                return;
4✔
300
            }
301

302
            ++$total;
4✔
303

304
            if ($tokens[$i]['code'] === \T_LNUMBER) {
4✔
305
                ++$integer;
4✔
306
                continue;
4✔
307
            }
308

309
            if (isset(Tokens::ARITHMETIC_TOKENS[$tokens[$i]['code']])) {
4✔
310
                ++$arithm;
4✔
311
                continue;
4✔
312
            }
313

314
            if (isset(Tokens::TEXT_STRING_TOKENS[$tokens[$i]['code']])
4✔
315
                || isset(Tokens::HEREDOC_TOKENS[$tokens[$i]['code']])
4✔
316
            ) {
317
                ++$string;
4✔
318
                continue;
4✔
319
            }
320

321
            if ($tokens[$i]['code'] === \T_STRING_CONCAT) {
4✔
322
                ++$concat;
4✔
323
                continue;
4✔
324
            }
325

326
            if ($tokens[$i]['code'] === \T_DNUMBER) {
4✔
327
                ++$float;
4✔
328
                continue;
4✔
329
            }
330

331
            if ($tokens[$i]['code'] === \T_TRUE || $tokens[$i]['code'] === \T_FALSE) {
4✔
332
                ++$boolean;
4✔
333
                continue;
4✔
334
            }
335

336
            if ($tokens[$i]['code'] === \T_NULL) {
4✔
337
                ++$null;
4✔
338
                continue;
4✔
339
            }
340
        }
341

342
        $unrecognized = ($total - $integer - $string - $boolean - $float - $null - $concat - $arithm);
4✔
343

344
        if ($unrecognized > 0) {
4✔
345
            // Ignore as undetermined.
346
            return;
4✔
347
        }
348

349
        if (($integer > 0 && ($total - $integer - $arithm) === 0)
4✔
350
            || ($string > 0 && ($total - $string - $concat - $integer) === 0)
4✔
351
        ) {
352
            // This is fine, either a purely integer value, a purely string value or a simple operation involving only strings/integers.
353
            // No change in behaviour.
354
            return;
4✔
355
        }
356

357
        if ($boolean > 0 && ($total - $boolean) === 0) {
4✔
358
            if ($this->strictTypes === true) {
4✔
359
                $this->flagTypeError($phpcsFile, $i, $data, 'boolean');
4✔
360
                return;
4✔
361
            }
362

363
            $phpcsFile->addWarning(
4✔
364
                'Passing a boolean value to %s() will be interpreted as an exit code instead of as a status message since PHP 8.4. Found: "%s"',
4✔
365
                $i,
4✔
366
                'BooleanParamFound',
4✔
367
                $data
4✔
368
            );
2✔
369
            return;
4✔
370
        }
371

372
        if ($float > 0 && ($total - $float - $arithm - $integer) === 0) {
4✔
373
            if ($this->strictTypes === true) {
4✔
374
                $this->flagTypeError($phpcsFile, $i, $data, 'float');
4✔
375
                return;
4✔
376
            }
377

378
            $phpcsFile->addWarning(
4✔
379
                'Passing a floating point value to %s() will be interpreted as an exit code instead of as a status message since PHP 8.4. Found: "%s"',
4✔
380
                $i,
4✔
381
                'FloatParamFound',
4✔
382
                $data
4✔
383
            );
2✔
384
            return;
4✔
385
        }
386

387
        if ($null > 0 && ($total - $null) === 0) {
4✔
388
            if ($this->strictTypes === true) {
4✔
389
                $this->flagTypeError($phpcsFile, $i, $data, 'null');
4✔
390
                return;
4✔
391
            }
392

393
            $phpcsFile->addWarning(
4✔
394
                'Passing null to %s() will be interpreted as an exit code instead of as a status message since PHP 8.4. Found: "%s"',
4✔
395
                $i,
4✔
396
                'NullParamFound',
4✔
397
                $data
4✔
398
            );
2✔
399
            return;
4✔
400
        }
401

402
        // Ignore everything else as undetermined.
403
    }
2✔
404

405
    /**
406
     * Throw an error about a received parameter type which will be a type error as of PHP 8.4.
407
     *
408
     * @since 10.0.0
409
     *
410
     * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
411
     * @param int                         $stackPtr  The token position to throw the error on.
412
     * @param array<string>               $data      The data for the error message.
413
     * @param string                      $type      The inferred parameter type.
414
     *
415
     * @return void
416
     */
417
    private function flagTypeError(File $phpcsFile, $stackPtr, $data, $type)
4✔
418
    {
419
        $aOrAn = 'a ';
4✔
420
        if ($type === 'null') {
4✔
421
            $aOrAn = '';
4✔
422
        } elseif ($type === 'array') {
4✔
423
            $aOrAn = 'an ';
4✔
424
        }
425

426
        $data[] = $aOrAn;
4✔
427
        $data[] = $type;
4✔
428

429
        $phpcsFile->addError(
4✔
430
            'Passing %3$s%4$s to %1$s() will result in a TypeError since PHP 8.4. Found: "%2$s"',
4✔
431
            $stackPtr,
4✔
432
            'TypeError',
4✔
433
            $data
4✔
434
        );
2✔
435
    }
2✔
436
}
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

© 2026 Coveralls, Inc