Skip to content

Report finite-typed values in a constant array haystack that can never be the in_array()/array_search()/array_keys() needle - #6054

Merged
ondrejmirtes merged 2 commits into
phpstan:2.2.xfrom
phpstan-bot:create-pull-request/patch-1xjngun
Jul 15, 2026
Merged

Report finite-typed values in a constant array haystack that can never be the in_array()/array_search()/array_keys() needle#6054
ondrejmirtes merged 2 commits into
phpstan:2.2.xfrom
phpstan-bot:create-pull-request/patch-1xjngun

Conversation

@phpstan-bot

Copy link
Copy Markdown
Collaborator

Summary

in_array(), array_search() and array_keys() searched a constant array haystack that contained finite-typed values (enum cases, constant scalars, whole enums, small int ranges, …) which can never be the needle, yet PHPStan reported nothing as long as at least one other value in the haystack could match. For example in_array($int, [Foo::ONE, 1, 2], true) silently kept the dead Foo::ONE entry.

This adds a rule that reports each finite-typed haystack value that can never be the needle, using the type system (Type::getConstantArrays() + Type::getFiniteTypes()) rather than the array literal AST, as requested in the issue.

Changes

  • New src/Rules/Comparison/ImpossibleInArrayHaystackFiniteTypesRule.php, registered at level 4 via #[RegisteredRule(level: 4)].
    • Resolves the needle and haystack types (honoring treatPhpDocTypesAsCertain), enumerates every constant array of the haystack, and for each value type whose getFiniteTypes() is non-empty checks whether it can ever equal the needle.
    • Matching is decided with InitializerExprTypeResolver::resolveIdenticalType() (strict) / resolveEqualType() (loose), so PHP's === / == semantics are respected; an unknown (maybe) third argument requires both to be impossible.
    • For in_array(), if no value can match at all the rule stays silent so the whole-call impossibility keeps being reported once by ImpossibleCheckTypeFunctionCallRule (no double reporting).
  • Regression test ImpossibleInArrayHaystackFiniteTypesRuleTest + data file data/impossible-in-array-finite-types.php.

Root cause

The existing InArrayFunctionTypeSpecifyingExtension only narrows the whole call and only produces an impossible-type error when the entire in_array() evaluates to false. A partially-dead haystack (some values can match, some finite values never can) was invisible. The fix moves the check to the value level and is driven purely by the type system, so it works for any constant-array haystack regardless of whether it comes from a literal, a variable, or a class constant.

Analogous cases probed and fixed

The bug sits on the "needle/haystack search function" axis:

  • array_search() — same (needle, haystack, strict) signature — fixed (was silent). It has no companion impossible-type rule, so it reports even when the whole call is impossible.
  • array_keys($array, $filter_value, $strict) — search form with the needle and haystack arguments swappedfixed (was silent). The rule uses a per-function argument-position map.
  • in_array() whole-call impossibility — already covered by ImpossibleCheckTypeFunctionCallRule; the new rule deliberately defers to it to avoid duplicate errors.

Test

ImpossibleInArrayHaystackFiniteTypesRuleTest covers, for a Foo enum:

  • strict and loose in_array($int, [Foo::ONE, 1, 2]) (reports Foo::ONE, with "identical to"/"equal to" wording),
  • array_search() and array_keys() equivalents,
  • an enum needle narrowed to Foo::ONE with haystack [Foo::ONE, Foo::TWO] (reports the dead Foo::TWO),
  • and no-error cases: everything matches, mixed needle, whole-call-impossible in_array() (handled elsewhere), non-constant haystack, and a union needle that still matches.

Fixes phpstan/phpstan#14960

…r be the in_array()/array_search()/array_keys() needle

- Add ImpossibleInArrayHaystackFiniteTypesRule (level 4) that iterates the
  haystack via Type::getConstantArrays() and reports every value whose
  Type::getFiniteTypes() is non-empty but can never be the needle, instead of
  inspecting the Array_ AST node.
- Determine "can never match" through InitializerExprTypeResolver::resolveIdenticalType()
  for strict comparisons and resolveEqualType() for loose ones, matching PHP's
  runtime === / == semantics (a maybe-strict third argument requires both to be false).
- Skip reporting for in_array() when no haystack value can match at all, since that
  whole-call impossibility is already reported by ImpossibleCheckTypeFunctionCallRule.
- Handle the analogous array_search() (same needle/haystack argument positions) and
  array_keys() search form (needle and haystack swapped) with the same logic.

@ondrejmirtes ondrejmirtes left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Only in bleeding edge on level 4 pls. Can't have RegisteredRule attribute yet, needs to be registered as a service in config.level4.neon + rule tag applied in conditionalTags.

… level 4

Register ImpossibleInArrayHaystackFiniteTypesRule as a service in
config.level4.neon with a conditionalTags entry tied to the new
featureToggles.finiteTypesInHaystack toggle (on in bleedingEdge.neon)
instead of the #[RegisteredRule] attribute, so the rule only runs in
bleeding edge for now. The treatPhpDocTypesAsCertain argument is passed
explicitly since #[AutowiredParameter] is not processed for manually
registered services.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@phpstan-bot

Copy link
Copy Markdown
Collaborator Author

Done. I've addressed the review by moving the rule from an always-on #[RegisteredRule(level: 4)] registration to a bleeding-edge-gated service registration.

Changes made

src/Rules/Comparison/ImpossibleInArrayHaystackFiniteTypesRule.php

  • Removed the #[RegisteredRule(level: 4)] attribute and its import.
  • Removed the #[AutowiredParameter] attribute from treatPhpDocTypesAsCertain (that attribute is only processed for attribute-registered services, not manual services: entries), so the argument is now supplied explicitly via config.

Config wiring (the reviewer's requested pattern, mirroring UnusedLabelRule)

  • conf/parametersSchema.neon — added finiteTypesInHaystack: bool().
  • conf/config.neon — added finiteTypesInHaystack: false (off by default).
  • conf/bleedingEdge.neon — added finiteTypesInHaystack: true (on in bleeding edge).
  • conf/config.level4.neon — registered the rule as a service (passing treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain%) and added a conditionalTags entry tying phpstan.rules.rule to %featureToggles.finiteTypesInHaystack%.

Verification

  • Regenerated vendor/attributes.php (via composer dump-autoload) since the stale generated cache still carried the old RegisteredRule — it's gitignored so not committed.
  • Confirmed the rule now fires only under bleeding edge: 5 errors with bleedingEdge.neon included at level 4, 0 without.
  • make phpstan — no errors.
  • make tests — all 17,594 tests pass (97 skipped), including the existing ImpossibleInArrayHaystackFiniteTypesRuleTest (which instantiates the rule directly, so it's unaffected by the config gating).

Committed and pushed to create-pull-request/patch-1xjngun.

@ondrejmirtes
ondrejmirtes merged commit 873b78c into phpstan:2.2.x Jul 15, 2026
408 of 414 checks passed
@ondrejmirtes
ondrejmirtes deleted the create-pull-request/patch-1xjngun branch July 15, 2026 12:09
@staabm

staabm commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

CI fails in PHP 8.0/7.4

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.

in_array - report finite types in constant array haystack that can never be needle

3 participants