Skip to content

Report always-true in_array(..., true) when every finite needle value is guaranteed in the haystack#5944

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

Report always-true in_array(..., true) when every finite needle value is guaranteed in the haystack#5944
phpstan-bot wants to merge 1 commit into
phpstan:2.2.xfrom
phpstan-bot:create-pull-request/patch-u73aorw

Conversation

@phpstan-bot

Copy link
Copy Markdown
Collaborator

Summary

PHPStan did not report in_array($needle, $haystack, true) as always-true when $needle can take a finite set of values that is a subset of the haystack's values (e.g. $needle is 'a'|'b' and $haystack is array{'a','b','c'}). For the array-literal form of the same call it was even worse: the call's return type was wrongly inferred as false, producing a false "always false" report.

This change makes both forms correctly report (and infer) always-true, and extends the same behaviour to integer- and enum-case needles.

Changes

  • src/Rules/Comparison/ImpossibleCheckTypeHelper.php
    • Rewrote the strict-in_array() guard loop. Instead of bailing unless a single haystack value is a supertype of the entire needle, it now collects each haystack variant's guaranteed values and requires that every finite needle value is guaranteed present. Union needles that are subsets of the haystack are now provable.
    • Switched the guaranteed-value collection from getConstantScalarTypes() to getFiniteTypes(), so enum-case needles are treated like scalar needles (matching the symmetric false-context logic in InArrayFunctionTypeSpecifyingExtension::computeNeedleNarrowingType).
  • src/Type/Php/InArrayFunctionTypeSpecifyingExtension.php
    • After combining the per-item comparisons of an Array_ haystack, clear the resulting root expression when more than one item was combined. The per-item root expression is meaningless for the whole call.
  • src/Rules/Functions/ParameterCastableToStringRule.php
    • Replaced the now-redundant elseif (in_array($functionName, $checkFirstArgFunctions, true)) (plus its unreachable else { return []; }) with a plain else — the stricter analysis correctly proves this check always-true.
  • Tests: added tests/PHPStan/Analyser/nsrt/bug-14873.php (type inference) and tests/PHPStan/Rules/Comparison/data/bug-14873.php + testBug14873() (rule). Added the newly-detected always-true at line 259 of the existing check-type-function-call.php fixture.

Root cause

Two independent defects, both surfacing only for a finite needle that is a subset of the haystack:

  1. Helper (variable haystack). The guard loop asked "is there one haystack value that is a supertype of the needle?". For a single needle value 'a' that works, but for a union needle 'a'|'b' no single haystack value ('a', 'b', …) is a supertype of the union, so the helper always bailed to null. The correct question is per-needle-value: each finite value of the needle must be guaranteed present. Additionally the loop only considered constant scalar haystack values, so enum-case haystacks/needles were never provable.

  2. Type-specifying extension (array-literal haystack). The Array_ branch builds the call's narrowing by combining one SpecifiedTypes per array item, each carrying its own $needle === $item as its root expression. SpecifiedTypes::mergeRootExpr(null, X) returns X, so after the first two differing items zeroed the root expression, the last item's comparison ($needle === $lastItem) re-became the combined root expression. The helper then read getType($lastItem comparison) — which for a subset needle is the constant false — and concluded the whole in_array() call was always false. Clearing the root expression once multiple items are combined removes this leak; the call result is then derived from the (correct) per-needle narrowing.

Test

  • tests/PHPStan/Analyser/nsrt/bug-14873.php asserts the inferred return type of in_array(..., true) for variable and literal haystacks across string, integer and enum-case needles (subset ⇒ true, non-subset ⇒ bool, disjoint ⇒ false). Fails before the fix (literal subset case inferred false, variable/enum subset cases inferred bool).
  • tests/PHPStan/Rules/Comparison/data/bug-14873.php + testBug14873() assert the rule reports always-true for the reported variable-haystack scenario. Fails before the fix (no error reported).
  • The existing ImpossibleCheckTypeFunctionCallRuleTest gained the always-true at check-type-function-call.php:259 (in_array($fooOrBar, ['foo','bar'], true) with $fooOrBar of type 'foo'|'bar'), which was previously a missed report.
  • Probed analogous constructs: loose in_array() (intentionally left inconclusive), array_key_exists()/class_exists() type-specifying extensions (no per-item root-expression leak — they set their root expression explicitly), and integer/enum needle variants (now fixed via getFiniteTypes()).

Fixes phpstan/phpstan#14873

…ue is guaranteed in the haystack

- `ImpossibleCheckTypeHelper`: rewrite the strict `in_array()` guard so it checks that *each* finite needle value is guaranteed present in the haystack, instead of requiring a single haystack value to be a supertype of the whole needle. This makes union needles that are a subset of the haystack values (e.g. `'a'|'b'` in `array{'a','b','c'}`) report as always-true.
- Use `getFiniteTypes()` instead of `getConstantScalarTypes()` when collecting the haystack's guaranteed values, so enum-case needles are handled the same way as string/int/bool needles (mirrors `InArrayFunctionTypeSpecifyingExtension::computeNeedleNarrowingType`).
- `InArrayFunctionTypeSpecifyingExtension`: when combining the per-item comparisons of an array-literal haystack, clear the leftover root expression. Otherwise one item's `$needle === $oneItem` comparison (e.g. the always-false `$s === 'baz'`) was promoted by `mergeRootExpr(null, X)` to stand in for the whole call, making `in_array($subset, ['a','b','c'], true)` wrongly infer `false`.
- `ParameterCastableToStringRule`: drop the now-redundant `elseif (in_array(...))`/dead `else` branch that the stricter analysis correctly flags as always-true.
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.

PHPStan should report in_array(..., true) calls that are obviously always true

2 participants