Skip to content

Commit 1807f76

Browse files
committed
Merge branch 'feature/2857-3592-squiz-nonexecutable-code-allow-for-php8-throw-expressions' of https://github.com/jrfnl/PHP_CodeSniffer
2 parents ce123ff + 0f2c50f commit 1807f76

File tree

3 files changed

+150
-6
lines changed

3 files changed

+150
-6
lines changed

src/Standards/Squiz/Sniffs/PHP/NonExecutableCodeSniff.php

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,22 @@
1616
class NonExecutableCodeSniff implements Sniff
1717
{
1818

19+
/**
20+
* Tokens for terminating expressions, which can be used inline.
21+
*
22+
* This is in contrast to terminating statements, which cannot be used inline
23+
* and would result in a parse error (which is not the concern of this sniff).
24+
*
25+
* `throw` can be used as an expression since PHP 8.0.
26+
* {@link https://wiki.php.net/rfc/throw_expression}
27+
*
28+
* @var array
29+
*/
30+
private $expressionTokens = [
31+
T_EXIT => T_EXIT,
32+
T_THROW => T_THROW,
33+
];
34+
1935

2036
/**
2137
* Returns an array of tokens this test wants to listen for.
@@ -49,12 +65,34 @@ public function process(File $phpcsFile, $stackPtr)
4965
{
5066
$tokens = $phpcsFile->getTokens();
5167

52-
// If this token is preceded with an "or", it only relates to one line
53-
// and should be ignored. For example: fopen() or die().
5468
$prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true);
55-
if ($tokens[$prev]['code'] === T_LOGICAL_OR || $tokens[$prev]['code'] === T_BOOLEAN_OR) {
56-
return;
57-
}
69+
70+
// Tokens which can be used in inline expressions need special handling.
71+
if (isset($this->expressionTokens[$tokens[$stackPtr]['code']]) === true) {
72+
// If this token is preceded by a logical operator, it only relates to one line
73+
// and should be ignored. For example: fopen() or die().
74+
// Note: There is one exception: throw expressions can not be used with xor.
75+
if (isset(Tokens::$booleanOperators[$tokens[$prev]['code']]) === true
76+
&& ($tokens[$stackPtr]['code'] === T_THROW && $tokens[$prev]['code'] === T_LOGICAL_XOR) === false
77+
) {
78+
return;
79+
}
80+
81+
// Expressions are allowed in the `else` clause of ternaries.
82+
if ($tokens[$prev]['code'] === T_INLINE_THEN || $tokens[$prev]['code'] === T_INLINE_ELSE) {
83+
return;
84+
}
85+
86+
// Expressions are allowed with PHP 7.0+ null coalesce and PHP 7.4+ null coalesce equals.
87+
if ($tokens[$prev]['code'] === T_COALESCE || $tokens[$prev]['code'] === T_COALESCE_EQUAL) {
88+
return;
89+
}
90+
91+
// Expressions are allowed in arrow functions.
92+
if ($tokens[$prev]['code'] === T_FN_ARROW) {
93+
return;
94+
}
95+
}//end if
5896

5997
// Check if this token is actually part of a one-line IF or ELSE statement.
6098
for ($i = ($stackPtr - 1); $i > 0; $i--) {
@@ -139,7 +177,6 @@ public function process(File $phpcsFile, $stackPtr)
139177
// This token may be part of an inline condition.
140178
// If we find a closing parenthesis that belongs to a condition
141179
// we should ignore this token.
142-
$prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true);
143180
if (isset($tokens[$prev]['parenthesis_owner']) === true) {
144181
$owner = $tokens[$prev]['parenthesis_owner'];
145182
$ignore = [

src/Standards/Squiz/Tests/PHP/NonExecutableCodeUnitTest.1.inc

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,5 +297,104 @@ class TestAlternativeControlStructures {
297297
$var_after_class_in_global_space = 1;
298298
do_something_else();
299299

300+
// These are parse errors, but that's not the concern of the sniff.
301+
function parseError1() {
302+
defined('FOO') or return 'foo';
303+
echo 'unreachable';
304+
}
305+
306+
function parseError2() {
307+
defined('FOO') || continue;
308+
echo 'unreachable';
309+
}
310+
311+
// All logical operators are allowed with inline expressions (but this was not correctly handled by the sniff).
312+
function exitExpressionsWithLogicalOperators() {
313+
$condition = false;
314+
$condition || exit();
315+
$condition or die();
316+
317+
$condition = true;
318+
$condition && die();
319+
$condition and exit;
320+
321+
$condition xor die();
322+
323+
echo 'still executable as exit, in all of the above cases, is used as part of an expression';
324+
}
325+
326+
// Inline expressions are allowed in ternaries.
327+
function exitExpressionsInTernary() {
328+
$value = $myValue ? $myValue : exit();
329+
$value = $myValue ?: exit();
330+
$value = $var == 'foo' ? 'bar' : die( 'world' );
331+
332+
$value = (!$myValue ) ? exit() : $myValue;
333+
$value = $var != 'foo' ? die( 'world' ) : 'bar';
334+
335+
echo 'still executable';
336+
}
337+
338+
// Inline expressions are allowed with null coalesce and null coalesce equals.
339+
function exitExpressionsWithNullCoalesce() {
340+
$value = $nullableValue ?? exit();
341+
$value ??= die();
342+
echo 'still executable';
343+
}
344+
345+
// Inline expressions are allowed in arrow functions.
346+
function exitExpressionsInArrowFunction() {
347+
$callable = fn() => die();
348+
echo 'still executable';
349+
}
350+
351+
// PHP 8.0+: throw expressions which don't stop execution.
352+
function nonStoppingThrowExpressions() {
353+
$callable = fn() => throw new Exception();
354+
355+
$value = $myValue ? 'something' : throw new Exception();
356+
$value = $myValue ?: throw new Exception();
357+
$value = $myValue ? throw new Exception() : 'something';
358+
359+
$value = $nullableValue ?? throw new Exception();
360+
$value ??= throw new Exception();
361+
362+
$condition && throw new Exception();
363+
$condition || throw new Exception();
364+
$condition and throw new Exception();
365+
$condition or throw new Exception();
366+
367+
echo 'still executable as throw, in all of the above cases, is used as part of an expression';
368+
369+
throw new Exception();
370+
echo 'non-executable';
371+
}
372+
373+
// PHP 8.0+: throw expressions which do stop execution.
374+
function executionStoppingThrowExpressionsA() {
375+
$condition xor throw new Exception();
376+
echo 'non-executable';
377+
}
378+
379+
function executionStoppingThrowExpressionsB() {
380+
throw $userIsAuthorized ? new ForbiddenException() : new UnauthorizedException();
381+
echo 'non-executable';
382+
}
383+
384+
function executionStoppingThrowExpressionsC() {
385+
throw $condition1 && $condition2 ? new Exception1() : new Exception2();
386+
echo 'non-executable';
387+
}
388+
389+
function executionStoppingThrowExpressionsD() {
390+
throw $exception ??= new Exception();
391+
echo 'non-executable';
392+
}
393+
394+
function executionStoppingThrowExpressionsE() {
395+
throw $maybeNullException ?? new Exception();
396+
echo 'non-executable';
397+
}
398+
300399
// Intentional syntax error.
301400
return array_map(

src/Standards/Squiz/Tests/PHP/NonExecutableCodeUnitTest.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,14 @@ public function getWarningList($testFile='')
7474
252 => 1,
7575
253 => 1,
7676
254 => 2,
77+
303 => 1,
78+
308 => 1,
79+
370 => 1,
80+
376 => 1,
81+
381 => 1,
82+
386 => 1,
83+
391 => 1,
84+
396 => 1,
7785
];
7886
break;
7987
case 'NonExecutableCodeUnitTest.2.inc':

0 commit comments

Comments
 (0)