diff --git a/src/Type/Php/ArrayCombineFunctionReturnTypeExtension.php b/src/Type/Php/ArrayCombineFunctionReturnTypeExtension.php index a9b9c0e042..5d7ac326af 100644 --- a/src/Type/Php/ArrayCombineFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayCombineFunctionReturnTypeExtension.php @@ -3,33 +3,25 @@ namespace PHPStan\Type\Php; use PhpParser\Node\Expr\FuncCall; -use PhpParser\Node\Expr\Variable; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Type\Accessory\NonEmptyArrayType; -use PHPStan\Type\ArrayType; -use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\Constant\ConstantBooleanType; -use PHPStan\Type\ConstantScalarType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; -use PHPStan\Type\ErrorType; -use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; use PHPStan\Type\Type; -use PHPStan\Type\TypeCombinator; use PHPStan\Type\UnionType; -use function array_key_exists; use function count; -use function is_int; -use function is_string; #[AutowiredService] final class ArrayCombineFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { - public function __construct(private PhpVersion $phpVersion) + public function __construct( + private ArrayCombineHelper $arrayCombineHelper, + private PhpVersion $phpVersion, + ) { } @@ -47,119 +39,24 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $firstArg = $functionCall->getArgs()[0]->value; $secondArg = $functionCall->getArgs()[1]->value; - $keysParamType = $scope->getType($firstArg); - $valuesParamType = $scope->getType($secondArg); - - $constantKeysArrays = $keysParamType->getConstantArrays(); - $constantValuesArrays = $valuesParamType->getConstantArrays(); - if ( - $constantKeysArrays !== [] - && $constantValuesArrays !== [] - && count($constantKeysArrays) === count($constantValuesArrays) - ) { - $results = []; - foreach ($constantKeysArrays as $k => $constantKeysArray) { - $constantValueArrays = $constantValuesArrays[$k]; - - $keyTypes = $constantKeysArray->getValueTypes(); - $valueTypes = $constantValueArrays->getValueTypes(); - - if (count($keyTypes) !== count($valueTypes)) { - if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { - return new NeverType(); - } - return new ConstantBooleanType(false); - } - - $keyTypes = $this->sanitizeConstantArrayKeyTypes($keyTypes); - if ($keyTypes === null) { - continue; - } - - $builder = ConstantArrayTypeBuilder::createEmpty(); - foreach ($keyTypes as $i => $keyType) { - if (!array_key_exists($i, $valueTypes)) { - $results = []; - break 2; - } - $valueType = $valueTypes[$i]; - $builder->setOffsetValueType($keyType, $valueType); - } - - $results[] = $builder->getArray(); - } - - if ($results !== []) { - return TypeCombinator::union(...$results); - } + [$returnType, $hasValueError] = $this->arrayCombineHelper->getReturnAndThrowType($firstArg, $secondArg, $scope); + if ($hasValueError->no()) { + return $returnType; } - if ($keysParamType->isArray()->yes()) { - $itemType = $keysParamType->getIterableValueType(); - - if ($itemType->isInteger()->no()) { - if ($itemType->toString() instanceof ErrorType) { - return new NeverType(); - } - - $keyType = $itemType->toString(); - } else { - $keyType = $itemType; + if ($hasValueError->yes()) { + if ($this->phpVersion->throwsValueErrorForInternalFunctions()) { + return new NeverType(); } - } else { - $keyType = new MixedType(); - } - - $arrayType = new ArrayType( - $keyType, - $valuesParamType->isArray()->yes() ? $valuesParamType->getIterableValueType() : new MixedType(), - ); - if ($keysParamType->isIterableAtLeastOnce()->yes() && $valuesParamType->isIterableAtLeastOnce()->yes()) { - $arrayType = TypeCombinator::intersect($arrayType, new NonEmptyArrayType()); + return new ConstantBooleanType(false); } - if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { - return $arrayType; - } - - if ($firstArg instanceof Variable && $secondArg instanceof Variable && $firstArg->name === $secondArg->name) { - return $arrayType; - } - - return new UnionType([$arrayType, new ConstantBooleanType(false)]); - } - - /** - * @param array $types - * - * @return list|null - */ - private function sanitizeConstantArrayKeyTypes(array $types): ?array - { - $sanitizedTypes = []; - - foreach ($types as $type) { - if ($type->isInteger()->no() && ! $type->toString() instanceof ErrorType) { - $type = $type->toString(); - } - - $scalars = $type->getConstantScalarTypes(); - if (count($scalars) === 0) { - return null; - } - - foreach ($scalars as $scalar) { - $value = $scalar->getValue(); - if (!is_int($value) && !is_string($value)) { - return null; - } - - $sanitizedTypes[] = $scalar; - } + if ($this->phpVersion->throwsValueErrorForInternalFunctions()) { + return $returnType; } - return $sanitizedTypes; + return new UnionType([$returnType, new ConstantBooleanType(false)]); } } diff --git a/src/Type/Php/ArrayCombineFunctionThrowTypeExtension.php b/src/Type/Php/ArrayCombineFunctionThrowTypeExtension.php new file mode 100644 index 0000000000..1c28031b33 --- /dev/null +++ b/src/Type/Php/ArrayCombineFunctionThrowTypeExtension.php @@ -0,0 +1,43 @@ +getName() === 'array_combine'; + } + + public function getThrowTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $funcCall, Scope $scope): ?Type + { + if (count($funcCall->getArgs()) < 2) { + return $functionReflection->getThrowType(); + } + + $firstArg = $funcCall->getArgs()[0]->value; + $secondArg = $funcCall->getArgs()[1]->value; + + $hasValueError = $this->arrayCombineHelper->getReturnAndThrowType($firstArg, $secondArg, $scope)[1]; + if (!$hasValueError->no()) { + return $functionReflection->getThrowType(); + } + + return null; + } + +} diff --git a/src/Type/Php/ArrayCombineHelper.php b/src/Type/Php/ArrayCombineHelper.php new file mode 100644 index 0000000000..7171bb3d46 --- /dev/null +++ b/src/Type/Php/ArrayCombineHelper.php @@ -0,0 +1,141 @@ +getType($firstArg); + $valuesParamType = $scope->getType($secondArg); + + $constantKeysArrays = $keysParamType->getConstantArrays(); + $constantValuesArrays = $valuesParamType->getConstantArrays(); + if ( + $constantKeysArrays !== [] + && $constantValuesArrays !== [] + && count($constantKeysArrays) === count($constantValuesArrays) + ) { + $results = []; + foreach ($constantKeysArrays as $k => $constantKeysArray) { + $constantValueArrays = $constantValuesArrays[$k]; + + $keyTypes = $constantKeysArray->getValueTypes(); + $valueTypes = $constantValueArrays->getValueTypes(); + + if (count($keyTypes) !== count($valueTypes)) { + return [new NeverType(), TrinaryLogic::createYes()]; + } + + $keyTypes = $this->sanitizeConstantArrayKeyTypes($keyTypes); + if ($keyTypes === null) { + continue; + } + + $builder = ConstantArrayTypeBuilder::createEmpty(); + foreach ($keyTypes as $i => $keyType) { + if (!array_key_exists($i, $valueTypes)) { + $results = []; + break 2; + } + $valueType = $valueTypes[$i]; + $builder->setOffsetValueType($keyType, $valueType); + } + + $results[] = $builder->getArray(); + } + + if ($results !== []) { + return [TypeCombinator::union(...$results), TrinaryLogic::createNo()]; + } + } + + if ($keysParamType->isArray()->yes()) { + $itemType = $keysParamType->getIterableValueType(); + + if ($itemType->isInteger()->no()) { + if ($itemType->toString() instanceof ErrorType) { + return [new NeverType(), TrinaryLogic::createNo()]; + } + + $keyType = $itemType->toString(); + } else { + $keyType = $itemType; + } + } else { + $keyType = new MixedType(); + } + + $arrayType = new ArrayType( + $keyType, + $valuesParamType->isArray()->yes() ? $valuesParamType->getIterableValueType() : new MixedType(), + ); + + if ($keysParamType->isIterableAtLeastOnce()->yes() && $valuesParamType->isIterableAtLeastOnce()->yes()) { + $arrayType = TypeCombinator::intersect($arrayType, new NonEmptyArrayType()); + } + + if ($firstArg instanceof Variable && $secondArg instanceof Variable && $firstArg->name === $secondArg->name) { + return [$arrayType, TrinaryLogic::createNo()]; + } + + return [$arrayType, TrinaryLogic::createMaybe()]; + } + + /** + * @param array $types + * + * @return list|null + */ + private function sanitizeConstantArrayKeyTypes(array $types): ?array + { + $sanitizedTypes = []; + + foreach ($types as $type) { + if ($type->isInteger()->no() && ! $type->toString() instanceof ErrorType) { + $type = $type->toString(); + } + + $scalars = $type->getConstantScalarTypes(); + if (count($scalars) === 0) { + return null; + } + + foreach ($scalars as $scalar) { + $value = $scalar->getValue(); + if (!is_int($value) && !is_string($value)) { + return null; + } + + $sanitizedTypes[] = $scalar; + } + } + + return $sanitizedTypes; + } + +} diff --git a/tests/PHPStan/Rules/Exceptions/ThrowsVoidMethodWithExplicitThrowPointRuleTest.php b/tests/PHPStan/Rules/Exceptions/ThrowsVoidMethodWithExplicitThrowPointRuleTest.php index 196d99a84c..2b6ab34a92 100644 --- a/tests/PHPStan/Rules/Exceptions/ThrowsVoidMethodWithExplicitThrowPointRuleTest.php +++ b/tests/PHPStan/Rules/Exceptions/ThrowsVoidMethodWithExplicitThrowPointRuleTest.php @@ -8,6 +8,7 @@ use PHPUnit\Framework\Attributes\RequiresPhp; use ThrowsVoidMethod\MyException; use UnhandledMatchError; +use ValueError; /** * @extends RuleTestCase @@ -99,6 +100,13 @@ public function testRule(bool $missingCheckedExceptionInThrows, array $checkedEx $this->analyse([__DIR__ . '/data/throws-void-method.php'], $errors); } + public function testBug13642(): void + { + $this->missingCheckedExceptionInThrows = false; + $this->checkedExceptionClasses = [ValueError::class]; + $this->analyse([__DIR__ . '/data/bug-13642.php'], []); + } + #[RequiresPhp('>= 8.0')] public function testBug6910(): void { diff --git a/tests/PHPStan/Rules/Exceptions/data/bug-13642.php b/tests/PHPStan/Rules/Exceptions/data/bug-13642.php new file mode 100644 index 0000000000..749c882738 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/bug-13642.php @@ -0,0 +1,12 @@ +