Skip to content

Conversation

@jrfnl
Copy link
Member

@jrfnl jrfnl commented Nov 10, 2025

Description

Tokenizer/PHP: PHP close tag should be regarded as scope opener for switch case/default statements

In PHP, if the statement before a PHP close tag is "unclosed", PHP will automagically insert a semi-colon before the PHP close tag when compiling the file.

As case and default condition statements in a switch structure can be "closed" with a semi-colon, this will not result in a parse error, but will be perfectly valid code.

However, the practice of "closing" case/default statements with a semi-colon is deprecated as of PHP 8.5 and while looking into that deprecation, I realized that the deprecation also applies to the "implied"/automagically inserted semi-colon when a case/default statement ends on a PHP close tag.

Digging deeper, it turned out that a PHP close tag was not supported in PHPCS as a scope opener for case/default statements, which in turn meant that if that type of code was seen by sniffs, all sorts of false positives/negatives and potential Undefined array key "scope_opener/closer" notices would start to get thrown.

This commit fixes the tokenizer part of the issue and adds tests with a variety of scope openers for case/default statements to a number of Tokenizer related test files.

PSR2/SwitchDeclaration: make implied semicolon via close tag auto-fixable

The PSR2.ControlStructures.SwitchDeclaration sniff includes a rule/error flagging when the scope opener is not a : colon.
This error was previously already made auto-fixable when the scope_opener is a semi-colon (via PR #1161).

This commit now also makes that error auto-fixable if the scope_opener is a PHP close tag.

Includes tests.
These tests also document how the sniff otherwise behaves when the code within a switch moves in and out of PHP a lot.

Squiz/SwitchDeclaration: make implied semicolon via close tag auto-fixable

The Squiz.ControlStructures.SwitchDeclaration sniff does not include a rule/error for the scope opener not being a : colon, though all the error messages have the presumption of the scope opener being a colon in the textual content.

The sniff does have tests with a semicolon scope opener and as there is no error related to the semicolon being a scope opener, I can only presume that it was previously decided to allow that in the Squiz standard.
Considering that this is a code style sniff, it is not for this sniff to enforce PHP cross-version compatibility of code, so the PHP 8.5 deprecation is irrelevant and there is no reason to change the behaviour of the sniff for semicolon scope openers.

However, the sniff definitely did not function correctly for code moving in and out of PHP within a switch statement.
PR #1315 was a first step towards supporting this.

This commit now adds a new error to disallow a PHP close tag as the "scope_opener" for case/default statements.
The error is auto-fixable and will insert a colon before the PHP close tag as the fix.
With this additional error in place, the sniff's handling of code within a switch moving in and out of PHP has greatly improved.

Includes tests.

Squiz/NonExecutableCode: bug fix - false positive with PHP close tag after case/default

The Squiz.PHP.NonExecutableCode sniff would flag PHP open/close tags and indentation whitespace in inline HTML as (non-)executable code, while these tokens should be disregarded for the purposes of this sniff.

Fixed now.

Includes tests.

Various sniffs: update tests to safeguard handling of case/default with PHP close tag as scope opener

With the tokenizer fix in place, these sniffs already handle these test cases correctly. The tests are just being added to safeguard this for the future.

Suggested changelog entry

Changed:

  • The Squiz.ControlStructures.SwitchDeclaration sniff will now flag a PHP close tag as a "wrong opener" and will auto-fix this by inserting a colon.

Fixed:

  • Tokenizer/PHP: a PHP close tag after a switch case condition or after a default keyword, was not regarded as a "scope_opener" for the case/default body.
  • PSR2.ControlStructures.SwitchDeclaration: the WrongOpener error is now also auto-fixable if the wrong opener is a PHP close tag.
  • Squiz.PHP.NonExecutableCode would throw false positives when code within a switch control structure would move in and out of PHP.

Related issues/external references

Ref: https://wiki.php.net/rfc/deprecations_php_8_5#languagesyntax_deprecations

Types of changes

  • Bug fix (non-breaking change which fixes an issue)

…witch case/default statements

In PHP, if the statement before a PHP close tag is "unclosed", PHP will automagically insert a semi-colon before the PHP close tag when compiling the file.

As `case` and `default` condition statements in a `switch` structure can be "closed" with a semi-colon, this will not result in a parse error, but will be perfectly valid code.

However, the practice of "closing" `case`/`default` statements with a semi-colon is deprecated as of PHP 8.5 and while looking into that deprecation, I realized that the deprecation also applies to the "implied"/automagically inserted semi-colon when a `case`/`default` statement ends on a PHP close tag.

Digging deeper, it turned out that a PHP close tag was not supported in PHPCS as a scope opener for `case`/`default` statements, which in turn meant that if that type of code was seen by sniffs, all sorts of false positives/negatives and potential `Undefined array key "scope_opener/closer"` notices would start to get thrown.

This commit fixes the tokenizer part of the issue and adds tests with a variety of scope openers for `case`/`default` statements to a number of Tokenizer related test files.
…able

The `PSR2.ControlStructures.SwitchDeclaration` sniff includes a rule/error flagging when the scope opener is not a `:` colon.
This error was previously already made auto-fixable when the `scope_opener` is a semi-colon (via PR 1161).

This commit now also makes that error auto-fixable if the `scope_opener` is a PHP close tag.

Includes tests.
These tests also document how the sniff otherwise behaves when the code within a `switch` moves in and out of PHP a lot.
…xable

The `Squiz.ControlStructures.SwitchDeclaration` sniff does _not_ include a rule/error for the scope opener not being a `:` colon, though all the error messages have the presumption of the scope opener being a colon in the textual content.

The sniff does have tests with a semicolon scope opener and as there is no error related to the semicolon being a scope opener, I can only presume that it was previously decided to allow that in the Squiz standard.
Considering that this is a code style sniff, it is not for this sniff to enforce PHP cross-version compatibility of code, so the PHP 8.5 deprecation is irrelevant and there is no reason to change the behaviour of the sniff for semicolon scope openers.

However, the sniff definitely did not function correctly for code moving in and out of PHP within a `switch` statement.
PR 1315 was a first step towards supporting this.

This commit now adds a new error to disallow a PHP close tag as the "scope_opener" for `case`/`default` statements.
The error is auto-fixable and will insert a colon before the PHP close tag as the fix.
With this additional error in place, the sniff's handling of code within a `switch` moving in and out of PHP has greatly improved.

Includes tests.
…after case/default

The `Squiz.PHP.NonExecutableCode` sniff would flag PHP open/close tags and indentation whitespace in inline HTML as (non-)executable code, while these tokens should be disregarded for the purposes of this sniff.

Fixed now.

Includes tests.
…` with PHP close tag as scope opener

With the tokenizer fix in place, these sniffs already handle these test cases correctly. The tests are just being added to safeguard this for the future.
@jrfnl jrfnl added this to the 4.0.1 milestone Nov 10, 2025
@jrfnl jrfnl merged commit 7ed8ea2 into 4.x Nov 10, 2025
77 checks passed
@jrfnl jrfnl deleted the feature/tokenizer-php-switch-case-default-can-start-body-with-php-close-tag branch November 10, 2025 15:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants