From cea871566f1c1f67b28210ba6504239ca85911a5 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Wed, 23 Sep 2026 06:50:45 +0000 Subject: [PATCH 1/4] Resolve PHP version checks in src/Type/Php extensions from Scope::getPhpVersion() Return-type, throw-type and type-specifying extensions under src/Type/Php asked a DI-injected PhpVersion which PHP version is analysed. That ignores both PHP_VERSION_ID narrowing in the analysed code and a configured phpVersion min/max range. They now read the range-aware PhpVersions from the call's Scope, along with the helpers they share (ArrayColumnHelper, ArrayFilterFunctionReturnTypeHelper, FilterFunctionReturnTypeHelper, MbFunctionsReturnTypeExtensionTrait). A new build rule reports scope-aware extensions that inject PhpVersion. The three reflection extensions converted in a separate pull request are baselined here until that lands. Split out of #6529. The Regex* classes are left for a dedicated change. Co-Authored-By: Claude Opus 5.5 --- ...tedPhpVersionInScopeAwareExtensionRule.php | 130 ++++++++++++++++ build/phpstan.neon | 1 + phpstan-baseline.neon | 24 +++ src/Php/PhpVersions.php | 75 +++++++++ .../ArrayChunkFunctionReturnTypeExtension.php | 17 +- .../ArrayChunkFunctionThrowTypeExtension.php | 7 +- src/Type/Php/ArrayColumnHelper.php | 26 ++-- ...rrayCombineFunctionReturnTypeExtension.php | 7 +- .../ArrayFillFunctionReturnTypeExtension.php | 11 +- ...rayFillKeysFunctionReturnTypeExtension.php | 11 +- .../ArrayFilterFunctionReturnTypeHelper.php | 4 +- .../ArrayFlipFunctionReturnTypeExtension.php | 11 +- ...ntersectKeyFunctionReturnTypeExtension.php | 11 +- ...yExistsFunctionTypeSpecifyingExtension.php | 9 +- ...KeysFunctionDynamicReturnTypeExtension.php | 11 +- ...rrayReverseFunctionReturnTypeExtension.php | 11 +- ...archFunctionDynamicReturnTypeExtension.php | 11 +- .../ArraySliceFunctionReturnTypeExtension.php | 11 +- ...ArraySpliceFunctionReturnTypeExtension.php | 11 +- ...luesFunctionDynamicReturnTypeExtension.php | 11 +- .../BcMathStringOrNullReturnTypeExtension.php | 42 +++-- ...harsFunctionDynamicReturnTypeExtension.php | 10 +- ...eIntervalConstructorThrowTypeExtension.php | 13 +- ...CreateFromDateStringThrowTypeExtension.php | 16 +- ...DateIntervalDynamicReturnTypeExtension.php | 7 +- .../DateTimeConstructorThrowTypeExtension.php | 13 +- ...DateTimeModifyMethodThrowTypeExtension.php | 24 +-- .../Php/DateTimeModifyReturnTypeExtension.php | 4 +- .../DateTimeSubMethodThrowTypeExtension.php | 7 +- ...eTimeZoneConstructorThrowTypeExtension.php | 13 +- .../Php/FilterFunctionReturnTypeHelper.php | 28 ++-- .../Php/FilterFunctionsThrowTypeExtension.php | 4 +- .../FilterInputDynamicReturnTypeExtension.php | 1 + ...lterVarArrayDynamicReturnTypeExtension.php | 7 +- .../FilterVarDynamicReturnTypeExtension.php | 2 +- .../GetClassDynamicReturnTypeExtension.php | 20 +-- .../GetClassFunctionThrowTypeExtension.php | 7 +- .../Php/HashFunctionsReturnTypeExtension.php | 14 +- ...hlightStringDynamicReturnTypeExtension.php | 22 +-- ...ertEncodingFunctionReturnTypeExtension.php | 7 +- .../Php/MbFunctionsReturnTypeExtension.php | 21 ++- .../MbFunctionsReturnTypeExtensionTrait.php | 52 ++++--- .../MbStrlenFunctionReturnTypeExtension.php | 18 +-- ...uteCharacterDynamicReturnTypeExtension.php | 114 +++++++++----- .../Php/MinMaxFunctionReturnTypeExtension.php | 19 +-- .../Php/MinMaxFunctionThrowTypeExtension.php | 7 +- ...nsslCipherFunctionsReturnTypeExtension.php | 4 +- .../Php/PDOConnectReturnTypeExtension.php | 7 +- .../Php/PrintfFunctionThrowTypeExtension.php | 4 +- .../Php/RoundFunctionReturnTypeExtension.php | 22 +-- .../Php/RoundFunctionThrowTypeExtension.php | 7 +- .../StrSplitFunctionReturnTypeExtension.php | 45 ++++-- .../StrSplitFunctionThrowTypeExtension.php | 7 +- .../Php/SubstrDynamicReturnTypeExtension.php | 17 +- ...TriggerErrorDynamicReturnTypeExtension.php | 7 +- ...TriggerErrorFunctionThrowTypeExtension.php | 10 +- .../UnserializeFunctionThrowTypeExtension.php | 7 +- ...pareFunctionDynamicReturnTypeExtension.php | 32 +++- ...mpareFunctionDynamicThrowTypeExtension.php | 20 +-- ...hpVersionRangeReturnTypeExtensionsTest.php | 36 +++++ ...p-version-range-return-type-extensions.php | 28 ++++ ...ope-php-version-return-type-extensions.php | 141 +++++++++++++++++ .../Analyser/scopePhpVersionRange.neon | 4 + ...hpVersionInScopeAwareExtensionRuleTest.php | 37 +++++ .../Build/data/php-version-in-extension.php | 147 ++++++++++++++++++ ...WithUnthrownExceptionRuleConfigPhpTest.php | 42 +++++ ...rownExceptionRuleGetClassConfigPhpTest.php | 42 +++++ .../get-class-throw-type-php-version.neon | 4 + .../get-class-throw-type-php-versions.php | 31 ++++ .../data/throw-type-php-version.neon | 4 + .../data/throw-type-php-versions.php | 54 +++++++ 71 files changed, 1203 insertions(+), 458 deletions(-) create mode 100644 build/PHPStan/Build/NoInjectedPhpVersionInScopeAwareExtensionRule.php create mode 100644 tests/PHPStan/Analyser/ScopePhpVersionRangeReturnTypeExtensionsTest.php create mode 100644 tests/PHPStan/Analyser/data/scope-php-version-range-return-type-extensions.php create mode 100644 tests/PHPStan/Analyser/nsrt/scope-php-version-return-type-extensions.php create mode 100644 tests/PHPStan/Analyser/scopePhpVersionRange.neon create mode 100644 tests/PHPStan/Build/NoInjectedPhpVersionInScopeAwareExtensionRuleTest.php create mode 100644 tests/PHPStan/Build/data/php-version-in-extension.php create mode 100644 tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleConfigPhpTest.php create mode 100644 tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleGetClassConfigPhpTest.php create mode 100644 tests/PHPStan/Rules/Exceptions/data/get-class-throw-type-php-version.neon create mode 100644 tests/PHPStan/Rules/Exceptions/data/get-class-throw-type-php-versions.php create mode 100644 tests/PHPStan/Rules/Exceptions/data/throw-type-php-version.neon create mode 100644 tests/PHPStan/Rules/Exceptions/data/throw-type-php-versions.php diff --git a/build/PHPStan/Build/NoInjectedPhpVersionInScopeAwareExtensionRule.php b/build/PHPStan/Build/NoInjectedPhpVersionInScopeAwareExtensionRule.php new file mode 100644 index 00000000000..8dc08f09678 --- /dev/null +++ b/build/PHPStan/Build/NoInjectedPhpVersionInScopeAwareExtensionRule.php @@ -0,0 +1,130 @@ + + */ +final class NoInjectedPhpVersionInScopeAwareExtensionRule implements Rule +{ + + private const SCOPE_AWARE_EXTENSIONS = [ + DynamicFunctionReturnTypeExtension::class, + DynamicMethodReturnTypeExtension::class, + DynamicStaticMethodReturnTypeExtension::class, + DynamicFunctionThrowTypeExtension::class, + DynamicMethodThrowTypeExtension::class, + DynamicStaticMethodThrowTypeExtension::class, + FunctionTypeSpecifyingExtension::class, + MethodTypeSpecifyingExtension::class, + StaticMethodTypeSpecifyingExtension::class, + FunctionParameterOutTypeExtension::class, + MethodParameterOutTypeExtension::class, + StaticMethodParameterOutTypeExtension::class, + FunctionParameterClosureTypeExtension::class, + MethodParameterClosureTypeExtension::class, + StaticMethodParameterClosureTypeExtension::class, + FunctionParameterClosureThisExtension::class, + MethodParameterClosureThisExtension::class, + StaticMethodParameterClosureThisExtension::class, + RestrictedClassConstantUsageExtension::class, + RestrictedClassNameUsageExtension::class, + RestrictedFunctionUsageExtension::class, + RestrictedMethodUsageExtension::class, + RestrictedPropertyUsageExtension::class, + ExpressionTypeResolverExtension::class, + IgnoreErrorExtension::class, + Collector::class, + ]; + + public function getNodeType(): string + { + return InClassNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $classReflection = $node->getClassReflection(); + if (!$classReflection->hasConstructor()) { + return []; + } + + $implementedExtension = null; + foreach (self::SCOPE_AWARE_EXTENSIONS as $extensionInterface) { + if (!$classReflection->is($extensionInterface)) { + continue; + } + + $implementedExtension = $extensionInterface; + break; + } + + if ($implementedExtension === null) { + return []; + } + + $constructorVariant = $classReflection->getConstructor()->getOnlyVariant(); + $errors = []; + foreach ($constructorVariant->getParameters() as $parameter) { + foreach ($parameter->getType()->getObjectClassNames() as $className) { + if ($className !== PhpVersion::class) { + continue; + } + + $errors[] = RuleErrorBuilder::message(sprintf( + '%s implements %s and must not inject %s - read the analysed PHP version from Scope::getPhpVersion() instead.', + $classReflection->getDisplayName(), + $implementedExtension, + PhpVersion::class, + ))->identifier('phpstanBuild.phpVersionInExtension')->build(); + } + } + + return $errors; + } + +} diff --git a/build/phpstan.neon b/build/phpstan.neon index 5226880e48c..d7436d7df1a 100644 --- a/build/phpstan.neon +++ b/build/phpstan.neon @@ -201,6 +201,7 @@ parameters: rules: - PHPStan\Build\FinalClassRule - PHPStan\Build\NoScopeTypeReadInEngineRule + - PHPStan\Build\NoInjectedPhpVersionInScopeAwareExtensionRule - PHPStan\Build\AttributeNamedArgumentsRule - PHPStan\Build\NamedArgumentsRule - PHPStan\Build\OverrideAttributeThirdPartyMethodRule diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 5a96ce5c15e..c07cbd23769 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -378,6 +378,18 @@ parameters: count: 1 path: src/Reflection/BetterReflection/SourceLocator/SkipClassAliasSourceLocator.php + - + rawMessage: 'PHPStan\Reflection\BetterReflection\Type\AdapterReflectionEnumCaseDynamicReturnTypeExtension implements PHPStan\Type\DynamicMethodReturnTypeExtension and must not inject PHPStan\Php\PhpVersion - read the analysed PHP version from Scope::getPhpVersion() instead.' + identifier: phpstanBuild.phpVersionInExtension + count: 1 + path: src/Reflection/BetterReflection/Type/AdapterReflectionEnumCaseDynamicReturnTypeExtension.php + + - + rawMessage: 'PHPStan\Reflection\BetterReflection\Type\AdapterReflectionEnumDynamicReturnTypeExtension implements PHPStan\Type\DynamicMethodReturnTypeExtension and must not inject PHPStan\Php\PhpVersion - read the analysed PHP version from Scope::getPhpVersion() instead.' + identifier: phpstanBuild.phpVersionInExtension + count: 1 + path: src/Reflection/BetterReflection/Type/AdapterReflectionEnumDynamicReturnTypeExtension.php + - rawMessage: Doing instanceof PHPStan\Type\Generic\GenericObjectType is error-prone and deprecated. identifier: phpstanApi.instanceofType @@ -468,6 +480,12 @@ parameters: count: 6 path: src/Reflection/InitializerExprTypeResolver.php + - + rawMessage: 'PHPStan\Reflection\PHPStan\NativeReflectionEnumReturnDynamicReturnTypeExtension implements PHPStan\Type\DynamicMethodReturnTypeExtension and must not inject PHPStan\Php\PhpVersion - read the analysed PHP version from Scope::getPhpVersion() instead.' + identifier: phpstanBuild.phpVersionInExtension + count: 1 + path: src/Reflection/PHPStan/NativeReflectionEnumReturnDynamicReturnTypeExtension.php + - rawMessage: Creating new PHPStan\Php8StubsMap is not covered by backward compatibility promise. The class might change in a minor PHPStan version. identifier: phpstanApi.constructor @@ -1548,6 +1566,12 @@ parameters: count: 2 path: src/Type/Php/MinMaxFunctionReturnTypeExtension.php + - + rawMessage: 'PHPStan\Type\Php\PDOConnectReturnTypeExtension implements PHPStan\Type\DynamicStaticMethodReturnTypeExtension and must not inject PHPStan\Php\PhpVersion - read the analysed PHP version from Scope::getPhpVersion() instead.' + identifier: phpstanBuild.phpVersionInExtension + count: 1 + path: src/Type/Php/PDOConnectReturnTypeExtension.php + - rawMessage: 'Doing instanceof PHPStan\Type\Constant\ConstantStringType is error-prone and deprecated. Use Type::getConstantStrings() instead.' identifier: phpstanApi.instanceofType 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/MbFunctionsReturnTypeExtension.php b/src/Type/Php/MbFunctionsReturnTypeExtension.php index 659bd9f0072..cf75081b85b 100644 --- a/src/Type/Php/MbFunctionsReturnTypeExtension.php +++ b/src/Type/Php/MbFunctionsReturnTypeExtension.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\BooleanType; @@ -38,10 +37,6 @@ final class MbFunctionsReturnTypeExtension implements DynamicFunctionReturnTypeE 'mb_ord' => 2, ]; - public function __construct(private PhpVersion $phpVersion) - { - } - public function isFunctionSupported(FunctionReflection $functionReflection): bool { return array_key_exists($functionReflection->getName(), $this->encodingPositionMap); @@ -61,22 +56,24 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return TypeCombinator::remove($returnType, new BooleanType()); } + $phpVersions = $scope->getPhpVersion(); $strings = $scope->getType($args[$positionEncodingParam - 1]->value)->getConstantStrings(); - $results = array_unique(array_map(fn (ConstantStringType $encoding): bool => $this->isSupportedEncoding($encoding->getValue()), $strings)); + $results = array_unique(array_map(fn (ConstantStringType $encoding): bool => $this->isSupportedEncoding($encoding->getValue(), $phpVersions), $strings)); if ($returnType->equals(new UnionType([new StringType(), new BooleanType()]))) { return count($results) === 1 ? new ConstantBooleanType($results[0]) : new BooleanType(); } if (count($results) === 1) { - $invalidEncodingReturn = new ConstantBooleanType(false); - if ($this->phpVersion->throwsOnInvalidMbStringEncoding()) { - $invalidEncodingReturn = new NeverType(); + if ($results[0]) { + return TypeCombinator::remove($returnType, new ConstantBooleanType(false)); + } + + if ($phpVersions->throwsOnInvalidMbStringEncoding()->yes()) { + return new NeverType(); } - return $results[0] - ? TypeCombinator::remove($returnType, new ConstantBooleanType(false)) - : $invalidEncodingReturn; + return new ConstantBooleanType(false); } return $returnType; diff --git a/src/Type/Php/MbFunctionsReturnTypeExtensionTrait.php b/src/Type/Php/MbFunctionsReturnTypeExtensionTrait.php index 64036c984b8..a382aa497dc 100644 --- a/src/Type/Php/MbFunctionsReturnTypeExtensionTrait.php +++ b/src/Type/Php/MbFunctionsReturnTypeExtensionTrait.php @@ -2,13 +2,14 @@ namespace PHPStan\Type\Php; +use PHPStan\Php\PhpVersions; use PHPStan\ShouldNotHappenException; use function array_filter; use function array_map; use function array_merge; +use function array_values; use function function_exists; use function in_array; -use function is_null; use function mb_encoding_aliases; use function mb_list_encodings; use function strtoupper; @@ -19,36 +20,43 @@ trait MbFunctionsReturnTypeExtensionTrait /** @var string[]|null */ private ?array $supportedEncodings = null; - private function isSupportedEncoding(string $encoding): bool + /** @var string[]|null */ + private ?array $supportedEncodingsWithoutPassNone = null; + + private function isSupportedEncoding(string $encoding, PhpVersions $phpVersions): bool { - return in_array(strtoupper($encoding), $this->getSupportedEncodings(), true); + return in_array(strtoupper($encoding), $this->getSupportedEncodings($phpVersions), true); } /** @return string[] */ - private function getSupportedEncodings(): array + private function getSupportedEncodings(PhpVersions $phpVersions): array { - if (!is_null($this->supportedEncodings)) { - return $this->supportedEncodings; + // PHP 7.3 and 7.4 claims 'pass' and its alias 'none' to be supported, but actually 'pass' was removed in 7.3 + if ($phpVersions->supportsPassNoneEncodings()->no()) { + return $this->supportedEncodingsWithoutPassNone ??= array_values(array_filter( + $this->getAllSupportedEncodings(), + static fn (string $enc) => !in_array($enc, ['PASS', 'NONE'], true), + )); } - $supportedEncodings = []; - if (function_exists('mb_list_encodings')) { - foreach (mb_list_encodings() as $encoding) { - $aliases = @mb_encoding_aliases($encoding); - if ($aliases === false) { - throw new ShouldNotHappenException(); + return $this->getAllSupportedEncodings(); + } + + /** @return string[] */ + private function getAllSupportedEncodings(): array + { + if ($this->supportedEncodings === null) { + $supportedEncodings = []; + if (function_exists('mb_list_encodings')) { + foreach (mb_list_encodings() as $encoding) { + $aliases = @mb_encoding_aliases($encoding); + if ($aliases === false) { + throw new ShouldNotHappenException(); + } + $supportedEncodings = array_merge($supportedEncodings, $aliases, [$encoding]); } - $supportedEncodings = array_merge($supportedEncodings, $aliases, [$encoding]); } - } - $this->supportedEncodings = array_map('strtoupper', $supportedEncodings); - - // PHP 7.3 and 7.4 claims 'pass' and its alias 'none' to be supported, but actually 'pass' was removed in 7.3 - if (!$this->phpVersion->supportsPassNoneEncodings()) { - $this->supportedEncodings = array_filter( - $this->supportedEncodings, - static fn (string $enc) => !in_array($enc, ['PASS', 'NONE'], true), - ); + $this->supportedEncodings = array_map('strtoupper', $supportedEncodings); } return $this->supportedEncodings; diff --git a/src/Type/Php/MbStrlenFunctionReturnTypeExtension.php b/src/Type/Php/MbStrlenFunctionReturnTypeExtension.php index ee57cc3f16f..5d20771520d 100644 --- a/src/Type/Php/MbStrlenFunctionReturnTypeExtension.php +++ b/src/Type/Php/MbStrlenFunctionReturnTypeExtension.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\ShouldNotHappenException; @@ -42,10 +41,6 @@ final class MbStrlenFunctionReturnTypeExtension implements DynamicFunctionReturn use MbFunctionsReturnTypeExtensionTrait; - public function __construct(private PhpVersion $phpVersion) - { - } - public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'mb_strlen'; @@ -62,6 +57,8 @@ public function getTypeFromFunctionCall( return null; } + $phpVersions = $scope->getPhpVersion(); + $throwsOnInvalidEncoding = $phpVersions->throwsOnInvalidMbStringEncoding(); $encodings = []; if (count($args) === 1) { @@ -76,7 +73,7 @@ public function getTypeFromFunctionCall( if (count($encodings) > 0) { for ($i = 0; $i < count($encodings); $i++) { - if ($this->isSupportedEncoding($encodings[$i])) { + if ($this->isSupportedEncoding($encodings[$i], $phpVersions)) { continue; } $encodings[$i] = self::UNSUPPORTED_ENCODING; @@ -85,13 +82,14 @@ public function getTypeFromFunctionCall( $encodings = array_unique($encodings); if (in_array(self::UNSUPPORTED_ENCODING, $encodings, true) && count($encodings) === 1) { - if ($this->phpVersion->throwsOnInvalidMbStringEncoding()) { + if ($throwsOnInvalidEncoding->yes()) { return new NeverType(); } + return new ConstantBooleanType(false); } } else { // if there aren't encoding constants, use all available encodings - $encodings = array_merge($this->getSupportedEncodings(), [self::UNSUPPORTED_ENCODING]); + $encodings = array_merge($this->getSupportedEncodings($phpVersions), [self::UNSUPPORTED_ENCODING]); } $argType = $scope->getType($args[0]->value); @@ -102,7 +100,7 @@ public function getTypeFromFunctionCall( $stringScalar = (string) $constantScalar; foreach ($encodings as $encoding) { - if (!$this->isSupportedEncoding($encoding)) { + if (!$this->isSupportedEncoding($encoding, $phpVersions)) { continue; } @@ -145,7 +143,7 @@ public function getTypeFromFunctionCall( ); } - if (!$this->phpVersion->throwsOnInvalidMbStringEncoding() && in_array(self::UNSUPPORTED_ENCODING, $encodings, true)) { + if (!$throwsOnInvalidEncoding->yes() && in_array(self::UNSUPPORTED_ENCODING, $encodings, true)) { return TypeCombinator::union($range, new ConstantBooleanType(false)); } return $range; diff --git a/src/Type/Php/MbSubstituteCharacterDynamicReturnTypeExtension.php b/src/Type/Php/MbSubstituteCharacterDynamicReturnTypeExtension.php index 7b14c0cce7f..11b80bca108 100644 --- a/src/Type/Php/MbSubstituteCharacterDynamicReturnTypeExtension.php +++ b/src/Type/Php/MbSubstituteCharacterDynamicReturnTypeExtension.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; @@ -23,10 +22,6 @@ final class MbSubstituteCharacterDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { - public function __construct(private PhpVersion $phpVersion) - { - } - public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'mb_substitute_character'; @@ -34,17 +29,23 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type { - $minCodePoint = $this->phpVersion->getVersionId() < 80000 ? 1 : 0; - $maxCodePoint = $this->phpVersion->supportsAllUnicodeScalarCodePointsInMbSubstituteCharacter() ? 0x10FFFF : 0xFFFE; - $ranges = []; - - if ($this->phpVersion->supportsAllUnicodeScalarCodePointsInMbSubstituteCharacter()) { - // Surrogates aren't valid in PHP 7.2+ - $ranges[] = IntegerRangeType::fromInterval($minCodePoint, 0xD7FF); - $ranges[] = IntegerRangeType::fromInterval(0xE000, $maxCodePoint); - } else { - $ranges[] = IntegerRangeType::fromInterval($minCodePoint, $maxCodePoint); - } + $phpVersions = $scope->getPhpVersion(); + $supportsAllUnicodeScalarCodePoints = $phpVersions->supportsAllUnicodeScalarCodePointsInMbSubstituteCharacter(); + // The empty string is a valid alias for "none" in PHP < 8, where 0 is not a valid code point. + $isEmptyStringValidAlias = $phpVersions->isEmptyStringValidAliasForNoneInMbSubstituteCharacter(); + + // Code points valid on every PHP version the analysed range spans... + $ranges = self::codePointRanges( + $isEmptyStringValidAlias->no() ? 0 : 1, + $supportsAllUnicodeScalarCodePoints->yes() ? 0x10FFFF : 0xFFFE, + !$supportsAllUnicodeScalarCodePoints->no(), + ); + // ...and those valid on at least one of them. + $possibleRanges = self::codePointRanges( + $isEmptyStringValidAlias->yes() ? 1 : 0, + $supportsAllUnicodeScalarCodePoints->no() ? 0xFFFE : 0x10FFFF, + $supportsAllUnicodeScalarCodePoints->yes(), + ); if (!isset($functionCall->getArgs()[0])) { return TypeCombinator::union( @@ -61,7 +62,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $isInteger = $argType->isInteger(); if ($isString->no() && $isNull->no() && $isInteger->no()) { - if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { + if ($phpVersions->throwsTypeErrorForInternalFunctions()->yes()) { return new NeverType(); } @@ -69,20 +70,14 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } if ($isInteger->yes()) { - $invalidRanges = []; - foreach ($ranges as $range) { - $isInRange = $range->isSuperTypeOf($argType); - - if ($isInRange->yes()) { + if ($range->isSuperTypeOf($argType)->yes()) { return new ConstantBooleanType(true); } - - $invalidRanges[] = $isInRange->no(); } - if ($argType instanceof ConstantIntegerType || !in_array(false, $invalidRanges, true)) { - if ($this->phpVersion->throwsValueErrorForInternalFunctions()) { + if (!self::isPossiblyInRanges($possibleRanges, $argType)) { + if ($phpVersions->throwsValueErrorForInternalFunctions()->yes()) { return new NeverType(); } @@ -90,15 +85,14 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } } elseif ($isString->yes()) { if ($argType->isNonEmptyString()->no()) { - // The empty string was a valid alias for "none" in PHP < 8. - if ($this->phpVersion->isEmptyStringValidAliasForNoneInMbSubstituteCharacter()) { - return new ConstantBooleanType(true); + if ($isEmptyStringValidAlias->no()) { + return new NeverType(); } - return new NeverType(); + return new ConstantBooleanType(true); } - if (!$this->phpVersion->isNumericStringValidArgInMbSubstituteCharacter() && $argType->isNumericString()->yes()) { + if ($phpVersions->isNumericStringValidArgInMbSubstituteCharacter()->no() && $argType->isNumericString()->yes()) { return new NeverType(); } @@ -110,17 +104,22 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } if ($argType->isNumericString()->yes()) { - $codePoint = (int) $value; - $isValid = $codePoint >= $minCodePoint && $codePoint <= $maxCodePoint; + $codePoint = new ConstantIntegerType((int) $value); - if ($this->phpVersion->supportsAllUnicodeScalarCodePointsInMbSubstituteCharacter()) { - $isValid = $isValid && ($codePoint < 0xD800 || $codePoint > 0xDFFF); + foreach ($ranges as $range) { + if ($range->isSuperTypeOf($codePoint)->yes()) { + return new ConstantBooleanType(true); + } } - return new ConstantBooleanType($isValid); + if (!self::isPossiblyInRanges($possibleRanges, $codePoint)) { + return new ConstantBooleanType(false); + } + + return new BooleanType(); } - if ($this->phpVersion->throwsValueErrorForInternalFunctions()) { + if ($phpVersions->throwsValueErrorForInternalFunctions()->yes()) { return new NeverType(); } @@ -128,10 +127,49 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } } elseif ($isNull->yes()) { // The $substitute_character arg is nullable in PHP 8+ - return new ConstantBooleanType($this->phpVersion->isNullValidArgInMbSubstituteCharacter()); + $isNullValidArg = $phpVersions->isNullValidArgInMbSubstituteCharacter(); + if ($isNullValidArg->yes()) { + return new ConstantBooleanType(true); + } + + if ($isNullValidArg->no()) { + return new ConstantBooleanType(false); + } + + return new BooleanType(); } return new BooleanType(); } + /** + * @return Type[] + */ + private static function codePointRanges(int $minCodePoint, int $maxCodePoint, bool $excludeSurrogates): array + { + if ($excludeSurrogates) { + // Surrogates aren't valid in PHP 7.2+ + return [ + IntegerRangeType::fromInterval($minCodePoint, 0xD7FF), + IntegerRangeType::fromInterval(0xE000, $maxCodePoint), + ]; + } + + return [IntegerRangeType::fromInterval($minCodePoint, $maxCodePoint)]; + } + + /** + * @param Type[] $ranges + */ + private static function isPossiblyInRanges(array $ranges, Type $type): bool + { + foreach ($ranges as $range) { + if (!$range->isSuperTypeOf($type)->no()) { + return true; + } + } + + return 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/StrSplitFunctionReturnTypeExtension.php b/src/Type/Php/StrSplitFunctionReturnTypeExtension.php index 9738902437e..2f131daeedc 100644 --- a/src/Type/Php/StrSplitFunctionReturnTypeExtension.php +++ b/src/Type/Php/StrSplitFunctionReturnTypeExtension.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\TrinaryLogic; @@ -40,10 +39,6 @@ final class StrSplitFunctionReturnTypeExtension implements DynamicFunctionReturn use MbFunctionsReturnTypeExtensionTrait; - public function __construct(private PhpVersion $phpVersion) - { - } - public function isFunctionSupported(FunctionReflection $functionReflection): bool { return in_array($functionReflection->getName(), ['str_split', 'mb_str_split'], true); @@ -56,6 +51,10 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return null; } + $phpVersions = $scope->getPhpVersion(); + $throwsValueError = $phpVersions->throwsValueErrorForInternalFunctions(); + $returnsEmptyArray = $phpVersions->strSplitReturnsEmptyArray(); + if (count($args) >= 2) { $splitLengthType = $scope->getType($args[1]->value); } else { @@ -65,7 +64,11 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, if ($splitLengthType instanceof ConstantIntegerType) { $splitLength = $splitLengthType->getValue(); if ($splitLength < 1) { - return $this->phpVersion->throwsValueErrorForInternalFunctions() ? new NeverType() : new ConstantBooleanType(false); + if ($throwsValueError->yes()) { + return new NeverType(); + } + + return new ConstantBooleanType(false); } } @@ -77,8 +80,12 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, if (count($values) === 1) { $encoding = $values[0]; - if (!$this->isSupportedEncoding($encoding)) { - return $this->phpVersion->throwsValueErrorForInternalFunctions() ? new NeverType() : new ConstantBooleanType(false); + if (!$this->isSupportedEncoding($encoding, $phpVersions)) { + if ($throwsValueError->yes()) { + return new NeverType(); + } + + return new ConstantBooleanType(false); } } } else { @@ -99,13 +106,19 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, if ($encoding === null && $value === '') { // Simulate the str_split call with the analysed PHP Version instead of the runtime one. - $items = $this->phpVersion->strSplitReturnsEmptyArray() ? [] : ['']; - } else { - $items = $encoding === null - ? str_split($value, $splitLength) - : @mb_str_split($value, $splitLength, $encoding); + if (!$returnsEmptyArray->no()) { + $results[] = self::createConstantArrayFrom([], $scope); + } + if (!$returnsEmptyArray->yes()) { + $results[] = self::createConstantArrayFrom([''], $scope); + } + continue; } + $items = $encoding === null + ? str_split($value, $splitLength) + : @mb_str_split($value, $splitLength, $encoding); + $results[] = self::createConstantArrayFrom($items, $scope); } @@ -115,7 +128,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $isInputNonEmptyString = $stringType->isNonEmptyString()->yes(); - if ($isInputNonEmptyString || $this->phpVersion->strSplitReturnsEmptyArray()) { + if ($isInputNonEmptyString || $returnsEmptyArray->yes()) { $returnValueType = new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]); } else { $returnValueType = new StringType(); @@ -126,13 +139,13 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, // Non-empty-string will return an array with at least an element $isInputNonEmptyString // str_split('', 1) returns [''] on old PHP version and [] on new ones - || ($functionReflection->getName() === 'str_split' && !$this->phpVersion->strSplitReturnsEmptyArray()) + || ($functionReflection->getName() === 'str_split' && $returnsEmptyArray->no()) ) { $returnType = TypeCombinator::intersect($returnType, new NonEmptyArrayType()); } if ( // Length parameter accepts int<1, max> or throws a ValueError/return false based on PHP Version. - !$this->phpVersion->throwsValueErrorForInternalFunctions() + !$throwsValueError->yes() && !IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($splitLengthType)->yes() ) { $returnType = new UnionType([$returnType, new ConstantBooleanType(false)]); 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..1f439ee74b8 --- /dev/null +++ b/tests/PHPStan/Analyser/data/scope-php-version-range-return-type-extensions.php @@ -0,0 +1,28 @@ += 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 substrAndStrSplit(string $s): void +{ + if (PHP_VERSION_ID >= 80000) { + assertType('\'\'', substr('abc', 10)); + assertType('*NEVER*', str_split($s, 0)); + } else { + assertType('false', substr('abc', 10)); + assertType('false', str_split($s, 0)); + } + + if (PHP_VERSION_ID >= 80200) { + assertType('array{}', str_split('')); + } else { + assertType('array{\'\'}', str_split('')); + } +} + +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 mbSubstituteCharacterAndTriggerError(): void +{ + if (PHP_VERSION_ID >= 80000) { + assertType('true', mb_substitute_character(null)); + assertType('*NEVER*', trigger_error('foo', 12345)); + } else { + assertType('false', mb_substitute_character(null)); + assertType('false', trigger_error('foo', 12345)); + } +} + +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/Build/NoInjectedPhpVersionInScopeAwareExtensionRuleTest.php b/tests/PHPStan/Build/NoInjectedPhpVersionInScopeAwareExtensionRuleTest.php new file mode 100644 index 00000000000..d56df054d29 --- /dev/null +++ b/tests/PHPStan/Build/NoInjectedPhpVersionInScopeAwareExtensionRuleTest.php @@ -0,0 +1,37 @@ + + */ +final class NoInjectedPhpVersionInScopeAwareExtensionRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new NoInjectedPhpVersionInScopeAwareExtensionRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/php-version-in-extension.php'], [ + [ + 'PhpVersionInExtension\InjectsPhpVersion implements PHPStan\Type\DynamicFunctionReturnTypeExtension and must not inject PHPStan\Php\PhpVersion - read the analysed PHP version from Scope::getPhpVersion() instead.', + 25, + ], + [ + 'PhpVersionInExtension\InjectsPhpVersionInParameterOutExtension implements PHPStan\Type\MethodParameterOutTypeExtension and must not inject PHPStan\Php\PhpVersion - read the analysed PHP version from Scope::getPhpVersion() instead.', + 67, + ], + [ + 'PhpVersionInExtension\InjectsPhpVersionInRestrictedUsageExtension implements PHPStan\Rules\RestrictedUsage\RestrictedMethodUsageExtension and must not inject PHPStan\Php\PhpVersion - read the analysed PHP version from Scope::getPhpVersion() instead.', + 86, + ], + ]); + } + +} diff --git a/tests/PHPStan/Build/data/php-version-in-extension.php b/tests/PHPStan/Build/data/php-version-in-extension.php new file mode 100644 index 00000000000..b8bc1f7fce0 --- /dev/null +++ b/tests/PHPStan/Build/data/php-version-in-extension.php @@ -0,0 +1,147 @@ += 8.0 + +declare(strict_types = 1); + +namespace PhpVersionInExtension; + +use PhpParser\Node; +use PhpParser\Node\Expr\FuncCall; +use PhpParser\Node\Expr\MethodCall; +use PHPStan\Analyser\Scope; +use PHPStan\Php\PhpVersion; +use PHPStan\Reflection\ExtendedMethodReflection; +use PHPStan\Reflection\FunctionReflection; +use PHPStan\Reflection\MethodReflection; +use PHPStan\Reflection\ParameterReflection; +use PHPStan\Rules\RestrictedUsage\RestrictedMethodUsageExtension; +use PHPStan\Rules\RestrictedUsage\RestrictedUsage; +use PHPStan\Rules\Rule; +use PHPStan\Type\DynamicFunctionReturnTypeExtension; +use PHPStan\Type\MethodParameterOutTypeExtension; +use PHPStan\Type\NullType; +use PHPStan\Type\OperatorTypeSpecifyingExtension; +use PHPStan\Type\Type; + +final class InjectsPhpVersion implements DynamicFunctionReturnTypeExtension +{ + + public function __construct(private PhpVersion $phpVersion) + { + } + + public function isFunctionSupported(FunctionReflection $functionReflection): bool + { + return $functionReflection->getName() === 'foo' && $this->phpVersion->getVersionId() >= 80000; + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type + { + return null; + } + +} + +final class ReadsPhpVersionFromScope implements DynamicFunctionReturnTypeExtension +{ + + public function __construct(private string $functionName) + { + } + + public function isFunctionSupported(FunctionReflection $functionReflection): bool + { + return $functionReflection->getName() === $this->functionName; + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type + { + if ($scope->getPhpVersion()->throwsValueErrorForInternalFunctions()->yes()) { + return new NullType(); + } + + return null; + } + +} + +final class InjectsPhpVersionInParameterOutExtension implements MethodParameterOutTypeExtension +{ + + public function __construct(private PhpVersion $phpVersion) + { + } + + public function isMethodSupported(MethodReflection $methodReflection, ParameterReflection $parameter): bool + { + return $this->phpVersion->getVersionId() >= 80000; + } + + public function getParameterOutTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, ParameterReflection $parameter, Scope $scope): ?Type + { + return null; + } + +} + +final class InjectsPhpVersionInRestrictedUsageExtension implements RestrictedMethodUsageExtension +{ + + public function __construct(private PhpVersion $phpVersion) + { + } + + public function isRestrictedMethodUsage(ExtendedMethodReflection $methodReflection, Scope $scope): ?RestrictedUsage + { + if ($this->phpVersion->getVersionId() >= 80000) { + return null; + } + + return null; + } + +} + +final class OperatorExtensionWithoutScope implements OperatorTypeSpecifyingExtension +{ + + public function __construct(private PhpVersion $phpVersion) + { + } + + public function isOperatorSupported(string $operatorSigil, Type $leftSide, Type $rightSide): bool + { + return $this->phpVersion->getVersionId() >= 80400; + } + + public function specifyType(string $operatorSigil, Type $leftSide, Type $rightSide): Type + { + return new NullType(); + } + +} + +/** + * @implements Rule + */ +final class RuleWithInjectedPhpVersion implements Rule +{ + + public function __construct(private PhpVersion $phpVersion) + { + } + + public function getNodeType(): string + { + return Node::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if ($this->phpVersion->getVersionId() >= 80000) { + return []; + } + + return []; + } + +} 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) { + + } + } + +} From 4b21fe5b175f4d4227648b8b658867fb6c034e98 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 23 Sep 2026 09:34:20 +0200 Subject: [PATCH 2/4] Delete NoInjectedPhpVersionInScopeAwareExtensionRule.php --- ...tedPhpVersionInScopeAwareExtensionRule.php | 130 ------------------ 1 file changed, 130 deletions(-) delete mode 100644 build/PHPStan/Build/NoInjectedPhpVersionInScopeAwareExtensionRule.php diff --git a/build/PHPStan/Build/NoInjectedPhpVersionInScopeAwareExtensionRule.php b/build/PHPStan/Build/NoInjectedPhpVersionInScopeAwareExtensionRule.php deleted file mode 100644 index 8dc08f09678..00000000000 --- a/build/PHPStan/Build/NoInjectedPhpVersionInScopeAwareExtensionRule.php +++ /dev/null @@ -1,130 +0,0 @@ - - */ -final class NoInjectedPhpVersionInScopeAwareExtensionRule implements Rule -{ - - private const SCOPE_AWARE_EXTENSIONS = [ - DynamicFunctionReturnTypeExtension::class, - DynamicMethodReturnTypeExtension::class, - DynamicStaticMethodReturnTypeExtension::class, - DynamicFunctionThrowTypeExtension::class, - DynamicMethodThrowTypeExtension::class, - DynamicStaticMethodThrowTypeExtension::class, - FunctionTypeSpecifyingExtension::class, - MethodTypeSpecifyingExtension::class, - StaticMethodTypeSpecifyingExtension::class, - FunctionParameterOutTypeExtension::class, - MethodParameterOutTypeExtension::class, - StaticMethodParameterOutTypeExtension::class, - FunctionParameterClosureTypeExtension::class, - MethodParameterClosureTypeExtension::class, - StaticMethodParameterClosureTypeExtension::class, - FunctionParameterClosureThisExtension::class, - MethodParameterClosureThisExtension::class, - StaticMethodParameterClosureThisExtension::class, - RestrictedClassConstantUsageExtension::class, - RestrictedClassNameUsageExtension::class, - RestrictedFunctionUsageExtension::class, - RestrictedMethodUsageExtension::class, - RestrictedPropertyUsageExtension::class, - ExpressionTypeResolverExtension::class, - IgnoreErrorExtension::class, - Collector::class, - ]; - - public function getNodeType(): string - { - return InClassNode::class; - } - - public function processNode(Node $node, Scope $scope): array - { - $classReflection = $node->getClassReflection(); - if (!$classReflection->hasConstructor()) { - return []; - } - - $implementedExtension = null; - foreach (self::SCOPE_AWARE_EXTENSIONS as $extensionInterface) { - if (!$classReflection->is($extensionInterface)) { - continue; - } - - $implementedExtension = $extensionInterface; - break; - } - - if ($implementedExtension === null) { - return []; - } - - $constructorVariant = $classReflection->getConstructor()->getOnlyVariant(); - $errors = []; - foreach ($constructorVariant->getParameters() as $parameter) { - foreach ($parameter->getType()->getObjectClassNames() as $className) { - if ($className !== PhpVersion::class) { - continue; - } - - $errors[] = RuleErrorBuilder::message(sprintf( - '%s implements %s and must not inject %s - read the analysed PHP version from Scope::getPhpVersion() instead.', - $classReflection->getDisplayName(), - $implementedExtension, - PhpVersion::class, - ))->identifier('phpstanBuild.phpVersionInExtension')->build(); - } - } - - return $errors; - } - -} From d7dfbbb05732f79cb7f1096049f489c44461a6f4 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 23 Sep 2026 09:35:57 +0200 Subject: [PATCH 3/4] revert --- build/phpstan.neon | 1 - phpstan-baseline.neon | 24 --- ...hpVersionInScopeAwareExtensionRuleTest.php | 37 ----- .../Build/data/php-version-in-extension.php | 147 ------------------ 4 files changed, 209 deletions(-) delete mode 100644 tests/PHPStan/Build/NoInjectedPhpVersionInScopeAwareExtensionRuleTest.php delete mode 100644 tests/PHPStan/Build/data/php-version-in-extension.php diff --git a/build/phpstan.neon b/build/phpstan.neon index d7436d7df1a..5226880e48c 100644 --- a/build/phpstan.neon +++ b/build/phpstan.neon @@ -201,7 +201,6 @@ parameters: rules: - PHPStan\Build\FinalClassRule - PHPStan\Build\NoScopeTypeReadInEngineRule - - PHPStan\Build\NoInjectedPhpVersionInScopeAwareExtensionRule - PHPStan\Build\AttributeNamedArgumentsRule - PHPStan\Build\NamedArgumentsRule - PHPStan\Build\OverrideAttributeThirdPartyMethodRule diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index c07cbd23769..5a96ce5c15e 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -378,18 +378,6 @@ parameters: count: 1 path: src/Reflection/BetterReflection/SourceLocator/SkipClassAliasSourceLocator.php - - - rawMessage: 'PHPStan\Reflection\BetterReflection\Type\AdapterReflectionEnumCaseDynamicReturnTypeExtension implements PHPStan\Type\DynamicMethodReturnTypeExtension and must not inject PHPStan\Php\PhpVersion - read the analysed PHP version from Scope::getPhpVersion() instead.' - identifier: phpstanBuild.phpVersionInExtension - count: 1 - path: src/Reflection/BetterReflection/Type/AdapterReflectionEnumCaseDynamicReturnTypeExtension.php - - - - rawMessage: 'PHPStan\Reflection\BetterReflection\Type\AdapterReflectionEnumDynamicReturnTypeExtension implements PHPStan\Type\DynamicMethodReturnTypeExtension and must not inject PHPStan\Php\PhpVersion - read the analysed PHP version from Scope::getPhpVersion() instead.' - identifier: phpstanBuild.phpVersionInExtension - count: 1 - path: src/Reflection/BetterReflection/Type/AdapterReflectionEnumDynamicReturnTypeExtension.php - - rawMessage: Doing instanceof PHPStan\Type\Generic\GenericObjectType is error-prone and deprecated. identifier: phpstanApi.instanceofType @@ -480,12 +468,6 @@ parameters: count: 6 path: src/Reflection/InitializerExprTypeResolver.php - - - rawMessage: 'PHPStan\Reflection\PHPStan\NativeReflectionEnumReturnDynamicReturnTypeExtension implements PHPStan\Type\DynamicMethodReturnTypeExtension and must not inject PHPStan\Php\PhpVersion - read the analysed PHP version from Scope::getPhpVersion() instead.' - identifier: phpstanBuild.phpVersionInExtension - count: 1 - path: src/Reflection/PHPStan/NativeReflectionEnumReturnDynamicReturnTypeExtension.php - - rawMessage: Creating new PHPStan\Php8StubsMap is not covered by backward compatibility promise. The class might change in a minor PHPStan version. identifier: phpstanApi.constructor @@ -1566,12 +1548,6 @@ parameters: count: 2 path: src/Type/Php/MinMaxFunctionReturnTypeExtension.php - - - rawMessage: 'PHPStan\Type\Php\PDOConnectReturnTypeExtension implements PHPStan\Type\DynamicStaticMethodReturnTypeExtension and must not inject PHPStan\Php\PhpVersion - read the analysed PHP version from Scope::getPhpVersion() instead.' - identifier: phpstanBuild.phpVersionInExtension - count: 1 - path: src/Type/Php/PDOConnectReturnTypeExtension.php - - rawMessage: 'Doing instanceof PHPStan\Type\Constant\ConstantStringType is error-prone and deprecated. Use Type::getConstantStrings() instead.' identifier: phpstanApi.instanceofType diff --git a/tests/PHPStan/Build/NoInjectedPhpVersionInScopeAwareExtensionRuleTest.php b/tests/PHPStan/Build/NoInjectedPhpVersionInScopeAwareExtensionRuleTest.php deleted file mode 100644 index d56df054d29..00000000000 --- a/tests/PHPStan/Build/NoInjectedPhpVersionInScopeAwareExtensionRuleTest.php +++ /dev/null @@ -1,37 +0,0 @@ - - */ -final class NoInjectedPhpVersionInScopeAwareExtensionRuleTest extends RuleTestCase -{ - - protected function getRule(): Rule - { - return new NoInjectedPhpVersionInScopeAwareExtensionRule(); - } - - public function testRule(): void - { - $this->analyse([__DIR__ . '/data/php-version-in-extension.php'], [ - [ - 'PhpVersionInExtension\InjectsPhpVersion implements PHPStan\Type\DynamicFunctionReturnTypeExtension and must not inject PHPStan\Php\PhpVersion - read the analysed PHP version from Scope::getPhpVersion() instead.', - 25, - ], - [ - 'PhpVersionInExtension\InjectsPhpVersionInParameterOutExtension implements PHPStan\Type\MethodParameterOutTypeExtension and must not inject PHPStan\Php\PhpVersion - read the analysed PHP version from Scope::getPhpVersion() instead.', - 67, - ], - [ - 'PhpVersionInExtension\InjectsPhpVersionInRestrictedUsageExtension implements PHPStan\Rules\RestrictedUsage\RestrictedMethodUsageExtension and must not inject PHPStan\Php\PhpVersion - read the analysed PHP version from Scope::getPhpVersion() instead.', - 86, - ], - ]); - } - -} diff --git a/tests/PHPStan/Build/data/php-version-in-extension.php b/tests/PHPStan/Build/data/php-version-in-extension.php deleted file mode 100644 index b8bc1f7fce0..00000000000 --- a/tests/PHPStan/Build/data/php-version-in-extension.php +++ /dev/null @@ -1,147 +0,0 @@ -= 8.0 - -declare(strict_types = 1); - -namespace PhpVersionInExtension; - -use PhpParser\Node; -use PhpParser\Node\Expr\FuncCall; -use PhpParser\Node\Expr\MethodCall; -use PHPStan\Analyser\Scope; -use PHPStan\Php\PhpVersion; -use PHPStan\Reflection\ExtendedMethodReflection; -use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParameterReflection; -use PHPStan\Rules\RestrictedUsage\RestrictedMethodUsageExtension; -use PHPStan\Rules\RestrictedUsage\RestrictedUsage; -use PHPStan\Rules\Rule; -use PHPStan\Type\DynamicFunctionReturnTypeExtension; -use PHPStan\Type\MethodParameterOutTypeExtension; -use PHPStan\Type\NullType; -use PHPStan\Type\OperatorTypeSpecifyingExtension; -use PHPStan\Type\Type; - -final class InjectsPhpVersion implements DynamicFunctionReturnTypeExtension -{ - - public function __construct(private PhpVersion $phpVersion) - { - } - - public function isFunctionSupported(FunctionReflection $functionReflection): bool - { - return $functionReflection->getName() === 'foo' && $this->phpVersion->getVersionId() >= 80000; - } - - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type - { - return null; - } - -} - -final class ReadsPhpVersionFromScope implements DynamicFunctionReturnTypeExtension -{ - - public function __construct(private string $functionName) - { - } - - public function isFunctionSupported(FunctionReflection $functionReflection): bool - { - return $functionReflection->getName() === $this->functionName; - } - - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type - { - if ($scope->getPhpVersion()->throwsValueErrorForInternalFunctions()->yes()) { - return new NullType(); - } - - return null; - } - -} - -final class InjectsPhpVersionInParameterOutExtension implements MethodParameterOutTypeExtension -{ - - public function __construct(private PhpVersion $phpVersion) - { - } - - public function isMethodSupported(MethodReflection $methodReflection, ParameterReflection $parameter): bool - { - return $this->phpVersion->getVersionId() >= 80000; - } - - public function getParameterOutTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, ParameterReflection $parameter, Scope $scope): ?Type - { - return null; - } - -} - -final class InjectsPhpVersionInRestrictedUsageExtension implements RestrictedMethodUsageExtension -{ - - public function __construct(private PhpVersion $phpVersion) - { - } - - public function isRestrictedMethodUsage(ExtendedMethodReflection $methodReflection, Scope $scope): ?RestrictedUsage - { - if ($this->phpVersion->getVersionId() >= 80000) { - return null; - } - - return null; - } - -} - -final class OperatorExtensionWithoutScope implements OperatorTypeSpecifyingExtension -{ - - public function __construct(private PhpVersion $phpVersion) - { - } - - public function isOperatorSupported(string $operatorSigil, Type $leftSide, Type $rightSide): bool - { - return $this->phpVersion->getVersionId() >= 80400; - } - - public function specifyType(string $operatorSigil, Type $leftSide, Type $rightSide): Type - { - return new NullType(); - } - -} - -/** - * @implements Rule - */ -final class RuleWithInjectedPhpVersion implements Rule -{ - - public function __construct(private PhpVersion $phpVersion) - { - } - - public function getNodeType(): string - { - return Node::class; - } - - public function processNode(Node $node, Scope $scope): array - { - if ($this->phpVersion->getVersionId() >= 80000) { - return []; - } - - return []; - } - -} From 5ce825accf418d1b1079e549aa74596d416f2dc3 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 23 Sep 2026 09:48:24 +0200 Subject: [PATCH 4/4] revert mb* --- .../Php/MbFunctionsReturnTypeExtension.php | 21 ++-- .../MbFunctionsReturnTypeExtensionTrait.php | 52 ++++---- .../MbStrlenFunctionReturnTypeExtension.php | 18 +-- ...uteCharacterDynamicReturnTypeExtension.php | 114 ++++++------------ .../StrSplitFunctionReturnTypeExtension.php | 45 +++---- ...p-version-range-return-type-extensions.php | 12 -- ...ope-php-version-return-type-extensions.php | 28 ----- 7 files changed, 98 insertions(+), 192 deletions(-) diff --git a/src/Type/Php/MbFunctionsReturnTypeExtension.php b/src/Type/Php/MbFunctionsReturnTypeExtension.php index cf75081b85b..659bd9f0072 100644 --- a/src/Type/Php/MbFunctionsReturnTypeExtension.php +++ b/src/Type/Php/MbFunctionsReturnTypeExtension.php @@ -5,6 +5,7 @@ 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\BooleanType; @@ -37,6 +38,10 @@ final class MbFunctionsReturnTypeExtension implements DynamicFunctionReturnTypeE 'mb_ord' => 2, ]; + public function __construct(private PhpVersion $phpVersion) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return array_key_exists($functionReflection->getName(), $this->encodingPositionMap); @@ -56,24 +61,22 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return TypeCombinator::remove($returnType, new BooleanType()); } - $phpVersions = $scope->getPhpVersion(); $strings = $scope->getType($args[$positionEncodingParam - 1]->value)->getConstantStrings(); - $results = array_unique(array_map(fn (ConstantStringType $encoding): bool => $this->isSupportedEncoding($encoding->getValue(), $phpVersions), $strings)); + $results = array_unique(array_map(fn (ConstantStringType $encoding): bool => $this->isSupportedEncoding($encoding->getValue()), $strings)); if ($returnType->equals(new UnionType([new StringType(), new BooleanType()]))) { return count($results) === 1 ? new ConstantBooleanType($results[0]) : new BooleanType(); } if (count($results) === 1) { - if ($results[0]) { - return TypeCombinator::remove($returnType, new ConstantBooleanType(false)); - } - - if ($phpVersions->throwsOnInvalidMbStringEncoding()->yes()) { - return new NeverType(); + $invalidEncodingReturn = new ConstantBooleanType(false); + if ($this->phpVersion->throwsOnInvalidMbStringEncoding()) { + $invalidEncodingReturn = new NeverType(); } - return new ConstantBooleanType(false); + return $results[0] + ? TypeCombinator::remove($returnType, new ConstantBooleanType(false)) + : $invalidEncodingReturn; } return $returnType; diff --git a/src/Type/Php/MbFunctionsReturnTypeExtensionTrait.php b/src/Type/Php/MbFunctionsReturnTypeExtensionTrait.php index a382aa497dc..64036c984b8 100644 --- a/src/Type/Php/MbFunctionsReturnTypeExtensionTrait.php +++ b/src/Type/Php/MbFunctionsReturnTypeExtensionTrait.php @@ -2,14 +2,13 @@ namespace PHPStan\Type\Php; -use PHPStan\Php\PhpVersions; use PHPStan\ShouldNotHappenException; use function array_filter; use function array_map; use function array_merge; -use function array_values; use function function_exists; use function in_array; +use function is_null; use function mb_encoding_aliases; use function mb_list_encodings; use function strtoupper; @@ -20,43 +19,36 @@ trait MbFunctionsReturnTypeExtensionTrait /** @var string[]|null */ private ?array $supportedEncodings = null; - /** @var string[]|null */ - private ?array $supportedEncodingsWithoutPassNone = null; - - private function isSupportedEncoding(string $encoding, PhpVersions $phpVersions): bool + private function isSupportedEncoding(string $encoding): bool { - return in_array(strtoupper($encoding), $this->getSupportedEncodings($phpVersions), true); + return in_array(strtoupper($encoding), $this->getSupportedEncodings(), true); } /** @return string[] */ - private function getSupportedEncodings(PhpVersions $phpVersions): array + private function getSupportedEncodings(): array { - // PHP 7.3 and 7.4 claims 'pass' and its alias 'none' to be supported, but actually 'pass' was removed in 7.3 - if ($phpVersions->supportsPassNoneEncodings()->no()) { - return $this->supportedEncodingsWithoutPassNone ??= array_values(array_filter( - $this->getAllSupportedEncodings(), - static fn (string $enc) => !in_array($enc, ['PASS', 'NONE'], true), - )); + if (!is_null($this->supportedEncodings)) { + return $this->supportedEncodings; } - return $this->getAllSupportedEncodings(); - } - - /** @return string[] */ - private function getAllSupportedEncodings(): array - { - if ($this->supportedEncodings === null) { - $supportedEncodings = []; - if (function_exists('mb_list_encodings')) { - foreach (mb_list_encodings() as $encoding) { - $aliases = @mb_encoding_aliases($encoding); - if ($aliases === false) { - throw new ShouldNotHappenException(); - } - $supportedEncodings = array_merge($supportedEncodings, $aliases, [$encoding]); + $supportedEncodings = []; + if (function_exists('mb_list_encodings')) { + foreach (mb_list_encodings() as $encoding) { + $aliases = @mb_encoding_aliases($encoding); + if ($aliases === false) { + throw new ShouldNotHappenException(); } + $supportedEncodings = array_merge($supportedEncodings, $aliases, [$encoding]); } - $this->supportedEncodings = array_map('strtoupper', $supportedEncodings); + } + $this->supportedEncodings = array_map('strtoupper', $supportedEncodings); + + // PHP 7.3 and 7.4 claims 'pass' and its alias 'none' to be supported, but actually 'pass' was removed in 7.3 + if (!$this->phpVersion->supportsPassNoneEncodings()) { + $this->supportedEncodings = array_filter( + $this->supportedEncodings, + static fn (string $enc) => !in_array($enc, ['PASS', 'NONE'], true), + ); } return $this->supportedEncodings; diff --git a/src/Type/Php/MbStrlenFunctionReturnTypeExtension.php b/src/Type/Php/MbStrlenFunctionReturnTypeExtension.php index 5d20771520d..ee57cc3f16f 100644 --- a/src/Type/Php/MbStrlenFunctionReturnTypeExtension.php +++ b/src/Type/Php/MbStrlenFunctionReturnTypeExtension.php @@ -5,6 +5,7 @@ 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\ShouldNotHappenException; @@ -41,6 +42,10 @@ final class MbStrlenFunctionReturnTypeExtension implements DynamicFunctionReturn use MbFunctionsReturnTypeExtensionTrait; + public function __construct(private PhpVersion $phpVersion) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'mb_strlen'; @@ -57,8 +62,6 @@ public function getTypeFromFunctionCall( return null; } - $phpVersions = $scope->getPhpVersion(); - $throwsOnInvalidEncoding = $phpVersions->throwsOnInvalidMbStringEncoding(); $encodings = []; if (count($args) === 1) { @@ -73,7 +76,7 @@ public function getTypeFromFunctionCall( if (count($encodings) > 0) { for ($i = 0; $i < count($encodings); $i++) { - if ($this->isSupportedEncoding($encodings[$i], $phpVersions)) { + if ($this->isSupportedEncoding($encodings[$i])) { continue; } $encodings[$i] = self::UNSUPPORTED_ENCODING; @@ -82,14 +85,13 @@ public function getTypeFromFunctionCall( $encodings = array_unique($encodings); if (in_array(self::UNSUPPORTED_ENCODING, $encodings, true) && count($encodings) === 1) { - if ($throwsOnInvalidEncoding->yes()) { + if ($this->phpVersion->throwsOnInvalidMbStringEncoding()) { return new NeverType(); } - return new ConstantBooleanType(false); } } else { // if there aren't encoding constants, use all available encodings - $encodings = array_merge($this->getSupportedEncodings($phpVersions), [self::UNSUPPORTED_ENCODING]); + $encodings = array_merge($this->getSupportedEncodings(), [self::UNSUPPORTED_ENCODING]); } $argType = $scope->getType($args[0]->value); @@ -100,7 +102,7 @@ public function getTypeFromFunctionCall( $stringScalar = (string) $constantScalar; foreach ($encodings as $encoding) { - if (!$this->isSupportedEncoding($encoding, $phpVersions)) { + if (!$this->isSupportedEncoding($encoding)) { continue; } @@ -143,7 +145,7 @@ public function getTypeFromFunctionCall( ); } - if (!$throwsOnInvalidEncoding->yes() && in_array(self::UNSUPPORTED_ENCODING, $encodings, true)) { + if (!$this->phpVersion->throwsOnInvalidMbStringEncoding() && in_array(self::UNSUPPORTED_ENCODING, $encodings, true)) { return TypeCombinator::union($range, new ConstantBooleanType(false)); } return $range; diff --git a/src/Type/Php/MbSubstituteCharacterDynamicReturnTypeExtension.php b/src/Type/Php/MbSubstituteCharacterDynamicReturnTypeExtension.php index 11b80bca108..7b14c0cce7f 100644 --- a/src/Type/Php/MbSubstituteCharacterDynamicReturnTypeExtension.php +++ b/src/Type/Php/MbSubstituteCharacterDynamicReturnTypeExtension.php @@ -5,6 +5,7 @@ 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; @@ -22,6 +23,10 @@ final class MbSubstituteCharacterDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + public function __construct(private PhpVersion $phpVersion) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'mb_substitute_character'; @@ -29,23 +34,17 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type { - $phpVersions = $scope->getPhpVersion(); - $supportsAllUnicodeScalarCodePoints = $phpVersions->supportsAllUnicodeScalarCodePointsInMbSubstituteCharacter(); - // The empty string is a valid alias for "none" in PHP < 8, where 0 is not a valid code point. - $isEmptyStringValidAlias = $phpVersions->isEmptyStringValidAliasForNoneInMbSubstituteCharacter(); - - // Code points valid on every PHP version the analysed range spans... - $ranges = self::codePointRanges( - $isEmptyStringValidAlias->no() ? 0 : 1, - $supportsAllUnicodeScalarCodePoints->yes() ? 0x10FFFF : 0xFFFE, - !$supportsAllUnicodeScalarCodePoints->no(), - ); - // ...and those valid on at least one of them. - $possibleRanges = self::codePointRanges( - $isEmptyStringValidAlias->yes() ? 1 : 0, - $supportsAllUnicodeScalarCodePoints->no() ? 0xFFFE : 0x10FFFF, - $supportsAllUnicodeScalarCodePoints->yes(), - ); + $minCodePoint = $this->phpVersion->getVersionId() < 80000 ? 1 : 0; + $maxCodePoint = $this->phpVersion->supportsAllUnicodeScalarCodePointsInMbSubstituteCharacter() ? 0x10FFFF : 0xFFFE; + $ranges = []; + + if ($this->phpVersion->supportsAllUnicodeScalarCodePointsInMbSubstituteCharacter()) { + // Surrogates aren't valid in PHP 7.2+ + $ranges[] = IntegerRangeType::fromInterval($minCodePoint, 0xD7FF); + $ranges[] = IntegerRangeType::fromInterval(0xE000, $maxCodePoint); + } else { + $ranges[] = IntegerRangeType::fromInterval($minCodePoint, $maxCodePoint); + } if (!isset($functionCall->getArgs()[0])) { return TypeCombinator::union( @@ -62,7 +61,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $isInteger = $argType->isInteger(); if ($isString->no() && $isNull->no() && $isInteger->no()) { - if ($phpVersions->throwsTypeErrorForInternalFunctions()->yes()) { + if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { return new NeverType(); } @@ -70,14 +69,20 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } if ($isInteger->yes()) { + $invalidRanges = []; + foreach ($ranges as $range) { - if ($range->isSuperTypeOf($argType)->yes()) { + $isInRange = $range->isSuperTypeOf($argType); + + if ($isInRange->yes()) { return new ConstantBooleanType(true); } + + $invalidRanges[] = $isInRange->no(); } - if (!self::isPossiblyInRanges($possibleRanges, $argType)) { - if ($phpVersions->throwsValueErrorForInternalFunctions()->yes()) { + if ($argType instanceof ConstantIntegerType || !in_array(false, $invalidRanges, true)) { + if ($this->phpVersion->throwsValueErrorForInternalFunctions()) { return new NeverType(); } @@ -85,14 +90,15 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } } elseif ($isString->yes()) { if ($argType->isNonEmptyString()->no()) { - if ($isEmptyStringValidAlias->no()) { - return new NeverType(); + // The empty string was a valid alias for "none" in PHP < 8. + if ($this->phpVersion->isEmptyStringValidAliasForNoneInMbSubstituteCharacter()) { + return new ConstantBooleanType(true); } - return new ConstantBooleanType(true); + return new NeverType(); } - if ($phpVersions->isNumericStringValidArgInMbSubstituteCharacter()->no() && $argType->isNumericString()->yes()) { + if (!$this->phpVersion->isNumericStringValidArgInMbSubstituteCharacter() && $argType->isNumericString()->yes()) { return new NeverType(); } @@ -104,22 +110,17 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } if ($argType->isNumericString()->yes()) { - $codePoint = new ConstantIntegerType((int) $value); + $codePoint = (int) $value; + $isValid = $codePoint >= $minCodePoint && $codePoint <= $maxCodePoint; - foreach ($ranges as $range) { - if ($range->isSuperTypeOf($codePoint)->yes()) { - return new ConstantBooleanType(true); - } + if ($this->phpVersion->supportsAllUnicodeScalarCodePointsInMbSubstituteCharacter()) { + $isValid = $isValid && ($codePoint < 0xD800 || $codePoint > 0xDFFF); } - if (!self::isPossiblyInRanges($possibleRanges, $codePoint)) { - return new ConstantBooleanType(false); - } - - return new BooleanType(); + return new ConstantBooleanType($isValid); } - if ($phpVersions->throwsValueErrorForInternalFunctions()->yes()) { + if ($this->phpVersion->throwsValueErrorForInternalFunctions()) { return new NeverType(); } @@ -127,49 +128,10 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } } elseif ($isNull->yes()) { // The $substitute_character arg is nullable in PHP 8+ - $isNullValidArg = $phpVersions->isNullValidArgInMbSubstituteCharacter(); - if ($isNullValidArg->yes()) { - return new ConstantBooleanType(true); - } - - if ($isNullValidArg->no()) { - return new ConstantBooleanType(false); - } - - return new BooleanType(); + return new ConstantBooleanType($this->phpVersion->isNullValidArgInMbSubstituteCharacter()); } return new BooleanType(); } - /** - * @return Type[] - */ - private static function codePointRanges(int $minCodePoint, int $maxCodePoint, bool $excludeSurrogates): array - { - if ($excludeSurrogates) { - // Surrogates aren't valid in PHP 7.2+ - return [ - IntegerRangeType::fromInterval($minCodePoint, 0xD7FF), - IntegerRangeType::fromInterval(0xE000, $maxCodePoint), - ]; - } - - return [IntegerRangeType::fromInterval($minCodePoint, $maxCodePoint)]; - } - - /** - * @param Type[] $ranges - */ - private static function isPossiblyInRanges(array $ranges, Type $type): bool - { - foreach ($ranges as $range) { - if (!$range->isSuperTypeOf($type)->no()) { - return true; - } - } - - return false; - } - } diff --git a/src/Type/Php/StrSplitFunctionReturnTypeExtension.php b/src/Type/Php/StrSplitFunctionReturnTypeExtension.php index 2f131daeedc..9738902437e 100644 --- a/src/Type/Php/StrSplitFunctionReturnTypeExtension.php +++ b/src/Type/Php/StrSplitFunctionReturnTypeExtension.php @@ -5,6 +5,7 @@ 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\TrinaryLogic; @@ -39,6 +40,10 @@ final class StrSplitFunctionReturnTypeExtension implements DynamicFunctionReturn use MbFunctionsReturnTypeExtensionTrait; + public function __construct(private PhpVersion $phpVersion) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return in_array($functionReflection->getName(), ['str_split', 'mb_str_split'], true); @@ -51,10 +56,6 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return null; } - $phpVersions = $scope->getPhpVersion(); - $throwsValueError = $phpVersions->throwsValueErrorForInternalFunctions(); - $returnsEmptyArray = $phpVersions->strSplitReturnsEmptyArray(); - if (count($args) >= 2) { $splitLengthType = $scope->getType($args[1]->value); } else { @@ -64,11 +65,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, if ($splitLengthType instanceof ConstantIntegerType) { $splitLength = $splitLengthType->getValue(); if ($splitLength < 1) { - if ($throwsValueError->yes()) { - return new NeverType(); - } - - return new ConstantBooleanType(false); + return $this->phpVersion->throwsValueErrorForInternalFunctions() ? new NeverType() : new ConstantBooleanType(false); } } @@ -80,12 +77,8 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, if (count($values) === 1) { $encoding = $values[0]; - if (!$this->isSupportedEncoding($encoding, $phpVersions)) { - if ($throwsValueError->yes()) { - return new NeverType(); - } - - return new ConstantBooleanType(false); + if (!$this->isSupportedEncoding($encoding)) { + return $this->phpVersion->throwsValueErrorForInternalFunctions() ? new NeverType() : new ConstantBooleanType(false); } } } else { @@ -106,19 +99,13 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, if ($encoding === null && $value === '') { // Simulate the str_split call with the analysed PHP Version instead of the runtime one. - if (!$returnsEmptyArray->no()) { - $results[] = self::createConstantArrayFrom([], $scope); - } - if (!$returnsEmptyArray->yes()) { - $results[] = self::createConstantArrayFrom([''], $scope); - } - continue; + $items = $this->phpVersion->strSplitReturnsEmptyArray() ? [] : ['']; + } else { + $items = $encoding === null + ? str_split($value, $splitLength) + : @mb_str_split($value, $splitLength, $encoding); } - $items = $encoding === null - ? str_split($value, $splitLength) - : @mb_str_split($value, $splitLength, $encoding); - $results[] = self::createConstantArrayFrom($items, $scope); } @@ -128,7 +115,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $isInputNonEmptyString = $stringType->isNonEmptyString()->yes(); - if ($isInputNonEmptyString || $returnsEmptyArray->yes()) { + if ($isInputNonEmptyString || $this->phpVersion->strSplitReturnsEmptyArray()) { $returnValueType = new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]); } else { $returnValueType = new StringType(); @@ -139,13 +126,13 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, // Non-empty-string will return an array with at least an element $isInputNonEmptyString // str_split('', 1) returns [''] on old PHP version and [] on new ones - || ($functionReflection->getName() === 'str_split' && $returnsEmptyArray->no()) + || ($functionReflection->getName() === 'str_split' && !$this->phpVersion->strSplitReturnsEmptyArray()) ) { $returnType = TypeCombinator::intersect($returnType, new NonEmptyArrayType()); } if ( // Length parameter accepts int<1, max> or throws a ValueError/return false based on PHP Version. - !$throwsValueError->yes() + !$this->phpVersion->throwsValueErrorForInternalFunctions() && !IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($splitLengthType)->yes() ) { $returnType = new UnionType([$returnType, new ConstantBooleanType(false)]); 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 index 1f439ee74b8..8d291943f35 100644 --- 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 @@ -4,18 +4,6 @@ use function PHPStan\Testing\assertType; -// The analysed PHP version is configured as the range 7.4 - 8.5, so every version check -// inside these extensions is uncertain. An uncertain check must not produce a type that is -// narrower than the truth on either end of the range. - -function bothVersionsPossible(string $s): void -{ - assertType("''|false", substr('abc', 10)); - assertType("array{}|array{''}", str_split('')); - assertType('bool', mb_substitute_character(null)); - assertType('bool', mb_substitute_character(0)); -} - function validOperatorNeverReturnsNull(string $a, string $b, string $s): void { // version_compare() only returns null for an invalid operator, so a constant valid one diff --git a/tests/PHPStan/Analyser/nsrt/scope-php-version-return-type-extensions.php b/tests/PHPStan/Analyser/nsrt/scope-php-version-return-type-extensions.php index b8c2a26703e..e323b172683 100644 --- a/tests/PHPStan/Analyser/nsrt/scope-php-version-return-type-extensions.php +++ b/tests/PHPStan/Analyser/nsrt/scope-php-version-return-type-extensions.php @@ -37,23 +37,6 @@ function arrayChunkAndFill(array $arr): void } } -function substrAndStrSplit(string $s): void -{ - if (PHP_VERSION_ID >= 80000) { - assertType('\'\'', substr('abc', 10)); - assertType('*NEVER*', str_split($s, 0)); - } else { - assertType('false', substr('abc', 10)); - assertType('false', str_split($s, 0)); - } - - if (PHP_VERSION_ID >= 80200) { - assertType('array{}', str_split('')); - } else { - assertType('array{\'\'}', str_split('')); - } -} - function roundAndHighlight(): void { if (PHP_VERSION_ID >= 80000) { @@ -80,17 +63,6 @@ function countCharsAndHash(string $s): void } } -function mbSubstituteCharacterAndTriggerError(): void -{ - if (PHP_VERSION_ID >= 80000) { - assertType('true', mb_substitute_character(null)); - assertType('*NEVER*', trigger_error('foo', 12345)); - } else { - assertType('false', mb_substitute_character(null)); - assertType('false', trigger_error('foo', 12345)); - } -} - function versionCompareAndMinMax(string $a, string $b): void { if (PHP_VERSION_ID >= 80000) {