Skip to content

Fix DeprecatedFunctions override and PHP 8.5 tokenizer support - #71

Merged
dereuromark merged 1 commit into
masterfrom
php85-tokenizer-and-deprecated-functions
Aug 6, 2026
Merged

Fix DeprecatedFunctions override and PHP 8.5 tokenizer support#71
dereuromark merged 1 commit into
masterfrom
php85-tokenizer-and-deprecated-functions

Conversation

@dereuromark

Copy link
Copy Markdown
Contributor

Two independent ways the standard was checking less than it looked like it was checking.

Generic.PHP.DeprecatedFunctions was disabled by its own configuration

The sniff has no static list. Its constructor walks get_defined_functions() and asks ReflectionFunction::isDeprecated(), so it tracks whatever the running PHP marks deprecated - automatically, per version. The ruleset set forbiddenFunctions to a three-entry array, and a ruleset property assignment replaces the value rather than extending it. The reflection-built list was discarded on every run.

Before:

$ phpcs --standard=Generic --sniffs=Generic.PHP.DeprecatedFunctions dep.php
 3 | ERROR | Function utf8_encode() has been deprecated

$ phpcs --standard=PhpCollective --sniffs=Generic.PHP.DeprecatedFunctions dep.php
(nothing)

The three entries were also a poor trade: each and create_function were removed in PHP 8.0, so reflection cannot see them either way, and delete was never a PHP function. They now sit on Generic.PHP.ForbiddenFunctions, whose list is a plain property and is the right home for removed-in-8.0 names. Its two defaults are repeated there because that override replaces too.

After, all five report:

 2 | ERROR | The use of function create_function() is forbidden
 3 | ERROR | The use of function each() is forbidden
 4 | ERROR | The use of function delete() is forbidden; use unset() instead
 5 | ERROR | The use of function sizeof() is forbidden; use count() instead
 6 | ERROR | Function utf8_encode() has been deprecated

VoidCast and PipeOperatorSpacing matched nothing on PHP 8.5

Both sniffs target PHP 8.5 syntax, and both broke on the version that introduced it. PHP 8.5 collapses each construct into one token:

source PHP 8.4 tokens PHP 8.5 tokens
(void) T_OPEN_PARENTHESIS T_STRING T_CLOSE_PARENTHESIS T_VOID_CAST
( void ) same, with whitespace between T_VOID_CAST, content '( void )'
|> T_BITWISE_OR T_GREATER_THAN T_PIPE
| > T_BITWISE_OR T_GREATER_THAN unchanged - still two tokens

register() returned only the pre-8.5 shapes, so on 8.5 neither sniff was ever handed a token and every violation passed. Both now register both shapes. Inner cast whitespace moves into the token content on 8.5, so it is read from there instead of from surrounding tokens.

Two fixers needed a target change. Once the operator is a single token, MissingBefore and MissingAfter both mutated that same index and PHPCS dropped the second, yielding |>trim(...). They now grow the neighboring token, which also keeps the pre-8.5 path correct.

The pipe fixture gains a | > case: on 8.5 that is the only input still reaching the two-token branch, so without it that branch would go uncovered on the newest version.

CI

8.5 joins the validation matrix, which is what would have caught this. That surfaced one PHPStan finding only visible when analyzing on 8.5: getFileDocBlockLines() accepted ?int and used it as an array key. Both call sites already pass a non-null value, so the parameter is now int.

Generic.PHP.DeprecatedFunctions builds its forbiddenFunctions list in its
constructor from the Reflection API, covering every internal function the
running PHP version marks deprecated. The ruleset set that same property,
which replaces the list rather than extending it, so the standard reported
fewer deprecations than plain Generic did. utf8_encode(), for instance, went
unreported. The property is gone; create_function() and each() move to
Generic.PHP.ForbiddenFunctions, which is where removed-in-8.0 functions
belong since the reflection-driven list can no longer see them.

PHP 8.5 tokenizes both features these sniffs target as single tokens:
the void cast becomes T_VOID_CAST, carrying any inner whitespace in its
content, and the pipe operator becomes T_PIPE. Both sniffs registered only
the pre-8.5 multi-token shapes, so on PHP 8.5 they matched nothing at all and
silently passed everything. They now register both shapes, and the tests
confirm identical findings on 8.4 and 8.5.

Two fixers had to move which token they grow. With the operator collapsed
into one token, the before-fix and after-fix both landed on that same index
and the second was dropped. They now extend the neighboring token instead.

PHP 8.5 is added to the CI matrix so this class of breakage is visible.
That also required tightening getFileDocBlockLines() to a non-nullable int
parameter, which PHPStan only flags when analyzing on 8.5; both call sites
already pass a non-null value.
Copilot AI lite review requested due to automatic review settings August 6, 2026 10:19

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes two cases where the PhpCollective standard was unintentionally checking less than intended: (1) Generic.PHP.DeprecatedFunctions was effectively disabled by an overriding ruleset property, and (2) new PHP 8.5 tokenizer behavior prevented the VoidCast and PipeOperatorSpacing sniffs from ever receiving the relevant tokens. It also extends CI to include PHP 8.5 and updates tests/fixtures accordingly.

Changes:

  • Remove the Generic.PHP.DeprecatedFunctions property override that replaced the sniff’s reflection-built deprecated-function list, and move removed-function checks into Generic.PHP.ForbiddenFunctions.
  • Update VoidCastSniff and PipeOperatorSpacingSniff to support PHP 8.5’s single-token forms (T_VOID_CAST, T_PIPE) while preserving pre-8.5 behavior.
  • Add PHP 8.5 to CI and extend pipe operator fixtures/tests to cover the two-token (| >) branch on 8.5.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
PhpCollective/Sniffs/WhiteSpace/PipeOperatorSpacingSniff.php Registers and handles T_PIPE (PHP 8.5) and adjusts fix targeting to avoid fixer collisions.
PhpCollective/Sniffs/PHP/VoidCastSniff.php Registers and handles T_VOID_CAST (PHP 8.5) and validates inner whitespace via token content.
PhpCollective/Sniffs/Commenting/FileDocBlockSniff.php Tightens getFileDocBlockLines() parameter type to int based on non-null call sites.
PhpCollective/ruleset.xml Stops overriding DeprecatedFunctions list; configures ForbiddenFunctions with required replacements and removed functions.
tests/PhpCollective/Sniffs/WhiteSpace/PipeOperatorSpacingSniffTest.php Updates expected error counts to reflect the added fixture case.
tests/_data/PipeOperatorSpacing/before.php Adds a `
tests/_data/PipeOperatorSpacing/after.php Adds the expected fixed output for the new `
.github/workflows/ci.yml Adds PHP 8.5 to the validation matrix.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +110 to +112
// Grow the preceding token rather than the operator itself: on PHP 8.5 the
// whole `|>` is one token, so the "after" fix would target the same index and
// one of the two changes would be dropped.
@dereuromark
dereuromark merged commit 91d7608 into master Aug 6, 2026
6 checks passed
@dereuromark
dereuromark deleted the php85-tokenizer-and-deprecated-functions branch August 6, 2026 10:44
dereuromark added a commit to php-fig-rectified/psr2r-sniffer that referenced this pull request Aug 6, 2026
Generic.PHP.DeprecatedFunctions builds its forbiddenFunctions list in its
constructor from the Reflection API, covering every internal function the
running PHP version marks deprecated. The ruleset set that same property,
which replaces the list rather than extending it, so the standard reported
fewer deprecations than plain Generic did. utf8_encode(), for instance, went
unreported.

The property is gone; create_function() and each() move to
Generic.PHP.ForbiddenFunctions, which is where removed-in-8.0 functions belong
since the reflection-driven list can no longer see them. That sniff's own
defaults, sizeof and delete, are repeated there because setting the property
replaces them too.

Same fix as php-collective/code-sniffer#71.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants