Skip to content

Improve type inference for coalesce with ErrorType, strpos === int narrowing, and str_repeat return types#5508

Open
phpstan-bot wants to merge 1 commit intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-jdy91gn
Open

Improve type inference for coalesce with ErrorType, strpos === int narrowing, and str_repeat return types#5508
phpstan-bot wants to merge 1 commit intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-jdy91gn

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

Summary

Addresses several "could be" type inference improvement opportunities identified in test files (phpstan/phpstan#14510). Each fix improves the precision of PHPStan's type inference for specific patterns.

Changes

1. Handle ErrorType in constant expression coalesce (InitializerExprTypeResolver)

  • File: src/Reflection/InitializerExprTypeResolver.php
  • When the left side of a ?? operator in a constant expression evaluates to ErrorType (e.g., [][0] — accessing an undefined offset), the coalesce should return the right side
  • Before: [][0] ?? 42*ERROR*
  • After: [][0] ?? 4242

2. Apply truthy narrowing for function calls compared to truthy constants (TypeSpecifier)

  • File: src/Analyser/TypeSpecifier.php
  • When a function/method/static-call result is compared via === to a truthy constant (non-false, non-0, non-null, non-''), this implies the function returned non-false, enabling type-specifying extensions to narrow argument types
  • Limited to FuncCall, MethodCall, and StaticCall expressions to avoid interfering with other narrowing handlers (e.g., $object::class === 'Foo')
  • Before: strpos($s, ':') === 5$s stays as string
  • After: strpos($s, ':') === 5$s narrowed to non-falsy-string (via StrContainingTypeSpecifyingExtension)
  • Same improvement applies to mb_strpos, strrpos, stripos, strripos, and their mb_ variants

3. Improve str_repeat return type precision (StrRepeatFunctionReturnTypeExtension)

  • File: src/Type/Php/StrRepeatFunctionReturnTypeExtension.php
  • Non-falsy for multiplier >= 2: When input is non-empty-string and multiplier >= 2, the result has length >= 2, which cannot be '0' (single char) or '', so it's non-falsy-string
    • Before: str_repeat('0', 100)non-empty-string
    • After: str_repeat('0', 100)non-falsy-string
  • Preserve numeric-string for multiplier = 1: When input is numeric-string and multiplier is exactly 1, the result is the same string, so numeric-string is preserved
    • Before: str_repeat($numericString, 1)non-empty-string
    • After: str_repeat($numericString, 1)non-empty-string&numeric-string

Root cause

Each fix addresses a different gap in type inference:

  1. Coalesce with ErrorType: The ?? handler in InitializerExprTypeResolver only removed null from the left type. When the left side was ErrorType (from accessing an undefined array offset), removeNull(ErrorType) returned ErrorType unchanged.

  2. Truthy constant comparison narrowing: The specifyTypesForConstantBinaryExpression method in TypeSpecifier only handled === false and === true comparisons for triggering type-specifying extensions. Comparing to other truthy constants (like integers) didn't trigger the same narrowing, even though it implies the function returned non-false.

  3. str_repeat type loss: The StrRepeatFunctionReturnTypeExtension didn't account for the fact that repeating any non-empty string 2+ times guarantees non-falsiness (length >= 2). It also lost the numeric-string property when the multiplier was exactly 1.

Analogous cases probed

  • ConstantFloatType::toString() for 0.0/-0.0: Investigated but determined unsafe to fix — ConstantFloatType(0.0) from type narrowing ($num === 0.0) can represent -0.0 at runtime, so the union '-0'|'0' is correct for the narrowed case.
  • sprintf/vsprintf %d%d numeric-string: Investigated but the "could be" was incorrect — sprintf('%d%d', '5', '-3') produces '5-3' which is NOT numeric.
  • strlen($nonES) >= strlen($s) narrowing to non-empty-string: Investigated but the "could be" was incorrect — if $nonES has length 1, $s can still be empty.
  • Various other "could be" comments (instanceof with variables, finally scope with dead catches, filter_var, DOM narrowing, tagged unions, etc.) were evaluated but found to require more complex changes or were invalid improvement suggestions.

Test

  • Updated tests/PHPStan/Analyser/nsrt/initializer-expr-type-resolver.php*ERROR*42 for coalesce with undefined array access
  • Updated tests/PHPStan/Analyser/nsrt/non-empty-string-str-containing-fns.phpstringnon-falsy-string for strpos($s, ':') === 5 and mb_strpos($s, ':') === 5
  • Updated tests/PHPStan/Analyser/nsrt/literal-string.php:
    • str_repeat('0', 100): non-empty-stringnon-falsy-string
    • str_repeat($numericString, 100): non-empty-stringnon-falsy-string
    • str_repeat($numericString, 1): non-empty-stringnon-empty-string&numeric-string
    • str_repeat($numericString, 2): non-empty-stringnon-falsy-string

Fixes phpstan/phpstan#14510

…t narrowing, and `str_repeat` return types

- Handle ErrorType in InitializerExprTypeResolver coalesce: `[][0] ?? 42` now resolves to `42` instead of `*ERROR*`
- Apply truthy narrowing when function/method calls are compared to truthy constants via ===, enabling `strpos($s, ':') === 5` to narrow `$s` to non-falsy-string
- Return non-falsy-string from str_repeat when input is non-empty and multiplier >= 2 (result length >= 2 means it can't be '0' or '')
- Preserve numeric-string in str_repeat when multiplier is exactly 1
- Update test assertions for all improved type inference cases
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