Skip to content

Treat * as assignment suppression in sscanf/fscanf placeholder counting#5586

Merged
staabm merged 1 commit intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-zc0c12t
May 3, 2026
Merged

Treat * as assignment suppression in sscanf/fscanf placeholder counting#5586
staabm merged 1 commit intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-zc0c12t

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

Summary

PHPStan incorrectly reported placeholder count mismatches for sscanf/fscanf calls 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

  • Modified src/Rules/Functions/PrintfHelper.php:
    • Added bool $isScanf parameter to parsePlaceholders() and getPlaceholdersCount()
    • When $isScanf is true and * is captured (in the width regex group), the placeholder is skipped entirely via continue
    • getPrintfPlaceholdersCount() and getPrintfPlaceholders() pass false
    • getScanfPlaceholdersCount() passes true

Root cause

The parsePlaceholders() method was shared between printf and scanf without accounting for their different semantics for *:

  • printf: %*d means "take width from the next argument" → counts as an extra parameter
  • scanf: %*d means "match but don't assign" → should NOT count as a parameter at all

The fix adds a $isScanf flag 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, and fscanf with suppression.
  • tests/PHPStan/Analyser/nsrt/sscanf.php — Added type inference assertions for sscanf with %* to confirm the return type extension correctly excludes suppressed placeholders.

Fixes phpstan/phpstan#10260

…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 staabm requested a review from VincentLanglet May 3, 2026 10:14
@staabm staabm merged commit 25c2d62 into phpstan:2.1.x May 3, 2026
657 of 660 checks passed
@staabm staabm deleted the create-pull-request/patch-zc0c12t branch May 3, 2026 11:12
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.

3 participants