From b985b60784f2c5475852d87c4047cdca7e069121 Mon Sep 17 00:00:00 2001 From: ondrejmirtes <104888+ondrejmirtes@users.noreply.github.com> Date: Wed, 15 Jul 2026 11:49:54 +0000 Subject: [PATCH 1/2] Report finite-typed values in a constant array haystack that can never 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. --- ...possibleInArrayHaystackFiniteTypesRule.php | 145 ++++++++++++++++++ ...ibleInArrayHaystackFiniteTypesRuleTest.php | 51 ++++++ .../data/impossible-in-array-finite-types.php | 90 +++++++++++ 3 files changed, 286 insertions(+) create mode 100644 src/Rules/Comparison/ImpossibleInArrayHaystackFiniteTypesRule.php create mode 100644 tests/PHPStan/Rules/Comparison/ImpossibleInArrayHaystackFiniteTypesRuleTest.php create mode 100644 tests/PHPStan/Rules/Comparison/data/impossible-in-array-finite-types.php diff --git a/src/Rules/Comparison/ImpossibleInArrayHaystackFiniteTypesRule.php b/src/Rules/Comparison/ImpossibleInArrayHaystackFiniteTypesRule.php new file mode 100644 index 00000000000..6258bfcf70a --- /dev/null +++ b/src/Rules/Comparison/ImpossibleInArrayHaystackFiniteTypesRule.php @@ -0,0 +1,145 @@ + + */ +#[RegisteredRule(level: 4)] +final class ImpossibleInArrayHaystackFiniteTypesRule implements Rule +{ + + /** Argument positions of the needle and the haystack per supported function. */ + private const FUNCTIONS = [ + 'in_array' => ['needle' => 0, 'haystack' => 1], + 'array_search' => ['needle' => 0, 'haystack' => 1], + 'array_keys' => ['needle' => 1, 'haystack' => 0], + ]; + + public function __construct( + private InitializerExprTypeResolver $initializerExprTypeResolver, + #[AutowiredParameter] + private bool $treatPhpDocTypesAsCertain, + ) + { + } + + public function getNodeType(): string + { + return FuncCall::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$node->name instanceof Node\Name) { + return []; + } + + $functionName = strtolower((string) $node->name); + if (!array_key_exists($functionName, self::FUNCTIONS)) { + return []; + } + + $needleArg = self::FUNCTIONS[$functionName]['needle']; + $haystackArg = self::FUNCTIONS[$functionName]['haystack']; + + $args = $node->getArgs(); + if (count($args) <= max($needleArg, $haystackArg)) { + return []; + } + + $needleType = $this->treatPhpDocTypesAsCertain ? $scope->getType($args[$needleArg]->value) : $scope->getNativeType($args[$needleArg]->value); + $haystackType = $this->treatPhpDocTypesAsCertain ? $scope->getType($args[$haystackArg]->value) : $scope->getNativeType($args[$haystackArg]->value); + + $constantArrays = $haystackType->getConstantArrays(); + if (count($constantArrays) === 0) { + return []; + } + + $isStrict = count($args) >= 3 + ? ($this->treatPhpDocTypesAsCertain ? $scope->getType($args[2]->value) : $scope->getNativeType($args[2]->value))->isTrue() + : TrinaryLogic::createNo(); + + $deadValueTypes = []; + $anyPossibleMatch = false; + foreach ($constantArrays as $constantArray) { + foreach ($constantArray->getValueTypes() as $valueType) { + if (!$this->canNeverMatch($needleType, $valueType, $isStrict)) { + $anyPossibleMatch = true; + continue; + } + + if (count($valueType->getFiniteTypes()) === 0) { + continue; + } + + $deadValueTypes[$valueType->describe(VerbosityLevel::precise())] = $valueType; + } + } + + // When no haystack value can ever match, the whole in_array() call is + // impossible and reported by ImpossibleCheckTypeFunctionCallRule instead. + // array_search() has no such companion rule, so keep reporting there. + if (!$anyPossibleMatch && $functionName === 'in_array') { + return []; + } + + $verb = $isStrict->no() ? 'equal to' : 'identical to'; + + $errors = []; + foreach ($deadValueTypes as $valueType) { + $errors[] = $this->buildError($valueType, $needleType, $functionName, $verb); + } + + return $errors; + } + + private function canNeverMatch(Type $needleType, Type $valueType, TrinaryLogic $isStrict): bool + { + if ($isStrict->yes()) { + return $this->initializerExprTypeResolver->resolveIdenticalType($needleType, $valueType)->type->isFalse()->yes(); + } + + if ($isStrict->no()) { + return $this->initializerExprTypeResolver->resolveEqualType($needleType, $valueType)->type->isFalse()->yes(); + } + + return $this->initializerExprTypeResolver->resolveIdenticalType($needleType, $valueType)->type->isFalse()->yes() + && $this->initializerExprTypeResolver->resolveEqualType($needleType, $valueType)->type->isFalse()->yes(); + } + + private function buildError(Type $valueType, Type $needleType, string $functionName, string $verb): IdentifierRuleError + { + return RuleErrorBuilder::message(sprintf( + 'Value %s in the haystack passed to %s() can never be %s the needle type %s.', + $valueType->describe(VerbosityLevel::precise()), + $functionName, + $verb, + $needleType->describe(VerbosityLevel::precise()), + ))->identifier('function.impossibleHaystackValue')->build(); + } + +} diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleInArrayHaystackFiniteTypesRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleInArrayHaystackFiniteTypesRuleTest.php new file mode 100644 index 00000000000..e2fdfbda961 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/ImpossibleInArrayHaystackFiniteTypesRuleTest.php @@ -0,0 +1,51 @@ + + */ +class ImpossibleInArrayHaystackFiniteTypesRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new ImpossibleInArrayHaystackFiniteTypesRule( + self::getContainer()->getByType(InitializerExprTypeResolver::class), + true, + ); + } + + #[RequiresPhp('>= 8.1.0')] + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/impossible-in-array-finite-types.php'], [ + [ + 'Value ImpossibleInArrayFiniteTypes\Foo::ONE in the haystack passed to in_array() can never be identical to the needle type int.', + 19, + ], + [ + 'Value ImpossibleInArrayFiniteTypes\Foo::ONE in the haystack passed to in_array() can never be equal to the needle type int.', + 26, + ], + [ + 'Value ImpossibleInArrayFiniteTypes\Foo::ONE in the haystack passed to array_search() can never be identical to the needle type int.', + 33, + ], + [ + 'Value ImpossibleInArrayFiniteTypes\Foo::ONE in the haystack passed to array_keys() can never be identical to the needle type int.', + 38, + ], + [ + 'Value ImpossibleInArrayFiniteTypes\Foo::TWO in the haystack passed to in_array() can never be identical to the needle type ImpossibleInArrayFiniteTypes\Foo::ONE.', + 48, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/impossible-in-array-finite-types.php b/tests/PHPStan/Rules/Comparison/data/impossible-in-array-finite-types.php new file mode 100644 index 00000000000..f3961709d2d --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/impossible-in-array-finite-types.php @@ -0,0 +1,90 @@ += 8.1 + +declare(strict_types = 1); + +namespace ImpossibleInArrayFiniteTypes; + +enum Foo +{ + + case ONE; + case TWO; + case THREE; + +} + +function reportedFiniteValueStrict(int $i): void +{ + // Foo::ONE can never be an int, but 1 and 2 can. + if (in_array($i, [Foo::ONE, 1, 2], true)) { + echo 'yes'; + } +} + +function reportedFiniteValueLoose(int $i): void +{ + if (in_array($i, [Foo::ONE, 1, 2])) { + echo 'yes'; + } +} + +function reportedArraySearch(int $i): void +{ + array_search($i, [Foo::ONE, 1, 2], true); +} + +function reportedArrayKeys(int $i): void +{ + array_keys([Foo::ONE, 1, 2], $i, true); +} + +function reportedEnumNeedle(Foo $foo): void +{ + if ($foo !== Foo::ONE) { + return; + } + + // Foo::TWO can never be Foo::ONE (finite value in haystack), but Foo::ONE can. + if (in_array($foo, [Foo::ONE, Foo::TWO], true)) { + echo 'yes'; + } +} + +function noErrorEverythingMatches(int $i): void +{ + if (in_array($i, [1, 2, 3], true)) { + echo 'yes'; + } +} + +function noErrorMixedNeedle(mixed $i): void +{ + if (in_array($i, [Foo::ONE, 1, 2], true)) { + echo 'yes'; + } +} + +function noErrorWholeCallImpossible(int $i): void +{ + // Whole call is impossible - reported by ImpossibleCheckTypeFunctionCallRule instead. + if (in_array($i, [Foo::ONE, Foo::TWO], true)) { + echo 'yes'; + } +} + +function noErrorNonConstantHaystack(int $i, array $haystack): void +{ + if (in_array($i, $haystack, true)) { + echo 'yes'; + } +} + +/** + * @param int|string $i + */ +function noErrorUnionNeedleMatches(int|string $i): void +{ + if (in_array($i, ['a', 'b', 1], true)) { + echo 'yes'; + } +} From 1475e3cefbb937b04ae323033e202b081b48459d Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Wed, 15 Jul 2026 12:03:46 +0000 Subject: [PATCH 2/2] Gate finite-typed haystack rule behind bleedingEdge feature toggle at 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 --- conf/bleedingEdge.neon | 1 + conf/config.level4.neon | 7 +++++++ conf/config.neon | 1 + conf/parametersSchema.neon | 1 + .../ImpossibleInArrayHaystackFiniteTypesRule.php | 4 ---- 5 files changed, 10 insertions(+), 4 deletions(-) diff --git a/conf/bleedingEdge.neon b/conf/bleedingEdge.neon index 2ad962c8d32..f861a6ed7d3 100644 --- a/conf/bleedingEdge.neon +++ b/conf/bleedingEdge.neon @@ -22,3 +22,4 @@ parameters: checkDynamicConstantNameValues: true unusedLabel: true newOnNonObject: true + finiteTypesInHaystack: true diff --git a/conf/config.level4.neon b/conf/config.level4.neon index cd0b60d4c16..91bed20886f 100644 --- a/conf/config.level4.neon +++ b/conf/config.level4.neon @@ -14,6 +14,8 @@ conditionalTags: phpstan.rules.rule: %exceptions.check.tooWideThrowType% PHPStan\Rules\Keywords\UnusedLabelRule: phpstan.rules.rule: %featureToggles.unusedLabel% + PHPStan\Rules\Comparison\ImpossibleInArrayHaystackFiniteTypesRule: + phpstan.rules.rule: %featureToggles.finiteTypesInHaystack% parameters: checkAdvancedIsset: true @@ -35,3 +37,8 @@ services: - class: PHPStan\Rules\Keywords\UnusedLabelRule + + - + class: PHPStan\Rules\Comparison\ImpossibleInArrayHaystackFiniteTypesRule + arguments: + treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% diff --git a/conf/config.neon b/conf/config.neon index bddac2e56af..3ea277b817d 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -48,6 +48,7 @@ parameters: checkDynamicConstantNameValues: false unusedLabel: false newOnNonObject: false + finiteTypesInHaystack: false fileExtensions: - php checkAdvancedIsset: false diff --git a/conf/parametersSchema.neon b/conf/parametersSchema.neon index 3c1819a1463..7124bd3f7e2 100644 --- a/conf/parametersSchema.neon +++ b/conf/parametersSchema.neon @@ -51,6 +51,7 @@ parametersSchema: checkDynamicConstantNameValues: bool() unusedLabel: bool() newOnNonObject: bool() + finiteTypesInHaystack: bool() ]) fileExtensions: listOf(string()) checkAdvancedIsset: bool() diff --git a/src/Rules/Comparison/ImpossibleInArrayHaystackFiniteTypesRule.php b/src/Rules/Comparison/ImpossibleInArrayHaystackFiniteTypesRule.php index 6258bfcf70a..b1617e12164 100644 --- a/src/Rules/Comparison/ImpossibleInArrayHaystackFiniteTypesRule.php +++ b/src/Rules/Comparison/ImpossibleInArrayHaystackFiniteTypesRule.php @@ -5,8 +5,6 @@ use PhpParser\Node; use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; -use PHPStan\DependencyInjection\AutowiredParameter; -use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; @@ -28,7 +26,6 @@ * * @implements Rule */ -#[RegisteredRule(level: 4)] final class ImpossibleInArrayHaystackFiniteTypesRule implements Rule { @@ -41,7 +38,6 @@ final class ImpossibleInArrayHaystackFiniteTypesRule implements Rule public function __construct( private InitializerExprTypeResolver $initializerExprTypeResolver, - #[AutowiredParameter] private bool $treatPhpDocTypesAsCertain, ) {