Skip to content

Allow null values in native preg_replace_callback callback array type for PREG_UNMATCHED_AS_NULL#5987

Open
phpstan-bot wants to merge 1 commit into
phpstan:2.2.xfrom
phpstan-bot:create-pull-request/patch-acy650q
Open

Allow null values in native preg_replace_callback callback array type for PREG_UNMATCHED_AS_NULL#5987
phpstan-bot wants to merge 1 commit into
phpstan:2.2.xfrom
phpstan-bot:create-pull-request/patch-acy650q

Conversation

@phpstan-bot

Copy link
Copy Markdown
Collaborator

Summary

With treatPhpDocTypesAsCertain: false, PHPStan reported a spurious Strict comparison using !== between string and null will always evaluate to true. (and a follow-up Unreachable statement) inside a preg_replace_callback callback that checks a named capture group against null, even though PREG_UNMATCHED_AS_NULL was passed and the group can legitimately be null.

The regex shape extension already infers the precise PHPDoc type ('b'|null) for the match array, so the default configuration behaves correctly. The problem only appeared with treatPhpDocTypesAsCertain: false, where the always-true check falls back to the native type of $match. That native type came from the function signature map entry for preg_replace_callback, which declared the callback as callable(array<int|string, string>):string — asserting the values are always non-null string.

Changes

  • resources/functionMap.php: preg_replace_callback callback signature changed from callable(array<int|string, string>):string to callable(array<int|string, string|null>):string.
  • resources/functionMap_php74delta.php: same change (this delta overrides the base map for PHP ≥ 7.4).
  • tests/PHPStan/Rules/Comparison/data/bug-14904.php + StrictComparisonOfDifferentTypesRuleTest::testBug14904: regression test with treatPhpDocTypesAsCertain = false.
  • tests/PHPStan/Analyser/nsrt/preg_replace_callback_shapes.php: added assertNativeType('array<int|string, string|null>', $match) (alongside the existing precise PHPDoc assertType) to lock in the corrected native type.

Analogous cases probed:

  • preg_match / preg_match_all: their $matches out type is produced by PregMatchParameterOutTypeExtension, which sets both the PHPDoc and native type from the regex shape matcher (already null-aware). Not affected.
  • preg_replace_callback_array: no per-callback shape inference exists, so callback match arrays stay array (value type mixed) and never triggered a false always-true. Not affected.

Root cause

When treatPhpDocTypesAsCertain is false, always-true/false comparison rules re-evaluate using native types. The native type of the callback parameter is taken from the function signature map, not from the regex shape extension (which only overrides the PHPDoc parameter type). The signature map over-claimed the match values as non-null string, so $match['b'] !== null was deemed always true. Since PREG_UNMATCHED_AS_NULL genuinely makes unmatched groups null, the native signature is simply widened to string|null.

Test

  • StrictComparisonOfDifferentTypesRuleTest::testBug14904 reproduces the reported issue (fails before the fix with 10: Strict comparison ... will always evaluate to true., passes after).
  • preg_replace_callback_shapes.php gains a assertNativeType assertion locking the corrected native match-array type while keeping the precise PHPDoc shape.

Fixes phpstan/phpstan#14904

…pe for `PREG_UNMATCHED_AS_NULL`

- Change the `callback` parameter signature in `functionMap.php` and `functionMap_php74delta.php` from `callable(array<int|string, string>):string` to `callable(array<int|string, string|null>):string`, so the native match-array value type reflects that groups can be null when `PREG_UNMATCHED_AS_NULL` is passed.
- Fixes a spurious `notIdentical.alwaysTrue` / `deadCode.unreachable` report with `treatPhpDocTypesAsCertain: false`: the strict-comparison rule then falls back to the native type, which previously claimed match values were always non-null `string`.
- Adjacent regex functions were probed and found already correct: `preg_match`/`preg_match_all` set the native `$matches` out type via `PregMatchParameterOutTypeExtension` (already null-aware via the shape matcher), and `preg_replace_callback_array` performs no shape inference on its callbacks.
@staabm staabm requested a review from VincentLanglet July 4, 2026 06:28
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.

Spurious error report in regex when "treat phpdoc types as certain" is set to false

2 participants