Skip to content

Report str_replace()/str_ireplace()/preg_replace()/preg_filter() calls with fewer replacements than search values - #7007

Open
phpstan-bot wants to merge 4 commits into
phpstan:2.3.xfrom
phpstan-bot:create-pull-request/patch-kniiekt
Open

phpstan-bot wants to merge 4 commits into
phpstan:2.3.xfrom
phpstan-bot:create-pull-request/patch-kniiekt

Conversation

@phpstan-bot

Copy link
Copy Markdown
Collaborator

Summary

str_replace(['a', 'b', 'c'], ['x', 'y'], $s) is valid PHP: the search values without a matching replacement are replaced with an empty string. This is rarely intended and usually a typo. This PR adds a bleeding-edge rule that reports these calls.

Changes

  • src/Rules/Functions/ReplaceCountMismatchRule.php: new level-5 rule, gated by the replaceCountMismatch feature toggle (enabled in conf/bleedingEdge.neon, declared in conf/config.neon and conf/parametersSchema.neon).
  • Covers every function with the same search/replace array semantics: str_replace, str_ireplace, preg_replace and preg_filter. Identifiers are strReplace.replaceCount and pregReplace.replaceCount.
  • Not reported:
    • a string replacement (a single replacement used for all search values, as requested in the issue comments)
    • an explicitly empty replacement array (a deliberate way to remove everything)
    • sizes that only might differ (e.g. list<string>, or a union of shapes that overlaps)
  • Supports named arguments through ArgumentsNormalizer, and handles treatPhpDocTypesAsCertain, adding the tip when only PHPDoc types prove the mismatch.

Root cause

PHPStan had no check comparing the sizes of the search and replace arrays. The rule compares getArraySize() of both arguments with isSmallerThan(). This works for constant arrays, array shapes, unions of shapes and non-empty lists alike.

Probed and out of scope: str_replace('a', ['x'], $s) (string search with an array replace) throws a TypeError on PHP 8 but is not reported today. That comes from the function signature, not a count mismatch, so it is left for a separate change.

Test

tests/PHPStan/Rules/Functions/ReplaceCountMismatchRuleTest.php with data/bug-3889.php. It contains the issue's reproducer verbatim, the string-replacement case from the comments (no error), and variants for: str_ireplace, preg_replace, preg_filter, named arguments (separate PHP 8.0+ fixture data/bug-3889-named-args.php), PHPDoc-only shapes (with the tip), lists, non-empty lists, unions of shapes, and empty/equal/longer replacement arrays.

Fixes phpstan/phpstan#3889

🤖 Generated with Claude Code

…()` calls with fewer replacements than search values

- New bleeding-edge rule `ReplaceCountMismatchRule` (level 5, feature toggle
  `replaceCountMismatch`): when both the search/pattern and the
  replace/replacement arguments are arrays and the replacement array size is
  definitely smaller than the search array size, report it, because PHP
  silently replaces the remaining values with an empty string.
- Uses `Type::getArraySize()` + `isSmallerThan()` so constant arrays, unions
  of shapes and non-empty lists are handled; only definite mismatches are reported.
- A string replacement and an explicitly empty replacement array (a deliberate
  "remove all" idiom) are not reported.
- Applied to the whole family of replace functions with the same semantics:
  str_replace, str_ireplace, preg_replace, preg_filter.
- Respects treatPhpDocTypesAsCertain and adds the tip when only PHPDoc types
  prove the mismatch.
@staabm
staabm force-pushed the create-pull-request/patch-kniiekt branch from 1eb0d93 to c7d91d5 Compare November 9, 2026 20:12
Comment on lines +88 to +91
$args = $normalizedFuncCall->getArgs();
if (!array_key_exists(0, $args) || !array_key_exists(1, $args)) {
return [];
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

check the args count instead and do so very early in the method - because its fast

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I made all three changes staabm asked for in three local commits (not pushed). make tests, make phpstan and make cs all pass.

  1. Bail out early on argument count (fbd86667f): the rule now returns at the very start of processNode() when the call has fewer than two arguments, before looking up the function. I kept the existing key check after the arguments are reordered. With named arguments that leave out search or replace, the reordered list can have a gap at position 0 or 1, and a plain count wouldn't catch that.
  2. Identifier (75d824b6f): every reported call now uses argument.replaceCount. The strReplace/pregReplace names per function are gone, so FUNCTIONS is now just a lookup list.
  3. Error message (0bcc27082): the message is now Call to %s is using fewer replacements than search values., filled with the function name (e.g. Call to str_replace is using …). That made the parameter-name lookup and the size output unused, so I removed them, and updated the expected messages in ReplaceCountMismatchRuleTest.

The PR description still shows the old message and the strReplace.replaceCount / pregReplace.replaceCount identifiers, so it should be updated when these are pushed.

}

$errorBuilder = RuleErrorBuilder::message(sprintf(
'Parameter #2 $%s of function %s has fewer elements (%s) than parameter #1 $%s (%s), the remaining values will be replaced with an empty string.',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

a better error message would be:

Call to %s is using fewer replacements than search values.

$replaceType->getArraySize()->describe(VerbosityLevel::value()),
$parameters[0]->getName(),
$searchType->getArraySize()->describe(VerbosityLevel::value()),
))->identifier(sprintf('%s.replaceCount', self::FUNCTIONS[$functionName]));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

identifier should be argument.replaceCount

phpstan-bot and others added 3 commits November 9, 2026 20:26
…an two arguments

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

str_replace() with more search then replace values

2 participants