diff --git a/src/Rules/Comparison/ImpossibleCheckTypeHelper.php b/src/Rules/Comparison/ImpossibleCheckTypeHelper.php index d485dc6e734..b60b4ffef60 100644 --- a/src/Rules/Comparison/ImpossibleCheckTypeHelper.php +++ b/src/Rules/Comparison/ImpossibleCheckTypeHelper.php @@ -11,6 +11,7 @@ use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\Reflection\ReflectionProvider; +use PHPStan\TrinaryLogic; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantStringType; @@ -27,7 +28,6 @@ use function implode; use function in_array; use function is_string; -use function reset; use function sprintf; use function strtolower; @@ -189,50 +189,66 @@ public function findSpecifiedType( return null; } - if (count($sureTypes) === 1 && count($sureNotTypes) === 0) { - $sureType = reset($sureTypes); - if (self::isSpecified($scope, $node, $sureType[0])) { - return null; - } + if (count($sureTypes) > 0 && count($sureNotTypes) === 0) { + $results = []; - if ($this->treatPhpDocTypesAsCertain) { - $argumentType = $scope->getType($sureType[0]); - } else { - $argumentType = $scope->getNativeType($sureType[0]); - } + foreach ($sureTypes as $sureType) { + if (self::isSpecified($scope, $node, $sureType[0])) { + return null; + } - /** @var Type $resultType */ - $resultType = $sureType[1]; + if ($this->treatPhpDocTypesAsCertain) { + $argumentType = $scope->getType($sureType[0]); + } else { + $argumentType = $scope->getNativeType($sureType[0]); + } - $isSuperType = $resultType->isSuperTypeOf($argumentType); - if ($isSuperType->yes()) { - return true; - } elseif ($isSuperType->no()) { - return false; + /** @var Type $resultType */ + $resultType = $sureType[1]; + + $isSuperType = $resultType->isSuperTypeOf($argumentType); + if ($isSuperType->maybe()) { + continue; + } + + $results[] = $isSuperType; } - return null; - } elseif (count($sureNotTypes) === 1 && count($sureTypes) === 0) { - $sureNotType = reset($sureNotTypes); - if (self::isSpecified($scope, $node, $sureNotType[0])) { + if (count($results) === 0) { return null; } - if ($this->treatPhpDocTypesAsCertain) { - $argumentType = $scope->getType($sureNotType[0]); - } else { - $argumentType = $scope->getNativeType($sureNotType[0]); - } + return TrinaryLogic::createYes()->and(...$results)->yes(); + } elseif (count($sureNotTypes) > 0 && count($sureTypes) === 0) { + $results = []; - /** @var Type $resultType */ - $resultType = $sureNotType[1]; + foreach ($sureNotTypes as $sureNotType) { + if (self::isSpecified($scope, $node, $sureNotType[0])) { + return null; + } + + if ($this->treatPhpDocTypesAsCertain) { + $argumentType = $scope->getType($sureNotType[0]); + } else { + $argumentType = $scope->getNativeType($sureNotType[0]); + } + + /** @var Type $resultType */ + $resultType = $sureNotType[1]; - $isSuperType = $resultType->isSuperTypeOf($argumentType); - if ($isSuperType->yes()) { - return false; - } elseif ($isSuperType->no()) { - return true; + $isSuperType = $resultType->isSuperTypeOf($argumentType); + if ($isSuperType->maybe()) { + continue; + } + + $results[] = $isSuperType; } + + if (count($results) === 0) { + return null; + } + + return TrinaryLogic::createYes()->and(...$results)->no(); } return null; diff --git a/src/Type/Php/InArrayFunctionTypeSpecifyingExtension.php b/src/Type/Php/InArrayFunctionTypeSpecifyingExtension.php index 6988702c573..65b38f234cf 100644 --- a/src/Type/Php/InArrayFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/InArrayFunctionTypeSpecifyingExtension.php @@ -9,8 +9,11 @@ use PHPStan\Analyser\TypeSpecifierAwareExtension; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\Reflection\FunctionReflection; +use PHPStan\Type\ArrayType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\FunctionTypeSpecifyingExtension; +use PHPStan\Type\MixedType; +use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; use function count; use function strtolower; @@ -41,6 +44,7 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n return new SpecifiedTypes([], []); } + $needleType = $scope->getType($node->getArgs()[0]->value); $arrayValueType = $scope->getType($node->getArgs()[1]->value)->getIterableValueType(); if ( @@ -48,16 +52,38 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n || count(TypeUtils::getConstantScalars($arrayValueType)) > 0 || count(TypeUtils::getEnumCaseObjects($arrayValueType)) > 0 ) { - return $this->typeSpecifier->create( + $specifiedTypes = $this->typeSpecifier->create( $node->getArgs()[0]->value, $arrayValueType, $context, false, $scope, ); + } else { + $specifiedTypes = new SpecifiedTypes([], []); } - return new SpecifiedTypes([], []); + if ( + $context->truthy() + || count(TypeUtils::getConstantScalars($needleType)) > 0 + || count(TypeUtils::getEnumCaseObjects($needleType)) > 0 + ) { + if ($context->truthy()) { + $arrayValueType = TypeCombinator::union($arrayValueType, $needleType); + } else { + $arrayValueType = TypeCombinator::remove($arrayValueType, $needleType); + } + + $specifiedTypes = $specifiedTypes->unionWith($this->typeSpecifier->create( + $node->getArgs()[1]->value, + new ArrayType(new MixedType(), $arrayValueType), + TypeSpecifierContext::createTrue(), + false, + $scope, + )); + } + + return $specifiedTypes; } } diff --git a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php index e59509170b8..0b4b314dbf2 100644 --- a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php +++ b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php @@ -847,6 +847,18 @@ public function testBug7381(): void $this->assertNoErrors($errors); } + public function testBug7153(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-7153.php'); + $this->assertNoErrors($errors); + } + + public function testBug7275(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-7275.php'); + $this->assertNoErrors($errors); + } + /** * @param string[]|null $allAnalysedFiles * @return Error[] diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index b77b86240d4..ae77180c18d 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -913,6 +913,7 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7353.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7031.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/constant-array-intersect.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7153.php'); } /** diff --git a/tests/PHPStan/Analyser/data/bug-7153.php b/tests/PHPStan/Analyser/data/bug-7153.php new file mode 100644 index 00000000000..973beecf244 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-7153.php @@ -0,0 +1,35 @@ + 0 ? 'bleh' : null; +} + +function blih(string $blah, string $bleh): void +{ +} + +function () { + $data = [blah(), bleh()]; + + assertType('array{string, string|null}', $data); + + if (in_array(null, $data, true)) { + assertType('array{string, string|null}', $data); + throw new Exception(); + } + + assertType('array{string, string}', $data); + + blih($data[0], $data[1]); +}; diff --git a/tests/PHPStan/Analyser/data/bug-7275.php b/tests/PHPStan/Analyser/data/bug-7275.php new file mode 100644 index 00000000000..4f4224e3138 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-7275.php @@ -0,0 +1,28 @@ + $collectionWithPotentialNulls + * + * @return mixed[] + */ + public function doSomething(array $collectionWithPotentialNulls): array + { + return !in_array(null, $collectionWithPotentialNulls, true) + ? $this->doSomethingElse($collectionWithPotentialNulls) + : []; + } + + /** + * @param array $collectionWithoutNulls + * + * @return mixed[] + */ + public function doSomethingElse(array $collectionWithoutNulls): array + { + return []; + } +}