diff --git a/src/Php/PhpVersions.php b/src/Php/PhpVersions.php index 7ed0fe5c65c..f6c8bfc2f18 100644 --- a/src/Php/PhpVersions.php +++ b/src/Php/PhpVersions.php @@ -188,9 +188,84 @@ public function supportsDeprecatedTraits(): TrinaryLogic return IntegerRangeType::fromInterval(80500, null)->isSuperTypeOf($this->phpVersions)->result; } + public function arrayFunctionsReturnNullWithNonArray(): TrinaryLogic + { + return IntegerRangeType::fromInterval(null, 79999)->isSuperTypeOf($this->phpVersions)->result; + } + + public function hasDateTimeExceptions(): TrinaryLogic + { + return IntegerRangeType::fromInterval(80300, null)->isSuperTypeOf($this->phpVersions)->result; + } + + public function hasFilterThrowOnFailureConstant(): TrinaryLogic + { + return IntegerRangeType::fromInterval(80500, null)->isSuperTypeOf($this->phpVersions)->result; + } + + public function hasPDOSubclasses(): TrinaryLogic + { + return IntegerRangeType::fromInterval(80400, null)->isSuperTypeOf($this->phpVersions)->result; + } + + public function hasStricterRoundFunctions(): TrinaryLogic + { + return IntegerRangeType::fromInterval(80000, null)->isSuperTypeOf($this->phpVersions)->result; + } + + public function highlightStringDoesNotReturnFalse(): TrinaryLogic + { + return IntegerRangeType::fromInterval(80400, null)->isSuperTypeOf($this->phpVersions)->result; + } + + public function isEmptyStringValidAliasForNoneInMbSubstituteCharacter(): TrinaryLogic + { + return IntegerRangeType::fromInterval(null, 79999)->isSuperTypeOf($this->phpVersions)->result; + } + + public function isNullValidArgInMbSubstituteCharacter(): TrinaryLogic + { + return IntegerRangeType::fromInterval(80000, null)->isSuperTypeOf($this->phpVersions)->result; + } + + public function isNumericStringValidArgInMbSubstituteCharacter(): TrinaryLogic + { + return IntegerRangeType::fromInterval(null, 79999)->isSuperTypeOf($this->phpVersions)->result; + } + + public function strSplitReturnsEmptyArray(): TrinaryLogic + { + return IntegerRangeType::fromInterval(80200, null)->isSuperTypeOf($this->phpVersions)->result; + } + + public function substrReturnFalseInsteadOfEmptyString(): TrinaryLogic + { + return IntegerRangeType::fromInterval(null, 79999)->isSuperTypeOf($this->phpVersions)->result; + } + + public function supportsAllUnicodeScalarCodePointsInMbSubstituteCharacter(): TrinaryLogic + { + return IntegerRangeType::fromInterval(70200, null)->isSuperTypeOf($this->phpVersions)->result; + } + public function supportsHhPrintfSpecifier(): TrinaryLogic { return IntegerRangeType::fromInterval(80000, null)->isSuperTypeOf($this->phpVersions)->result; } + public function supportsPassNoneEncodings(): TrinaryLogic + { + return IntegerRangeType::fromInterval(null, 70299)->isSuperTypeOf($this->phpVersions)->result; + } + + public function throwsOnInvalidMbStringEncoding(): TrinaryLogic + { + return IntegerRangeType::fromInterval(80000, null)->isSuperTypeOf($this->phpVersions)->result; + } + + public function throwsValueErrorForInvalidRoundingMode(): TrinaryLogic + { + return IntegerRangeType::fromInterval(80400, null)->isSuperTypeOf($this->phpVersions)->result; + } + } diff --git a/src/Type/Php/ArrayChunkFunctionReturnTypeExtension.php b/src/Type/Php/ArrayChunkFunctionReturnTypeExtension.php index cfd69286ecf..a1c1e2721c1 100644 --- a/src/Type/Php/ArrayChunkFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayChunkFunctionReturnTypeExtension.php @@ -5,7 +5,6 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; @@ -19,10 +18,6 @@ final class ArrayChunkFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { - public function __construct(private PhpVersion $phpVersion) - { - } - public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'array_chunk'; @@ -37,13 +32,21 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $arrayType = $scope->getType($args[0]->value); if ($arrayType->isArray()->no()) { - return $this->phpVersion->arrayFunctionsReturnNullWithNonArray() ? new NullType() : new NeverType(); + if ($scope->getPhpVersion()->arrayFunctionsReturnNullWithNonArray()->no()) { + return new NeverType(); + } + + return new NullType(); } $lengthType = $scope->getType($args[1]->value); $negativeOrZero = IntegerRangeType::fromInterval(null, 0); if ($negativeOrZero->isSuperTypeOf($lengthType)->yes()) { - return $this->phpVersion->throwsValueErrorForInternalFunctions() ? new NeverType() : new NullType(); + if ($scope->getPhpVersion()->throwsValueErrorForInternalFunctions()->yes()) { + return new NeverType(); + } + + return new NullType(); } $preserveKeysType = isset($args[2]) ? $scope->getType($args[2]->value) : new ConstantBooleanType(false); diff --git a/src/Type/Php/ArrayChunkFunctionThrowTypeExtension.php b/src/Type/Php/ArrayChunkFunctionThrowTypeExtension.php index 97e8a4361f5..03e7d0ee1bc 100644 --- a/src/Type/Php/ArrayChunkFunctionThrowTypeExtension.php +++ b/src/Type/Php/ArrayChunkFunctionThrowTypeExtension.php @@ -5,7 +5,6 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\DynamicFunctionThrowTypeExtension; use PHPStan\Type\IntegerRangeType; @@ -20,10 +19,6 @@ final class ArrayChunkFunctionThrowTypeExtension implements DynamicFunctionThrowTypeExtension { - public function __construct(private PhpVersion $phpVersion) - { - } - public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'array_chunk'; @@ -31,7 +26,7 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getThrowTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $funcCall, Scope $scope): ?Type { - if (!$this->phpVersion->throwsValueErrorForInternalFunctions()) { + if ($scope->getPhpVersion()->throwsValueErrorForInternalFunctions()->no()) { return new VoidType(); } diff --git a/src/Type/Php/ArrayColumnHelper.php b/src/Type/Php/ArrayColumnHelper.php index aeb414875f0..c2121f50f4d 100644 --- a/src/Type/Php/ArrayColumnHelper.php +++ b/src/Type/Php/ArrayColumnHelper.php @@ -4,7 +4,6 @@ use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Php\PhpVersion; use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\NonEmptyArrayType; @@ -21,12 +20,6 @@ final class ArrayColumnHelper { - public function __construct( - private PhpVersion $phpVersion, - ) - { - } - /** * @return array{Type, TrinaryLogic} */ @@ -74,7 +67,7 @@ public function handleAnyArray(Type $arrayType, Type $columnType, Type $indexTyp } $returnKeyType = $this->getReturnIndexType($arrayType, $indexType, $scope); - $returnType = new ArrayType($this->castToArrayKeyType($returnKeyType), $returnValueType); + $returnType = new ArrayType($this->castToArrayKeyType($returnKeyType, $scope), $returnValueType); if ($iterableAtLeastOnce->yes()) { $returnType = TypeCombinator::intersect($returnType, new NonEmptyArrayType()); @@ -113,7 +106,7 @@ public function handleConstantArray(ConstantArrayType $arrayType, Type $columnTy } if ($keyType !== null) { - $keyType = $this->castToArrayKeyType($keyType); + $keyType = $this->castToArrayKeyType($keyType, $scope); } $builder->setOffsetValueType($keyType, $valueType, $arrayType->isOptionalKey($i)); } @@ -131,9 +124,9 @@ public function handleConstantArray(ConstantArrayType $arrayType, Type $columnTy if ($unsealedKeyFromIndex instanceof NeverType) { $unsealedKey = $unsealedTypes[0]; } elseif ($unsealedKeyCertainty->yes()) { - $unsealedKey = $this->castToArrayKeyType($unsealedKeyFromIndex); + $unsealedKey = $this->castToArrayKeyType($unsealedKeyFromIndex, $scope); } else { - $unsealedKey = $this->castToArrayKeyType(TypeCombinator::union($unsealedKeyFromIndex, new IntegerType())); + $unsealedKey = $this->castToArrayKeyType(TypeCombinator::union($unsealedKeyFromIndex, new IntegerType()), $scope); } } else { // `null` indexType keeps integer-keyed list semantics — @@ -221,18 +214,23 @@ private function getOffsetOrProperty(Type $type, Type $offsetOrProperty, Scope $ return [TypeCombinator::union(...$returnTypes), $certainty]; } - private function castToArrayKeyType(Type $type): Type + private function castToArrayKeyType(Type $type, Scope $scope): Type { + $throwsTypeError = $scope->getPhpVersion()->throwsTypeErrorForInternalFunctions(); $isArray = $type->isArray(); if ($isArray->yes()) { - return $this->phpVersion->throwsTypeErrorForInternalFunctions() ? new NeverType() : new IntegerType(); + if ($throwsTypeError->yes()) { + return new NeverType(); + } + + return new IntegerType(); } if ($isArray->no()) { return $type->toArrayKey(); } $withoutArrayType = TypeCombinator::remove($type, new ArrayType(new MixedType(), new MixedType())); $keyType = $withoutArrayType->toArrayKey(); - if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { + if ($throwsTypeError->yes()) { return $keyType; } return TypeCombinator::union($keyType, new IntegerType()); diff --git a/src/Type/Php/ArrayCombineFunctionReturnTypeExtension.php b/src/Type/Php/ArrayCombineFunctionReturnTypeExtension.php index d33a56f2fca..ba2c1e0a0fb 100644 --- a/src/Type/Php/ArrayCombineFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayCombineFunctionReturnTypeExtension.php @@ -5,7 +5,6 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; @@ -20,7 +19,6 @@ final class ArrayCombineFunctionReturnTypeExtension implements DynamicFunctionRe public function __construct( private ArrayCombineHelper $arrayCombineHelper, - private PhpVersion $phpVersion, ) { } @@ -45,15 +43,16 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return $returnType; } + $throwsValueError = $scope->getPhpVersion()->throwsValueErrorForInternalFunctions(); if ($hasValueError->yes()) { - if ($this->phpVersion->throwsValueErrorForInternalFunctions()) { + if ($throwsValueError->yes()) { return new NeverType(); } return new ConstantBooleanType(false); } - if ($this->phpVersion->throwsValueErrorForInternalFunctions()) { + if ($throwsValueError->yes()) { return $returnType; } diff --git a/src/Type/Php/ArrayFillFunctionReturnTypeExtension.php b/src/Type/Php/ArrayFillFunctionReturnTypeExtension.php index fd3fde84d5a..a8a0270120f 100644 --- a/src/Type/Php/ArrayFillFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayFillFunctionReturnTypeExtension.php @@ -5,7 +5,6 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\NonEmptyArrayType; @@ -28,10 +27,6 @@ final class ArrayFillFunctionReturnTypeExtension implements DynamicFunctionRetur private const MAX_SIZE_USE_CONSTANT_ARRAY = 100; - public function __construct(private PhpVersion $phpVersion) - { - } - public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'array_fill'; @@ -46,12 +41,14 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $numberType = $scope->getType($args[1]->value); $isValidNumberType = IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($numberType); + $throwsValueError = $scope->getPhpVersion()->throwsValueErrorForInternalFunctions(); // check against negative-int, which is not allowed if ($isValidNumberType->no()) { - if ($this->phpVersion->throwsValueErrorForInternalFunctions()) { + if ($throwsValueError->yes()) { return new NeverType(); } + return new ConstantBooleanType(false); } @@ -96,7 +93,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $resultType = TypeCombinator::intersect($resultType, new NonEmptyArrayType()); } - if (!$isValidNumberType->yes() && !$this->phpVersion->throwsValueErrorForInternalFunctions()) { + if (!$isValidNumberType->yes() && !$throwsValueError->yes()) { $resultType = TypeCombinator::union($resultType, new ConstantBooleanType(false)); } diff --git a/src/Type/Php/ArrayFillKeysFunctionReturnTypeExtension.php b/src/Type/Php/ArrayFillKeysFunctionReturnTypeExtension.php index 7a9474f5c67..f1486d274b9 100644 --- a/src/Type/Php/ArrayFillKeysFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayFillKeysFunctionReturnTypeExtension.php @@ -5,7 +5,6 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; @@ -19,10 +18,6 @@ final class ArrayFillKeysFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { - public function __construct(private PhpVersion $phpVersion) - { - } - public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'array_fill_keys'; @@ -37,7 +32,11 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $keysType = $scope->getType($args[0]->value); if ($keysType->isArray()->no()) { - return $this->phpVersion->arrayFunctionsReturnNullWithNonArray() ? new NullType() : new NeverType(); + if ($scope->getPhpVersion()->arrayFunctionsReturnNullWithNonArray()->no()) { + return new NeverType(); + } + + return new NullType(); } $filled = $keysType->fillKeysArray($scope->getType($args[1]->value)); diff --git a/src/Type/Php/ArrayFilterFunctionReturnTypeHelper.php b/src/Type/Php/ArrayFilterFunctionReturnTypeHelper.php index 83de6b7f622..b5affda6003 100644 --- a/src/Type/Php/ArrayFilterFunctionReturnTypeHelper.php +++ b/src/Type/Php/ArrayFilterFunctionReturnTypeHelper.php @@ -16,7 +16,6 @@ use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Php\PhpVersion; use PHPStan\Reflection\ReflectionProvider; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; @@ -49,7 +48,6 @@ final class ArrayFilterFunctionReturnTypeHelper public function __construct( private ReflectionProvider $reflectionProvider, - private PhpVersion $phpVersion, ) { } @@ -70,7 +68,7 @@ public function getType(Scope $scope, ?Expr $arrayArg, ?Expr $callbackArg, ?Expr } if ($arrayArgType instanceof MixedType) { - if ($this->phpVersion->throwsValueErrorForInternalFunctions()) { + if ($scope->getPhpVersion()->throwsValueErrorForInternalFunctions()->yes()) { return new ArrayType(new MixedType(), new MixedType()); } diff --git a/src/Type/Php/ArrayFlipFunctionReturnTypeExtension.php b/src/Type/Php/ArrayFlipFunctionReturnTypeExtension.php index f21e9dc294d..f1b66321416 100644 --- a/src/Type/Php/ArrayFlipFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayFlipFunctionReturnTypeExtension.php @@ -5,7 +5,6 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; @@ -19,10 +18,6 @@ final class ArrayFlipFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { - public function __construct(private PhpVersion $phpVersion) - { - } - public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'array_flip'; @@ -37,7 +32,11 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $arrayType = $scope->getType($args[0]->value); if ($arrayType->isArray()->no()) { - return $this->phpVersion->arrayFunctionsReturnNullWithNonArray() ? new NullType() : new NeverType(); + if ($scope->getPhpVersion()->arrayFunctionsReturnNullWithNonArray()->no()) { + return new NeverType(); + } + + return new NullType(); } $flipped = $arrayType->flipArray(); diff --git a/src/Type/Php/ArrayIntersectKeyFunctionReturnTypeExtension.php b/src/Type/Php/ArrayIntersectKeyFunctionReturnTypeExtension.php index c6212c02cc0..cc09f9c735c 100644 --- a/src/Type/Php/ArrayIntersectKeyFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayIntersectKeyFunctionReturnTypeExtension.php @@ -5,7 +5,6 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\NeverType; @@ -19,10 +18,6 @@ final class ArrayIntersectKeyFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { - public function __construct(private PhpVersion $phpVersion) - { - } - public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'array_intersect_key'; @@ -51,7 +46,11 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $onlyOneArrayGiven = count($argTypes) === 1; if ($firstArrayType->isArray()->no() || (!$onlyOneArrayGiven && $otherArraysType->isArray()->no())) { - return $this->phpVersion->arrayFunctionsReturnNullWithNonArray() ? new NullType() : new NeverType(); + if ($scope->getPhpVersion()->arrayFunctionsReturnNullWithNonArray()->no()) { + return new NeverType(); + } + + return new NullType(); } if ($onlyOneArrayGiven) { diff --git a/src/Type/Php/ArrayKeyExistsFunctionTypeSpecifyingExtension.php b/src/Type/Php/ArrayKeyExistsFunctionTypeSpecifyingExtension.php index 6782f408112..c9d437c87ff 100644 --- a/src/Type/Php/ArrayKeyExistsFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/ArrayKeyExistsFunctionTypeSpecifyingExtension.php @@ -10,7 +10,6 @@ use PHPStan\Analyser\TypeSpecifierAwareExtension; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Accessory\HasOffsetType; use PHPStan\Type\Accessory\NonEmptyArrayType; @@ -30,12 +29,6 @@ final class ArrayKeyExistsFunctionTypeSpecifyingExtension implements FunctionTyp private TypeSpecifier $typeSpecifier; - public function __construct( - private PhpVersion $phpVersion, - ) - { - } - public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void { $this->typeSpecifier = $typeSpecifier; @@ -123,7 +116,7 @@ public function specifyTypes( new ArrayType(new MixedType(), new MixedType()), new HasOffsetType($keyType), ]); - } elseif ($this->phpVersion->throwsValueErrorForInternalFunctions()) { + } elseif ($scope->getPhpVersion()->throwsValueErrorForInternalFunctions()->yes()) { $specifiedTypes = $this->typeSpecifier->create( $array, new HasOffsetType($keyType), diff --git a/src/Type/Php/ArrayKeysFunctionDynamicReturnTypeExtension.php b/src/Type/Php/ArrayKeysFunctionDynamicReturnTypeExtension.php index bb7a7bea880..881e58d7d6c 100644 --- a/src/Type/Php/ArrayKeysFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArrayKeysFunctionDynamicReturnTypeExtension.php @@ -5,7 +5,6 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; use PHPStan\TrinaryLogic; use PHPStan\Type\DynamicFunctionReturnTypeExtension; @@ -19,10 +18,6 @@ final class ArrayKeysFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { - public function __construct(private PhpVersion $phpVersion) - { - } - public function isFunctionSupported(FunctionReflection $functionReflection): bool { return strtolower($functionReflection->getName()) === 'array_keys'; @@ -37,7 +32,11 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $arrayType = $scope->getType($args[0]->value); if ($arrayType->isArray()->no()) { - return $this->phpVersion->arrayFunctionsReturnNullWithNonArray() ? new NullType() : new NeverType(); + if ($scope->getPhpVersion()->arrayFunctionsReturnNullWithNonArray()->no()) { + return new NeverType(); + } + + return new NullType(); } if (count($args) >= 2) { diff --git a/src/Type/Php/ArrayReverseFunctionReturnTypeExtension.php b/src/Type/Php/ArrayReverseFunctionReturnTypeExtension.php index 7f4f2a754fc..9cce71b4051 100644 --- a/src/Type/Php/ArrayReverseFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayReverseFunctionReturnTypeExtension.php @@ -5,7 +5,6 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; @@ -17,10 +16,6 @@ final class ArrayReverseFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { - public function __construct(private PhpVersion $phpVersion) - { - } - public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'array_reverse'; @@ -35,7 +30,11 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $type = $scope->getType($args[0]->value); if ($type->isArray()->no()) { - return $this->phpVersion->arrayFunctionsReturnNullWithNonArray() ? new NullType() : new NeverType(); + if ($scope->getPhpVersion()->arrayFunctionsReturnNullWithNonArray()->no()) { + return new NeverType(); + } + + return new NullType(); } $preserveKeysType = isset($args[1]) ? $scope->getType($args[1]->value) : new ConstantBooleanType(false); diff --git a/src/Type/Php/ArraySearchFunctionDynamicReturnTypeExtension.php b/src/Type/Php/ArraySearchFunctionDynamicReturnTypeExtension.php index 87810f8fb02..c1ec70bf746 100644 --- a/src/Type/Php/ArraySearchFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArraySearchFunctionDynamicReturnTypeExtension.php @@ -5,7 +5,6 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; @@ -18,10 +17,6 @@ final class ArraySearchFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { - public function __construct(private PhpVersion $phpVersion) - { - } - public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'array_search'; @@ -37,7 +32,11 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $haystackArgType = $scope->getType($args[1]->value); if ($haystackArgType->isArray()->no()) { - return $this->phpVersion->arrayFunctionsReturnNullWithNonArray() ? new NullType() : new NeverType(); + if ($scope->getPhpVersion()->arrayFunctionsReturnNullWithNonArray()->no()) { + return new NeverType(); + } + + return new NullType(); } if ($argsCount < 3) { diff --git a/src/Type/Php/ArraySliceFunctionReturnTypeExtension.php b/src/Type/Php/ArraySliceFunctionReturnTypeExtension.php index 5ac2ba46063..42f20f7d61f 100644 --- a/src/Type/Php/ArraySliceFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArraySliceFunctionReturnTypeExtension.php @@ -5,7 +5,6 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; @@ -18,10 +17,6 @@ final class ArraySliceFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { - public function __construct(private PhpVersion $phpVersion) - { - } - public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'array_slice'; @@ -36,7 +31,11 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $arrayType = $scope->getType($args[0]->value); if ($arrayType->isArray()->no()) { - return $this->phpVersion->arrayFunctionsReturnNullWithNonArray() ? new NullType() : new NeverType(); + if ($scope->getPhpVersion()->arrayFunctionsReturnNullWithNonArray()->no()) { + return new NeverType(); + } + + return new NullType(); } $offsetType = $scope->getType($args[1]->value); diff --git a/src/Type/Php/ArraySpliceFunctionReturnTypeExtension.php b/src/Type/Php/ArraySpliceFunctionReturnTypeExtension.php index 5dd3c941744..c9959b1a935 100644 --- a/src/Type/Php/ArraySpliceFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArraySpliceFunctionReturnTypeExtension.php @@ -5,7 +5,6 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; use PHPStan\TrinaryLogic; use PHPStan\Type\DynamicFunctionReturnTypeExtension; @@ -18,10 +17,6 @@ final class ArraySpliceFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { - public function __construct(private PhpVersion $phpVersion) - { - } - public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'array_splice'; @@ -40,7 +35,11 @@ public function getTypeFromFunctionCall( $arrayType = $scope->getType($args[0]->value); if ($arrayType->isArray()->no()) { - return $this->phpVersion->arrayFunctionsReturnNullWithNonArray() ? new NullType() : new NeverType(); + if ($scope->getPhpVersion()->arrayFunctionsReturnNullWithNonArray()->no()) { + return new NeverType(); + } + + return new NullType(); } $offsetType = $scope->getType($args[1]->value); diff --git a/src/Type/Php/ArrayValuesFunctionDynamicReturnTypeExtension.php b/src/Type/Php/ArrayValuesFunctionDynamicReturnTypeExtension.php index 0015c4e1268..594551a3e34 100644 --- a/src/Type/Php/ArrayValuesFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArrayValuesFunctionDynamicReturnTypeExtension.php @@ -5,7 +5,6 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\NeverType; @@ -18,10 +17,6 @@ final class ArrayValuesFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { - public function __construct(private PhpVersion $phpVersion) - { - } - public function isFunctionSupported(FunctionReflection $functionReflection): bool { return strtolower($functionReflection->getName()) === 'array_values'; @@ -36,7 +31,11 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $arrayType = $scope->getType($args[0]->value); if ($arrayType->isArray()->no()) { - return $this->phpVersion->arrayFunctionsReturnNullWithNonArray() ? new NullType() : new NeverType(); + if ($scope->getPhpVersion()->arrayFunctionsReturnNullWithNonArray()->no()) { + return new NeverType(); + } + + return new NullType(); } return $arrayType->getValuesArray(); diff --git a/src/Type/Php/BcMathStringOrNullReturnTypeExtension.php b/src/Type/Php/BcMathStringOrNullReturnTypeExtension.php index 97ea61c70a6..8ade43ae493 100644 --- a/src/Type/Php/BcMathStringOrNullReturnTypeExtension.php +++ b/src/Type/Php/BcMathStringOrNullReturnTypeExtension.php @@ -6,7 +6,6 @@ use PhpParser\Node\Expr\UnaryMinus; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Accessory\AccessoryNumericStringType; use PHPStan\Type\Constant\ConstantBooleanType; @@ -26,10 +25,6 @@ final class BcMathStringOrNullReturnTypeExtension implements DynamicFunctionReturnTypeExtension { - public function __construct(private PhpVersion $phpVersion) - { - } - public function isFunctionSupported(FunctionReflection $functionReflection): bool { return in_array($functionReflection->getName(), ['bcdiv', 'bcmod', 'bcpowmod', 'bcsqrt'], true); @@ -45,18 +40,19 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return $this->getTypeForBcPowMod($functionCall, $scope); } + $throwsTypeError = $scope->getPhpVersion()->throwsTypeErrorForInternalFunctions(); $stringAndNumericStringType = new IntersectionType([new StringType(), new AccessoryNumericStringType()]); $args = $functionCall->getArgs(); if (isset($args[1]) === false) { - if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { + if ($throwsTypeError->yes()) { return new NeverType(); } return new NullType(); } - if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { + if ($throwsTypeError->yes()) { $defaultReturnType = $stringAndNumericStringType; } else { $defaultReturnType = new UnionType([$stringAndNumericStringType, new NullType()]); @@ -66,7 +62,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $secondArgumentIsNumeric = ($secondArgument instanceof ConstantScalarType && is_numeric($secondArgument->getValue())) || $secondArgument->isInteger()->yes(); if ($secondArgument instanceof ConstantScalarType && ($this->isZero($secondArgument->getValue()) || !$secondArgumentIsNumeric)) { - if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { + if ($throwsTypeError->yes()) { return new NeverType(); } @@ -95,14 +91,14 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } if ($thirdArgument instanceof ConstantScalarType && !is_numeric($thirdArgument->getValue())) { - if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { + if ($throwsTypeError->yes()) { return new NeverType(); } return new NullType(); } - if ($this->phpVersion->throwsTypeErrorForInternalFunctions() && $thirdArgumentIsNegative) { + if ($throwsTypeError->yes() && $thirdArgumentIsNegative) { return new NeverType(); } @@ -121,8 +117,9 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, */ private function getTypeForBcSqrt(FuncCall $functionCall, Scope $scope): Type { + $throwsTypeError = $scope->getPhpVersion()->throwsTypeErrorForInternalFunctions(); $stringAndNumericStringType = new IntersectionType([new StringType(), new AccessoryNumericStringType()]); - if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { + if ($throwsTypeError->yes()) { $defaultReturnType = $stringAndNumericStringType; } else { $defaultReturnType = new UnionType([$stringAndNumericStringType, new NullType()]); @@ -130,7 +127,7 @@ private function getTypeForBcSqrt(FuncCall $functionCall, Scope $scope): Type $args = $functionCall->getArgs(); if (isset($args[0]) === false) { - if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { + if ($throwsTypeError->yes()) { return new NeverType(); } @@ -143,7 +140,7 @@ private function getTypeForBcSqrt(FuncCall $functionCall, Scope $scope): Type $firstArgumentIsNegative = $firstArgument instanceof ConstantScalarType && is_numeric($firstArgument->getValue()) && $firstArgument->getValue() < 0; if ($firstArgument instanceof UnaryMinus || $firstArgumentIsNegative) { - if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { + if ($throwsTypeError->yes()) { return new NeverType(); } @@ -164,7 +161,7 @@ private function getTypeForBcSqrt(FuncCall $functionCall, Scope $scope): Type $secondArgumentIsNegative = $secondArgument instanceof ConstantScalarType && is_numeric($secondArgument->getValue()) && $secondArgument->getValue() < 0; if ($secondArgumentIsNonNumeric) { - if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { + if ($throwsTypeError->yes()) { return new NeverType(); } @@ -172,7 +169,7 @@ private function getTypeForBcSqrt(FuncCall $functionCall, Scope $scope): Type } if ($secondArgument instanceof UnaryMinus || $secondArgumentIsNegative) { - if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { + if ($throwsTypeError->yes()) { return new NeverType(); } } @@ -191,15 +188,16 @@ private function getTypeForBcSqrt(FuncCall $functionCall, Scope $scope): Type */ private function getTypeForBcPowMod(FuncCall $functionCall, Scope $scope): Type { + $throwsTypeError = $scope->getPhpVersion()->throwsTypeErrorForInternalFunctions(); $args = $functionCall->getArgs(); - if ($this->phpVersion->throwsTypeErrorForInternalFunctions() && isset($args[0]) === false) { + if ($throwsTypeError->yes() && isset($args[0]) === false) { return new NeverType(); } $stringAndNumericStringType = new IntersectionType([new StringType(), new AccessoryNumericStringType()]); if (isset($args[1]) === false) { - if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { + if ($throwsTypeError->yes()) { return new NeverType(); } @@ -209,7 +207,7 @@ private function getTypeForBcPowMod(FuncCall $functionCall, Scope $scope): Type $exponent = $scope->getType($args[1]->value); // Expontent is non numeric - if ($this->phpVersion->throwsTypeErrorForInternalFunctions() + if ($throwsTypeError->yes() && $exponent instanceof ConstantScalarType && !is_numeric($exponent->getValue()) ) { return new NeverType(); @@ -222,7 +220,7 @@ private function getTypeForBcPowMod(FuncCall $functionCall, Scope $scope): Type } if ($exponentIsNegative) { - if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { + if ($throwsTypeError->yes()) { return new NeverType(); } @@ -235,7 +233,7 @@ private function getTypeForBcPowMod(FuncCall $functionCall, Scope $scope): Type $modulusIsNonNumeric = $modulus instanceof ConstantScalarType && !is_numeric($modulus->getValue()); if ($modulusIsZero || $modulusIsNonNumeric) { - if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { + if ($throwsTypeError->yes()) { return new NeverType(); } @@ -246,12 +244,12 @@ private function getTypeForBcPowMod(FuncCall $functionCall, Scope $scope): Type return $stringAndNumericStringType; } } else { - if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { + if ($throwsTypeError->yes()) { return new NeverType(); } } - if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { + if ($throwsTypeError->yes()) { return $stringAndNumericStringType; } diff --git a/src/Type/Php/CountCharsFunctionDynamicReturnTypeExtension.php b/src/Type/Php/CountCharsFunctionDynamicReturnTypeExtension.php index 70d08ba9e00..cac46a05dad 100644 --- a/src/Type/Php/CountCharsFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/CountCharsFunctionDynamicReturnTypeExtension.php @@ -5,7 +5,6 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\ArrayType; use PHPStan\Type\Constant\ConstantBooleanType; @@ -23,10 +22,6 @@ final class CountCharsFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { - public function __construct(private PhpVersion $phpVersion) - { - } - public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'count_chars'; @@ -45,18 +40,19 @@ public function getTypeFromFunctionCall( } $modeType = count($args) === 2 ? $scope->getType($args[1]->value) : new ConstantIntegerType(0); + $throwsValueError = $scope->getPhpVersion()->throwsValueErrorForInternalFunctions(); if (IntegerRangeType::fromInterval(0, 2)->isSuperTypeOf($modeType)->yes()) { $arrayType = new ArrayType(new IntegerType(), new IntegerType()); - return $this->phpVersion->throwsValueErrorForInternalFunctions() + return $throwsValueError->yes() ? $arrayType : TypeUtils::toBenevolentUnion(new UnionType([$arrayType, new ConstantBooleanType(false)])); } $stringType = new StringType(); - return $this->phpVersion->throwsValueErrorForInternalFunctions() + return $throwsValueError->yes() ? $stringType : TypeUtils::toBenevolentUnion(new UnionType([$stringType, new ConstantBooleanType(false)])); } diff --git a/src/Type/Php/DateIntervalConstructorThrowTypeExtension.php b/src/Type/Php/DateIntervalConstructorThrowTypeExtension.php index da3cc191234..0d3487bd1c5 100644 --- a/src/Type/Php/DateIntervalConstructorThrowTypeExtension.php +++ b/src/Type/Php/DateIntervalConstructorThrowTypeExtension.php @@ -6,7 +6,6 @@ use PhpParser\Node\Expr\StaticCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Php\PhpVersion; use PHPStan\Reflection\MethodReflection; use PHPStan\Type\DynamicStaticMethodThrowTypeExtension; use PHPStan\Type\NeverType; @@ -20,10 +19,6 @@ final class DateIntervalConstructorThrowTypeExtension implements DynamicStaticMethodThrowTypeExtension { - public function __construct(private PhpVersion $phpVersion) - { - } - public function isStaticMethodSupported(MethodReflection $methodReflection): bool { return $methodReflection->getName() === '__construct' && $methodReflection->getDeclaringClass()->getName() === DateInterval::class; @@ -42,22 +37,22 @@ public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflect try { new DateInterval($constantString->getValue()); } catch (Throwable) { - return $this->exceptionType(); + return $this->exceptionType($scope); } $valueType = TypeCombinator::remove($valueType, $constantString); } if (!$valueType instanceof NeverType) { - return $this->exceptionType(); + return $this->exceptionType($scope); } return null; } - private function exceptionType(): Type + private function exceptionType(Scope $scope): Type { - if ($this->phpVersion->hasDateTimeExceptions()) { + if ($scope->getPhpVersion()->hasDateTimeExceptions()->yes()) { return new ObjectType('DateMalformedIntervalStringException'); } diff --git a/src/Type/Php/DateIntervalCreateFromDateStringThrowTypeExtension.php b/src/Type/Php/DateIntervalCreateFromDateStringThrowTypeExtension.php index 2a1c10b29f9..7cbb046d2b6 100644 --- a/src/Type/Php/DateIntervalCreateFromDateStringThrowTypeExtension.php +++ b/src/Type/Php/DateIntervalCreateFromDateStringThrowTypeExtension.php @@ -6,7 +6,6 @@ use PhpParser\Node\Expr\StaticCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Php\PhpVersion; use PHPStan\Reflection\MethodReflection; use PHPStan\Type\DynamicStaticMethodThrowTypeExtension; use PHPStan\Type\NeverType; @@ -19,10 +18,6 @@ final class DateIntervalCreateFromDateStringThrowTypeExtension implements DynamicStaticMethodThrowTypeExtension { - public function __construct(private PhpVersion $phpVersion) - { - } - public function isStaticMethodSupported(MethodReflection $methodReflection): bool { return $methodReflection->getName() === 'createFromDateString' @@ -35,7 +30,7 @@ public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflect return null; } - if (!$this->phpVersion->hasDateTimeExceptions()) { + if ($scope->getPhpVersion()->hasDateTimeExceptions()->no()) { return null; } @@ -43,9 +38,16 @@ public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflect $constantStrings = $valueType->getConstantStrings(); foreach ($constantStrings as $constantString) { + // createFromDateString() only throws since PHP 8.3, before that it warns and returns false. + // The analysed version can be 8.3+ while this process runs on an older one, + // so detect the failure through the return value instead of the exception. try { - @DateInterval::createFromDateString($constantString->getValue()); + $result = @DateInterval::createFromDateString($constantString->getValue()); } catch (Throwable) { + $result = false; + } + + if ($result === false) { return $methodReflection->getThrowType(); } diff --git a/src/Type/Php/DateIntervalDynamicReturnTypeExtension.php b/src/Type/Php/DateIntervalDynamicReturnTypeExtension.php index bc57c6b7e24..a2d126fc0d4 100644 --- a/src/Type/Php/DateIntervalDynamicReturnTypeExtension.php +++ b/src/Type/Php/DateIntervalDynamicReturnTypeExtension.php @@ -6,7 +6,6 @@ use PhpParser\Node\Expr\StaticCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Php\PhpVersion; use PHPStan\Reflection\MethodReflection; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\DynamicStaticMethodReturnTypeExtension; @@ -19,10 +18,6 @@ final class DateIntervalDynamicReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension { - public function __construct(private PhpVersion $phpVersion) - { - } - public function getClass(): string { return DateInterval::class; @@ -61,7 +56,7 @@ public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, if ($hasFalse) { if (!$hasDateInterval) { - if ($this->phpVersion->hasDateTimeExceptions()) { + if ($scope->getPhpVersion()->hasDateTimeExceptions()->yes()) { return new NeverType(); } diff --git a/src/Type/Php/DateTimeConstructorThrowTypeExtension.php b/src/Type/Php/DateTimeConstructorThrowTypeExtension.php index 45cdb606a3a..84cecacd475 100644 --- a/src/Type/Php/DateTimeConstructorThrowTypeExtension.php +++ b/src/Type/Php/DateTimeConstructorThrowTypeExtension.php @@ -7,7 +7,6 @@ use PhpParser\Node\Expr\StaticCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Php\PhpVersion; use PHPStan\Reflection\MethodReflection; use PHPStan\Type\DynamicStaticMethodThrowTypeExtension; use PHPStan\Type\NeverType; @@ -22,10 +21,6 @@ final class DateTimeConstructorThrowTypeExtension implements DynamicStaticMethodThrowTypeExtension { - public function __construct(private PhpVersion $phpVersion) - { - } - public function isStaticMethodSupported(MethodReflection $methodReflection): bool { return $methodReflection->getName() === '__construct' && in_array($methodReflection->getDeclaringClass()->getName(), [DateTime::class, DateTimeImmutable::class], true); @@ -44,22 +39,22 @@ public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflect try { new DateTime($constantString->getValue()); } catch (Throwable) { - return $this->exceptionType(); + return $this->exceptionType($scope); } $valueType = TypeCombinator::remove($valueType, $constantString); } if (!$valueType instanceof NeverType) { - return $this->exceptionType(); + return $this->exceptionType($scope); } return null; } - private function exceptionType(): Type + private function exceptionType(Scope $scope): Type { - if ($this->phpVersion->hasDateTimeExceptions()) { + if ($scope->getPhpVersion()->hasDateTimeExceptions()->yes()) { return new ObjectType('DateMalformedStringException'); } diff --git a/src/Type/Php/DateTimeModifyMethodThrowTypeExtension.php b/src/Type/Php/DateTimeModifyMethodThrowTypeExtension.php index f3b91b8b358..f3a2ccfdf0f 100644 --- a/src/Type/Php/DateTimeModifyMethodThrowTypeExtension.php +++ b/src/Type/Php/DateTimeModifyMethodThrowTypeExtension.php @@ -7,7 +7,6 @@ use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Php\PhpVersion; use PHPStan\Reflection\MethodReflection; use PHPStan\Type\DynamicMethodThrowTypeExtension; use PHPStan\Type\NeverType; @@ -22,10 +21,6 @@ final class DateTimeModifyMethodThrowTypeExtension implements DynamicMethodThrowTypeExtension { - public function __construct(private PhpVersion $phpVersion) - { - } - public function isMethodSupported(MethodReflection $methodReflection): bool { return $methodReflection->getName() === 'modify' && in_array($methodReflection->getDeclaringClass()->getName(), [DateTime::class, DateTimeImmutable::class], true); @@ -37,7 +32,7 @@ public function getThrowTypeFromMethodCall(MethodReflection $methodReflection, M return null; } - if (!$this->phpVersion->hasDateTimeExceptions()) { + if ($scope->getPhpVersion()->hasDateTimeExceptions()->no()) { return null; } @@ -45,26 +40,33 @@ public function getThrowTypeFromMethodCall(MethodReflection $methodReflection, M $constantStrings = $valueType->getConstantStrings(); foreach ($constantStrings as $constantString) { + // modify() only throws since PHP 8.3, before that it warns and returns false. + // The analysed version can be 8.3+ while this process runs on an older one, + // so detect the failure through the return value instead of the exception. try { $dateTime = new DateTime(); - $dateTime->modify($constantString->getValue()); + $result = @$dateTime->modify($constantString->getValue()); } catch (Throwable) { - return $this->exceptionType(); + $result = false; + } + + if ($result === false) { + return $this->exceptionType($scope); } $valueType = TypeCombinator::remove($valueType, $constantString); } if (!$valueType instanceof NeverType) { - return $this->exceptionType(); + return $this->exceptionType($scope); } return null; } - private function exceptionType(): Type + private function exceptionType(Scope $scope): Type { - if ($this->phpVersion->hasDateTimeExceptions()) { + if ($scope->getPhpVersion()->hasDateTimeExceptions()->yes()) { return new ObjectType('DateMalformedStringException'); } diff --git a/src/Type/Php/DateTimeModifyReturnTypeExtension.php b/src/Type/Php/DateTimeModifyReturnTypeExtension.php index a5a3223dc62..91238c8ceb3 100644 --- a/src/Type/Php/DateTimeModifyReturnTypeExtension.php +++ b/src/Type/Php/DateTimeModifyReturnTypeExtension.php @@ -6,7 +6,6 @@ use DateTimeInterface; use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; -use PHPStan\Php\PhpVersion; use PHPStan\Reflection\MethodReflection; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\DynamicMethodReturnTypeExtension; @@ -24,7 +23,6 @@ final class DateTimeModifyReturnTypeExtension implements DynamicMethodReturnType /** @param class-string $dateTimeClass */ public function __construct( - private PhpVersion $phpVersion, private string $dateTimeClass, ) { @@ -75,7 +73,7 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method if ($hasFalse) { if (!$hasDateTime) { - if ($this->phpVersion->hasDateTimeExceptions()) { + if ($scope->getPhpVersion()->hasDateTimeExceptions()->yes()) { return new NeverType(); } diff --git a/src/Type/Php/DateTimeSubMethodThrowTypeExtension.php b/src/Type/Php/DateTimeSubMethodThrowTypeExtension.php index fc92e2b91d5..48c2784122d 100644 --- a/src/Type/Php/DateTimeSubMethodThrowTypeExtension.php +++ b/src/Type/Php/DateTimeSubMethodThrowTypeExtension.php @@ -7,7 +7,6 @@ use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Php\PhpVersion; use PHPStan\Reflection\MethodReflection; use PHPStan\Type\DynamicMethodThrowTypeExtension; use PHPStan\Type\ObjectType; @@ -19,10 +18,6 @@ final class DateTimeSubMethodThrowTypeExtension implements DynamicMethodThrowTypeExtension { - public function __construct(private PhpVersion $phpVersion) - { - } - public function isMethodSupported(MethodReflection $methodReflection): bool { return $methodReflection->getName() === 'sub' @@ -35,7 +30,7 @@ public function getThrowTypeFromMethodCall(MethodReflection $methodReflection, M return null; } - if (!$this->phpVersion->hasDateTimeExceptions()) { + if ($scope->getPhpVersion()->hasDateTimeExceptions()->no()) { return null; } diff --git a/src/Type/Php/DateTimeZoneConstructorThrowTypeExtension.php b/src/Type/Php/DateTimeZoneConstructorThrowTypeExtension.php index d58e183886a..c14eba4d010 100644 --- a/src/Type/Php/DateTimeZoneConstructorThrowTypeExtension.php +++ b/src/Type/Php/DateTimeZoneConstructorThrowTypeExtension.php @@ -6,7 +6,6 @@ use PhpParser\Node\Expr\StaticCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Php\PhpVersion; use PHPStan\Reflection\MethodReflection; use PHPStan\Type\DynamicStaticMethodThrowTypeExtension; use PHPStan\Type\NeverType; @@ -20,10 +19,6 @@ final class DateTimeZoneConstructorThrowTypeExtension implements DynamicStaticMethodThrowTypeExtension { - public function __construct(private PhpVersion $phpVersion) - { - } - public function isStaticMethodSupported(MethodReflection $methodReflection): bool { return $methodReflection->getName() === '__construct' && $methodReflection->getDeclaringClass()->getName() === DateTimeZone::class; @@ -42,22 +37,22 @@ public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflect try { new DateTimeZone($constantString->getValue()); } catch (Throwable) { - return $this->exceptionType(); + return $this->exceptionType($scope); } $valueType = TypeCombinator::remove($valueType, $constantString); } if (!$valueType instanceof NeverType) { - return $this->exceptionType(); + return $this->exceptionType($scope); } return null; } - private function exceptionType(): Type + private function exceptionType(Scope $scope): Type { - if ($this->phpVersion->hasDateTimeExceptions()) { + if ($scope->getPhpVersion()->hasDateTimeExceptions()->yes()) { return new ObjectType('DateInvalidTimeZoneException'); } diff --git a/src/Type/Php/FilterFunctionReturnTypeHelper.php b/src/Type/Php/FilterFunctionReturnTypeHelper.php index 484738fa9fc..af128a42272 100644 --- a/src/Type/Php/FilterFunctionReturnTypeHelper.php +++ b/src/Type/Php/FilterFunctionReturnTypeHelper.php @@ -4,7 +4,7 @@ use PhpParser\Node; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Php\PhpVersion; +use PHPStan\Php\PhpVersions; use PHPStan\Reflection\ReflectionProvider; use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; @@ -52,15 +52,15 @@ final class FilterFunctionReturnTypeHelper private ?Type $supportedFilterInputTypes = null; - public function __construct(private ReflectionProvider $reflectionProvider, private PhpVersion $phpVersion) + public function __construct(private ReflectionProvider $reflectionProvider) { $this->flagsString = new ConstantStringType('flags'); } - private function getOffsetValueType(Type $inputType, Type $offsetType, ?Type $filterType, ?Type $flagsType): Type + private function getOffsetValueType(Type $inputType, Type $offsetType, ?Type $filterType, ?Type $flagsType, PhpVersions $phpVersions): Type { $hasNullOnFailure = $this->hasFlag('FILTER_NULL_ON_FAILURE', $flagsType); - if ($this->hasThrowOnFailureFlag($flagsType)->yes()) { + if ($this->hasThrowOnFailureFlag($flagsType, $phpVersions)->yes()) { // a missing input value throws instead of being reported through the return value $inexistentOffsetType = new NeverType(); } elseif ($hasNullOnFailure->yes()) { @@ -76,14 +76,14 @@ private function getOffsetValueType(Type $inputType, Type $offsetType, ?Type $fi return $inexistentOffsetType; } - $filteredType = $this->getType($inputType->getOffsetValueType($offsetType), $filterType, $flagsType); + $filteredType = $this->getType($inputType->getOffsetValueType($offsetType), $filterType, $flagsType, $phpVersions); return $hasOffsetValueType->maybe() ? TypeCombinator::union($filteredType, $inexistentOffsetType) : $filteredType; } - public function getInputType(Type $typeType, Type $varNameType, ?Type $filterType, ?Type $flagsType): Type + public function getInputType(Type $typeType, Type $varNameType, ?Type $filterType, ?Type $flagsType, PhpVersions $phpVersions): Type { $this->supportedFilterInputTypes ??= TypeCombinator::union( $this->reflectionProvider->getConstant(new Node\Name('INPUT_GET'), null)->getValueType(), @@ -94,7 +94,7 @@ public function getInputType(Type $typeType, Type $varNameType, ?Type $filterTyp ); if (!$typeType->isInteger()->yes() || $this->supportedFilterInputTypes->isSuperTypeOf($typeType)->no()) { - if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { + if ($phpVersions->throwsTypeErrorForInternalFunctions()->yes()) { return new NeverType(); } @@ -107,10 +107,10 @@ public function getInputType(Type $typeType, Type $varNameType, ?Type $filterTyp $inputType = new ArrayType(new StringType(), new MixedType()); } - return $this->getOffsetValueType($inputType, $varNameType, $filterType, $flagsType); + return $this->getOffsetValueType($inputType, $varNameType, $filterType, $flagsType, $phpVersions); } - public function getType(Type $inputType, ?Type $filterType, ?Type $flagsType): Type + public function getType(Type $inputType, ?Type $filterType, ?Type $flagsType, PhpVersions $phpVersions): Type { $mixedType = new MixedType(); @@ -154,7 +154,7 @@ public function getType(Type $inputType, ?Type $filterType, ?Type $flagsType): T $inputIsArray = $inputType->isArray(); $hasRequireArrayFlag = $this->hasFlag('FILTER_REQUIRE_ARRAY', $flagsType); - $hasThrowOnFailureFlag = $this->hasThrowOnFailureFlag($flagsType); + $hasThrowOnFailureFlag = $this->hasThrowOnFailureFlag($flagsType, $phpVersions); if ($inputIsArray->no() && $hasRequireArrayFlag->yes()) { if ($hasThrowOnFailureFlag->yes()) { return new ErrorType(); @@ -515,11 +515,11 @@ public function hasFlag(string $flagName, ?Type $flagsType): TrinaryLogic ); } - private function hasThrowOnFailureFlag(?Type $flagsType): TrinaryLogic + private function hasThrowOnFailureFlag(?Type $flagsType, PhpVersions $phpVersions): TrinaryLogic { - return $this->phpVersion->hasFilterThrowOnFailureConstant() - ? $this->hasFlag('FILTER_THROW_ON_FAILURE', $flagsType) - : TrinaryLogic::createNo(); + return $phpVersions->hasFilterThrowOnFailureConstant()->no() + ? TrinaryLogic::createNo() + : $this->hasFlag('FILTER_THROW_ON_FAILURE', $flagsType); } private function getFlagsValue(Type $exprType): Type diff --git a/src/Type/Php/FilterFunctionsThrowTypeExtension.php b/src/Type/Php/FilterFunctionsThrowTypeExtension.php index cb1649ae103..6d5d78610be 100644 --- a/src/Type/Php/FilterFunctionsThrowTypeExtension.php +++ b/src/Type/Php/FilterFunctionsThrowTypeExtension.php @@ -6,7 +6,6 @@ use PhpParser\Node\Name; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Type\DynamicFunctionThrowTypeExtension; @@ -19,7 +18,6 @@ final class FilterFunctionsThrowTypeExtension implements DynamicFunctionThrowTyp public function __construct( private ReflectionProvider $reflectionProvider, - private PhpVersion $phpVersion, private FilterFunctionReturnTypeHelper $filterFunctionReturnTypeHelper, private FilterFunctionFlagsHelper $filterFunctionFlagsHelper, ) @@ -40,7 +38,7 @@ public function getThrowTypeFromFunctionCall( ): ?Type { if ( - !$this->phpVersion->hasFilterThrowOnFailureConstant() + $scope->getPhpVersion()->hasFilterThrowOnFailureConstant()->no() || !$this->reflectionProvider->hasConstant(new Name\FullyQualified('FILTER_THROW_ON_FAILURE'), null) ) { return null; diff --git a/src/Type/Php/FilterInputDynamicReturnTypeExtension.php b/src/Type/Php/FilterInputDynamicReturnTypeExtension.php index 79448c97682..a693b674e9f 100644 --- a/src/Type/Php/FilterInputDynamicReturnTypeExtension.php +++ b/src/Type/Php/FilterInputDynamicReturnTypeExtension.php @@ -35,6 +35,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $scope->getType($args[1]->value), isset($args[2]) ? $scope->getType($args[2]->value) : null, isset($args[3]) ? $scope->getType($args[3]->value) : null, + $scope->getPhpVersion(), ); } diff --git a/src/Type/Php/FilterVarArrayDynamicReturnTypeExtension.php b/src/Type/Php/FilterVarArrayDynamicReturnTypeExtension.php index 3be499415ca..3315035d340 100644 --- a/src/Type/Php/FilterVarArrayDynamicReturnTypeExtension.php +++ b/src/Type/Php/FilterVarArrayDynamicReturnTypeExtension.php @@ -88,6 +88,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $inputArgType->getIterableValueType(), $filterArgType, null, + $scope->getPhpVersion(), ); $arrayType = new ArrayType($inputArgType->getIterableKeyType(), $valueType); @@ -112,7 +113,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } elseif ($filterConstantArrayType === null) { if ($inputConstantArrayType === null) { $isList = $inputArgType->isList()->yes(); - $valueType = $this->filterFunctionReturnTypeHelper->getType($inputArgType, $filterArgType, null); + $valueType = $this->filterFunctionReturnTypeHelper->getType($inputArgType, $filterArgType, null, $scope->getPhpVersion()); $arrayType = new ArrayType( $inputArgType->getIterableKeyType(), @@ -160,7 +161,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } [$filterType, $flagsType] = $this->fetchFilter($filterTypesMap[$key] ?? new MixedType()); - $valueType = $this->filterFunctionReturnTypeHelper->getType($inputType, $filterType, $flagsType); + $valueType = $this->filterFunctionReturnTypeHelper->getType($inputType, $filterType, $flagsType, $scope->getPhpVersion()); if (in_array($key, $optionalKeys, true)) { if ($addEmpty) { @@ -186,7 +187,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } else { [$unsealedFilter, $unsealedFlags] = $this->fetchFilter(new MixedType()); } - $unsealedValueType = $this->filterFunctionReturnTypeHelper->getType($unsealedTypes[1], $unsealedFilter, $unsealedFlags); + $unsealedValueType = $this->filterFunctionReturnTypeHelper->getType($unsealedTypes[1], $unsealedFilter, $unsealedFlags, $scope->getPhpVersion()); if ($addEmpty) { $unsealedValueType = TypeCombinator::addNull($unsealedValueType); } diff --git a/src/Type/Php/FilterVarDynamicReturnTypeExtension.php b/src/Type/Php/FilterVarDynamicReturnTypeExtension.php index 7248566e843..81e2ba13eff 100644 --- a/src/Type/Php/FilterVarDynamicReturnTypeExtension.php +++ b/src/Type/Php/FilterVarDynamicReturnTypeExtension.php @@ -35,7 +35,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $filterType = isset($args[1]) ? $scope->getType($args[1]->value) : null; $flagsType = isset($args[2]) ? $scope->getType($args[2]->value) : null; - return $this->filterFunctionReturnTypeHelper->getType($inputType, $filterType, $flagsType); + return $this->filterFunctionReturnTypeHelper->getType($inputType, $filterType, $flagsType, $scope->getPhpVersion()); } } diff --git a/src/Type/Php/GetClassDynamicReturnTypeExtension.php b/src/Type/Php/GetClassDynamicReturnTypeExtension.php index 8b8ae644401..46b5bbccb86 100644 --- a/src/Type/Php/GetClassDynamicReturnTypeExtension.php +++ b/src/Type/Php/GetClassDynamicReturnTypeExtension.php @@ -5,7 +5,6 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\ClassStringType; use PHPStan\Type\Constant\ConstantBooleanType; @@ -13,6 +12,7 @@ use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\NeverType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; use function count; @@ -20,10 +20,6 @@ final class GetClassDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { - public function __construct(private PhpVersion $phpVersion) - { - } - public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'get_class'; @@ -44,15 +40,21 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, // PHP 8 throws Error instead of returning false. Top-level code // might be included from a method and a closure bound to an object. - if ($this->phpVersion->throwsValueErrorForInternalFunctions()) { + $throwsValueError = $scope->getPhpVersion()->throwsValueErrorForInternalFunctions(); + $types = []; + if (!$throwsValueError->no()) { if ($scope->getFunction() !== null && !$scope->isInAnonymousFunction()) { - return new NeverType(true); + $types[] = new NeverType(true); + } else { + $types[] = new ClassStringType(); } + } - return new ClassStringType(); + if (!$throwsValueError->yes()) { + $types[] = new ConstantBooleanType(false); } - return new ConstantBooleanType(false); + return TypeCombinator::union(...$types); } $argType = $scope->getType($args[0]->value); diff --git a/src/Type/Php/GetClassFunctionThrowTypeExtension.php b/src/Type/Php/GetClassFunctionThrowTypeExtension.php index 997fd53bc96..a1409df7243 100644 --- a/src/Type/Php/GetClassFunctionThrowTypeExtension.php +++ b/src/Type/Php/GetClassFunctionThrowTypeExtension.php @@ -6,7 +6,6 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\DynamicFunctionThrowTypeExtension; use PHPStan\Type\ObjectType; @@ -23,10 +22,6 @@ final class GetClassFunctionThrowTypeExtension implements DynamicFunctionThrowTypeExtension { - public function __construct(private PhpVersion $phpVersion) - { - } - public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'get_class'; @@ -34,7 +29,7 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getThrowTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $funcCall, Scope $scope): ?Type { - if (!$this->phpVersion->throwsValueErrorForInternalFunctions()) { + if ($scope->getPhpVersion()->throwsValueErrorForInternalFunctions()->no()) { return new VoidType(); } diff --git a/src/Type/Php/HashFunctionsReturnTypeExtension.php b/src/Type/Php/HashFunctionsReturnTypeExtension.php index d0c13ef4d90..0e484ce78cf 100644 --- a/src/Type/Php/HashFunctionsReturnTypeExtension.php +++ b/src/Type/Php/HashFunctionsReturnTypeExtension.php @@ -5,7 +5,6 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; use PHPStan\ShouldNotHappenException; use PHPStan\Type\Accessory\AccessoryLowercaseStringType; @@ -86,10 +85,6 @@ final class HashFunctionsReturnTypeExtension implements DynamicFunctionReturnTyp /** @var array|null */ private ?array $hashAlgorithms = null; - public function __construct(private PhpVersion $phpVersion) - { - } - public function isFunctionSupported(FunctionReflection $functionReflection): bool { $name = strtolower($functionReflection->getName()); @@ -125,10 +120,11 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } $stringReturnType = new IntersectionType($stringTypes); + $throwsValueError = $scope->getPhpVersion()->throwsValueErrorForInternalFunctions(); $algorithmType = $scope->getType($args[0]->value); $constantAlgorithmTypes = $algorithmType->getConstantStrings(); if (count($constantAlgorithmTypes) === 0) { - if ($functionData['possiblyFalse'] || !$this->phpVersion->throwsValueErrorForInternalFunctions()) { + if ($functionData['possiblyFalse'] || !$throwsValueError->yes()) { return new BenevolentUnionType([$stringReturnType, new ConstantBooleanType(false)]); } @@ -137,7 +133,11 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $neverType = new NeverType(); $falseType = new ConstantBooleanType(false); - $invalidAlgorithmType = $this->phpVersion->throwsValueErrorForInternalFunctions() ? $neverType : $falseType; + if ($throwsValueError->yes()) { + $invalidAlgorithmType = $neverType; + } else { + $invalidAlgorithmType = $falseType; + } $returnTypes = array_map( function (ConstantStringType $type) use ($functionData, $stringReturnType, $invalidAlgorithmType) { diff --git a/src/Type/Php/HighlightStringDynamicReturnTypeExtension.php b/src/Type/Php/HighlightStringDynamicReturnTypeExtension.php index 71f8e6643fe..209bb42c530 100644 --- a/src/Type/Php/HighlightStringDynamicReturnTypeExtension.php +++ b/src/Type/Php/HighlightStringDynamicReturnTypeExtension.php @@ -5,7 +5,6 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\BooleanType; use PHPStan\Type\Constant\ConstantBooleanType; @@ -18,10 +17,6 @@ final class HighlightStringDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { - public function __construct(private PhpVersion $phpVersion) - { - } - public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'highlight_string'; @@ -29,21 +24,16 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type { + $doesNotReturnFalse = $scope->getPhpVersion()->highlightStringDoesNotReturnFalse(); $args = $functionCall->getArgs(); - if (count($args) < 2) { - if ($this->phpVersion->highlightStringDoesNotReturnFalse()) { - return new ConstantBooleanType(true); + if (count($args) >= 2) { + $returnType = $scope->getType($args[1]->value); + if ($returnType->isTrue()->yes()) { + return new StringType(); } - - return new BooleanType(); - } - - $returnType = $scope->getType($args[1]->value); - if ($returnType->isTrue()->yes()) { - return new StringType(); } - if ($this->phpVersion->highlightStringDoesNotReturnFalse()) { + if ($doesNotReturnFalse->yes()) { return new ConstantBooleanType(true); } diff --git a/src/Type/Php/MbConvertEncodingFunctionReturnTypeExtension.php b/src/Type/Php/MbConvertEncodingFunctionReturnTypeExtension.php index 11c7e86fbe6..e38ad1ffd95 100644 --- a/src/Type/Php/MbConvertEncodingFunctionReturnTypeExtension.php +++ b/src/Type/Php/MbConvertEncodingFunctionReturnTypeExtension.php @@ -5,7 +5,6 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\Accessory\AccessoryArrayListType; @@ -27,10 +26,6 @@ final class MbConvertEncodingFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { - public function __construct(private PhpVersion $phpVersion) - { - } - public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'mb_convert_encoding'; @@ -60,7 +55,7 @@ public function getTypeFromFunctionCall( $result = $initialReturnType; } - if ($this->phpVersion->throwsValueErrorForInternalFunctions()) { + if ($scope->getPhpVersion()->throwsValueErrorForInternalFunctions()->yes()) { if (!isset($args[2])) { return TypeCombinator::remove($result, new ConstantBooleanType(false)); } diff --git a/src/Type/Php/MinMaxFunctionReturnTypeExtension.php b/src/Type/Php/MinMaxFunctionReturnTypeExtension.php index a142784318d..cf068f4a0d6 100644 --- a/src/Type/Php/MinMaxFunctionReturnTypeExtension.php +++ b/src/Type/Php/MinMaxFunctionReturnTypeExtension.php @@ -8,7 +8,6 @@ use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\Expr\AlwaysRememberedExpr; -use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantBooleanType; @@ -25,12 +24,6 @@ final class MinMaxFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { - public function __construct( - private PhpVersion $phpVersion, - ) - { - } - public function isFunctionSupported(FunctionReflection $functionReflection): bool { return in_array($functionReflection->getName(), ['min', 'max'], true); @@ -49,6 +42,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return $this->processArrayType( $functionReflection->getName(), $argType, + $scope, ); } @@ -108,19 +102,20 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, ); } - private function processArrayType(string $functionName, Type $argType): Type + private function processArrayType(string $functionName, Type $argType, Scope $scope): Type { + $throwsValueError = $scope->getPhpVersion()->throwsValueErrorForInternalFunctions(); $constArrayTypes = $argType->getConstantArrays(); if (count($constArrayTypes) > 0) { $resultTypes = []; foreach ($constArrayTypes as $constArrayType) { $isIterable = $constArrayType->isIterableAtLeastOnce(); - if ($isIterable->no() && !$this->phpVersion->throwsValueErrorForInternalFunctions()) { + if ($isIterable->no() && !$throwsValueError->yes()) { $resultTypes[] = new ConstantBooleanType(false); continue; } $argumentTypes = []; - if (!$isIterable->yes() && !$this->phpVersion->throwsValueErrorForInternalFunctions()) { + if (!$isIterable->yes() && !$throwsValueError->yes()) { $argumentTypes[] = new ConstantBooleanType(false); } @@ -143,12 +138,12 @@ private function processArrayType(string $functionName, Type $argType): Type } $isIterable = $argType->isIterableAtLeastOnce(); - if ($isIterable->no() && !$this->phpVersion->throwsValueErrorForInternalFunctions()) { + if ($isIterable->no() && !$throwsValueError->yes()) { return new ConstantBooleanType(false); } $iterableValueType = $argType->getIterableValueType(); $argumentTypes = []; - if (!$isIterable->yes() && !$this->phpVersion->throwsValueErrorForInternalFunctions()) { + if (!$isIterable->yes() && !$throwsValueError->yes()) { $argumentTypes[] = new ConstantBooleanType(false); } diff --git a/src/Type/Php/MinMaxFunctionThrowTypeExtension.php b/src/Type/Php/MinMaxFunctionThrowTypeExtension.php index 1dac8af4385..0ec781780de 100644 --- a/src/Type/Php/MinMaxFunctionThrowTypeExtension.php +++ b/src/Type/Php/MinMaxFunctionThrowTypeExtension.php @@ -5,7 +5,6 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\DynamicFunctionThrowTypeExtension; @@ -22,10 +21,6 @@ final class MinMaxFunctionThrowTypeExtension implements DynamicFunctionThrowTypeExtension { - public function __construct(private PhpVersion $phpVersion) - { - } - public function isFunctionSupported(FunctionReflection $functionReflection): bool { return in_array($functionReflection->getName(), ['min', 'max'], true); @@ -38,7 +33,7 @@ public function getThrowTypeFromFunctionCall(FunctionReflection $functionReflect return $functionReflection->getThrowType(); } - if (!$this->phpVersion->throwsValueErrorForInternalFunctions()) { + if (!$scope->getPhpVersion()->throwsValueErrorForInternalFunctions()->yes()) { return new VoidType(); } diff --git a/src/Type/Php/OpensslCipherFunctionsReturnTypeExtension.php b/src/Type/Php/OpensslCipherFunctionsReturnTypeExtension.php index c59e80ac87f..2339c44f8f4 100644 --- a/src/Type/Php/OpensslCipherFunctionsReturnTypeExtension.php +++ b/src/Type/Php/OpensslCipherFunctionsReturnTypeExtension.php @@ -5,7 +5,6 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\Constant\ConstantBooleanType; @@ -23,7 +22,6 @@ final class OpensslCipherFunctionsReturnTypeExtension implements DynamicFunction { public function __construct( - private PhpVersion $phpVersion, private OpenSslCipherMethodsProvider $cipherMethodsProvider, ) { @@ -36,7 +34,7 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { - if (!$this->phpVersion->throwsValueErrorForInternalFunctions()) { + if (!$scope->getPhpVersion()->throwsValueErrorForInternalFunctions()->yes()) { return null; } diff --git a/src/Type/Php/PDOConnectReturnTypeExtension.php b/src/Type/Php/PDOConnectReturnTypeExtension.php index 9aa71ccd275..82ceb6118ee 100644 --- a/src/Type/Php/PDOConnectReturnTypeExtension.php +++ b/src/Type/Php/PDOConnectReturnTypeExtension.php @@ -22,7 +22,9 @@ final class PDOConnectReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension { - public function __construct(private PhpVersion $phpVersion) + public function __construct( + private PhpVersion $phpVersion, + ) { } @@ -33,7 +35,8 @@ public function getClass(): string public function isStaticMethodSupported(MethodReflection $methodReflection): bool { - return $this->phpVersion->hasPDOSubclasses() && $methodReflection->getName() === 'connect'; + return $this->phpVersion->hasPDOSubclasses() + && $methodReflection->getName() === 'connect'; } public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): ?Type diff --git a/src/Type/Php/PrintfFunctionThrowTypeExtension.php b/src/Type/Php/PrintfFunctionThrowTypeExtension.php index 83a6456eae9..c7ee32d4d71 100644 --- a/src/Type/Php/PrintfFunctionThrowTypeExtension.php +++ b/src/Type/Php/PrintfFunctionThrowTypeExtension.php @@ -8,7 +8,6 @@ use PHPStan\Analyser\ArgumentsNormalizer; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\DynamicFunctionThrowTypeExtension; use PHPStan\Type\IntegerRangeType; @@ -43,7 +42,6 @@ final class PrintfFunctionThrowTypeExtension implements DynamicFunctionThrowType private const NEGATIVE_PRECISION_SPECIFIERS = ['g', 'G', 'h', 'H']; public function __construct( - private PhpVersion $phpVersion, private PrintfFormatParser $formatParser, ) { @@ -56,7 +54,7 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getThrowTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $funcCall, Scope $scope): ?Type { - if (!$this->phpVersion->throwsValueErrorForInternalFunctions()) { + if ($scope->getPhpVersion()->throwsValueErrorForInternalFunctions()->no()) { return new VoidType(); } diff --git a/src/Type/Php/RoundFunctionReturnTypeExtension.php b/src/Type/Php/RoundFunctionReturnTypeExtension.php index 2bc17560339..7adafbf76d5 100644 --- a/src/Type/Php/RoundFunctionReturnTypeExtension.php +++ b/src/Type/Php/RoundFunctionReturnTypeExtension.php @@ -5,7 +5,6 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Accessory\AccessoryNumericStringType; use PHPStan\Type\BooleanType; @@ -27,10 +26,6 @@ final class RoundFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { - public function __construct(private PhpVersion $phpVersion) - { - } - public function isFunctionSupported(FunctionReflection $functionReflection): bool { return in_array( @@ -50,16 +45,15 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, // PHP 8 can either return a float or fatal. $defaultReturnType = null; - if ($this->phpVersion->hasStricterRoundFunctions()) { - // PHP 8 fatals with a missing parameter. - $noArgsReturnType = new NeverType(true); - } else { - // PHP 7 returns null with a missing parameter. - $noArgsReturnType = new NullType(); - } + $hasStricterRoundFunctions = $scope->getPhpVersion()->hasStricterRoundFunctions(); if (count($functionCall->getArgs()) < 1) { - return $noArgsReturnType; + // PHP 8 fatals with a missing parameter, PHP 7 returns null. + if ($hasStricterRoundFunctions->yes()) { + return new NeverType(true); + } + + return new NullType(); } $firstArgType = $scope->getType($functionCall->getArgs()[0]->value); @@ -68,7 +62,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return $defaultReturnType; } - if ($this->phpVersion->hasStricterRoundFunctions()) { + if ($hasStricterRoundFunctions->yes()) { if (!$scope->isDeclareStrictTypes()) { $allowed = new UnionType([ new IntegerType(), diff --git a/src/Type/Php/RoundFunctionThrowTypeExtension.php b/src/Type/Php/RoundFunctionThrowTypeExtension.php index 7a79ec7b2b1..2f3af9c16ce 100644 --- a/src/Type/Php/RoundFunctionThrowTypeExtension.php +++ b/src/Type/Php/RoundFunctionThrowTypeExtension.php @@ -5,7 +5,6 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\DynamicFunctionThrowTypeExtension; use PHPStan\Type\IntegerRangeType; @@ -23,10 +22,6 @@ final class RoundFunctionThrowTypeExtension implements DynamicFunctionThrowTypeExtension { - public function __construct(private PhpVersion $phpVersion) - { - } - public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'round'; @@ -34,7 +29,7 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getThrowTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $funcCall, Scope $scope): ?Type { - if (!$this->phpVersion->throwsValueErrorForInvalidRoundingMode()) { + if ($scope->getPhpVersion()->throwsValueErrorForInvalidRoundingMode()->no()) { return new VoidType(); } diff --git a/src/Type/Php/StrSplitFunctionThrowTypeExtension.php b/src/Type/Php/StrSplitFunctionThrowTypeExtension.php index bcc1815efad..10891dbc717 100644 --- a/src/Type/Php/StrSplitFunctionThrowTypeExtension.php +++ b/src/Type/Php/StrSplitFunctionThrowTypeExtension.php @@ -5,7 +5,6 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\DynamicFunctionThrowTypeExtension; use PHPStan\Type\IntegerRangeType; @@ -20,10 +19,6 @@ final class StrSplitFunctionThrowTypeExtension implements DynamicFunctionThrowTypeExtension { - public function __construct(private PhpVersion $phpVersion) - { - } - public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'str_split'; @@ -31,7 +26,7 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getThrowTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $funcCall, Scope $scope): ?Type { - if (!$this->phpVersion->throwsValueErrorForInternalFunctions()) { + if ($scope->getPhpVersion()->throwsValueErrorForInternalFunctions()->no()) { return new VoidType(); } diff --git a/src/Type/Php/SubstrDynamicReturnTypeExtension.php b/src/Type/Php/SubstrDynamicReturnTypeExtension.php index be4ec43a315..0e92a4835f6 100644 --- a/src/Type/Php/SubstrDynamicReturnTypeExtension.php +++ b/src/Type/Php/SubstrDynamicReturnTypeExtension.php @@ -5,7 +5,6 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; @@ -31,10 +30,6 @@ final class SubstrDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { - public function __construct(private PhpVersion $phpVersion) - { - } - public function isFunctionSupported(FunctionReflection $functionReflection): bool { return in_array($functionReflection->getName(), ['substr', 'mb_substr'], true); @@ -51,6 +46,7 @@ public function getTypeFromFunctionCall( return null; } + $returnsFalseInsteadOfEmptyString = $scope->getPhpVersion()->substrReturnFalseInsteadOfEmptyString(); $string = $scope->getType($args[0]->value); $offset = $scope->getType($args[1]->value); @@ -90,9 +86,12 @@ public function getTypeFromFunctionCall( ? $this->substrOrFalse($constantString->getValue(), $offset->getValue(), $length->getValue()) : $this->substrOrFalse($constantString->getValue(), $offset->getValue()); if ($substr === false) { - $results[] = $this->phpVersion->substrReturnFalseInsteadOfEmptyString() - ? new ConstantBooleanType(false) - : new ConstantStringType(''); + if (!$returnsFalseInsteadOfEmptyString->no()) { + $results[] = new ConstantBooleanType(false); + } + if (!$returnsFalseInsteadOfEmptyString->yes()) { + $results[] = new ConstantStringType(''); + } } else { $results[] = new ConstantStringType($substr); } @@ -120,7 +119,7 @@ public function getTypeFromFunctionCall( if (count($accessoryTypes) > 0) { $accessoryTypes[] = new StringType(); - if (!$isNotEmpty && $this->phpVersion->substrReturnFalseInsteadOfEmptyString()) { + if (!$isNotEmpty && !$returnsFalseInsteadOfEmptyString->no()) { return new UnionType([ new ConstantBooleanType(false), new IntersectionType($accessoryTypes), diff --git a/src/Type/Php/TriggerErrorDynamicReturnTypeExtension.php b/src/Type/Php/TriggerErrorDynamicReturnTypeExtension.php index 3f3e09242df..fcc0aad7f1b 100644 --- a/src/Type/Php/TriggerErrorDynamicReturnTypeExtension.php +++ b/src/Type/Php/TriggerErrorDynamicReturnTypeExtension.php @@ -5,7 +5,6 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; @@ -23,10 +22,6 @@ final class TriggerErrorDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { - public function __construct(private PhpVersion $phpVersion) - { - } - public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'trigger_error'; @@ -54,7 +49,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } if (!in_array($errorLevel, [E_USER_WARNING, E_USER_NOTICE, E_USER_DEPRECATED], true)) { - if ($this->phpVersion->throwsValueErrorForInternalFunctions()) { + if ($scope->getPhpVersion()->throwsValueErrorForInternalFunctions()->yes()) { return new NeverType(true); } diff --git a/src/Type/Php/TriggerErrorFunctionThrowTypeExtension.php b/src/Type/Php/TriggerErrorFunctionThrowTypeExtension.php index 49c61fc2642..422c6782ac7 100644 --- a/src/Type/Php/TriggerErrorFunctionThrowTypeExtension.php +++ b/src/Type/Php/TriggerErrorFunctionThrowTypeExtension.php @@ -6,7 +6,6 @@ use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\DynamicFunctionThrowTypeExtension; use PHPStan\Type\ObjectType; @@ -35,7 +34,6 @@ final class TriggerErrorFunctionThrowTypeExtension implements DynamicFunctionThr private const NON_FATAL_ERROR_LEVELS = [E_USER_WARNING, E_USER_NOTICE, E_USER_DEPRECATED]; public function __construct( - private PhpVersion $phpVersion, #[AutowiredParameter(ref: '%exceptions.implicitThrows%')] private bool $implicitThrows, ) @@ -65,7 +63,7 @@ public function getThrowTypeFromFunctionCall(FunctionReflection $functionReflect return $errorHandlerThrowType; } - return $this->getInvalidErrorLevelThrowType(); + return $this->getInvalidErrorLevelThrowType($scope); } $throwTypes = []; @@ -82,7 +80,7 @@ public function getThrowTypeFromFunctionCall(FunctionReflection $functionReflect continue; } - $invalidErrorLevelThrowType = $this->getInvalidErrorLevelThrowType(); + $invalidErrorLevelThrowType = $this->getInvalidErrorLevelThrowType($scope); if ($invalidErrorLevelThrowType === null) { continue; } @@ -97,9 +95,9 @@ public function getThrowTypeFromFunctionCall(FunctionReflection $functionReflect return TypeCombinator::union(...$throwTypes); } - private function getInvalidErrorLevelThrowType(): ?Type + private function getInvalidErrorLevelThrowType(Scope $scope): ?Type { - if (!$this->phpVersion->throwsValueErrorForInternalFunctions()) { + if ($scope->getPhpVersion()->throwsValueErrorForInternalFunctions()->no()) { return null; } diff --git a/src/Type/Php/UnserializeFunctionThrowTypeExtension.php b/src/Type/Php/UnserializeFunctionThrowTypeExtension.php index 3a5a95c8384..56cfb20301e 100644 --- a/src/Type/Php/UnserializeFunctionThrowTypeExtension.php +++ b/src/Type/Php/UnserializeFunctionThrowTypeExtension.php @@ -5,7 +5,6 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantStringType; @@ -27,10 +26,6 @@ final class UnserializeFunctionThrowTypeExtension implements DynamicFunctionThrowTypeExtension { - public function __construct(private PhpVersion $phpVersion) - { - } - public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'unserialize'; @@ -38,7 +33,7 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getThrowTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $funcCall, Scope $scope): ?Type { - if (!$this->phpVersion->throwsValueErrorForInternalFunctions()) { + if ($scope->getPhpVersion()->throwsValueErrorForInternalFunctions()->no()) { return new VoidType(); } diff --git a/src/Type/Php/VersionCompareFunctionDynamicReturnTypeExtension.php b/src/Type/Php/VersionCompareFunctionDynamicReturnTypeExtension.php index f6a8d4a7282..5f6d818b822 100644 --- a/src/Type/Php/VersionCompareFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/VersionCompareFunctionDynamicReturnTypeExtension.php @@ -7,7 +7,6 @@ use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Php\ConfiguredPhpVersionRangeHelper; -use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\BenevolentUnionType; use PHPStan\Type\BooleanType; @@ -47,7 +46,6 @@ final class VersionCompareFunctionDynamicReturnTypeExtension implements DynamicF public function __construct( private ConfiguredPhpVersionRangeHelper $phpVersionRangeHelper, - private PhpVersion $phpVersion, ) { } @@ -68,6 +66,7 @@ public function getTypeFromFunctionCall( return null; } + $throwsValueError = $scope->getPhpVersion()->throwsValueErrorForInternalFunctions(); $version1Strings = $this->getVersionStrings($args[0]->value, $scope); $version2Strings = $this->getVersionStrings($args[1]->value, $scope); $counts = [ @@ -78,9 +77,9 @@ public function getTypeFromFunctionCall( if (isset($args[2])) { $operatorStrings = $scope->getType($args[2]->value)->getConstantStrings(); $counts[] = count($operatorStrings); - $returnType = $this->phpVersion->throwsValueErrorForInternalFunctions() - ? new BooleanType() - : new BenevolentUnionType([new BooleanType(), new NullType()]); + $returnType = !$throwsValueError->yes() && self::mightBeInvalidOperator($operatorStrings) + ? new BenevolentUnionType([new BooleanType(), new NullType()]) + : new BooleanType(); } else { $returnType = new UnionType([ new ConstantIntegerType(-1), @@ -105,7 +104,7 @@ public function getTypeFromFunctionCall( foreach ($operatorStrings as $operatorString) { $operatorValue = $operatorString->getValue(); if (!in_array($operatorValue, self::VALID_OPERATORS, true)) { - if (!$this->phpVersion->throwsValueErrorForInternalFunctions()) { + if (!$throwsValueError->yes()) { $canBeNull = true; } @@ -129,6 +128,27 @@ public function getTypeFromFunctionCall( return TypeCombinator::union(...$types); } + /** + * An invalid operator is the only thing that makes version_compare() return null or throw, + * so a call that certainly passes a valid one does neither, on any analysed version. + * + * @param ConstantStringType[] $operatorStrings + */ + public static function mightBeInvalidOperator(array $operatorStrings): bool + { + if (count($operatorStrings) === 0) { + return true; // the operator is not a constant string, it might be invalid + } + + foreach ($operatorStrings as $operatorString) { + if (!in_array($operatorString->getValue(), self::VALID_OPERATORS, true)) { + return true; + } + } + + return false; + } + /** * @return ConstantStringType[] */ diff --git a/src/Type/Php/VersionCompareFunctionDynamicThrowTypeExtension.php b/src/Type/Php/VersionCompareFunctionDynamicThrowTypeExtension.php index e4c4f92b34d..96ae2961c44 100644 --- a/src/Type/Php/VersionCompareFunctionDynamicThrowTypeExtension.php +++ b/src/Type/Php/VersionCompareFunctionDynamicThrowTypeExtension.php @@ -5,23 +5,14 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\DynamicFunctionThrowTypeExtension; use PHPStan\Type\Type; -use function count; -use function in_array; #[AutowiredService] final class VersionCompareFunctionDynamicThrowTypeExtension implements DynamicFunctionThrowTypeExtension { - public function __construct( - private PhpVersion $phpVersion, - ) - { - } - public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'version_compare'; @@ -33,7 +24,7 @@ public function getThrowTypeFromFunctionCall( Scope $scope, ): ?Type { - if (!$this->phpVersion->throwsValueErrorForInternalFunctions()) { + if ($scope->getPhpVersion()->throwsValueErrorForInternalFunctions()->no()) { return null; } @@ -43,17 +34,10 @@ public function getThrowTypeFromFunctionCall( } $operatorStrings = $scope->getType($args[2]->value)->getConstantStrings(); - if (count($operatorStrings) === 0) { + if (VersionCompareFunctionDynamicReturnTypeExtension::mightBeInvalidOperator($operatorStrings)) { return $functionReflection->getThrowType(); } - foreach ($operatorStrings as $operatorString) { - $operatorValue = $operatorString->getValue(); - if (!in_array($operatorValue, VersionCompareFunctionDynamicReturnTypeExtension::VALID_OPERATORS, true)) { - return $functionReflection->getThrowType(); - } - } - return null; } diff --git a/tests/PHPStan/Analyser/ScopePhpVersionRangeReturnTypeExtensionsTest.php b/tests/PHPStan/Analyser/ScopePhpVersionRangeReturnTypeExtensionsTest.php new file mode 100644 index 00000000000..d16c74f2b39 --- /dev/null +++ b/tests/PHPStan/Analyser/ScopePhpVersionRangeReturnTypeExtensionsTest.php @@ -0,0 +1,36 @@ +assertFileAsserts($assertType, $file, ...$args); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/scopePhpVersionRange.neon', + ]; + } + +} diff --git a/tests/PHPStan/Analyser/data/scope-php-version-range-return-type-extensions.php b/tests/PHPStan/Analyser/data/scope-php-version-range-return-type-extensions.php new file mode 100644 index 00000000000..8d291943f35 --- /dev/null +++ b/tests/PHPStan/Analyser/data/scope-php-version-range-return-type-extensions.php @@ -0,0 +1,16 @@ += 80000) { + assertType('*NEVER*', array_keys($s)); + assertType('*NEVER*', array_values($s)); + assertType('*NEVER*', array_flip($s)); + assertType('*NEVER*', array_reverse($s)); + assertType('*NEVER*', array_slice($s, 1)); + } else { + assertType('null', array_keys($s)); + assertType('null', array_values($s)); + assertType('null', array_flip($s)); + assertType('null', array_reverse($s)); + assertType('null', array_slice($s, 1)); + } +} + +/** + * @param array $arr + */ +function arrayChunkAndFill(array $arr): void +{ + if (PHP_VERSION_ID >= 80000) { + assertType('*NEVER*', array_chunk($arr, 0)); + assertType('*NEVER*', array_fill(0, -1, 'x')); + assertType('*NEVER*', array_combine(['a'], [])); + } else { + assertType('null', array_chunk($arr, 0)); + assertType('false', array_fill(0, -1, 'x')); + assertType('false', array_combine(['a'], [])); + } +} + +function roundAndHighlight(): void +{ + if (PHP_VERSION_ID >= 80000) { + assertType('*NEVER*', round()); + } else { + assertType('null', round()); + } + + if (PHP_VERSION_ID >= 80400) { + assertType('true', highlight_string('= 80000) { + assertType('string', count_chars($s, 3)); + assertType('*NEVER*', hash('nope', $s)); + } else { + assertType('(string|false)', count_chars($s, 3)); + assertType('false', hash('nope', $s)); + } +} + +function versionCompareAndMinMax(string $a, string $b): void +{ + if (PHP_VERSION_ID >= 80000) { + assertType('bool', version_compare($a, $b, 'nope')); + assertType('*ERROR*', min([])); + } else { + assertType('(bool|null)', version_compare($a, $b, 'nope')); + assertType('false', min([])); + } +} + +function dateTimeModify(\DateTime $dt): void +{ + if (PHP_VERSION_ID >= 80300) { + assertType('*NEVER*', $dt->modify('nope')); + assertType('*NEVER*', \DateInterval::createFromDateString('nope')); + } else { + assertType('false', $dt->modify('nope')); + assertType('false', \DateInterval::createFromDateString('nope')); + } +} + +function bcMath(): void +{ + if (PHP_VERSION_ID >= 80000) { + assertType('*NEVER*', bcdiv('1', '0')); + } else { + assertType('null', bcdiv('1', '0')); + } +} + +function filterVarThrowOnFailure(string $s): void +{ + if (PHP_VERSION_ID >= 80500) { + assertType('int', filter_var($s, FILTER_VALIDATE_INT, FILTER_THROW_ON_FAILURE)); + } else { + assertType('int|false', filter_var($s, FILTER_VALIDATE_INT, FILTER_THROW_ON_FAILURE)); + } +} + +function getClassWithoutArguments(): void +{ + if (PHP_VERSION_ID >= 80000) { + assertType('*NEVER*', get_class()); + } else { + assertType('false', get_class()); + } +} diff --git a/tests/PHPStan/Analyser/scopePhpVersionRange.neon b/tests/PHPStan/Analyser/scopePhpVersionRange.neon new file mode 100644 index 00000000000..7bfac80ba3b --- /dev/null +++ b/tests/PHPStan/Analyser/scopePhpVersionRange.neon @@ -0,0 +1,4 @@ +parameters: + phpVersion: + min: 70400 + max: 80500 diff --git a/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleConfigPhpTest.php b/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleConfigPhpTest.php new file mode 100644 index 00000000000..85cbc4a08e4 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleConfigPhpTest.php @@ -0,0 +1,42 @@ + + */ +class CatchWithUnthrownExceptionRuleConfigPhpTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new CatchWithUnthrownExceptionRule(new DefaultExceptionTypeResolver( + self::createReflectionProvider(), + [], + [], + [], + [], + ), true); + } + + public function testThrowTypesInPhpVersionRange(): void + { + $this->analyse([__DIR__ . '/data/throw-type-php-versions.php'], [ + [ + 'Dead catch - ValueError is never thrown in the try block.', + 19, + ], + ]); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/data/throw-type-php-version.neon', + ]; + } + +} diff --git a/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleGetClassConfigPhpTest.php b/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleGetClassConfigPhpTest.php new file mode 100644 index 00000000000..af9dbc277b8 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleGetClassConfigPhpTest.php @@ -0,0 +1,42 @@ + + */ +class CatchWithUnthrownExceptionRuleGetClassConfigPhpTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new CatchWithUnthrownExceptionRule(new DefaultExceptionTypeResolver( + self::createReflectionProvider(), + [], + [], + [], + [], + ), true); + } + + public function testGetClassThrowTypeInPhpVersionRange(): void + { + $this->analyse([__DIR__ . '/data/get-class-throw-type-php-versions.php'], [ + [ + 'Dead catch - TypeError is never thrown in the try block.', + 19, + ], + ]); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/data/get-class-throw-type-php-version.neon', + ]; + } + +} diff --git a/tests/PHPStan/Rules/Exceptions/data/get-class-throw-type-php-version.neon b/tests/PHPStan/Rules/Exceptions/data/get-class-throw-type-php-version.neon new file mode 100644 index 00000000000..e6683ba7e70 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/get-class-throw-type-php-version.neon @@ -0,0 +1,4 @@ +parameters: + phpVersion: + min: 70400 + max: 80500 diff --git a/tests/PHPStan/Rules/Exceptions/data/get-class-throw-type-php-versions.php b/tests/PHPStan/Rules/Exceptions/data/get-class-throw-type-php-versions.php new file mode 100644 index 00000000000..4522057d202 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/get-class-throw-type-php-versions.php @@ -0,0 +1,31 @@ += 80000) { + try { + $a = get_class($s); + } catch (\TypeError $e) { + + } + } else { + try { + $a = get_class($s); + } catch (\TypeError $e) { + + } + } + + try { + $a = get_class($s); + } catch (\TypeError $e) { + + } + } + +} diff --git a/tests/PHPStan/Rules/Exceptions/data/throw-type-php-version.neon b/tests/PHPStan/Rules/Exceptions/data/throw-type-php-version.neon new file mode 100644 index 00000000000..1768b64451f --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/throw-type-php-version.neon @@ -0,0 +1,4 @@ +parameters: + phpVersion: + min: 80000 + max: 80500 diff --git a/tests/PHPStan/Rules/Exceptions/data/throw-type-php-versions.php b/tests/PHPStan/Rules/Exceptions/data/throw-type-php-versions.php new file mode 100644 index 00000000000..f3f83eedf7d --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/throw-type-php-versions.php @@ -0,0 +1,54 @@ += 80400) { + try { + $a = round($f, 0, $mode); + } catch (\ValueError $e) { + + } + } else { + try { + $a = round($f, 0, $mode); + } catch (\ValueError $e) { + + } + } + + try { + $a = round($f, 0, $mode); + } catch (\ValueError $e) { + + } + } + + public function doStrSplit(string $s, int $length): void + { + if (PHP_VERSION_ID >= 80000) { + try { + $a = str_split($s, $length); + } catch (\ValueError $e) { + + } + } else { + try { + $a = str_split($s, $length); + } catch (\ValueError $e) { + + } + } + + try { + $a = str_split($s, $length); + } catch (\ValueError $e) { + + } + } + +}