Treat * as assignment suppression in sscanf/fscanf placeholder counting#5586
Merged
staabm merged 1 commit intophpstan:2.1.xfrom May 3, 2026
Merged
Conversation
…counting - Add `$isScanf` parameter to `PrintfHelper::parsePlaceholders()` and `getPlaceholdersCount()` to distinguish printf vs scanf semantics - In scanf context, `*` means "match but don't assign" (assignment suppression), so placeholders like `%*d`, `%*s`, `%*[a-z]` are skipped entirely and do not count toward the expected argument count - In printf context, `*` continues to mean "take width from next argument" (existing behavior unchanged) - Verified that `SscanfFunctionDynamicReturnTypeExtension` already handles suppressed placeholders correctly (its regex doesn't match them)
staabm
approved these changes
May 3, 2026
VincentLanglet
approved these changes
May 3, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
PHPStan incorrectly reported placeholder count mismatches for
sscanf/fscanfcalls using the%*assignment suppression modifier (e.g.%*[a-z],%*d). In scanf,*means "match the input but don't store it", so these placeholders should not be counted as requiring a variable argument. The fix teaches the placeholder parser to distinguish scanf semantics from printf semantics.Changes
src/Rules/Functions/PrintfHelper.php:bool $isScanfparameter toparsePlaceholders()andgetPlaceholdersCount()$isScanfis true and*is captured (in thewidthregex group), the placeholder is skipped entirely viacontinuegetPrintfPlaceholdersCount()andgetPrintfPlaceholders()passfalsegetScanfPlaceholdersCount()passestrueRoot cause
The
parsePlaceholders()method was shared between printf and scanf without accounting for their different semantics for*:%*dmeans "take width from the next argument" → counts as an extra parameter%*dmeans "match but don't assign" → should NOT count as a parameter at allThe fix adds a
$isScanfflag so that when processing scanf format strings, any placeholder with*is recognized as assignment-suppressed and skipped.Analogous cases investigated
PrintfArrayParametersRule(vprintf/vsprintf): Only uses printf semantics — not affected.PrintfParameterTypeRule(printf/sprintf/fprintf): Only uses printf semantics — not affected.SscanfFunctionDynamicReturnTypeExtension: Uses its own regex (/%(\d*)(\[[^\]]+\]|[cdeEfosux]{1})/) which accidentally already handles%*correctly — the\d*group doesn't match*, so suppressed placeholders are never included in the inferred return array type. Added NSRT assertions to lock this in.Test
tests/PHPStan/Rules/Functions/data/bug-10260.php— Rule test covering%*[a-z],%*d,%*s, multiple suppressions, mixed suppressed/non-suppressed, andfscanfwith suppression.tests/PHPStan/Analyser/nsrt/sscanf.php— Added type inference assertions forsscanfwith%*to confirm the return type extension correctly excludes suppressed placeholders.Fixes phpstan/phpstan#10260