diff --git a/src/Rules/Comparison/ImpossibleCheckTypeHelper.php b/src/Rules/Comparison/ImpossibleCheckTypeHelper.php index f5599c6394b..f729683ecab 100644 --- a/src/Rules/Comparison/ImpossibleCheckTypeHelper.php +++ b/src/Rules/Comparison/ImpossibleCheckTypeHelper.php @@ -187,33 +187,46 @@ private function getSpecifiedType( } if ($isNeedleSupertype->maybe() || $isNeedleSupertype->yes()) { + $needleFiniteTypes = $needleType->getFiniteTypes(); + if ($needleFiniteTypes === []) { + return null; + } + foreach ($haystackArrayTypes as $haystackArrayType) { - if ($haystackArrayType instanceof ConstantArrayType) { - foreach ($haystackArrayType->getValueTypes() as $i => $haystackArrayValueType) { - if ($haystackArrayType->isOptionalKey($i)) { - continue; - } + $guaranteedValueTypes = []; + if (!($haystackArrayType instanceof ConstantArrayType)) { + // A general array cannot guarantee any specific value is present. + return null; + } - $haystackArrayValueConstantScalarTypes = $haystackArrayValueType->getConstantScalarTypes(); - if (count($haystackArrayValueConstantScalarTypes) > 1) { - continue; - } + foreach ($haystackArrayType->getValueTypes() as $i => $haystackArrayValueType) { + if ($haystackArrayType->isOptionalKey($i)) { + continue; + } - foreach ($haystackArrayValueConstantScalarTypes as $constantScalarType) { - if ($constantScalarType->isSuperTypeOf($needleType)->yes()) { - continue 3; - } - } + $haystackArrayValueFiniteTypes = $haystackArrayValueType->getFiniteTypes(); + if (count($haystackArrayValueFiniteTypes) !== 1) { + continue; } - } else { - foreach ($haystackArrayType->getIterableValueType()->getConstantScalarTypes() as $constantScalarType) { - if ($constantScalarType->isSuperTypeOf($needleType)->yes()) { - continue 2; + + $guaranteedValueTypes[] = $haystackArrayValueFiniteTypes[0]; + } + + // in_array() is only guaranteed true when every possible needle value + // is guaranteed to be present in this haystack variant. + foreach ($needleFiniteTypes as $needleFiniteType) { + $found = false; + foreach ($guaranteedValueTypes as $guaranteedValueType) { + if ($guaranteedValueType->isSuperTypeOf($needleFiniteType)->yes()) { + $found = true; + break; } } - } - return null; + if (!$found) { + return null; + } + } } } diff --git a/src/Rules/Functions/ParameterCastableToStringRule.php b/src/Rules/Functions/ParameterCastableToStringRule.php index ebf48791625..8142ec7320c 100644 --- a/src/Rules/Functions/ParameterCastableToStringRule.php +++ b/src/Rules/Functions/ParameterCastableToStringRule.php @@ -74,7 +74,7 @@ public function processNode(Node $node, Scope $scope): array if (in_array($functionName, $checkAllArgsFunctions, true)) { $argsToCheck = $origArgs; - } elseif (in_array($functionName, $checkFirstArgFunctions, true)) { + } else { $normalizedFuncCall = ArgumentsNormalizer::reorderFuncArguments($parametersAcceptor, $node); if ($normalizedFuncCall === null) { @@ -86,8 +86,6 @@ public function processNode(Node $node, Scope $scope): array return []; } $argsToCheck = [0 => $normalizedArgs[0]]; - } else { - return []; } $errors = []; diff --git a/src/Type/Php/InArrayFunctionTypeSpecifyingExtension.php b/src/Type/Php/InArrayFunctionTypeSpecifyingExtension.php index df07f5d11d7..d6e3f0e3587 100644 --- a/src/Type/Php/InArrayFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/InArrayFunctionTypeSpecifyingExtension.php @@ -72,6 +72,7 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n if ($arrayExpr instanceof Array_) { $types = null; + $combinedMultipleItems = false; foreach ($arrayExpr->items as $item) { if ($item->unpack) { $types = null; @@ -89,10 +90,21 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n continue; } + $combinedMultipleItems = true; $types = $context->true() ? $types->normalize($scope)->intersectWith($itemTypes->normalize($scope)) : $types->unionWith($itemTypes); } if ($types !== null) { + if ($combinedMultipleItems) { + // Each per-item SpecifiedTypes carries its own "$needle === $item" + // as its root expression. Once the comparisons of multiple items are + // combined, that per-item root expression no longer describes the whole + // in_array() call, so it must be cleared. Leaving it set lets callers + // such as ImpossibleCheckTypeHelper read the type of a single item's + // comparison and draw a wrong conclusion about the whole call. + $types = $types->setRootExpr(null); + } + return $types; } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-14873.php b/tests/PHPStan/Analyser/nsrt/bug-14873.php new file mode 100644 index 00000000000..1677b3c5f08 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-14873.php @@ -0,0 +1,114 @@ += 8.1 + +declare(strict_types = 1); + +namespace Bug14873; + +use function PHPStan\Testing\assertType; +use function in_array; + +enum Suit: string +{ + + case Hearts = 'H'; + case Spades = 'S'; + case Clubs = 'C'; + +} + +class HelloWorld +{ + + /** + * @param 'a'|'b'|'c' $full + * @param 'a'|'b' $subset + * @param 'a'|'x' $partial + */ + public function variableHaystack(string $full, string $subset, string $partial): void + { + $a = ['a', 'b', 'c']; + + assertType('true', in_array($full, $a, true)); + assertType('true', in_array($subset, $a, true)); + assertType('bool', in_array($partial, $a, true)); + } + + /** + * @param 'a'|'b'|'c' $full + * @param 'a'|'b' $subset + * @param 'a'|'x' $partial + */ + public function literalHaystack(string $full, string $subset, string $partial): void + { + assertType('true', in_array($full, ['a', 'b', 'c'], true)); + assertType('bool', in_array($full, ['a', 'b', 'c'], false)); // non-strict + assertType('true', in_array($subset, ['a', 'b', 'c'], true)); + assertType('bool', in_array($partial, ['a', 'b', 'c'], true)); + assertType('false', in_array($subset, ['x', 'y'], true)); + + $fullOrEmpty = rand(0,1) ? $full : []; + assertType('bool', in_array($fullOrEmpty, ['a', 'b', 'c'], true)); + } + + /** + * @param 1|2 $full + * @param 1 $subset + */ + public function integers(int $full, int $subset): void + { + $a = [1, 2]; + + assertType('true', in_array($full, $a, true)); + assertType('true', in_array($subset, $a, true)); + assertType('true', in_array($full, [1, 2, 3], true)); + } + + /** + * @param Suit::Hearts|Suit::Spades $subset + */ + public function enums(Suit $subset): void + { + $a = [Suit::Hearts, Suit::Spades, Suit::Clubs]; + + assertType('true', in_array($subset, $a, true)); + assertType('true', in_array($subset, [Suit::Hearts, Suit::Spades, Suit::Clubs], true)); + assertType('bool', in_array($subset, [Suit::Hearts], true)); + } + + /** + * Plain objects do not have a finite set of possible values, so in_array() + * must not be reported as always-true even when the needle's class matches + * every haystack value type. + */ + public function objects(Article $article, ?Article $a, ?Article $b): void + { + $haystack = [$a, $b]; + + assertType('bool', in_array($article, $haystack, true)); + assertType('bool', in_array($article, [$a, $b], true)); + } + + /** + * A general (non-constant) array does not guarantee that any particular value + * is present, so a subset needle must not be reported as always-true. Only a + * non-empty array whose values all share a single finite type guarantees that + * value's presence. + * + * @param 1|2 $needle + * @param array $maybeEmpty + * @param non-empty-array $nonEmptyMulti + * @param non-empty-array $nonEmptySingle + */ + public function generalArrays(int $needle, array $maybeEmpty, array $nonEmptyMulti, array $nonEmptySingle): void + { + assertType('bool', in_array($needle, $maybeEmpty, true)); + assertType('bool', in_array($needle, $nonEmptyMulti, true)); + assertType('true', in_array(1, $nonEmptySingle, true)); + } + +} + +class Article +{ + +} diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php index 4c8c7695324..89f671f8df8 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php @@ -127,6 +127,10 @@ public function testImpossibleCheckTypeFunctionCall(): void 'Call to function in_array() with arguments \'bar\'|\'foo\', array{\'baz\', \'lorem\'} and true will always evaluate to false.', 255, ], + [ + 'Call to function in_array() with arguments \'bar\'|\'foo\', array{\'foo\', \'bar\'} and true will always evaluate to true.', + 259, + ], [ 'Call to function in_array() with arguments \'foo\', array{\'foo\'} and true will always evaluate to true.', 263, @@ -1094,6 +1098,25 @@ public function testBug12412(): void $this->analyse([__DIR__ . '/data/bug-12412.php'], []); } + public function testBug14873(): void + { + $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; + + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-14873.php'], [ + [ + 'Call to function in_array() with arguments \'a\'|\'b\'|\'c\', array{\'a\', \'b\', \'c\'} and true will always evaluate to true.', + 17, + $tipText, + ], + [ + 'Call to function in_array() with arguments \'a\'|\'b\', array{\'a\', \'b\', \'c\'} and true will always evaluate to true.', + 21, + $tipText, + ], + ]); + } + public function testBug2730(): void { $this->treatPhpDocTypesAsCertain = true; diff --git a/tests/PHPStan/Rules/Comparison/data/bug-14873.php b/tests/PHPStan/Rules/Comparison/data/bug-14873.php new file mode 100644 index 00000000000..a64b5e809ba --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-14873.php @@ -0,0 +1,50 @@ + $maybeEmpty + * @param non-empty-array $nonEmptyMulti + */ + public function generalArrays(int $needle, array $maybeEmpty, array $nonEmptyMulti): void + { + if (in_array($needle, $maybeEmpty, true)) { + echo 'maybeEmpty'; + } + + if (in_array($needle, $nonEmptyMulti, true)) { + echo 'nonEmptyMulti'; + } + } + +}