Internal.Exception on valid switch statements with curly braces
r1pped opened this issue · 2 comments
Describe the bug
Running sniffer with PSR-12 standard on some valid switch statements (mixing curly braces syntax with classic syntax) shows an Internal.Exception, which aborts checking rest of the code
Code sample
<?php
switch (true) {
case 1 === 1: {
echo "foo";
// omitting break; causes Internal.Exception
}
case 2 === 2:
echo "bar";
break;
}
// code below is not checked anymore
if(false){
echo 'foo'.'bar';
}
To reproduce
Steps to reproduce the behavior:
- Create a file called
test.php
with the code sample above... - Run
phpcs --standard=psr12 test.php
- See error message displayed
FILE: test.php
--------------------------------------------------------------------------------------------------------------------------------------
FOUND 2 ERRORS AFFECTING 2 LINES
--------------------------------------------------------------------------------------------------------------------------------------
1 | ERROR | An error occurred during processing; checking has been aborted. The error message was: Undefined array key
| | "scope_condition" in
| | /home/sample/scripts/test/vendor/squizlabs/php_codesniffer/src/Standards/PSR2/Sniffs/ControlStructures/SwitchDeclarationSniff.php
| | on line 146 (Internal.Exception)
4 | ERROR | CASE statements must be defined using a colon (PSR2.ControlStructures.SwitchDeclaration.WrongOpenercase)
--------------------------------------------------------------------------------------------------------------------------------------
Time: 31ms; Memory: 8MB
Expected behavior
Maybe still show PSR2/PSR12 errors in lines 12-14 where if(false){
is?
Versions (please complete the following information):
- OS: Ubuntu 20.04.5 LTS
- PHP: 8.1.16
- PHPCS: 3.7.2
- Standard: PSR12
Additional context
Even if we ignore PSR2.ControlStructures.SwitchDeclaration.WrongOpenercase sniffer which occurs in line 4, the exception still happens and the rest of the code is not checked anymore.
@r1pped Thanks for reporting this. I can confirm the issue.
It's actually the combination of the curly braces being used for the first case
AND the deliberately fall-through (no break
/continue
) which causes the issue.
It results in the second case
having a the T_COLON
as a scope_opener
, the T_BREAK
as the scope_closer
, but the T_BREAK
doesn't get its scope_condition
set to the first case
, which is what the sniff expects and what should have happened.
This will need some digging into what exactly is going wrong with the setting of the scope_*
indexes.
I'm noticing another inconsistency in the token arrays related to this.
switch (true) {
case 2 === 2:
echo "bar";
break;
case 3 === 3:
echo "bar";
break;
}
In the above, the T_COLON
and the T_BREAK
tokens have only the T_SWITCH
in the conditions
array. The tokens in between have T_SWITCH
and T_CASE
.
The T_CASE
and the associated T_COLON
and T_BREAK
each have the same set of scope_condition
(T_CASE
), scope_opener
(T_COLON
) and scope_closer
(T_BREAK
) set.
switch (true) {
case 1 === 1: {
echo "foo";
break;
}
case 2 === 2: {
echo "bar";
break;
}
}
In this second code sample, the T_OPEN_CURLY_BRACKET has both the
T_SWITCHand the
T_CASEin the
conditionsarray, while the
T_CLOSE_CURLY_BRACKETonly has the
T_SWITCHin the
conditionsarray. The
T_CASEand the associated
T_COLONand
T_BREAKeach have the same set of
scope_condition (
T_CASE),
scope_opener (
T_COLON) and
scope_closer (
T_BREAK`) set.
👉🏻 IMO it would be more consistent if the T_OPEN_CURLY_BRACKET
would not have the T_CASE
in the conditions
array.
If we then take (a variation on) the original code sample:
switch (true) {
case 1 === 1: {
echo "foo";
// omitting break; causes Internal.Exception
}
case 2 === 2:
echo "bar";
break;
case 3 === 3:
echo "bar";
break;
}
The first case
behaves like the second code sample above (open curly has two conditions, close curly one, rest consistent).
The second case
is where things get weird:
- The
T_COLON
has only theT_SWITCH
in theconditions
array, while theT_BREAK
has both theT_SWITCH
as well as the (second)T_CASE
in theconditions
array. - The
T_COLON
has thescope_condition
set to the secondT_CASE
, thescope_opener
to itself and thescope_closer
to theT_BREAK
(in the second case). - The
T_BREAK
(in the second case) has NOscope_condition
,scope_opener
orscope_closer
set AT ALL.
The third case
behaves as expected, exactly like the first code sample above (everything consistent).
I'm fairly confident that the culprit is somewhere in the last part of the Tokenizer\PHP::processAdditional()
method (after the // Only interested in CASE and DEFAULT statements from here on in.
comment), but as there are no dedicated tests covering that part of the code, changing anything there has a high risk of breaking things elsewhere.
@gsherwood Got an opinion on this ?