From 8b4783ebf75fdbddb514227d056782498a4ec6b1 Mon Sep 17 00:00:00 2001 From: staabm <120441+staabm@users.noreply.github.com> Date: Tue, 22 Sep 2026 10:34:37 +0000 Subject: [PATCH 01/21] Resolve PHP version checks in return-type, throw-type and type-specifying extensions from `Scope::getPhpVersion()` * Add the range-aware counterparts of the `PhpVersion` methods the extensions needed to `PhpVersions`: `arrayFunctionsReturnNullWithNonArray()`, `hasDateTimeExceptions()`, `hasFilterThrowOnFailureConstant()`, `hasPDOSubclasses()`, `hasStricterRoundFunctions()`, `highlightStringDoesNotReturnFalse()`, `strSplitReturnsEmptyArray()`, `substrReturnFalseInsteadOfEmptyString()`, `throwsOnInvalidMbStringEncoding()`, `supportsHhPrintfSpecifier()`, `supportsPassNoneEncodings()`, `supportsPregUnmatchedAsNull()`, `supportsPregCaptureOnlyNamedGroups()`, the three `mb_substitute_character` predicates and `supportsNativeReflectionAdapterReturnTypes()`. * Add `PhpVersions::pickType()`, which returns the branch matching a certain version check and the union of both branches when the analysed version range spans them, so a `Maybe` answer never produces a type narrower than the truth. * Convert every `Dynamic*ReturnTypeExtension`, `Dynamic*ThrowTypeExtension` and `*TypeSpecifyingExtension` in `src/Type/Php/` that injected `PhpVersion` (array functions, `substr`, `str_split`, `round`, `highlight_string`, `count_chars`, `hash`, `mb_*`, `min`/`max`, `openssl_cipher_*`, `printf`, `trigger_error`, `version_compare`, `filter_*`, `bcmath`, `PDO::connect`, the `DateTime`/ `DateInterval`/`DateTimeZone` family) to read the version from the call's Scope. * Thread `PhpVersions` through the helpers those extensions share: `ArrayColumnHelper`, `ArrayFilterFunctionReturnTypeHelper`, `FilterFunctionReturnTypeHelper`, `MbFunctionsReturnTypeExtensionTrait`, `PrintfFormatParser`, `RegexArrayShapeMatcher` and `RegexGroupParser`. * `RegexArrayShapeMatcher` now folds the analysed version's `PREG_UNMATCHED_AS_NULL` support into the parsed flags, so `containsUnmatchedAsNull()` no longer needs a version of its own. * `PDOConnectReturnTypeExtension` moved its version gate out of `isStaticMethodSupported()` (which gets no Scope) into `getTypeFromStaticMethodCall()`. * The same treatment for the three BetterReflection adapter return-type extensions in `src/Reflection/`, and for `FilterVarRule` and the three `Printf*Rule`s that share the converted helpers. * Add `PHPStan\Build\NoInjectedPhpVersionInScopeAwareExtensionRule` so extensions implementing a Scope-receiving interface cannot drift back to injecting `PhpVersion`. `OperatorTypeSpecifyingExtension` is deliberately not covered - its interface hands over no Scope, so `BcMathNumber*OperatorTypeSpecifyingExtension` keep their injected `PhpVersion`. --- ...tedPhpVersionInScopeAwareExtensionRule.php | 91 ++++++++++ build/phpstan.neon | 1 + src/Php/PhpVersions.php | 112 ++++++++++++ ...tionEnumCaseDynamicReturnTypeExtension.php | 5 +- ...flectionEnumDynamicReturnTypeExtension.php | 7 +- ...onEnumReturnDynamicReturnTypeExtension.php | 5 +- src/Rules/Functions/FilterVarRule.php | 4 +- .../ArrayChunkFunctionReturnTypeExtension.php | 10 +- src/Type/Php/ArrayColumnHelper.php | 23 +-- ...rrayCombineFunctionReturnTypeExtension.php | 12 +- .../ArrayFillFunctionReturnTypeExtension.php | 14 +- ...rayFillKeysFunctionReturnTypeExtension.php | 8 +- .../ArrayFilterFunctionReturnTypeHelper.php | 4 +- .../ArrayFlipFunctionReturnTypeExtension.php | 8 +- ...ntersectKeyFunctionReturnTypeExtension.php | 8 +- ...yExistsFunctionTypeSpecifyingExtension.php | 9 +- ...KeysFunctionDynamicReturnTypeExtension.php | 8 +- ...rrayReverseFunctionReturnTypeExtension.php | 8 +- ...archFunctionDynamicReturnTypeExtension.php | 8 +- .../ArraySliceFunctionReturnTypeExtension.php | 8 +- ...ArraySpliceFunctionReturnTypeExtension.php | 8 +- ...luesFunctionDynamicReturnTypeExtension.php | 8 +- .../BcMathStringOrNullReturnTypeExtension.php | 42 +++-- ...harsFunctionDynamicReturnTypeExtension.php | 10 +- ...eIntervalConstructorThrowTypeExtension.php | 22 +-- ...CreateFromDateStringThrowTypeExtension.php | 7 +- ...DateIntervalDynamicReturnTypeExtension.php | 16 +- .../DateTimeConstructorThrowTypeExtension.php | 22 +-- ...DateTimeModifyMethodThrowTypeExtension.php | 24 ++- .../Php/DateTimeModifyReturnTypeExtension.php | 13 +- .../DateTimeSubMethodThrowTypeExtension.php | 7 +- ...eTimeZoneConstructorThrowTypeExtension.php | 22 +-- .../Php/FilterFunctionReturnTypeHelper.php | 28 +-- .../Php/FilterFunctionsThrowTypeExtension.php | 4 +- .../FilterInputDynamicReturnTypeExtension.php | 1 + ...lterVarArrayDynamicReturnTypeExtension.php | 7 +- .../FilterVarDynamicReturnTypeExtension.php | 2 +- .../Php/HashFunctionsReturnTypeExtension.php | 11 +- ...hlightStringDynamicReturnTypeExtension.php | 19 +- ...ertEncodingFunctionReturnTypeExtension.php | 7 +- .../Php/MbFunctionsReturnTypeExtension.php | 18 +- .../MbFunctionsReturnTypeExtensionTrait.php | 52 +++--- .../MbStrlenFunctionReturnTypeExtension.php | 21 +-- ...uteCharacterDynamicReturnTypeExtension.php | 132 ++++++++------ .../Php/MinMaxFunctionReturnTypeExtension.php | 19 +- .../Php/MinMaxFunctionThrowTypeExtension.php | 7 +- ...nsslCipherFunctionsReturnTypeExtension.php | 4 +- .../Php/PDOConnectReturnTypeExtension.php | 11 +- .../Php/PrintfFunctionThrowTypeExtension.php | 4 +- src/Type/Php/RegexArrayShapeMatcher.php | 25 +-- .../Php/RoundFunctionReturnTypeExtension.php | 19 +- .../StrSplitFunctionReturnTypeExtension.php | 37 ++-- .../Php/SubstrDynamicReturnTypeExtension.php | 17 +- ...TriggerErrorDynamicReturnTypeExtension.php | 16 +- ...TriggerErrorFunctionThrowTypeExtension.php | 10 +- ...pareFunctionDynamicReturnTypeExtension.php | 7 +- ...mpareFunctionDynamicThrowTypeExtension.php | 9 +- src/Type/Regex/RegexGroupParser.php | 7 +- ...ope-php-version-return-type-extensions.php | 167 ++++++++++++++++++ ...hpVersionInScopeAwareExtensionRuleTest.php | 29 +++ .../Build/data/php-version-in-extension.php | 75 ++++++++ .../Rules/Functions/FilterVarRuleTest.php | 2 - 62 files changed, 834 insertions(+), 487 deletions(-) create mode 100644 build/PHPStan/Build/NoInjectedPhpVersionInScopeAwareExtensionRule.php create mode 100644 tests/PHPStan/Analyser/nsrt/scope-php-version-return-type-extensions.php create mode 100644 tests/PHPStan/Build/NoInjectedPhpVersionInScopeAwareExtensionRuleTest.php create mode 100644 tests/PHPStan/Build/data/php-version-in-extension.php diff --git a/build/PHPStan/Build/NoInjectedPhpVersionInScopeAwareExtensionRule.php b/build/PHPStan/Build/NoInjectedPhpVersionInScopeAwareExtensionRule.php new file mode 100644 index 00000000000..01b9d113d15 --- /dev/null +++ b/build/PHPStan/Build/NoInjectedPhpVersionInScopeAwareExtensionRule.php @@ -0,0 +1,91 @@ + + */ +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, + ]; + + 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/src/Php/PhpVersions.php b/src/Php/PhpVersions.php index 7ed0fe5c65c..467ee7c941a 100644 --- a/src/Php/PhpVersions.php +++ b/src/Php/PhpVersions.php @@ -6,6 +6,7 @@ use PHPStan\Turbo\ReferencedByTurboExtension; use PHPStan\Type\IntegerRangeType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; /** * Range-aware PHP version check that handles version uncertainty. @@ -188,9 +189,120 @@ 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 supportsPregCaptureOnlyNamedGroups(): TrinaryLogic + { + // https://php.watch/versions/8.2/preg-n-no-capture-modifier + return IntegerRangeType::fromInterval(80200, null)->isSuperTypeOf($this->phpVersions)->result; + } + + public function supportsPregUnmatchedAsNull(): TrinaryLogic + { + // while PREG_UNMATCHED_AS_NULL is defined in php-src since 7.2.x it starts working as expected with 7.4.x + // https://3v4l.org/v3HE4 + return IntegerRangeType::fromInterval(70400, null)->isSuperTypeOf($this->phpVersions)->result; + } + + public function throwsOnInvalidMbStringEncoding(): TrinaryLogic + { + return IntegerRangeType::fromInterval(80000, null)->isSuperTypeOf($this->phpVersions)->result; + } + + /** + * PHPStan's BetterReflection adapters keep their narrowed native return types only on PHP 8+; + * the downgraded PHP 7 build widens them back to the core Reflection ones. + */ + public function supportsNativeReflectionAdapterReturnTypes(): TrinaryLogic + { + return IntegerRangeType::fromInterval(80000, null)->isSuperTypeOf($this->phpVersions)->result; + } + + /** + * Resolves a version-dependent type. + * + * When the analysed PHP version range spans both sides of $versionCheck, + * neither branch can be ruled out and the union of both is returned. + */ + public static function pickType(TrinaryLogic $versionCheck, Type $ifYes, Type $ifNo): Type + { + if ($versionCheck->yes()) { + return $ifYes; + } + + if ($versionCheck->no()) { + return $ifNo; + } + + return TypeCombinator::union($ifYes, $ifNo); + } + } diff --git a/src/Reflection/BetterReflection/Type/AdapterReflectionEnumCaseDynamicReturnTypeExtension.php b/src/Reflection/BetterReflection/Type/AdapterReflectionEnumCaseDynamicReturnTypeExtension.php index 4e1cfbcea63..0bd0ab18198 100644 --- a/src/Reflection/BetterReflection/Type/AdapterReflectionEnumCaseDynamicReturnTypeExtension.php +++ b/src/Reflection/BetterReflection/Type/AdapterReflectionEnumCaseDynamicReturnTypeExtension.php @@ -5,7 +5,6 @@ use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; use PHPStan\BetterReflection\Reflection\Adapter\ReflectionType; -use PHPStan\Php\PhpVersion; use PHPStan\Reflection\MethodReflection; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\DynamicMethodReturnTypeExtension; @@ -22,7 +21,7 @@ final class AdapterReflectionEnumCaseDynamicReturnTypeExtension implements Dynam /** * @param class-string $class */ - public function __construct(private PhpVersion $phpVersion, private string $class) + public function __construct(private string $class) { } @@ -41,7 +40,7 @@ public function isMethodSupported(MethodReflection $methodReflection): bool public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type { - if ($this->phpVersion->getVersionId() >= 80000) { + if (!$scope->getPhpVersion()->supportsNativeReflectionAdapterReturnTypes()->no()) { return null; } diff --git a/src/Reflection/BetterReflection/Type/AdapterReflectionEnumDynamicReturnTypeExtension.php b/src/Reflection/BetterReflection/Type/AdapterReflectionEnumDynamicReturnTypeExtension.php index 2d3920e771c..4dc2c2194db 100644 --- a/src/Reflection/BetterReflection/Type/AdapterReflectionEnumDynamicReturnTypeExtension.php +++ b/src/Reflection/BetterReflection/Type/AdapterReflectionEnumDynamicReturnTypeExtension.php @@ -9,7 +9,6 @@ use PHPStan\BetterReflection\Reflection\Adapter\ReflectionEnum; use PHPStan\BetterReflection\Reflection\Adapter\ReflectionNamedType; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Php\PhpVersion; use PHPStan\Reflection\MethodReflection; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Constant\ConstantBooleanType; @@ -27,10 +26,6 @@ final class AdapterReflectionEnumDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension { - public function __construct(private PhpVersion $phpVersion) - { - } - public function getClass(): string { return ReflectionEnum::class; @@ -52,7 +47,7 @@ public function isMethodSupported(MethodReflection $methodReflection): bool public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type { - if ($this->phpVersion->getVersionId() >= 80000) { + if (!$scope->getPhpVersion()->supportsNativeReflectionAdapterReturnTypes()->no()) { return null; } diff --git a/src/Reflection/PHPStan/NativeReflectionEnumReturnDynamicReturnTypeExtension.php b/src/Reflection/PHPStan/NativeReflectionEnumReturnDynamicReturnTypeExtension.php index fc9b44f79a6..5da1c9c82a8 100644 --- a/src/Reflection/PHPStan/NativeReflectionEnumReturnDynamicReturnTypeExtension.php +++ b/src/Reflection/PHPStan/NativeReflectionEnumReturnDynamicReturnTypeExtension.php @@ -5,7 +5,6 @@ use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; use PHPStan\BetterReflection\Reflection\Adapter\ReflectionClass; -use PHPStan\Php\PhpVersion; use PHPStan\Reflection\MethodReflection; use PHPStan\Type\DynamicMethodReturnTypeExtension; use PHPStan\Type\ObjectType; @@ -17,7 +16,7 @@ final class NativeReflectionEnumReturnDynamicReturnTypeExtension implements Dyna /** * @param class-string $className */ - public function __construct(private PhpVersion $phpVersion, private string $className, private string $methodName) + public function __construct(private string $className, private string $methodName) { } @@ -33,7 +32,7 @@ public function isMethodSupported(MethodReflection $methodReflection): bool public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type { - if ($this->phpVersion->getVersionId() >= 80000) { + if (!$scope->getPhpVersion()->supportsNativeReflectionAdapterReturnTypes()->no()) { return null; } diff --git a/src/Rules/Functions/FilterVarRule.php b/src/Rules/Functions/FilterVarRule.php index 147dfd0cf86..db6edba91cc 100644 --- a/src/Rules/Functions/FilterVarRule.php +++ b/src/Rules/Functions/FilterVarRule.php @@ -7,7 +7,6 @@ use PhpParser\Node\Name; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\RegisteredRule; -use PHPStan\Php\PhpVersion; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -25,7 +24,6 @@ public function __construct( private ReflectionProvider $reflectionProvider, private FilterFunctionReturnTypeHelper $filterFunctionReturnTypeHelper, private FilterFunctionFlagsHelper $filterFunctionFlagsHelper, - private PhpVersion $phpVersion, ) { } @@ -51,7 +49,7 @@ public function processNode(Node $node, Scope $scope): array } if ( - !$this->phpVersion->hasFilterThrowOnFailureConstant() + $scope->getPhpVersion()->hasFilterThrowOnFailureConstant()->no() || !$this->reflectionProvider->hasConstant(new Name\FullyQualified('FILTER_THROW_ON_FAILURE'), null) ) { return []; diff --git a/src/Type/Php/ArrayChunkFunctionReturnTypeExtension.php b/src/Type/Php/ArrayChunkFunctionReturnTypeExtension.php index cfd69286ecf..f9051b20876 100644 --- a/src/Type/Php/ArrayChunkFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayChunkFunctionReturnTypeExtension.php @@ -5,7 +5,7 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Php\PhpVersion; +use PHPStan\Php\PhpVersions; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; @@ -19,10 +19,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 +33,13 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $arrayType = $scope->getType($args[0]->value); if ($arrayType->isArray()->no()) { - return $this->phpVersion->arrayFunctionsReturnNullWithNonArray() ? new NullType() : new NeverType(); + return PhpVersions::pickType($scope->getPhpVersion()->arrayFunctionsReturnNullWithNonArray(), new NullType(), new NeverType()); } $lengthType = $scope->getType($args[1]->value); $negativeOrZero = IntegerRangeType::fromInterval(null, 0); if ($negativeOrZero->isSuperTypeOf($lengthType)->yes()) { - return $this->phpVersion->throwsValueErrorForInternalFunctions() ? new NeverType() : new NullType(); + return PhpVersions::pickType($scope->getPhpVersion()->throwsValueErrorForInternalFunctions(), new NeverType(), new NullType()); } $preserveKeysType = isset($args[2]) ? $scope->getType($args[2]->value) : new ConstantBooleanType(false); diff --git a/src/Type/Php/ArrayColumnHelper.php b/src/Type/Php/ArrayColumnHelper.php index aeb414875f0..3517ebf4db5 100644 --- a/src/Type/Php/ArrayColumnHelper.php +++ b/src/Type/Php/ArrayColumnHelper.php @@ -4,7 +4,7 @@ use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Php\PhpVersion; +use PHPStan\Php\PhpVersions; use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\NonEmptyArrayType; @@ -21,12 +21,6 @@ final class ArrayColumnHelper { - public function __construct( - private PhpVersion $phpVersion, - ) - { - } - /** * @return array{Type, TrinaryLogic} */ @@ -74,7 +68,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 +107,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 +125,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 +215,19 @@ 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(); + return PhpVersions::pickType($throwsTypeError, new NeverType(), 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..6b766631ea4 100644 --- a/src/Type/Php/ArrayCombineFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayCombineFunctionReturnTypeExtension.php @@ -5,7 +5,7 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Php\PhpVersion; +use PHPStan\Php\PhpVersions; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; @@ -20,7 +20,6 @@ final class ArrayCombineFunctionReturnTypeExtension implements DynamicFunctionRe public function __construct( private ArrayCombineHelper $arrayCombineHelper, - private PhpVersion $phpVersion, ) { } @@ -45,15 +44,12 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return $returnType; } + $throwsValueError = $scope->getPhpVersion()->throwsValueErrorForInternalFunctions(); if ($hasValueError->yes()) { - if ($this->phpVersion->throwsValueErrorForInternalFunctions()) { - return new NeverType(); - } - - return new ConstantBooleanType(false); + return PhpVersions::pickType($throwsValueError, new NeverType(), 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..0e18a9dd2dd 100644 --- a/src/Type/Php/ArrayFillFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayFillFunctionReturnTypeExtension.php @@ -5,7 +5,7 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Php\PhpVersion; +use PHPStan\Php\PhpVersions; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\NonEmptyArrayType; @@ -28,10 +28,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,13 +42,11 @@ 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()) { - return new NeverType(); - } - return new ConstantBooleanType(false); + return PhpVersions::pickType($throwsValueError, new NeverType(), new ConstantBooleanType(false)); } $startIndexType = $scope->getType($args[0]->value); @@ -96,7 +90,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..faf8a672536 100644 --- a/src/Type/Php/ArrayFillKeysFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayFillKeysFunctionReturnTypeExtension.php @@ -5,7 +5,7 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Php\PhpVersion; +use PHPStan\Php\PhpVersions; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; @@ -19,10 +19,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 +33,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $keysType = $scope->getType($args[0]->value); if ($keysType->isArray()->no()) { - return $this->phpVersion->arrayFunctionsReturnNullWithNonArray() ? new NullType() : new NeverType(); + return PhpVersions::pickType($scope->getPhpVersion()->arrayFunctionsReturnNullWithNonArray(), new NullType(), new NeverType()); } $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..ba4f722ad50 100644 --- a/src/Type/Php/ArrayFlipFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayFlipFunctionReturnTypeExtension.php @@ -5,7 +5,7 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Php\PhpVersion; +use PHPStan\Php\PhpVersions; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; @@ -19,10 +19,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 +33,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $arrayType = $scope->getType($args[0]->value); if ($arrayType->isArray()->no()) { - return $this->phpVersion->arrayFunctionsReturnNullWithNonArray() ? new NullType() : new NeverType(); + return PhpVersions::pickType($scope->getPhpVersion()->arrayFunctionsReturnNullWithNonArray(), new NullType(), new NeverType()); } $flipped = $arrayType->flipArray(); diff --git a/src/Type/Php/ArrayIntersectKeyFunctionReturnTypeExtension.php b/src/Type/Php/ArrayIntersectKeyFunctionReturnTypeExtension.php index c6212c02cc0..680f4c57ef7 100644 --- a/src/Type/Php/ArrayIntersectKeyFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayIntersectKeyFunctionReturnTypeExtension.php @@ -5,7 +5,7 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Php\PhpVersion; +use PHPStan\Php\PhpVersions; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\NeverType; @@ -19,10 +19,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 +47,7 @@ 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(); + return PhpVersions::pickType($scope->getPhpVersion()->arrayFunctionsReturnNullWithNonArray(), new NullType(), new NeverType()); } 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..c331f4cd8ed 100644 --- a/src/Type/Php/ArrayKeysFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArrayKeysFunctionDynamicReturnTypeExtension.php @@ -5,7 +5,7 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Php\PhpVersion; +use PHPStan\Php\PhpVersions; use PHPStan\Reflection\FunctionReflection; use PHPStan\TrinaryLogic; use PHPStan\Type\DynamicFunctionReturnTypeExtension; @@ -19,10 +19,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 +33,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $arrayType = $scope->getType($args[0]->value); if ($arrayType->isArray()->no()) { - return $this->phpVersion->arrayFunctionsReturnNullWithNonArray() ? new NullType() : new NeverType(); + return PhpVersions::pickType($scope->getPhpVersion()->arrayFunctionsReturnNullWithNonArray(), new NullType(), new NeverType()); } if (count($args) >= 2) { diff --git a/src/Type/Php/ArrayReverseFunctionReturnTypeExtension.php b/src/Type/Php/ArrayReverseFunctionReturnTypeExtension.php index 7f4f2a754fc..b39170dcd25 100644 --- a/src/Type/Php/ArrayReverseFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayReverseFunctionReturnTypeExtension.php @@ -5,7 +5,7 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Php\PhpVersion; +use PHPStan\Php\PhpVersions; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; @@ -17,10 +17,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 +31,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $type = $scope->getType($args[0]->value); if ($type->isArray()->no()) { - return $this->phpVersion->arrayFunctionsReturnNullWithNonArray() ? new NullType() : new NeverType(); + return PhpVersions::pickType($scope->getPhpVersion()->arrayFunctionsReturnNullWithNonArray(), new NullType(), new NeverType()); } $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..4958afc8285 100644 --- a/src/Type/Php/ArraySearchFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArraySearchFunctionDynamicReturnTypeExtension.php @@ -5,7 +5,7 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Php\PhpVersion; +use PHPStan\Php\PhpVersions; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; @@ -18,10 +18,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 +33,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $haystackArgType = $scope->getType($args[1]->value); if ($haystackArgType->isArray()->no()) { - return $this->phpVersion->arrayFunctionsReturnNullWithNonArray() ? new NullType() : new NeverType(); + return PhpVersions::pickType($scope->getPhpVersion()->arrayFunctionsReturnNullWithNonArray(), new NullType(), new NeverType()); } if ($argsCount < 3) { diff --git a/src/Type/Php/ArraySliceFunctionReturnTypeExtension.php b/src/Type/Php/ArraySliceFunctionReturnTypeExtension.php index 5ac2ba46063..d2437ffc937 100644 --- a/src/Type/Php/ArraySliceFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArraySliceFunctionReturnTypeExtension.php @@ -5,7 +5,7 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Php\PhpVersion; +use PHPStan\Php\PhpVersions; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; @@ -18,10 +18,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 +32,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $arrayType = $scope->getType($args[0]->value); if ($arrayType->isArray()->no()) { - return $this->phpVersion->arrayFunctionsReturnNullWithNonArray() ? new NullType() : new NeverType(); + return PhpVersions::pickType($scope->getPhpVersion()->arrayFunctionsReturnNullWithNonArray(), new NullType(), new NeverType()); } $offsetType = $scope->getType($args[1]->value); diff --git a/src/Type/Php/ArraySpliceFunctionReturnTypeExtension.php b/src/Type/Php/ArraySpliceFunctionReturnTypeExtension.php index 5dd3c941744..be1782bffe7 100644 --- a/src/Type/Php/ArraySpliceFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArraySpliceFunctionReturnTypeExtension.php @@ -5,7 +5,7 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Php\PhpVersion; +use PHPStan\Php\PhpVersions; use PHPStan\Reflection\FunctionReflection; use PHPStan\TrinaryLogic; use PHPStan\Type\DynamicFunctionReturnTypeExtension; @@ -18,10 +18,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 +36,7 @@ public function getTypeFromFunctionCall( $arrayType = $scope->getType($args[0]->value); if ($arrayType->isArray()->no()) { - return $this->phpVersion->arrayFunctionsReturnNullWithNonArray() ? new NullType() : new NeverType(); + return PhpVersions::pickType($scope->getPhpVersion()->arrayFunctionsReturnNullWithNonArray(), new NullType(), new NeverType()); } $offsetType = $scope->getType($args[1]->value); diff --git a/src/Type/Php/ArrayValuesFunctionDynamicReturnTypeExtension.php b/src/Type/Php/ArrayValuesFunctionDynamicReturnTypeExtension.php index 0015c4e1268..9c86e30873c 100644 --- a/src/Type/Php/ArrayValuesFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArrayValuesFunctionDynamicReturnTypeExtension.php @@ -5,7 +5,7 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Php\PhpVersion; +use PHPStan\Php\PhpVersions; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\NeverType; @@ -18,10 +18,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 +32,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $arrayType = $scope->getType($args[0]->value); if ($arrayType->isArray()->no()) { - return $this->phpVersion->arrayFunctionsReturnNullWithNonArray() ? new NullType() : new NeverType(); + return PhpVersions::pickType($scope->getPhpVersion()->arrayFunctionsReturnNullWithNonArray(), new NullType(), new NeverType()); } 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..9635617e679 100644 --- a/src/Type/Php/DateIntervalConstructorThrowTypeExtension.php +++ b/src/Type/Php/DateIntervalConstructorThrowTypeExtension.php @@ -6,7 +6,7 @@ use PhpParser\Node\Expr\StaticCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Php\PhpVersion; +use PHPStan\Php\PhpVersions; use PHPStan\Reflection\MethodReflection; use PHPStan\Type\DynamicStaticMethodThrowTypeExtension; use PHPStan\Type\NeverType; @@ -20,10 +20,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,26 +38,26 @@ 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()) { - return new ObjectType('DateMalformedIntervalStringException'); - } - - return new ObjectType('Exception'); + return PhpVersions::pickType( + $scope->getPhpVersion()->hasDateTimeExceptions(), + new ObjectType('DateMalformedIntervalStringException'), + new ObjectType('Exception'), + ); } } diff --git a/src/Type/Php/DateIntervalCreateFromDateStringThrowTypeExtension.php b/src/Type/Php/DateIntervalCreateFromDateStringThrowTypeExtension.php index 2a1c10b29f9..aea6f4e43b0 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; } diff --git a/src/Type/Php/DateIntervalDynamicReturnTypeExtension.php b/src/Type/Php/DateIntervalDynamicReturnTypeExtension.php index bc57c6b7e24..f073eaa0f17 100644 --- a/src/Type/Php/DateIntervalDynamicReturnTypeExtension.php +++ b/src/Type/Php/DateIntervalDynamicReturnTypeExtension.php @@ -6,7 +6,7 @@ use PhpParser\Node\Expr\StaticCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Php\PhpVersion; +use PHPStan\Php\PhpVersions; use PHPStan\Reflection\MethodReflection; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\DynamicStaticMethodReturnTypeExtension; @@ -19,10 +19,6 @@ final class DateIntervalDynamicReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension { - public function __construct(private PhpVersion $phpVersion) - { - } - public function getClass(): string { return DateInterval::class; @@ -61,11 +57,11 @@ public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, if ($hasFalse) { if (!$hasDateInterval) { - if ($this->phpVersion->hasDateTimeExceptions()) { - return new NeverType(); - } - - return new ConstantBooleanType(false); + return PhpVersions::pickType( + $scope->getPhpVersion()->hasDateTimeExceptions(), + new NeverType(), + new ConstantBooleanType(false), + ); } return null; diff --git a/src/Type/Php/DateTimeConstructorThrowTypeExtension.php b/src/Type/Php/DateTimeConstructorThrowTypeExtension.php index 45cdb606a3a..fb5ba743b3c 100644 --- a/src/Type/Php/DateTimeConstructorThrowTypeExtension.php +++ b/src/Type/Php/DateTimeConstructorThrowTypeExtension.php @@ -7,7 +7,7 @@ use PhpParser\Node\Expr\StaticCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Php\PhpVersion; +use PHPStan\Php\PhpVersions; use PHPStan\Reflection\MethodReflection; use PHPStan\Type\DynamicStaticMethodThrowTypeExtension; use PHPStan\Type\NeverType; @@ -22,10 +22,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,26 +40,26 @@ 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()) { - return new ObjectType('DateMalformedStringException'); - } - - return new ObjectType('Exception'); + return PhpVersions::pickType( + $scope->getPhpVersion()->hasDateTimeExceptions(), + new ObjectType('DateMalformedStringException'), + new ObjectType('Exception'), + ); } } diff --git a/src/Type/Php/DateTimeModifyMethodThrowTypeExtension.php b/src/Type/Php/DateTimeModifyMethodThrowTypeExtension.php index f3b91b8b358..109356ae1ce 100644 --- a/src/Type/Php/DateTimeModifyMethodThrowTypeExtension.php +++ b/src/Type/Php/DateTimeModifyMethodThrowTypeExtension.php @@ -7,7 +7,7 @@ use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Php\PhpVersion; +use PHPStan\Php\PhpVersions; use PHPStan\Reflection\MethodReflection; use PHPStan\Type\DynamicMethodThrowTypeExtension; use PHPStan\Type\NeverType; @@ -22,10 +22,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 +33,7 @@ public function getThrowTypeFromMethodCall(MethodReflection $methodReflection, M return null; } - if (!$this->phpVersion->hasDateTimeExceptions()) { + if ($scope->getPhpVersion()->hasDateTimeExceptions()->no()) { return null; } @@ -49,26 +45,26 @@ public function getThrowTypeFromMethodCall(MethodReflection $methodReflection, M $dateTime = new DateTime(); $dateTime->modify($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()) { - return new ObjectType('DateMalformedStringException'); - } - - return new ObjectType('Exception'); + return PhpVersions::pickType( + $scope->getPhpVersion()->hasDateTimeExceptions(), + new ObjectType('DateMalformedStringException'), + new ObjectType('Exception'), + ); } } diff --git a/src/Type/Php/DateTimeModifyReturnTypeExtension.php b/src/Type/Php/DateTimeModifyReturnTypeExtension.php index a5a3223dc62..0147fe08484 100644 --- a/src/Type/Php/DateTimeModifyReturnTypeExtension.php +++ b/src/Type/Php/DateTimeModifyReturnTypeExtension.php @@ -6,7 +6,7 @@ use DateTimeInterface; use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; -use PHPStan\Php\PhpVersion; +use PHPStan\Php\PhpVersions; use PHPStan\Reflection\MethodReflection; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\DynamicMethodReturnTypeExtension; @@ -24,7 +24,6 @@ final class DateTimeModifyReturnTypeExtension implements DynamicMethodReturnType /** @param class-string $dateTimeClass */ public function __construct( - private PhpVersion $phpVersion, private string $dateTimeClass, ) { @@ -75,11 +74,11 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method if ($hasFalse) { if (!$hasDateTime) { - if ($this->phpVersion->hasDateTimeExceptions()) { - return new NeverType(); - } - - return new ConstantBooleanType(false); + return PhpVersions::pickType( + $scope->getPhpVersion()->hasDateTimeExceptions(), + new NeverType(), + new ConstantBooleanType(false), + ); } return null; 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..60b50896d34 100644 --- a/src/Type/Php/DateTimeZoneConstructorThrowTypeExtension.php +++ b/src/Type/Php/DateTimeZoneConstructorThrowTypeExtension.php @@ -6,7 +6,7 @@ use PhpParser\Node\Expr\StaticCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Php\PhpVersion; +use PHPStan\Php\PhpVersions; use PHPStan\Reflection\MethodReflection; use PHPStan\Type\DynamicStaticMethodThrowTypeExtension; use PHPStan\Type\NeverType; @@ -20,10 +20,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,26 +38,26 @@ 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()) { - return new ObjectType('DateInvalidTimeZoneException'); - } - - return new ObjectType('Exception'); + return PhpVersions::pickType( + $scope->getPhpVersion()->hasDateTimeExceptions(), + new ObjectType('DateInvalidTimeZoneException'), + new ObjectType('Exception'), + ); } } 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/HashFunctionsReturnTypeExtension.php b/src/Type/Php/HashFunctionsReturnTypeExtension.php index d0c13ef4d90..a79791b54ab 100644 --- a/src/Type/Php/HashFunctionsReturnTypeExtension.php +++ b/src/Type/Php/HashFunctionsReturnTypeExtension.php @@ -5,7 +5,7 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Php\PhpVersion; +use PHPStan\Php\PhpVersions; use PHPStan\Reflection\FunctionReflection; use PHPStan\ShouldNotHappenException; use PHPStan\Type\Accessory\AccessoryLowercaseStringType; @@ -86,10 +86,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 +121,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 +134,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $neverType = new NeverType(); $falseType = new ConstantBooleanType(false); - $invalidAlgorithmType = $this->phpVersion->throwsValueErrorForInternalFunctions() ? $neverType : $falseType; + $invalidAlgorithmType = PhpVersions::pickType($throwsValueError, $neverType, $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..354f92e5843 100644 --- a/src/Type/Php/HighlightStringDynamicReturnTypeExtension.php +++ b/src/Type/Php/HighlightStringDynamicReturnTypeExtension.php @@ -5,7 +5,7 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Php\PhpVersion; +use PHPStan\Php\PhpVersions; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\BooleanType; use PHPStan\Type\Constant\ConstantBooleanType; @@ -18,10 +18,6 @@ final class HighlightStringDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { - public function __construct(private PhpVersion $phpVersion) - { - } - public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'highlight_string'; @@ -29,13 +25,10 @@ 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); - } - - return new BooleanType(); + return PhpVersions::pickType($doesNotReturnFalse, new ConstantBooleanType(true), new BooleanType()); } $returnType = $scope->getType($args[1]->value); @@ -43,11 +36,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return new StringType(); } - if ($this->phpVersion->highlightStringDoesNotReturnFalse()) { - return new ConstantBooleanType(true); - } - - return new BooleanType(); + return PhpVersions::pickType($doesNotReturnFalse, new ConstantBooleanType(true), new BooleanType()); } } 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..80d31c464d0 100644 --- a/src/Type/Php/MbFunctionsReturnTypeExtension.php +++ b/src/Type/Php/MbFunctionsReturnTypeExtension.php @@ -5,7 +5,7 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Php\PhpVersion; +use PHPStan\Php\PhpVersions; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\BooleanType; @@ -38,10 +38,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,18 +57,20 @@ 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(); - } + $invalidEncodingReturn = PhpVersions::pickType( + $phpVersions->throwsOnInvalidMbStringEncoding(), + new NeverType(), + new ConstantBooleanType(false), + ); return $results[0] ? TypeCombinator::remove($returnType, new ConstantBooleanType(false)) 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..f4348096038 100644 --- a/src/Type/Php/MbStrlenFunctionReturnTypeExtension.php +++ b/src/Type/Php/MbStrlenFunctionReturnTypeExtension.php @@ -5,7 +5,7 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Php\PhpVersion; +use PHPStan\Php\PhpVersions; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\ShouldNotHappenException; @@ -42,10 +42,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 +58,8 @@ public function getTypeFromFunctionCall( return null; } + $phpVersions = $scope->getPhpVersion(); + $throwsOnInvalidEncoding = $phpVersions->throwsOnInvalidMbStringEncoding(); $encodings = []; if (count($args) === 1) { @@ -76,7 +74,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 +83,10 @@ public function getTypeFromFunctionCall( $encodings = array_unique($encodings); if (in_array(self::UNSUPPORTED_ENCODING, $encodings, true) && count($encodings) === 1) { - if ($this->phpVersion->throwsOnInvalidMbStringEncoding()) { - return new NeverType(); - } - return new ConstantBooleanType(false); + return PhpVersions::pickType($throwsOnInvalidEncoding, new NeverType(), 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 +97,7 @@ public function getTypeFromFunctionCall( $stringScalar = (string) $constantScalar; foreach ($encodings as $encoding) { - if (!$this->isSupportedEncoding($encoding)) { + if (!$this->isSupportedEncoding($encoding, $phpVersions)) { continue; } @@ -145,7 +140,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..b2cbecd10cc 100644 --- a/src/Type/Php/MbSubstituteCharacterDynamicReturnTypeExtension.php +++ b/src/Type/Php/MbSubstituteCharacterDynamicReturnTypeExtension.php @@ -5,7 +5,7 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Php\PhpVersion; +use PHPStan\Php\PhpVersions; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\BooleanType; use PHPStan\Type\Constant\ConstantBooleanType; @@ -23,10 +23,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 +30,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,44 +63,33 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $isInteger = $argType->isInteger(); if ($isString->no() && $isNull->no() && $isInteger->no()) { - if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { - return new NeverType(); - } - - return new BooleanType(); + return PhpVersions::pickType( + $phpVersions->throwsTypeErrorForInternalFunctions(), + new NeverType(), + new BooleanType(), + ); } 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()) { - return new NeverType(); - } - - return new ConstantBooleanType(false); + if (!self::isPossiblyInRanges($possibleRanges, $argType)) { + return PhpVersions::pickType( + $phpVersions->throwsValueErrorForInternalFunctions(), + new NeverType(), + new ConstantBooleanType(false), + ); } } 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); - } - - return new NeverType(); + return PhpVersions::pickType($isEmptyStringValidAlias, new ConstantBooleanType(true), new NeverType()); } - if (!$this->phpVersion->isNumericStringValidArgInMbSubstituteCharacter() && $argType->isNumericString()->yes()) { + if ($phpVersions->isNumericStringValidArgInMbSubstituteCharacter()->no() && $argType->isNumericString()->yes()) { return new NeverType(); } @@ -110,28 +101,67 @@ 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); + } - if ($this->phpVersion->throwsValueErrorForInternalFunctions()) { - return new NeverType(); + return new BooleanType(); } - return new ConstantBooleanType(false); + return PhpVersions::pickType( + $phpVersions->throwsValueErrorForInternalFunctions(), + new NeverType(), + new ConstantBooleanType(false), + ); } } elseif ($isNull->yes()) { // The $substitute_character arg is nullable in PHP 8+ - return new ConstantBooleanType($this->phpVersion->isNullValidArgInMbSubstituteCharacter()); + return PhpVersions::pickType( + $phpVersions->isNullValidArgInMbSubstituteCharacter(), + new ConstantBooleanType(true), + new ConstantBooleanType(false), + ); } 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..ee87a0fe252 100644 --- a/src/Type/Php/PDOConnectReturnTypeExtension.php +++ b/src/Type/Php/PDOConnectReturnTypeExtension.php @@ -5,7 +5,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\DynamicStaticMethodReturnTypeExtension; use PHPStan\Type\ObjectType; @@ -22,10 +21,6 @@ final class PDOConnectReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension { - public function __construct(private PhpVersion $phpVersion) - { - } - public function getClass(): string { return 'PDO'; @@ -33,11 +28,15 @@ public function getClass(): string public function isStaticMethodSupported(MethodReflection $methodReflection): bool { - return $this->phpVersion->hasPDOSubclasses() && $methodReflection->getName() === 'connect'; + return $methodReflection->getName() === 'connect'; } public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): ?Type { + if (!$scope->getPhpVersion()->hasPDOSubclasses()->yes()) { + return null; + } + if (count($methodCall->getArgs()) < 1) { return null; } 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/RegexArrayShapeMatcher.php b/src/Type/Php/RegexArrayShapeMatcher.php index 4badd12420f..3c26f55c1a9 100644 --- a/src/Type/Php/RegexArrayShapeMatcher.php +++ b/src/Type/Php/RegexArrayShapeMatcher.php @@ -5,7 +5,7 @@ use PhpParser\Node\Expr; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Php\PhpVersion; +use PHPStan\Php\PhpVersions; use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\NonEmptyArrayType; @@ -47,19 +47,18 @@ final class RegexArrayShapeMatcher public function __construct( private RegexGroupParser $regexGroupParser, private RegexExpressionHelper $regexExpressionHelper, - private PhpVersion $phpVersion, ) { } public function matchAllExpr(Expr $patternExpr, ?Type $flagsType, TrinaryLogic $wasMatched, Scope $scope): ?Type { - return $this->matchPatternType($this->getPatternType($patternExpr, $scope), $flagsType, $wasMatched, true); + return $this->matchPatternType($this->getPatternType($patternExpr, $scope), $flagsType, $wasMatched, true, $scope->getPhpVersion()); } public function matchExpr(Expr $patternExpr, ?Type $flagsType, TrinaryLogic $wasMatched, Scope $scope): ?Type { - return $this->matchPatternType($this->getPatternType($patternExpr, $scope), $flagsType, $wasMatched, false); + return $this->matchPatternType($this->getPatternType($patternExpr, $scope), $flagsType, $wasMatched, false, $scope->getPhpVersion()); } public function matchSubjectExpr(Expr $patternExpr, Scope $scope): ?Type @@ -72,7 +71,7 @@ public function matchSubjectExpr(Expr $patternExpr, Scope $scope): ?Type $subjectTypes = []; foreach ($constantStrings as $constantString) { - $astWalkResult = $this->regexGroupParser->parseGroups($constantString->getValue()); + $astWalkResult = $this->regexGroupParser->parseGroups($constantString->getValue(), $scope->getPhpVersion()); if ($astWalkResult === null) { return null; } @@ -83,7 +82,7 @@ public function matchSubjectExpr(Expr $patternExpr, Scope $scope): ?Type return TypeCombinator::union(...$subjectTypes); } - private function matchPatternType(Type $patternType, ?Type $flagsType, TrinaryLogic $wasMatched, bool $matchesAll): ?Type + private function matchPatternType(Type $patternType, ?Type $flagsType, TrinaryLogic $wasMatched, bool $matchesAll, PhpVersions $phpVersions): ?Type { if ($wasMatched->no()) { return ConstantArrayTypeBuilder::createEmpty()->getArray(); @@ -107,11 +106,17 @@ private function matchPatternType(Type $patternType, ?Type $flagsType, TrinaryLo if ($flags !== $flagsType->getValue()) { return null; } + + // PREG_UNMATCHED_AS_NULL only behaves as documented since PHP 7.4, so the + // analysed version turns it on the same way the emulation flag does + if ($phpVersions->supportsPregUnmatchedAsNull()->yes()) { + $flags |= self::PREG_UNMATCHED_AS_NULL_ON_72_73; + } } $matchedTypes = []; foreach ($constantStrings as $constantString) { - $matched = $this->matchRegex($constantString->getValue(), $flags, $wasMatched, $matchesAll); + $matched = $this->matchRegex($constantString->getValue(), $flags, $wasMatched, $matchesAll, $phpVersions); if ($matched === null) { return null; } @@ -129,9 +134,9 @@ private function matchPatternType(Type $patternType, ?Type $flagsType, TrinaryLo /** * @param int-mask|null $flags */ - private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched, bool $matchesAll): ?Type + private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched, bool $matchesAll, PhpVersions $phpVersions): ?Type { - $astWalkResult = $this->regexGroupParser->parseGroups($regex); + $astWalkResult = $this->regexGroupParser->parseGroups($regex, $phpVersions); if ($astWalkResult === null) { // regex could not be parsed by Hoa/Regex return null; @@ -479,7 +484,7 @@ private function containsUnmatchedAsNull(int $flags, bool $matchesAll): bool return ($flags & PREG_UNMATCHED_AS_NULL) !== 0; } - return ($flags & PREG_UNMATCHED_AS_NULL) !== 0 && (($flags & self::PREG_UNMATCHED_AS_NULL_ON_72_73) !== 0 || $this->phpVersion->supportsPregUnmatchedAsNull()); + return ($flags & PREG_UNMATCHED_AS_NULL) !== 0 && ($flags & self::PREG_UNMATCHED_AS_NULL_ON_72_73) !== 0; } private function getKeyType(int|string $key): Type diff --git a/src/Type/Php/RoundFunctionReturnTypeExtension.php b/src/Type/Php/RoundFunctionReturnTypeExtension.php index 2bc17560339..a3db19f8bdd 100644 --- a/src/Type/Php/RoundFunctionReturnTypeExtension.php +++ b/src/Type/Php/RoundFunctionReturnTypeExtension.php @@ -5,7 +5,7 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Php\PhpVersion; +use PHPStan\Php\PhpVersions; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Accessory\AccessoryNumericStringType; use PHPStan\Type\BooleanType; @@ -27,10 +27,6 @@ final class RoundFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { - public function __construct(private PhpVersion $phpVersion) - { - } - public function isFunctionSupported(FunctionReflection $functionReflection): bool { return in_array( @@ -50,13 +46,10 @@ 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(); + + // PHP 8 fatals with a missing parameter, PHP 7 returns null. + $noArgsReturnType = PhpVersions::pickType($hasStricterRoundFunctions, new NeverType(true), new NullType()); if (count($functionCall->getArgs()) < 1) { return $noArgsReturnType; @@ -68,7 +61,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/StrSplitFunctionReturnTypeExtension.php b/src/Type/Php/StrSplitFunctionReturnTypeExtension.php index 9738902437e..49625abeb63 100644 --- a/src/Type/Php/StrSplitFunctionReturnTypeExtension.php +++ b/src/Type/Php/StrSplitFunctionReturnTypeExtension.php @@ -5,7 +5,7 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Php\PhpVersion; +use PHPStan\Php\PhpVersions; use PHPStan\Reflection\FunctionReflection; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; @@ -40,10 +40,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 +52,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 +65,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, if ($splitLengthType instanceof ConstantIntegerType) { $splitLength = $splitLengthType->getValue(); if ($splitLength < 1) { - return $this->phpVersion->throwsValueErrorForInternalFunctions() ? new NeverType() : new ConstantBooleanType(false); + return PhpVersions::pickType($throwsValueError, new NeverType(), new ConstantBooleanType(false)); } } @@ -77,8 +77,8 @@ 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)) { + return PhpVersions::pickType($throwsValueError, new NeverType(), new ConstantBooleanType(false)); } } } else { @@ -99,13 +99,18 @@ 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); + $results[] = PhpVersions::pickType( + $returnsEmptyArray, + self::createConstantArrayFrom([], $scope), + self::createConstantArrayFrom([''], $scope), + ); + continue; } + $items = $encoding === null + ? str_split($value, $splitLength) + : @mb_str_split($value, $splitLength, $encoding); + $results[] = self::createConstantArrayFrom($items, $scope); } @@ -115,7 +120,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 +131,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/SubstrDynamicReturnTypeExtension.php b/src/Type/Php/SubstrDynamicReturnTypeExtension.php index be4ec43a315..201b8af99ad 100644 --- a/src/Type/Php/SubstrDynamicReturnTypeExtension.php +++ b/src/Type/Php/SubstrDynamicReturnTypeExtension.php @@ -5,7 +5,7 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Php\PhpVersion; +use PHPStan\Php\PhpVersions; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; @@ -31,10 +31,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 +47,7 @@ public function getTypeFromFunctionCall( return null; } + $returnsFalseInsteadOfEmptyString = $scope->getPhpVersion()->substrReturnFalseInsteadOfEmptyString(); $string = $scope->getType($args[0]->value); $offset = $scope->getType($args[1]->value); @@ -90,9 +87,11 @@ 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(''); + $results[] = PhpVersions::pickType( + $returnsFalseInsteadOfEmptyString, + new ConstantBooleanType(false), + 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..330720277f3 100644 --- a/src/Type/Php/TriggerErrorDynamicReturnTypeExtension.php +++ b/src/Type/Php/TriggerErrorDynamicReturnTypeExtension.php @@ -5,7 +5,7 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Php\PhpVersion; +use PHPStan\Php\PhpVersions; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; @@ -23,10 +23,6 @@ final class TriggerErrorDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { - public function __construct(private PhpVersion $phpVersion) - { - } - public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'trigger_error'; @@ -54,11 +50,11 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } if (!in_array($errorLevel, [E_USER_WARNING, E_USER_NOTICE, E_USER_DEPRECATED], true)) { - if ($this->phpVersion->throwsValueErrorForInternalFunctions()) { - return new NeverType(true); - } - - return new ConstantBooleanType(false); + return PhpVersions::pickType( + $scope->getPhpVersion()->throwsValueErrorForInternalFunctions(), + new NeverType(true), + new ConstantBooleanType(false), + ); } return new ConstantBooleanType(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/VersionCompareFunctionDynamicReturnTypeExtension.php b/src/Type/Php/VersionCompareFunctionDynamicReturnTypeExtension.php index f6a8d4a7282..d454e3630a2 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,7 +77,7 @@ public function getTypeFromFunctionCall( if (isset($args[2])) { $operatorStrings = $scope->getType($args[2]->value)->getConstantStrings(); $counts[] = count($operatorStrings); - $returnType = $this->phpVersion->throwsValueErrorForInternalFunctions() + $returnType = $throwsValueError->yes() ? new BooleanType() : new BenevolentUnionType([new BooleanType(), new NullType()]); } else { @@ -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; } diff --git a/src/Type/Php/VersionCompareFunctionDynamicThrowTypeExtension.php b/src/Type/Php/VersionCompareFunctionDynamicThrowTypeExtension.php index e4c4f92b34d..3e93128d05b 100644 --- a/src/Type/Php/VersionCompareFunctionDynamicThrowTypeExtension.php +++ b/src/Type/Php/VersionCompareFunctionDynamicThrowTypeExtension.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\Type; @@ -16,12 +15,6 @@ final class VersionCompareFunctionDynamicThrowTypeExtension implements DynamicFunctionThrowTypeExtension { - public function __construct( - private PhpVersion $phpVersion, - ) - { - } - public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'version_compare'; @@ -33,7 +26,7 @@ public function getThrowTypeFromFunctionCall( Scope $scope, ): ?Type { - if (!$this->phpVersion->throwsValueErrorForInternalFunctions()) { + if ($scope->getPhpVersion()->throwsValueErrorForInternalFunctions()->no()) { return null; } diff --git a/src/Type/Regex/RegexGroupParser.php b/src/Type/Regex/RegexGroupParser.php index 048b2a3beb3..32b38211a8d 100644 --- a/src/Type/Regex/RegexGroupParser.php +++ b/src/Type/Regex/RegexGroupParser.php @@ -10,7 +10,7 @@ use Nette\Utils\RegexpException; use Nette\Utils\Strings; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Php\PhpVersion; +use PHPStan\Php\PhpVersions; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; @@ -55,13 +55,12 @@ final class RegexGroupParser private static array $parsedAst = []; public function __construct( - private PhpVersion $phpVersion, private RegexExpressionHelper $regexExpressionHelper, ) { } - public function parseGroups(string $regex): ?RegexAstWalkResult + public function parseGroups(string $regex, PhpVersions $phpVersions): ?RegexAstWalkResult { /** @throws void */ self::$parser ??= Llk::load(new Read(__DIR__ . '/../../../resources/RegexGrammar.pp')); @@ -106,7 +105,7 @@ public function parseGroups(string $regex): ?RegexAstWalkResult $this->updateCapturingAstAddEmptyToken($ast); $captureOnlyNamed = false; - if ($this->phpVersion->supportsPregCaptureOnlyNamedGroups()) { + if ($phpVersions->supportsPregCaptureOnlyNamedGroups()->yes()) { $captureOnlyNamed = str_contains($modifiers, 'n'); } 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 new file mode 100644 index 00000000000..f193fe343e9 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/scope-php-version-return-type-extensions.php @@ -0,0 +1,167 @@ += 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 pdoConnect(): void +{ + if (PHP_VERSION_ID >= 80400) { + assertType('PDO\Sqlite', \PDO::connect('sqlite::memory:')); + } else { + assertType('PDO', \PDO::connect('sqlite::memory:')); + } +} + +function bcMath(): void +{ + if (PHP_VERSION_ID >= 80000) { + assertType('*NEVER*', bcdiv('1', '0')); + } else { + assertType('null', bcdiv('1', '0')); + } +} + +function pregMatchUnmatchedAsNull(string $s): void +{ + if (PHP_VERSION_ID >= 70400) { + if (preg_match('/(a)(b)?/', $s, $matches, PREG_UNMATCHED_AS_NULL) === 1) { + assertType('array{non-falsy-string, \'a\', \'b\'|null}', $matches); + } + } else { + if (preg_match('/(a)(b)?/', $s, $matches, PREG_UNMATCHED_AS_NULL) === 1) { + assertType('array{0: non-falsy-string, 1: \'a\', 2?: \'b\'}', $matches); + } + } +} + +function pregCaptureOnlyNamedGroups(string $s): void +{ + if (PHP_VERSION_ID >= 80200) { + if (preg_match('/(?a)(b)/n', $s, $named) === 1) { + assertType('array{0: non-falsy-string, name: \'a\', 1: \'a\'}', $named); + } + } else { + if (preg_match('/(?a)(b)/n', $s, $named) === 1) { + assertType('array{0: non-falsy-string, name: \'a\', 1: \'a\', 2: \'b\'}', $named); + } + } +} + +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)); + } +} diff --git a/tests/PHPStan/Build/NoInjectedPhpVersionInScopeAwareExtensionRuleTest.php b/tests/PHPStan/Build/NoInjectedPhpVersionInScopeAwareExtensionRuleTest.php new file mode 100644 index 00000000000..e2b90c5ea82 --- /dev/null +++ b/tests/PHPStan/Build/NoInjectedPhpVersionInScopeAwareExtensionRuleTest.php @@ -0,0 +1,29 @@ + + */ +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.', + 14, + ], + ]); + } + +} 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..099c3d7524b --- /dev/null +++ b/tests/PHPStan/Build/data/php-version-in-extension.php @@ -0,0 +1,75 @@ += 8.0 + +declare(strict_types = 1); + +namespace PhpVersionInExtension; + +use PhpParser\Node\Expr\FuncCall; +use PHPStan\Analyser\Scope; +use PHPStan\Php\PhpVersion; +use PHPStan\Reflection\FunctionReflection; +use PHPStan\Type\DynamicFunctionReturnTypeExtension; +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 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(); + } + +} diff --git a/tests/PHPStan/Rules/Functions/FilterVarRuleTest.php b/tests/PHPStan/Rules/Functions/FilterVarRuleTest.php index 91edc0ed7f4..a84798b849f 100644 --- a/tests/PHPStan/Rules/Functions/FilterVarRuleTest.php +++ b/tests/PHPStan/Rules/Functions/FilterVarRuleTest.php @@ -2,7 +2,6 @@ namespace PHPStan\Rules\Functions; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use PHPStan\Type\Php\FilterFunctionFlagsHelper; @@ -19,7 +18,6 @@ protected function getRule(): Rule self::createReflectionProvider(), self::getContainer()->getByType(FilterFunctionReturnTypeHelper::class), self::getContainer()->getByType(FilterFunctionFlagsHelper::class), - self::getContainer()->getByType(PhpVersion::class), ); } From c006f1f446a1089febacc326ed3ac50b803c2b00 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Tue, 22 Sep 2026 10:58:24 +0000 Subject: [PATCH 02/21] Resolve PHP version checks in round() and str_split() throw type extensions from Scope::getPhpVersion() Both DynamicFunctionThrowTypeExtensions still asked a DI-injected PhpVersion whether the function throws, so they ignored PHP_VERSION_ID narrowing in the analysed code and flattened a configured phpVersion range to a single version. They now read the version from the call's Scope and only rule the throw out when the analysed range certainly does not throw. Co-Authored-By: Claude Opus 5 --- src/Php/PhpVersions.php | 5 ++ .../Php/RoundFunctionThrowTypeExtension.php | 7 +-- .../StrSplitFunctionThrowTypeExtension.php | 7 +-- ...WithUnthrownExceptionRuleConfigPhpTest.php | 42 +++++++++++++++ .../data/throw-type-php-version.neon | 4 ++ .../data/throw-type-php-versions.php | 54 +++++++++++++++++++ 6 files changed, 107 insertions(+), 12 deletions(-) create mode 100644 tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleConfigPhpTest.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/src/Php/PhpVersions.php b/src/Php/PhpVersions.php index 467ee7c941a..18c2c69e89e 100644 --- a/src/Php/PhpVersions.php +++ b/src/Php/PhpVersions.php @@ -277,6 +277,11 @@ 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; + } + /** * PHPStan's BetterReflection adapters keep their narrowed native return types only on PHP 8+; * the downgraded PHP 7 build widens them back to the core Reflection ones. diff --git a/src/Type/Php/RoundFunctionThrowTypeExtension.php b/src/Type/Php/RoundFunctionThrowTypeExtension.php index 7a79ec7b2b1..2f3af9c16ce 100644 --- a/src/Type/Php/RoundFunctionThrowTypeExtension.php +++ b/src/Type/Php/RoundFunctionThrowTypeExtension.php @@ -5,7 +5,6 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\DynamicFunctionThrowTypeExtension; use PHPStan\Type\IntegerRangeType; @@ -23,10 +22,6 @@ final class RoundFunctionThrowTypeExtension implements DynamicFunctionThrowTypeExtension { - public function __construct(private PhpVersion $phpVersion) - { - } - public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'round'; @@ -34,7 +29,7 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getThrowTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $funcCall, Scope $scope): ?Type { - if (!$this->phpVersion->throwsValueErrorForInvalidRoundingMode()) { + if ($scope->getPhpVersion()->throwsValueErrorForInvalidRoundingMode()->no()) { return new VoidType(); } diff --git a/src/Type/Php/StrSplitFunctionThrowTypeExtension.php b/src/Type/Php/StrSplitFunctionThrowTypeExtension.php index bcc1815efad..10891dbc717 100644 --- a/src/Type/Php/StrSplitFunctionThrowTypeExtension.php +++ b/src/Type/Php/StrSplitFunctionThrowTypeExtension.php @@ -5,7 +5,6 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\DynamicFunctionThrowTypeExtension; use PHPStan\Type\IntegerRangeType; @@ -20,10 +19,6 @@ final class StrSplitFunctionThrowTypeExtension implements DynamicFunctionThrowTypeExtension { - public function __construct(private PhpVersion $phpVersion) - { - } - public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'str_split'; @@ -31,7 +26,7 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getThrowTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $funcCall, Scope $scope): ?Type { - if (!$this->phpVersion->throwsValueErrorForInternalFunctions()) { + if ($scope->getPhpVersion()->throwsValueErrorForInternalFunctions()->no()) { return new VoidType(); } diff --git a/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/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 56816b0ad2a14e58627ee8c26ff3503dcb9ef586 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Tue, 22 Sep 2026 10:58:28 +0000 Subject: [PATCH 03/21] Discover scope-aware extensions in the build rule instead of listing them The rule only knew nine hardcoded extension interfaces, so parameter-out, parameter-closure, expression-type-resolver, ignore-error and restricted-usage extensions could keep injecting PhpVersion unnoticed - and any extension interface added later would have to be added here by hand. It now reports every class that implements an interface marked with #[ExtensionInterface] declaring a method that takes the public Scope. Co-Authored-By: Claude Opus 5 --- ...tedPhpVersionInScopeAwareExtensionRule.php | 101 ++++++++++++------ ...hpVersionInScopeAwareExtensionRuleTest.php | 10 +- .../Build/data/php-version-in-extension.php | 72 +++++++++++++ 3 files changed, 148 insertions(+), 35 deletions(-) diff --git a/build/PHPStan/Build/NoInjectedPhpVersionInScopeAwareExtensionRule.php b/build/PHPStan/Build/NoInjectedPhpVersionInScopeAwareExtensionRule.php index 01b9d113d15..d51acdbf162 100644 --- a/build/PHPStan/Build/NoInjectedPhpVersionInScopeAwareExtensionRule.php +++ b/build/PHPStan/Build/NoInjectedPhpVersionInScopeAwareExtensionRule.php @@ -4,43 +4,34 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\ExtensionInterface; use PHPStan\Node\InClassNode; use PHPStan\Php\PhpVersion; +use PHPStan\Reflection\ClassReflection; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\Type\DynamicFunctionReturnTypeExtension; -use PHPStan\Type\DynamicFunctionThrowTypeExtension; -use PHPStan\Type\DynamicMethodReturnTypeExtension; -use PHPStan\Type\DynamicMethodThrowTypeExtension; -use PHPStan\Type\DynamicStaticMethodReturnTypeExtension; -use PHPStan\Type\DynamicStaticMethodThrowTypeExtension; -use PHPStan\Type\FunctionTypeSpecifyingExtension; -use PHPStan\Type\MethodTypeSpecifyingExtension; -use PHPStan\Type\StaticMethodTypeSpecifyingExtension; +use function array_key_exists; use function sprintf; /** - * Every one of these extension interfaces hands the call's Scope to the extension, - * so the analysed PHP version must be read from Scope::getPhpVersion(). A DI-injected - * PhpVersion always answers with the configured version and silently ignores both - * PHP_VERSION_ID narrowing in the analysed code and configured version ranges. + * An extension that is handed the call's Scope must read the analysed PHP version from + * Scope::getPhpVersion(). A DI-injected PhpVersion always answers with the configured version + * and silently ignores both PHP_VERSION_ID narrowing in the analysed code and configured + * version ranges. + * + * Scope-aware extensions are discovered instead of listed: every interface marked with + * #[ExtensionInterface] that declares a method taking the public Scope counts, so extension + * interfaces added later are covered automatically. Interfaces taking the engine-internal + * MutatingScope (ExprHandler, StmtHandler) are not extensions in this sense, and neither is + * PHPStan\Rules\Rule, whose processNode() takes Scope&NodeCallbackInvoker&CollectedDataEmitter. * * @implements Rule */ 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, - ]; + /** @var array */ + private array $scopeAwareExtensions = []; public function getNodeType(): string { @@ -54,16 +45,7 @@ public function processNode(Node $node, Scope $scope): array return []; } - $implementedExtension = null; - foreach (self::SCOPE_AWARE_EXTENSIONS as $extensionInterface) { - if (!$classReflection->is($extensionInterface)) { - continue; - } - - $implementedExtension = $extensionInterface; - break; - } - + $implementedExtension = $this->findScopeAwareExtension($classReflection); if ($implementedExtension === null) { return []; } @@ -88,4 +70,55 @@ public function processNode(Node $node, Scope $scope): array return $errors; } + private function findScopeAwareExtension(ClassReflection $classReflection): ?string + { + foreach ($classReflection->getInterfaces() as $interface) { + if (!$this->isScopeAwareExtension($interface)) { + continue; + } + + return $interface->getName(); + } + + return null; + } + + private function isScopeAwareExtension(ClassReflection $interface): bool + { + $interfaceName = $interface->getName(); + if (!array_key_exists($interfaceName, $this->scopeAwareExtensions)) { + $this->scopeAwareExtensions[$interfaceName] = $this->isExtensionInterface($interface) + && $this->acceptsScope($interface); + } + + return $this->scopeAwareExtensions[$interfaceName]; + } + + private function isExtensionInterface(ClassReflection $interface): bool + { + foreach ($interface->getAttributes() as $attribute) { + if ($attribute->getName() === ExtensionInterface::class) { + return true; + } + } + + return false; + } + + private function acceptsScope(ClassReflection $interface): bool + { + foreach ($interface->getNativeReflection()->getMethods() as $nativeMethod) { + $method = $interface->getNativeMethod($nativeMethod->getName()); + foreach ($method->getOnlyVariant()->getParameters() as $parameter) { + if ($parameter->getType()->getObjectClassNames() !== [Scope::class]) { + continue; + } + + return true; + } + } + + return false; + } + } diff --git a/tests/PHPStan/Build/NoInjectedPhpVersionInScopeAwareExtensionRuleTest.php b/tests/PHPStan/Build/NoInjectedPhpVersionInScopeAwareExtensionRuleTest.php index e2b90c5ea82..45a45f1cc00 100644 --- a/tests/PHPStan/Build/NoInjectedPhpVersionInScopeAwareExtensionRuleTest.php +++ b/tests/PHPStan/Build/NoInjectedPhpVersionInScopeAwareExtensionRuleTest.php @@ -21,7 +21,15 @@ 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.', - 14, + 23, + ], + [ + 'PhpVersionInExtension\InjectsPhpVersionInParameterOutExtension implements PHPStan\Type\MethodParameterOutTypeExtension and must not inject PHPStan\Php\PhpVersion - read the analysed PHP version from Scope::getPhpVersion() instead.', + 65, + ], + [ + 'PhpVersionInExtension\InjectsPhpVersionInRestrictedUsageExtension implements PHPStan\Rules\RestrictedUsage\RestrictedMethodUsageExtension and must not inject PHPStan\Php\PhpVersion - read the analysed PHP version from Scope::getPhpVersion() instead.', + 84, ], ]); } diff --git a/tests/PHPStan/Build/data/php-version-in-extension.php b/tests/PHPStan/Build/data/php-version-in-extension.php index 099c3d7524b..b8bc1f7fce0 100644 --- a/tests/PHPStan/Build/data/php-version-in-extension.php +++ b/tests/PHPStan/Build/data/php-version-in-extension.php @@ -4,11 +4,20 @@ 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; @@ -55,6 +64,43 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } +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 { @@ -73,3 +119,29 @@ public function specifyType(string $operatorSigil, Type $leftSide, Type $rightSi } } + +/** + * @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 037ddad34f9fd63202e01a4cf97fffd74a929421 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Tue, 22 Sep 2026 11:57:23 +0000 Subject: [PATCH 04/21] List the scope-aware extension interfaces in the build rule explicitly Reverts the reflection-based discovery: the rule again holds a constant with the extension interfaces whose methods take the public Scope, now covering all of them - the return-type, throw-type and type-specifying interfaces plus the parameter-out, parameter-closure-type, parameter-closure-this, restricted-usage, expression-type-resolver, ignore-error and collector ones. ExprHandler/StmtHandler (MutatingScope) and OperatorTypeSpecifyingExtension (no Scope at all) stay out, as documented in the class docblock. Co-Authored-By: Claude Opus 5 --- ...tedPhpVersionInScopeAwareExtensionRule.php | 138 +++++++++--------- 1 file changed, 72 insertions(+), 66 deletions(-) diff --git a/build/PHPStan/Build/NoInjectedPhpVersionInScopeAwareExtensionRule.php b/build/PHPStan/Build/NoInjectedPhpVersionInScopeAwareExtensionRule.php index d51acdbf162..8dc08f09678 100644 --- a/build/PHPStan/Build/NoInjectedPhpVersionInScopeAwareExtensionRule.php +++ b/build/PHPStan/Build/NoInjectedPhpVersionInScopeAwareExtensionRule.php @@ -3,35 +3,83 @@ namespace PHPStan\Build; use PhpParser\Node; +use PHPStan\Analyser\IgnoreErrorExtension; use PHPStan\Analyser\Scope; -use PHPStan\DependencyInjection\ExtensionInterface; +use PHPStan\Collectors\Collector; use PHPStan\Node\InClassNode; use PHPStan\Php\PhpVersion; -use PHPStan\Reflection\ClassReflection; +use PHPStan\Rules\RestrictedUsage\RestrictedClassConstantUsageExtension; +use PHPStan\Rules\RestrictedUsage\RestrictedClassNameUsageExtension; +use PHPStan\Rules\RestrictedUsage\RestrictedFunctionUsageExtension; +use PHPStan\Rules\RestrictedUsage\RestrictedMethodUsageExtension; +use PHPStan\Rules\RestrictedUsage\RestrictedPropertyUsageExtension; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -use function array_key_exists; +use PHPStan\Type\DynamicFunctionReturnTypeExtension; +use PHPStan\Type\DynamicFunctionThrowTypeExtension; +use PHPStan\Type\DynamicMethodReturnTypeExtension; +use PHPStan\Type\DynamicMethodThrowTypeExtension; +use PHPStan\Type\DynamicStaticMethodReturnTypeExtension; +use PHPStan\Type\DynamicStaticMethodThrowTypeExtension; +use PHPStan\Type\ExpressionTypeResolverExtension; +use PHPStan\Type\FunctionParameterClosureThisExtension; +use PHPStan\Type\FunctionParameterClosureTypeExtension; +use PHPStan\Type\FunctionParameterOutTypeExtension; +use PHPStan\Type\FunctionTypeSpecifyingExtension; +use PHPStan\Type\MethodParameterClosureThisExtension; +use PHPStan\Type\MethodParameterClosureTypeExtension; +use PHPStan\Type\MethodParameterOutTypeExtension; +use PHPStan\Type\MethodTypeSpecifyingExtension; +use PHPStan\Type\StaticMethodParameterClosureThisExtension; +use PHPStan\Type\StaticMethodParameterClosureTypeExtension; +use PHPStan\Type\StaticMethodParameterOutTypeExtension; +use PHPStan\Type\StaticMethodTypeSpecifyingExtension; use function sprintf; /** - * An extension that is handed the call's Scope must read the analysed PHP version from - * Scope::getPhpVersion(). A DI-injected PhpVersion always answers with the configured version - * and silently ignores both PHP_VERSION_ID narrowing in the analysed code and configured - * version ranges. + * Every one of these extension interfaces hands the call's Scope to the extension, + * so the analysed PHP version must be read from Scope::getPhpVersion(). A DI-injected + * PhpVersion always answers with the configured version and silently ignores both + * PHP_VERSION_ID narrowing in the analysed code and configured version ranges. * - * Scope-aware extensions are discovered instead of listed: every interface marked with - * #[ExtensionInterface] that declares a method taking the public Scope counts, so extension - * interfaces added later are covered automatically. Interfaces taking the engine-internal - * MutatingScope (ExprHandler, StmtHandler) are not extensions in this sense, and neither is - * PHPStan\Rules\Rule, whose processNode() takes Scope&NodeCallbackInvoker&CollectedDataEmitter. + * The list holds every extension interface with a method taking the public Scope. + * Interfaces taking the engine-internal MutatingScope (ExprHandler, StmtHandler) are not + * extensions in this sense, and OperatorTypeSpecifyingExtension gets no Scope at all, + * so both are absent on purpose. An extension interface added later belongs here too. * * @implements Rule */ final class NoInjectedPhpVersionInScopeAwareExtensionRule implements Rule { - /** @var array */ - private array $scopeAwareExtensions = []; + 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 { @@ -45,7 +93,16 @@ public function processNode(Node $node, Scope $scope): array return []; } - $implementedExtension = $this->findScopeAwareExtension($classReflection); + $implementedExtension = null; + foreach (self::SCOPE_AWARE_EXTENSIONS as $extensionInterface) { + if (!$classReflection->is($extensionInterface)) { + continue; + } + + $implementedExtension = $extensionInterface; + break; + } + if ($implementedExtension === null) { return []; } @@ -70,55 +127,4 @@ public function processNode(Node $node, Scope $scope): array return $errors; } - private function findScopeAwareExtension(ClassReflection $classReflection): ?string - { - foreach ($classReflection->getInterfaces() as $interface) { - if (!$this->isScopeAwareExtension($interface)) { - continue; - } - - return $interface->getName(); - } - - return null; - } - - private function isScopeAwareExtension(ClassReflection $interface): bool - { - $interfaceName = $interface->getName(); - if (!array_key_exists($interfaceName, $this->scopeAwareExtensions)) { - $this->scopeAwareExtensions[$interfaceName] = $this->isExtensionInterface($interface) - && $this->acceptsScope($interface); - } - - return $this->scopeAwareExtensions[$interfaceName]; - } - - private function isExtensionInterface(ClassReflection $interface): bool - { - foreach ($interface->getAttributes() as $attribute) { - if ($attribute->getName() === ExtensionInterface::class) { - return true; - } - } - - return false; - } - - private function acceptsScope(ClassReflection $interface): bool - { - foreach ($interface->getNativeReflection()->getMethods() as $nativeMethod) { - $method = $interface->getNativeMethod($nativeMethod->getName()); - foreach ($method->getOnlyVariant()->getParameters() as $parameter) { - if ($parameter->getType()->getObjectClassNames() !== [Scope::class]) { - continue; - } - - return true; - } - } - - return false; - } - } From add898464f202785c29598c9c428ae8f8c85f0f3 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Tue, 22 Sep 2026 12:01:22 +0000 Subject: [PATCH 05/21] Branch on the version check with a plain if() instead of PhpVersions::pickType() The extensions converted to Scope::getPhpVersion() went through a PhpVersions::pickType() helper that mapped a TrinaryLogic to one of two types. Inline the branching at every call site and drop the helper. Behaviour is unchanged: where the union of both branches collapses to one of them (never|false is false, null|never is null, true|bool is bool), the plain if() returns that branch on Maybe; where both remain possible - substr()'s false vs empty string, str_split('')'s [] vs [''] and mb_substitute_character() with null - both are still added to the result. Co-Authored-By: Claude Opus 5 --- src/Php/PhpVersions.php | 20 ------- .../ArrayChunkFunctionReturnTypeExtension.php | 13 +++-- src/Type/Php/ArrayColumnHelper.php | 7 ++- ...rrayCombineFunctionReturnTypeExtension.php | 7 ++- .../ArrayFillFunctionReturnTypeExtension.php | 7 ++- ...rayFillKeysFunctionReturnTypeExtension.php | 7 ++- .../ArrayFlipFunctionReturnTypeExtension.php | 7 ++- ...ntersectKeyFunctionReturnTypeExtension.php | 7 ++- ...KeysFunctionDynamicReturnTypeExtension.php | 7 ++- ...rrayReverseFunctionReturnTypeExtension.php | 7 ++- ...archFunctionDynamicReturnTypeExtension.php | 7 ++- .../ArraySliceFunctionReturnTypeExtension.php | 7 ++- ...ArraySpliceFunctionReturnTypeExtension.php | 7 ++- ...luesFunctionDynamicReturnTypeExtension.php | 7 ++- ...eIntervalConstructorThrowTypeExtension.php | 11 ++-- ...DateIntervalDynamicReturnTypeExtension.php | 11 ++-- .../DateTimeConstructorThrowTypeExtension.php | 11 ++-- ...DateTimeModifyMethodThrowTypeExtension.php | 11 ++-- .../Php/DateTimeModifyReturnTypeExtension.php | 11 ++-- ...eTimeZoneConstructorThrowTypeExtension.php | 11 ++-- .../Php/HashFunctionsReturnTypeExtension.php | 7 ++- ...hlightStringDynamicReturnTypeExtension.php | 15 +++--- .../Php/MbFunctionsReturnTypeExtension.php | 17 +++--- .../MbStrlenFunctionReturnTypeExtension.php | 7 ++- ...uteCharacterDynamicReturnTypeExtension.php | 52 +++++++++++-------- .../Php/RoundFunctionReturnTypeExtension.php | 11 ++-- .../StrSplitFunctionReturnTypeExtension.php | 24 ++++++--- .../Php/SubstrDynamicReturnTypeExtension.php | 12 ++--- ...TriggerErrorDynamicReturnTypeExtension.php | 11 ++-- 29 files changed, 189 insertions(+), 150 deletions(-) diff --git a/src/Php/PhpVersions.php b/src/Php/PhpVersions.php index 18c2c69e89e..188728fd5fc 100644 --- a/src/Php/PhpVersions.php +++ b/src/Php/PhpVersions.php @@ -6,7 +6,6 @@ use PHPStan\Turbo\ReferencedByTurboExtension; use PHPStan\Type\IntegerRangeType; use PHPStan\Type\Type; -use PHPStan\Type\TypeCombinator; /** * Range-aware PHP version check that handles version uncertainty. @@ -291,23 +290,4 @@ public function supportsNativeReflectionAdapterReturnTypes(): TrinaryLogic return IntegerRangeType::fromInterval(80000, null)->isSuperTypeOf($this->phpVersions)->result; } - /** - * Resolves a version-dependent type. - * - * When the analysed PHP version range spans both sides of $versionCheck, - * neither branch can be ruled out and the union of both is returned. - */ - public static function pickType(TrinaryLogic $versionCheck, Type $ifYes, Type $ifNo): Type - { - if ($versionCheck->yes()) { - return $ifYes; - } - - if ($versionCheck->no()) { - return $ifNo; - } - - return TypeCombinator::union($ifYes, $ifNo); - } - } diff --git a/src/Type/Php/ArrayChunkFunctionReturnTypeExtension.php b/src/Type/Php/ArrayChunkFunctionReturnTypeExtension.php index f9051b20876..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\PhpVersions; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; @@ -33,13 +32,21 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $arrayType = $scope->getType($args[0]->value); if ($arrayType->isArray()->no()) { - return PhpVersions::pickType($scope->getPhpVersion()->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 PhpVersions::pickType($scope->getPhpVersion()->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/ArrayColumnHelper.php b/src/Type/Php/ArrayColumnHelper.php index 3517ebf4db5..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\PhpVersions; use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\NonEmptyArrayType; @@ -220,7 +219,11 @@ private function castToArrayKeyType(Type $type, Scope $scope): Type $throwsTypeError = $scope->getPhpVersion()->throwsTypeErrorForInternalFunctions(); $isArray = $type->isArray(); if ($isArray->yes()) { - return PhpVersions::pickType($throwsTypeError, new NeverType(), new IntegerType()); + if ($throwsTypeError->yes()) { + return new NeverType(); + } + + return new IntegerType(); } if ($isArray->no()) { return $type->toArrayKey(); diff --git a/src/Type/Php/ArrayCombineFunctionReturnTypeExtension.php b/src/Type/Php/ArrayCombineFunctionReturnTypeExtension.php index 6b766631ea4..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\PhpVersions; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; @@ -46,7 +45,11 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $throwsValueError = $scope->getPhpVersion()->throwsValueErrorForInternalFunctions(); if ($hasValueError->yes()) { - return PhpVersions::pickType($throwsValueError, new NeverType(), new ConstantBooleanType(false)); + if ($throwsValueError->yes()) { + return new NeverType(); + } + + return new ConstantBooleanType(false); } if ($throwsValueError->yes()) { diff --git a/src/Type/Php/ArrayFillFunctionReturnTypeExtension.php b/src/Type/Php/ArrayFillFunctionReturnTypeExtension.php index 0e18a9dd2dd..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\PhpVersions; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\NonEmptyArrayType; @@ -46,7 +45,11 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, // check against negative-int, which is not allowed if ($isValidNumberType->no()) { - return PhpVersions::pickType($throwsValueError, new NeverType(), new ConstantBooleanType(false)); + if ($throwsValueError->yes()) { + return new NeverType(); + } + + return new ConstantBooleanType(false); } $startIndexType = $scope->getType($args[0]->value); diff --git a/src/Type/Php/ArrayFillKeysFunctionReturnTypeExtension.php b/src/Type/Php/ArrayFillKeysFunctionReturnTypeExtension.php index faf8a672536..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\PhpVersions; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; @@ -33,7 +32,11 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $keysType = $scope->getType($args[0]->value); if ($keysType->isArray()->no()) { - return PhpVersions::pickType($scope->getPhpVersion()->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/ArrayFlipFunctionReturnTypeExtension.php b/src/Type/Php/ArrayFlipFunctionReturnTypeExtension.php index ba4f722ad50..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\PhpVersions; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; @@ -33,7 +32,11 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $arrayType = $scope->getType($args[0]->value); if ($arrayType->isArray()->no()) { - return PhpVersions::pickType($scope->getPhpVersion()->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 680f4c57ef7..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\PhpVersions; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\NeverType; @@ -47,7 +46,11 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $onlyOneArrayGiven = count($argTypes) === 1; if ($firstArrayType->isArray()->no() || (!$onlyOneArrayGiven && $otherArraysType->isArray()->no())) { - return PhpVersions::pickType($scope->getPhpVersion()->arrayFunctionsReturnNullWithNonArray(), new NullType(), new NeverType()); + if ($scope->getPhpVersion()->arrayFunctionsReturnNullWithNonArray()->no()) { + return new NeverType(); + } + + return new NullType(); } if ($onlyOneArrayGiven) { diff --git a/src/Type/Php/ArrayKeysFunctionDynamicReturnTypeExtension.php b/src/Type/Php/ArrayKeysFunctionDynamicReturnTypeExtension.php index c331f4cd8ed..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\PhpVersions; use PHPStan\Reflection\FunctionReflection; use PHPStan\TrinaryLogic; use PHPStan\Type\DynamicFunctionReturnTypeExtension; @@ -33,7 +32,11 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $arrayType = $scope->getType($args[0]->value); if ($arrayType->isArray()->no()) { - return PhpVersions::pickType($scope->getPhpVersion()->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 b39170dcd25..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\PhpVersions; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; @@ -31,7 +30,11 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $type = $scope->getType($args[0]->value); if ($type->isArray()->no()) { - return PhpVersions::pickType($scope->getPhpVersion()->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 4958afc8285..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\PhpVersions; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; @@ -33,7 +32,11 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $haystackArgType = $scope->getType($args[1]->value); if ($haystackArgType->isArray()->no()) { - return PhpVersions::pickType($scope->getPhpVersion()->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 d2437ffc937..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\PhpVersions; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; @@ -32,7 +31,11 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $arrayType = $scope->getType($args[0]->value); if ($arrayType->isArray()->no()) { - return PhpVersions::pickType($scope->getPhpVersion()->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 be1782bffe7..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\PhpVersions; use PHPStan\Reflection\FunctionReflection; use PHPStan\TrinaryLogic; use PHPStan\Type\DynamicFunctionReturnTypeExtension; @@ -36,7 +35,11 @@ public function getTypeFromFunctionCall( $arrayType = $scope->getType($args[0]->value); if ($arrayType->isArray()->no()) { - return PhpVersions::pickType($scope->getPhpVersion()->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 9c86e30873c..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\PhpVersions; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\NeverType; @@ -32,7 +31,11 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $arrayType = $scope->getType($args[0]->value); if ($arrayType->isArray()->no()) { - return PhpVersions::pickType($scope->getPhpVersion()->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/DateIntervalConstructorThrowTypeExtension.php b/src/Type/Php/DateIntervalConstructorThrowTypeExtension.php index 9635617e679..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\PhpVersions; use PHPStan\Reflection\MethodReflection; use PHPStan\Type\DynamicStaticMethodThrowTypeExtension; use PHPStan\Type\NeverType; @@ -53,11 +52,11 @@ public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflect private function exceptionType(Scope $scope): Type { - return PhpVersions::pickType( - $scope->getPhpVersion()->hasDateTimeExceptions(), - new ObjectType('DateMalformedIntervalStringException'), - new ObjectType('Exception'), - ); + if ($scope->getPhpVersion()->hasDateTimeExceptions()->yes()) { + return new ObjectType('DateMalformedIntervalStringException'); + } + + return new ObjectType('Exception'); } } diff --git a/src/Type/Php/DateIntervalDynamicReturnTypeExtension.php b/src/Type/Php/DateIntervalDynamicReturnTypeExtension.php index f073eaa0f17..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\PhpVersions; use PHPStan\Reflection\MethodReflection; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\DynamicStaticMethodReturnTypeExtension; @@ -57,11 +56,11 @@ public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, if ($hasFalse) { if (!$hasDateInterval) { - return PhpVersions::pickType( - $scope->getPhpVersion()->hasDateTimeExceptions(), - new NeverType(), - new ConstantBooleanType(false), - ); + if ($scope->getPhpVersion()->hasDateTimeExceptions()->yes()) { + return new NeverType(); + } + + return new ConstantBooleanType(false); } return null; diff --git a/src/Type/Php/DateTimeConstructorThrowTypeExtension.php b/src/Type/Php/DateTimeConstructorThrowTypeExtension.php index fb5ba743b3c..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\PhpVersions; use PHPStan\Reflection\MethodReflection; use PHPStan\Type\DynamicStaticMethodThrowTypeExtension; use PHPStan\Type\NeverType; @@ -55,11 +54,11 @@ public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflect private function exceptionType(Scope $scope): Type { - return PhpVersions::pickType( - $scope->getPhpVersion()->hasDateTimeExceptions(), - new ObjectType('DateMalformedStringException'), - new ObjectType('Exception'), - ); + if ($scope->getPhpVersion()->hasDateTimeExceptions()->yes()) { + return new ObjectType('DateMalformedStringException'); + } + + return new ObjectType('Exception'); } } diff --git a/src/Type/Php/DateTimeModifyMethodThrowTypeExtension.php b/src/Type/Php/DateTimeModifyMethodThrowTypeExtension.php index 109356ae1ce..e61bb177215 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\PhpVersions; use PHPStan\Reflection\MethodReflection; use PHPStan\Type\DynamicMethodThrowTypeExtension; use PHPStan\Type\NeverType; @@ -60,11 +59,11 @@ public function getThrowTypeFromMethodCall(MethodReflection $methodReflection, M private function exceptionType(Scope $scope): Type { - return PhpVersions::pickType( - $scope->getPhpVersion()->hasDateTimeExceptions(), - new ObjectType('DateMalformedStringException'), - new ObjectType('Exception'), - ); + if ($scope->getPhpVersion()->hasDateTimeExceptions()->yes()) { + return new ObjectType('DateMalformedStringException'); + } + + return new ObjectType('Exception'); } } diff --git a/src/Type/Php/DateTimeModifyReturnTypeExtension.php b/src/Type/Php/DateTimeModifyReturnTypeExtension.php index 0147fe08484..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\PhpVersions; use PHPStan\Reflection\MethodReflection; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\DynamicMethodReturnTypeExtension; @@ -74,11 +73,11 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method if ($hasFalse) { if (!$hasDateTime) { - return PhpVersions::pickType( - $scope->getPhpVersion()->hasDateTimeExceptions(), - new NeverType(), - new ConstantBooleanType(false), - ); + if ($scope->getPhpVersion()->hasDateTimeExceptions()->yes()) { + return new NeverType(); + } + + return new ConstantBooleanType(false); } return null; diff --git a/src/Type/Php/DateTimeZoneConstructorThrowTypeExtension.php b/src/Type/Php/DateTimeZoneConstructorThrowTypeExtension.php index 60b50896d34..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\PhpVersions; use PHPStan\Reflection\MethodReflection; use PHPStan\Type\DynamicStaticMethodThrowTypeExtension; use PHPStan\Type\NeverType; @@ -53,11 +52,11 @@ public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflect private function exceptionType(Scope $scope): Type { - return PhpVersions::pickType( - $scope->getPhpVersion()->hasDateTimeExceptions(), - new ObjectType('DateInvalidTimeZoneException'), - new ObjectType('Exception'), - ); + if ($scope->getPhpVersion()->hasDateTimeExceptions()->yes()) { + return new ObjectType('DateInvalidTimeZoneException'); + } + + return new ObjectType('Exception'); } } diff --git a/src/Type/Php/HashFunctionsReturnTypeExtension.php b/src/Type/Php/HashFunctionsReturnTypeExtension.php index a79791b54ab..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\PhpVersions; use PHPStan\Reflection\FunctionReflection; use PHPStan\ShouldNotHappenException; use PHPStan\Type\Accessory\AccessoryLowercaseStringType; @@ -134,7 +133,11 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $neverType = new NeverType(); $falseType = new ConstantBooleanType(false); - $invalidAlgorithmType = PhpVersions::pickType($throwsValueError, $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 354f92e5843..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\PhpVersions; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\BooleanType; use PHPStan\Type\Constant\ConstantBooleanType; @@ -27,16 +26,18 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, { $doesNotReturnFalse = $scope->getPhpVersion()->highlightStringDoesNotReturnFalse(); $args = $functionCall->getArgs(); - if (count($args) < 2) { - return PhpVersions::pickType($doesNotReturnFalse, new ConstantBooleanType(true), new BooleanType()); + if (count($args) >= 2) { + $returnType = $scope->getType($args[1]->value); + if ($returnType->isTrue()->yes()) { + return new StringType(); + } } - $returnType = $scope->getType($args[1]->value); - if ($returnType->isTrue()->yes()) { - return new StringType(); + if ($doesNotReturnFalse->yes()) { + return new ConstantBooleanType(true); } - return PhpVersions::pickType($doesNotReturnFalse, new ConstantBooleanType(true), new BooleanType()); + return new BooleanType(); } } diff --git a/src/Type/Php/MbFunctionsReturnTypeExtension.php b/src/Type/Php/MbFunctionsReturnTypeExtension.php index 80d31c464d0..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\PhpVersions; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\BooleanType; @@ -66,15 +65,15 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } if (count($results) === 1) { - $invalidEncodingReturn = PhpVersions::pickType( - $phpVersions->throwsOnInvalidMbStringEncoding(), - new NeverType(), - new ConstantBooleanType(false), - ); + if ($results[0]) { + return TypeCombinator::remove($returnType, new ConstantBooleanType(false)); + } - return $results[0] - ? TypeCombinator::remove($returnType, new ConstantBooleanType(false)) - : $invalidEncodingReturn; + if ($phpVersions->throwsOnInvalidMbStringEncoding()->yes()) { + return new NeverType(); + } + + return new ConstantBooleanType(false); } return $returnType; diff --git a/src/Type/Php/MbStrlenFunctionReturnTypeExtension.php b/src/Type/Php/MbStrlenFunctionReturnTypeExtension.php index f4348096038..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\PhpVersions; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\ShouldNotHappenException; @@ -83,7 +82,11 @@ public function getTypeFromFunctionCall( $encodings = array_unique($encodings); if (in_array(self::UNSUPPORTED_ENCODING, $encodings, true) && count($encodings) === 1) { - return PhpVersions::pickType($throwsOnInvalidEncoding, new NeverType(), new ConstantBooleanType(false)); + 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($phpVersions), [self::UNSUPPORTED_ENCODING]); diff --git a/src/Type/Php/MbSubstituteCharacterDynamicReturnTypeExtension.php b/src/Type/Php/MbSubstituteCharacterDynamicReturnTypeExtension.php index b2cbecd10cc..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\PhpVersions; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\BooleanType; use PHPStan\Type\Constant\ConstantBooleanType; @@ -63,11 +62,11 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $isInteger = $argType->isInteger(); if ($isString->no() && $isNull->no() && $isInteger->no()) { - return PhpVersions::pickType( - $phpVersions->throwsTypeErrorForInternalFunctions(), - new NeverType(), - new BooleanType(), - ); + if ($phpVersions->throwsTypeErrorForInternalFunctions()->yes()) { + return new NeverType(); + } + + return new BooleanType(); } if ($isInteger->yes()) { @@ -78,15 +77,19 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } if (!self::isPossiblyInRanges($possibleRanges, $argType)) { - return PhpVersions::pickType( - $phpVersions->throwsValueErrorForInternalFunctions(), - new NeverType(), - new ConstantBooleanType(false), - ); + if ($phpVersions->throwsValueErrorForInternalFunctions()->yes()) { + return new NeverType(); + } + + return new ConstantBooleanType(false); } } elseif ($isString->yes()) { if ($argType->isNonEmptyString()->no()) { - return PhpVersions::pickType($isEmptyStringValidAlias, new ConstantBooleanType(true), new NeverType()); + if ($isEmptyStringValidAlias->no()) { + return new NeverType(); + } + + return new ConstantBooleanType(true); } if ($phpVersions->isNumericStringValidArgInMbSubstituteCharacter()->no() && $argType->isNumericString()->yes()) { @@ -116,19 +119,24 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return new BooleanType(); } - return PhpVersions::pickType( - $phpVersions->throwsValueErrorForInternalFunctions(), - new NeverType(), - new ConstantBooleanType(false), - ); + if ($phpVersions->throwsValueErrorForInternalFunctions()->yes()) { + return new NeverType(); + } + + return new ConstantBooleanType(false); } } elseif ($isNull->yes()) { // The $substitute_character arg is nullable in PHP 8+ - return PhpVersions::pickType( - $phpVersions->isNullValidArgInMbSubstituteCharacter(), - new ConstantBooleanType(true), - new ConstantBooleanType(false), - ); + $isNullValidArg = $phpVersions->isNullValidArgInMbSubstituteCharacter(); + if ($isNullValidArg->yes()) { + return new ConstantBooleanType(true); + } + + if ($isNullValidArg->no()) { + return new ConstantBooleanType(false); + } + + return new BooleanType(); } return new BooleanType(); diff --git a/src/Type/Php/RoundFunctionReturnTypeExtension.php b/src/Type/Php/RoundFunctionReturnTypeExtension.php index a3db19f8bdd..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\PhpVersions; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Accessory\AccessoryNumericStringType; use PHPStan\Type\BooleanType; @@ -48,11 +47,13 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $hasStricterRoundFunctions = $scope->getPhpVersion()->hasStricterRoundFunctions(); - // PHP 8 fatals with a missing parameter, PHP 7 returns null. - $noArgsReturnType = PhpVersions::pickType($hasStricterRoundFunctions, new NeverType(true), new NullType()); - 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); diff --git a/src/Type/Php/StrSplitFunctionReturnTypeExtension.php b/src/Type/Php/StrSplitFunctionReturnTypeExtension.php index 49625abeb63..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\PhpVersions; use PHPStan\Reflection\FunctionReflection; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; @@ -65,7 +64,11 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, if ($splitLengthType instanceof ConstantIntegerType) { $splitLength = $splitLengthType->getValue(); if ($splitLength < 1) { - return PhpVersions::pickType($throwsValueError, new NeverType(), new ConstantBooleanType(false)); + if ($throwsValueError->yes()) { + return new NeverType(); + } + + return new ConstantBooleanType(false); } } @@ -78,7 +81,11 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, if (count($values) === 1) { $encoding = $values[0]; if (!$this->isSupportedEncoding($encoding, $phpVersions)) { - return PhpVersions::pickType($throwsValueError, new NeverType(), new ConstantBooleanType(false)); + if ($throwsValueError->yes()) { + return new NeverType(); + } + + return new ConstantBooleanType(false); } } } else { @@ -99,11 +106,12 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, if ($encoding === null && $value === '') { // Simulate the str_split call with the analysed PHP Version instead of the runtime one. - $results[] = PhpVersions::pickType( - $returnsEmptyArray, - self::createConstantArrayFrom([], $scope), - self::createConstantArrayFrom([''], $scope), - ); + if (!$returnsEmptyArray->no()) { + $results[] = self::createConstantArrayFrom([], $scope); + } + if (!$returnsEmptyArray->yes()) { + $results[] = self::createConstantArrayFrom([''], $scope); + } continue; } diff --git a/src/Type/Php/SubstrDynamicReturnTypeExtension.php b/src/Type/Php/SubstrDynamicReturnTypeExtension.php index 201b8af99ad..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\PhpVersions; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; @@ -87,11 +86,12 @@ public function getTypeFromFunctionCall( ? $this->substrOrFalse($constantString->getValue(), $offset->getValue(), $length->getValue()) : $this->substrOrFalse($constantString->getValue(), $offset->getValue()); if ($substr === false) { - $results[] = PhpVersions::pickType( - $returnsFalseInsteadOfEmptyString, - new ConstantBooleanType(false), - new ConstantStringType(''), - ); + if (!$returnsFalseInsteadOfEmptyString->no()) { + $results[] = new ConstantBooleanType(false); + } + if (!$returnsFalseInsteadOfEmptyString->yes()) { + $results[] = new ConstantStringType(''); + } } else { $results[] = new ConstantStringType($substr); } diff --git a/src/Type/Php/TriggerErrorDynamicReturnTypeExtension.php b/src/Type/Php/TriggerErrorDynamicReturnTypeExtension.php index 330720277f3..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\PhpVersions; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; @@ -50,11 +49,11 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } if (!in_array($errorLevel, [E_USER_WARNING, E_USER_NOTICE, E_USER_DEPRECATED], true)) { - return PhpVersions::pickType( - $scope->getPhpVersion()->throwsValueErrorForInternalFunctions(), - new NeverType(true), - new ConstantBooleanType(false), - ); + if ($scope->getPhpVersion()->throwsValueErrorForInternalFunctions()->yes()) { + return new NeverType(true); + } + + return new ConstantBooleanType(false); } return new ConstantBooleanType(true); From 5ee6d3489768baf290a79db7d014dc9f6a4bb82a Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Tue, 22 Sep 2026 12:07:54 +0000 Subject: [PATCH 06/21] Move the printf format parser change to its own pull request PrintfFormatParser::parse() reading the analysed PHP version from the call site is a self-contained change with its own test, so it is reviewed separately. The parser keeps its injected PhpVersion here. PrintfFunctionThrowTypeExtension still resolves its own throwsValueErrorForInternalFunctions() check from Scope::getPhpVersion(), which is what this pull request is about. Co-Authored-By: Claude Opus 5 --- src/Php/PhpVersions.php | 5 ---- .../Functions/PrintfArrayParametersRule.php | 2 +- .../Functions/PrintfParameterTypeRule.php | 7 +++--- src/Rules/Functions/PrintfParametersRule.php | 7 +++--- src/Type/Php/PrintfFormatParser.php | 14 +++++++---- .../Php/PrintfFunctionThrowTypeExtension.php | 2 +- .../PrintfArrayParametersRuleTest.php | 4 ++- .../Functions/PrintfParameterTypeRuleTest.php | 4 ++- .../Functions/PrintfParametersRuleTest.php | 3 ++- .../Type/Php/PrintfFormatParserTest.php | 25 +++++++------------ 10 files changed, 34 insertions(+), 39 deletions(-) diff --git a/src/Php/PhpVersions.php b/src/Php/PhpVersions.php index 188728fd5fc..761ddc1f875 100644 --- a/src/Php/PhpVersions.php +++ b/src/Php/PhpVersions.php @@ -248,11 +248,6 @@ public function supportsAllUnicodeScalarCodePointsInMbSubstituteCharacter(): Tri 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; diff --git a/src/Rules/Functions/PrintfArrayParametersRule.php b/src/Rules/Functions/PrintfArrayParametersRule.php index d2c05be4417..b30c80ccd3d 100644 --- a/src/Rules/Functions/PrintfArrayParametersRule.php +++ b/src/Rules/Functions/PrintfArrayParametersRule.php @@ -67,7 +67,7 @@ public function processNode(Node $node, Scope $scope): array foreach ($formatArgType->getConstantStrings() as $formatString) { $format = $formatString->getValue(); - $uses = $this->printfFormatParser->parse($format, $scope->getPhpVersion()); + $uses = $this->printfFormatParser->parse($format); if ($uses === null) { return [ RuleErrorBuilder::message(sprintf( diff --git a/src/Rules/Functions/PrintfParameterTypeRule.php b/src/Rules/Functions/PrintfParameterTypeRule.php index 3ebbfc91d6e..060fe2b8049 100644 --- a/src/Rules/Functions/PrintfParameterTypeRule.php +++ b/src/Rules/Functions/PrintfParameterTypeRule.php @@ -4,7 +4,6 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; -use PHPStan\Php\PhpVersions; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -95,7 +94,7 @@ public function processNode(Node $node, Scope $scope): array $formatString = $formatArgTypeStrings[0]; $format = $formatString->getValue(); - $placeholderMap = $this->getPlaceholders($format, $scope->getPhpVersion()); + $placeholderMap = $this->getPlaceholders($format); if ($placeholderMap === null) { // Already reported by PrintfParametersRule. return []; @@ -177,9 +176,9 @@ public function processNode(Node $node, Scope $scope): array /** * @return array>|null parameter index => placeholders */ - private function getPlaceholders(string $format, PhpVersions $phpVersions): ?array + private function getPlaceholders(string $format): ?array { - $uses = $this->printfFormatParser->parse($format, $phpVersions); + $uses = $this->printfFormatParser->parse($format); if ($uses === null) { return null; } diff --git a/src/Rules/Functions/PrintfParametersRule.php b/src/Rules/Functions/PrintfParametersRule.php index 5a7e8faba87..a24dbabe4b6 100644 --- a/src/Rules/Functions/PrintfParametersRule.php +++ b/src/Rules/Functions/PrintfParametersRule.php @@ -6,7 +6,6 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\RegisteredRule; -use PHPStan\Php\PhpVersions; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -85,7 +84,7 @@ public function processNode(Node $node, Scope $scope): array $format = $formatString->getValue(); if (in_array($name, ['sprintf', 'printf'], true)) { - $tempPlaceHoldersCount = $this->getPrintfPlaceholdersCount($format, $scope->getPhpVersion()); + $tempPlaceHoldersCount = $this->getPrintfPlaceholdersCount($format); } else { $tempPlaceHoldersCount = $this->getScanfPlaceholdersCount($format); } @@ -130,9 +129,9 @@ public function processNode(Node $node, Scope $scope): array return []; } - private function getPrintfPlaceholdersCount(string $format, PhpVersions $phpVersions): ?int + private function getPrintfPlaceholdersCount(string $format): ?int { - $uses = $this->printfFormatParser->parse($format, $phpVersions); + $uses = $this->printfFormatParser->parse($format); if ($uses === null) { return null; } diff --git a/src/Type/Php/PrintfFormatParser.php b/src/Type/Php/PrintfFormatParser.php index ebf6b51a401..52c8b4f6891 100644 --- a/src/Type/Php/PrintfFormatParser.php +++ b/src/Type/Php/PrintfFormatParser.php @@ -3,7 +3,7 @@ namespace PHPStan\Type\Php; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Php\PhpVersions; +use PHPStan\Php\PhpVersion; use function in_array; use function ltrim; use function strlen; @@ -29,6 +29,10 @@ final class PrintfFormatParser private const ARG_NUM_NEXT = -1; + public function __construct(private PhpVersion $phpVersion) + { + } + /** * Returns the argument uses in the order the format consumes them, or null * when the format throws ValueError for any arguments. Index 0 is the first @@ -37,7 +41,7 @@ final class PrintfFormatParser * * @return list|null */ - public function parse(string $format, PhpVersions $phpVersions): ?array + public function parse(string $format): ?array { $length = strlen($format); $i = 0; @@ -134,7 +138,7 @@ public function parse(string $format, PhpVersions $phpVersions): ?array } $specifier = $format[$i]; - if (!self::isSpecifier($specifier, $phpVersions)) { + if (!$this->isSpecifier($specifier)) { return null; } @@ -210,13 +214,13 @@ private static function parseNumber(string $format, int $length, int &$i): int return (int) $digits; } - private static function isSpecifier(string $char, PhpVersions $phpVersions): bool + private function isSpecifier(string $char): bool { if (strpos(self::SPECIFIERS, $char) !== false) { return true; } - return !$phpVersions->supportsHhPrintfSpecifier()->no() && strpos(self::HH_SPECIFIERS, $char) !== false; + return $this->phpVersion->supportsHhPrintfSpecifier() && strpos(self::HH_SPECIFIERS, $char) !== false; } private static function isDigit(string $char): bool diff --git a/src/Type/Php/PrintfFunctionThrowTypeExtension.php b/src/Type/Php/PrintfFunctionThrowTypeExtension.php index c7ee32d4d71..e136b618512 100644 --- a/src/Type/Php/PrintfFunctionThrowTypeExtension.php +++ b/src/Type/Php/PrintfFunctionThrowTypeExtension.php @@ -84,7 +84,7 @@ public function getThrowTypeFromFunctionCall(FunctionReflection $functionReflect $uses = []; $requiredArgumentsCount = 0; foreach ($formats as $format) { - $formatUses = $this->formatParser->parse($format->getValue(), $scope->getPhpVersion()); + $formatUses = $this->formatParser->parse($format->getValue()); if ($formatUses === null) { return $functionReflection->getThrowType(); } diff --git a/tests/PHPStan/Rules/Functions/PrintfArrayParametersRuleTest.php b/tests/PHPStan/Rules/Functions/PrintfArrayParametersRuleTest.php index 261d5953827..fa567d15e53 100644 --- a/tests/PHPStan/Rules/Functions/PrintfArrayParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/PrintfArrayParametersRuleTest.php @@ -2,9 +2,11 @@ namespace PHPStan\Rules\Functions; +use PHPStan\Php\PhpVersion; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use PHPStan\Type\Php\PrintfFormatParser; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -15,7 +17,7 @@ class PrintfArrayParametersRuleTest extends RuleTestCase protected function getRule(): Rule { return new PrintfArrayParametersRule( - new PrintfFormatParser(), + new PrintfFormatParser(new PhpVersion(PHP_VERSION_ID)), self::createReflectionProvider(), ); } diff --git a/tests/PHPStan/Rules/Functions/PrintfParameterTypeRuleTest.php b/tests/PHPStan/Rules/Functions/PrintfParameterTypeRuleTest.php index 19910d17a5f..82afb051880 100644 --- a/tests/PHPStan/Rules/Functions/PrintfParameterTypeRuleTest.php +++ b/tests/PHPStan/Rules/Functions/PrintfParameterTypeRuleTest.php @@ -2,10 +2,12 @@ namespace PHPStan\Rules\Functions; +use PHPStan\Php\PhpVersion; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; use PHPStan\Type\Php\PrintfFormatParser; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -19,7 +21,7 @@ protected function getRule(): Rule { $reflectionProvider = $this->createReflectionProvider(); return new PrintfParameterTypeRule( - new PrintfFormatParser(), + new PrintfFormatParser(new PhpVersion(PHP_VERSION_ID)), $reflectionProvider, new RuleLevelHelper( $reflectionProvider, diff --git a/tests/PHPStan/Rules/Functions/PrintfParametersRuleTest.php b/tests/PHPStan/Rules/Functions/PrintfParametersRuleTest.php index f09b273cf9b..5c5ef342858 100644 --- a/tests/PHPStan/Rules/Functions/PrintfParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/PrintfParametersRuleTest.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules\Functions; +use PHPStan\Php\PhpVersion; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use PHPStan\Type\Php\PrintfFormatParser; @@ -16,7 +17,7 @@ class PrintfParametersRuleTest extends RuleTestCase protected function getRule(): Rule { return new PrintfParametersRule( - new PrintfFormatParser(), + new PrintfFormatParser(new PhpVersion(PHP_VERSION_ID)), self::createReflectionProvider(), ); } diff --git a/tests/PHPStan/Type/Php/PrintfFormatParserTest.php b/tests/PHPStan/Type/Php/PrintfFormatParserTest.php index 2801e90a318..fe5b4085b19 100644 --- a/tests/PHPStan/Type/Php/PrintfFormatParserTest.php +++ b/tests/PHPStan/Type/Php/PrintfFormatParserTest.php @@ -2,8 +2,7 @@ namespace PHPStan\Type\Php; -use PHPStan\Php\PhpVersions; -use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Php\PhpVersion; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; @@ -80,8 +79,8 @@ public static function dataRequiredArgumentsCount(): iterable #[DataProvider('dataRequiredArgumentsCount')] public function testRequiredArgumentsCount(string $format, ?int $expectedCount): void { - $parser = new PrintfFormatParser(); - $uses = $parser->parse($format, self::phpVersions(80000)); + $parser = new PrintfFormatParser(new PhpVersion(80000)); + $uses = $parser->parse($format); if ($expectedCount === null) { $this->assertNull($uses); return; @@ -93,7 +92,7 @@ public function testRequiredArgumentsCount(string $format, ?int $expectedCount): public function testUses(): void { - $parser = new PrintfFormatParser(); + $parser = new PrintfFormatParser(new PhpVersion(80000)); $this->assertSame([ ['index' => 0, 'kind' => 'width', 'specifier' => 'g', 'placeholder' => '%*.*g', 'number' => 1], ['index' => 1, 'kind' => 'precision', 'specifier' => 'g', 'placeholder' => '%*.*g', 'number' => 1], @@ -101,21 +100,15 @@ public function testUses(): void ['index' => 0, 'kind' => 'value', 'specifier' => 's', 'placeholder' => '%1$s', 'number' => 2], ['index' => 4, 'kind' => 'width', 'specifier' => 'd', 'placeholder' => '%*5$d', 'number' => 3], ['index' => 3, 'kind' => 'value', 'specifier' => 'd', 'placeholder' => '%*5$d', 'number' => 3], - ], $parser->parse('%*.*g %1$s %*5$d', self::phpVersions(80000))); + ], $parser->parse('%*.*g %1$s %*5$d')); } public function testHhSpecifiersBeforePhp8(): void { - $parser = new PrintfFormatParser(); - $phpVersions = self::phpVersions(70400); - $this->assertNull($parser->parse('%h', $phpVersions)); - $this->assertNull($parser->parse('%H', $phpVersions)); - $this->assertNotNull($parser->parse('%g', $phpVersions)); - } - - private static function phpVersions(int $versionId): PhpVersions - { - return new PhpVersions(new ConstantIntegerType($versionId)); + $parser = new PrintfFormatParser(new PhpVersion(70400)); + $this->assertNull($parser->parse('%h')); + $this->assertNull($parser->parse('%H')); + $this->assertNotNull($parser->parse('%g')); } } From 6dd96dd71e9973d688ff2e6e1d01ebef86feee81 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Tue, 22 Sep 2026 13:56:40 +0000 Subject: [PATCH 07/21] Keep PrintfFormatParser reading the PHP version from the call site The printf format parser change was split off into #6536, which has since been merged into the base branch. Reverting it on this branch therefore undid already-merged work instead of separating it, so the parser keeps parse(string $format, PhpVersions $phpVersions) and the rules keep passing Scope::getPhpVersion(). PrintfFunctionThrowTypeExtension keeps this pull request's own change: its throwsValueErrorForInternalFunctions() check resolved from the Scope. This reverts commit fba025783cd82c737bd79406a0e82c1ab94eac71. Co-Authored-By: Claude Opus 5 --- src/Php/PhpVersions.php | 5 ++++ .../Functions/PrintfArrayParametersRule.php | 2 +- .../Functions/PrintfParameterTypeRule.php | 7 +++--- src/Rules/Functions/PrintfParametersRule.php | 7 +++--- src/Type/Php/PrintfFormatParser.php | 14 ++++------- .../Php/PrintfFunctionThrowTypeExtension.php | 2 +- .../PrintfArrayParametersRuleTest.php | 4 +-- .../Functions/PrintfParameterTypeRuleTest.php | 4 +-- .../Functions/PrintfParametersRuleTest.php | 3 +-- .../Type/Php/PrintfFormatParserTest.php | 25 ++++++++++++------- 10 files changed, 39 insertions(+), 34 deletions(-) diff --git a/src/Php/PhpVersions.php b/src/Php/PhpVersions.php index 761ddc1f875..188728fd5fc 100644 --- a/src/Php/PhpVersions.php +++ b/src/Php/PhpVersions.php @@ -248,6 +248,11 @@ public function supportsAllUnicodeScalarCodePointsInMbSubstituteCharacter(): Tri 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; diff --git a/src/Rules/Functions/PrintfArrayParametersRule.php b/src/Rules/Functions/PrintfArrayParametersRule.php index b30c80ccd3d..d2c05be4417 100644 --- a/src/Rules/Functions/PrintfArrayParametersRule.php +++ b/src/Rules/Functions/PrintfArrayParametersRule.php @@ -67,7 +67,7 @@ public function processNode(Node $node, Scope $scope): array foreach ($formatArgType->getConstantStrings() as $formatString) { $format = $formatString->getValue(); - $uses = $this->printfFormatParser->parse($format); + $uses = $this->printfFormatParser->parse($format, $scope->getPhpVersion()); if ($uses === null) { return [ RuleErrorBuilder::message(sprintf( diff --git a/src/Rules/Functions/PrintfParameterTypeRule.php b/src/Rules/Functions/PrintfParameterTypeRule.php index 060fe2b8049..3ebbfc91d6e 100644 --- a/src/Rules/Functions/PrintfParameterTypeRule.php +++ b/src/Rules/Functions/PrintfParameterTypeRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Php\PhpVersions; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -94,7 +95,7 @@ public function processNode(Node $node, Scope $scope): array $formatString = $formatArgTypeStrings[0]; $format = $formatString->getValue(); - $placeholderMap = $this->getPlaceholders($format); + $placeholderMap = $this->getPlaceholders($format, $scope->getPhpVersion()); if ($placeholderMap === null) { // Already reported by PrintfParametersRule. return []; @@ -176,9 +177,9 @@ public function processNode(Node $node, Scope $scope): array /** * @return array>|null parameter index => placeholders */ - private function getPlaceholders(string $format): ?array + private function getPlaceholders(string $format, PhpVersions $phpVersions): ?array { - $uses = $this->printfFormatParser->parse($format); + $uses = $this->printfFormatParser->parse($format, $phpVersions); if ($uses === null) { return null; } diff --git a/src/Rules/Functions/PrintfParametersRule.php b/src/Rules/Functions/PrintfParametersRule.php index a24dbabe4b6..5a7e8faba87 100644 --- a/src/Rules/Functions/PrintfParametersRule.php +++ b/src/Rules/Functions/PrintfParametersRule.php @@ -6,6 +6,7 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\RegisteredRule; +use PHPStan\Php\PhpVersions; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -84,7 +85,7 @@ public function processNode(Node $node, Scope $scope): array $format = $formatString->getValue(); if (in_array($name, ['sprintf', 'printf'], true)) { - $tempPlaceHoldersCount = $this->getPrintfPlaceholdersCount($format); + $tempPlaceHoldersCount = $this->getPrintfPlaceholdersCount($format, $scope->getPhpVersion()); } else { $tempPlaceHoldersCount = $this->getScanfPlaceholdersCount($format); } @@ -129,9 +130,9 @@ public function processNode(Node $node, Scope $scope): array return []; } - private function getPrintfPlaceholdersCount(string $format): ?int + private function getPrintfPlaceholdersCount(string $format, PhpVersions $phpVersions): ?int { - $uses = $this->printfFormatParser->parse($format); + $uses = $this->printfFormatParser->parse($format, $phpVersions); if ($uses === null) { return null; } diff --git a/src/Type/Php/PrintfFormatParser.php b/src/Type/Php/PrintfFormatParser.php index 52c8b4f6891..ebf6b51a401 100644 --- a/src/Type/Php/PrintfFormatParser.php +++ b/src/Type/Php/PrintfFormatParser.php @@ -3,7 +3,7 @@ namespace PHPStan\Type\Php; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Php\PhpVersion; +use PHPStan\Php\PhpVersions; use function in_array; use function ltrim; use function strlen; @@ -29,10 +29,6 @@ final class PrintfFormatParser private const ARG_NUM_NEXT = -1; - public function __construct(private PhpVersion $phpVersion) - { - } - /** * Returns the argument uses in the order the format consumes them, or null * when the format throws ValueError for any arguments. Index 0 is the first @@ -41,7 +37,7 @@ public function __construct(private PhpVersion $phpVersion) * * @return list|null */ - public function parse(string $format): ?array + public function parse(string $format, PhpVersions $phpVersions): ?array { $length = strlen($format); $i = 0; @@ -138,7 +134,7 @@ public function parse(string $format): ?array } $specifier = $format[$i]; - if (!$this->isSpecifier($specifier)) { + if (!self::isSpecifier($specifier, $phpVersions)) { return null; } @@ -214,13 +210,13 @@ private static function parseNumber(string $format, int $length, int &$i): int return (int) $digits; } - private function isSpecifier(string $char): bool + private static function isSpecifier(string $char, PhpVersions $phpVersions): bool { if (strpos(self::SPECIFIERS, $char) !== false) { return true; } - return $this->phpVersion->supportsHhPrintfSpecifier() && strpos(self::HH_SPECIFIERS, $char) !== false; + return !$phpVersions->supportsHhPrintfSpecifier()->no() && strpos(self::HH_SPECIFIERS, $char) !== false; } private static function isDigit(string $char): bool diff --git a/src/Type/Php/PrintfFunctionThrowTypeExtension.php b/src/Type/Php/PrintfFunctionThrowTypeExtension.php index e136b618512..c7ee32d4d71 100644 --- a/src/Type/Php/PrintfFunctionThrowTypeExtension.php +++ b/src/Type/Php/PrintfFunctionThrowTypeExtension.php @@ -84,7 +84,7 @@ public function getThrowTypeFromFunctionCall(FunctionReflection $functionReflect $uses = []; $requiredArgumentsCount = 0; foreach ($formats as $format) { - $formatUses = $this->formatParser->parse($format->getValue()); + $formatUses = $this->formatParser->parse($format->getValue(), $scope->getPhpVersion()); if ($formatUses === null) { return $functionReflection->getThrowType(); } diff --git a/tests/PHPStan/Rules/Functions/PrintfArrayParametersRuleTest.php b/tests/PHPStan/Rules/Functions/PrintfArrayParametersRuleTest.php index fa567d15e53..261d5953827 100644 --- a/tests/PHPStan/Rules/Functions/PrintfArrayParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/PrintfArrayParametersRuleTest.php @@ -2,11 +2,9 @@ namespace PHPStan\Rules\Functions; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use PHPStan\Type\Php\PrintfFormatParser; -use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -17,7 +15,7 @@ class PrintfArrayParametersRuleTest extends RuleTestCase protected function getRule(): Rule { return new PrintfArrayParametersRule( - new PrintfFormatParser(new PhpVersion(PHP_VERSION_ID)), + new PrintfFormatParser(), self::createReflectionProvider(), ); } diff --git a/tests/PHPStan/Rules/Functions/PrintfParameterTypeRuleTest.php b/tests/PHPStan/Rules/Functions/PrintfParameterTypeRuleTest.php index 82afb051880..19910d17a5f 100644 --- a/tests/PHPStan/Rules/Functions/PrintfParameterTypeRuleTest.php +++ b/tests/PHPStan/Rules/Functions/PrintfParameterTypeRuleTest.php @@ -2,12 +2,10 @@ namespace PHPStan\Rules\Functions; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; use PHPStan\Type\Php\PrintfFormatParser; -use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -21,7 +19,7 @@ protected function getRule(): Rule { $reflectionProvider = $this->createReflectionProvider(); return new PrintfParameterTypeRule( - new PrintfFormatParser(new PhpVersion(PHP_VERSION_ID)), + new PrintfFormatParser(), $reflectionProvider, new RuleLevelHelper( $reflectionProvider, diff --git a/tests/PHPStan/Rules/Functions/PrintfParametersRuleTest.php b/tests/PHPStan/Rules/Functions/PrintfParametersRuleTest.php index 5c5ef342858..f09b273cf9b 100644 --- a/tests/PHPStan/Rules/Functions/PrintfParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/PrintfParametersRuleTest.php @@ -2,7 +2,6 @@ namespace PHPStan\Rules\Functions; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use PHPStan\Type\Php\PrintfFormatParser; @@ -17,7 +16,7 @@ class PrintfParametersRuleTest extends RuleTestCase protected function getRule(): Rule { return new PrintfParametersRule( - new PrintfFormatParser(new PhpVersion(PHP_VERSION_ID)), + new PrintfFormatParser(), self::createReflectionProvider(), ); } diff --git a/tests/PHPStan/Type/Php/PrintfFormatParserTest.php b/tests/PHPStan/Type/Php/PrintfFormatParserTest.php index fe5b4085b19..2801e90a318 100644 --- a/tests/PHPStan/Type/Php/PrintfFormatParserTest.php +++ b/tests/PHPStan/Type/Php/PrintfFormatParserTest.php @@ -2,7 +2,8 @@ namespace PHPStan\Type\Php; -use PHPStan\Php\PhpVersion; +use PHPStan\Php\PhpVersions; +use PHPStan\Type\Constant\ConstantIntegerType; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; @@ -79,8 +80,8 @@ public static function dataRequiredArgumentsCount(): iterable #[DataProvider('dataRequiredArgumentsCount')] public function testRequiredArgumentsCount(string $format, ?int $expectedCount): void { - $parser = new PrintfFormatParser(new PhpVersion(80000)); - $uses = $parser->parse($format); + $parser = new PrintfFormatParser(); + $uses = $parser->parse($format, self::phpVersions(80000)); if ($expectedCount === null) { $this->assertNull($uses); return; @@ -92,7 +93,7 @@ public function testRequiredArgumentsCount(string $format, ?int $expectedCount): public function testUses(): void { - $parser = new PrintfFormatParser(new PhpVersion(80000)); + $parser = new PrintfFormatParser(); $this->assertSame([ ['index' => 0, 'kind' => 'width', 'specifier' => 'g', 'placeholder' => '%*.*g', 'number' => 1], ['index' => 1, 'kind' => 'precision', 'specifier' => 'g', 'placeholder' => '%*.*g', 'number' => 1], @@ -100,15 +101,21 @@ public function testUses(): void ['index' => 0, 'kind' => 'value', 'specifier' => 's', 'placeholder' => '%1$s', 'number' => 2], ['index' => 4, 'kind' => 'width', 'specifier' => 'd', 'placeholder' => '%*5$d', 'number' => 3], ['index' => 3, 'kind' => 'value', 'specifier' => 'd', 'placeholder' => '%*5$d', 'number' => 3], - ], $parser->parse('%*.*g %1$s %*5$d')); + ], $parser->parse('%*.*g %1$s %*5$d', self::phpVersions(80000))); } public function testHhSpecifiersBeforePhp8(): void { - $parser = new PrintfFormatParser(new PhpVersion(70400)); - $this->assertNull($parser->parse('%h')); - $this->assertNull($parser->parse('%H')); - $this->assertNotNull($parser->parse('%g')); + $parser = new PrintfFormatParser(); + $phpVersions = self::phpVersions(70400); + $this->assertNull($parser->parse('%h', $phpVersions)); + $this->assertNull($parser->parse('%H', $phpVersions)); + $this->assertNotNull($parser->parse('%g', $phpVersions)); + } + + private static function phpVersions(int $versionId): PhpVersions + { + return new PhpVersions(new ConstantIntegerType($versionId)); } } From f6b28364982eb56944356c0dd11167f61ab7e3dc Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Tue, 22 Sep 2026 14:04:40 +0000 Subject: [PATCH 08/21] Resolve PHP version checks in the get_class(), array_chunk() and unserialize() extensions from Scope::getPhpVersion() These four extensions landed on the base branch after this pull request started, so the build rule added here reports them. They follow the same conversion as the rest: the throw type extensions return VoidType only when the analysed range certainly does not throw, and get_class() without arguments unions the PHP 8 result with false when the range spans PHP 7. Co-Authored-By: Claude Opus 5 --- .../ArrayChunkFunctionThrowTypeExtension.php | 7 +--- .../GetClassDynamicReturnTypeExtension.php | 20 +++++---- .../GetClassFunctionThrowTypeExtension.php | 7 +--- .../UnserializeFunctionThrowTypeExtension.php | 7 +--- ...ope-php-version-return-type-extensions.php | 9 ++++ ...rownExceptionRuleGetClassConfigPhpTest.php | 42 +++++++++++++++++++ .../get-class-throw-type-php-version.neon | 4 ++ .../get-class-throw-type-php-versions.php | 31 ++++++++++++++ 8 files changed, 100 insertions(+), 27 deletions(-) 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 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/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/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/tests/PHPStan/Analyser/nsrt/scope-php-version-return-type-extensions.php b/tests/PHPStan/Analyser/nsrt/scope-php-version-return-type-extensions.php index f193fe343e9..b458cf703fe 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 @@ -165,3 +165,12 @@ function filterVarThrowOnFailure(string $s): void 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/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) { + + } + } + +} From b74fdd42b4c284f4adc16aa15dbb8ea42fec6cda Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Tue, 22 Sep 2026 14:06:56 +0000 Subject: [PATCH 09/21] Fix the expected lines in NoInjectedPhpVersionInScopeAwareExtensionRuleTest The data file's class declarations moved by two lines, so the rule reports lines 25, 67 and 86. Co-Authored-By: Claude Opus 5 --- .../NoInjectedPhpVersionInScopeAwareExtensionRuleTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/PHPStan/Build/NoInjectedPhpVersionInScopeAwareExtensionRuleTest.php b/tests/PHPStan/Build/NoInjectedPhpVersionInScopeAwareExtensionRuleTest.php index 45a45f1cc00..d56df054d29 100644 --- a/tests/PHPStan/Build/NoInjectedPhpVersionInScopeAwareExtensionRuleTest.php +++ b/tests/PHPStan/Build/NoInjectedPhpVersionInScopeAwareExtensionRuleTest.php @@ -21,15 +21,15 @@ 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.', - 23, + 25, ], [ 'PhpVersionInExtension\InjectsPhpVersionInParameterOutExtension implements PHPStan\Type\MethodParameterOutTypeExtension and must not inject PHPStan\Php\PhpVersion - read the analysed PHP version from Scope::getPhpVersion() instead.', - 65, + 67, ], [ 'PhpVersionInExtension\InjectsPhpVersionInRestrictedUsageExtension implements PHPStan\Rules\RestrictedUsage\RestrictedMethodUsageExtension and must not inject PHPStan\Php\PhpVersion - read the analysed PHP version from Scope::getPhpVersion() instead.', - 84, + 86, ], ]); } From 892a037eefcdd5e8babea8d5ba559611133570ff Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 22 Sep 2026 16:30:03 +0200 Subject: [PATCH 10/21] fix build --- src/Type/Php/PDOConnectReturnTypeExtension.php | 15 ++++++++++----- .../scope-php-version-return-type-extensions.php | 9 --------- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/src/Type/Php/PDOConnectReturnTypeExtension.php b/src/Type/Php/PDOConnectReturnTypeExtension.php index ee87a0fe252..344a08da016 100644 --- a/src/Type/Php/PDOConnectReturnTypeExtension.php +++ b/src/Type/Php/PDOConnectReturnTypeExtension.php @@ -5,6 +5,7 @@ use PhpParser\Node\Expr\StaticCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\Php\PhpVersion; use PHPStan\Reflection\MethodReflection; use PHPStan\Type\DynamicStaticMethodReturnTypeExtension; use PHPStan\Type\ObjectType; @@ -17,10 +18,17 @@ * @see https://wiki.php.net/rfc/pdo_driver_specific_subclasses * @see https://github.com/php/php-src/pull/12804 */ +// @phpstan-ignore phpstanBuild.phpVersionInExtension (this rule requires php-src builtin classes, only available in PHP 8.4+) #[AutowiredService] final class PDOConnectReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension { + public function __construct( + private PhpVersion $phpVersion, + ) + { + } + public function getClass(): string { return 'PDO'; @@ -28,15 +36,12 @@ public function getClass(): string public function isStaticMethodSupported(MethodReflection $methodReflection): bool { - return $methodReflection->getName() === 'connect'; + return $this->phpVersion->hasPDOSubclasses() + && $methodReflection->getName() === 'connect'; } public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): ?Type { - if (!$scope->getPhpVersion()->hasPDOSubclasses()->yes()) { - return null; - } - if (count($methodCall->getArgs()) < 1) { return null; } 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 b458cf703fe..81253deeb39 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 @@ -113,15 +113,6 @@ function dateTimeModify(\DateTime $dt): void } } -function pdoConnect(): void -{ - if (PHP_VERSION_ID >= 80400) { - assertType('PDO\Sqlite', \PDO::connect('sqlite::memory:')); - } else { - assertType('PDO', \PDO::connect('sqlite::memory:')); - } -} - function bcMath(): void { if (PHP_VERSION_ID >= 80000) { From 365835ccb9f9cc79538292beb21efdccd8ed9942 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Tue, 22 Sep 2026 14:51:51 +0000 Subject: [PATCH 11/21] Keep the PHP version check inside RegexArrayShapeMatcher::containsUnmatchedAsNull() The version half of the PREG_UNMATCHED_AS_NULL condition had been folded into the flag mask in matchPatternType(), which reused the public PREG_UNMATCHED_AS_NULL_ON_72_73 constant - documented for libraries that emulate the flag on 7.2/7.3 - to carry an unrelated answer. Thread PhpVersions down to containsUnmatchedAsNull() instead, alongside the $flags it already receives, and restore the original condition. An analysed range that still reaches PHP 7.4 now keeps the documented behaviour rather than falling back to the 7.2 one, because inferring a string where the group can be null would hide a real null. Co-Authored-By: Claude Opus 5 --- src/Type/Php/RegexArrayShapeMatcher.php | 68 ++++++++++--------- ...lToFunctionParametersRuleConfigPhpTest.php | 64 +++++++++++++++++ .../preg-unmatched-as-null-php-version.neon | 4 ++ .../preg-unmatched-as-null-php-versions.php | 14 ++++ 4 files changed, 117 insertions(+), 33 deletions(-) create mode 100644 tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleConfigPhpTest.php create mode 100644 tests/PHPStan/Rules/Functions/data/preg-unmatched-as-null-php-version.neon create mode 100644 tests/PHPStan/Rules/Functions/data/preg-unmatched-as-null-php-versions.php diff --git a/src/Type/Php/RegexArrayShapeMatcher.php b/src/Type/Php/RegexArrayShapeMatcher.php index 3c26f55c1a9..13ddfb86f0d 100644 --- a/src/Type/Php/RegexArrayShapeMatcher.php +++ b/src/Type/Php/RegexArrayShapeMatcher.php @@ -106,12 +106,6 @@ private function matchPatternType(Type $patternType, ?Type $flagsType, TrinaryLo if ($flags !== $flagsType->getValue()) { return null; } - - // PREG_UNMATCHED_AS_NULL only behaves as documented since PHP 7.4, so the - // analysed version turns it on the same way the emulation flag does - if ($phpVersions->supportsPregUnmatchedAsNull()->yes()) { - $flags |= self::PREG_UNMATCHED_AS_NULL_ON_72_73; - } } $matchedTypes = []; @@ -171,12 +165,13 @@ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched $flags, $markVerbs, $matchesAll, + $phpVersions, ); - if (!$this->containsUnmatchedAsNull($flags, $matchesAll)) { + if (!$this->containsUnmatchedAsNull($flags, $matchesAll, $phpVersions)) { // positive match has a subject but not any capturing group $builder = ConstantArrayTypeBuilder::createEmpty(); - $builder->setOffsetValueType(new ConstantIntegerType(0), $this->createSubjectValueType($subjectBaseType, $flags, $matchesAll, $wasMatched)); + $builder->setOffsetValueType(new ConstantIntegerType(0), $this->createSubjectValueType($subjectBaseType, $flags, $matchesAll, $wasMatched, $phpVersions)); $combiType = TypeCombinator::union( $builder->getArray(), @@ -207,11 +202,11 @@ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched } elseif ($beforeCurrentCombo && !$group->resetsGroupCounter()) { $comboList = $comboList->forceGroupTypeAndNonOptional( $group, - $this->containsUnmatchedAsNull($flags, $matchesAll) ? new NullType() : new ConstantStringType(''), + $this->containsUnmatchedAsNull($flags, $matchesAll, $phpVersions) ? new NullType() : new ConstantStringType(''), ); } elseif ( $group->getAlternationId() === $onlyTopLevelAlternation->getId() - && !$this->containsUnmatchedAsNull($flags, $matchesAll) + && !$this->containsUnmatchedAsNull($flags, $matchesAll, $phpVersions) ) { $comboList = $comboList->removeGroup($group); } @@ -225,13 +220,14 @@ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched $flags, $markVerbs, $matchesAll, + $phpVersions, ); $combiTypes[] = $combiType; } if ( - !$this->containsUnmatchedAsNull($flags, $matchesAll) + !$this->containsUnmatchedAsNull($flags, $matchesAll, $phpVersions) && ( $onlyTopLevelAlternation->getAlternationsCount() !== count($onlyTopLevelAlternation->getGroupCombinations()) || $isOptionalAlternation @@ -239,7 +235,7 @@ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched ) { // positive match has a subject but not any capturing group $builder = ConstantArrayTypeBuilder::createEmpty(); - $builder->setOffsetValueType(new ConstantIntegerType(0), $this->createSubjectValueType($subjectBaseType, $flags, $matchesAll, $wasMatched)); + $builder->setOffsetValueType(new ConstantIntegerType(0), $this->createSubjectValueType($subjectBaseType, $flags, $matchesAll, $wasMatched, $phpVersions)); $combiTypes[] = $builder->getArray(); } @@ -257,6 +253,7 @@ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched $flags, $markVerbs, $matchesAll, + $phpVersions, ); } @@ -271,6 +268,7 @@ private function buildArrayType( int $flags, array $markVerbs, bool $matchesAll, + PhpVersions $phpVersions, ): Type { $forceList = count($markVerbs) === 0; @@ -279,7 +277,7 @@ private function buildArrayType( // first item in matches contains the overall match. $builder->setOffsetValueType( $this->getKeyType(0), - $this->createSubjectValueType($subjectBaseType, $flags, $matchesAll, $wasMatched), + $this->createSubjectValueType($subjectBaseType, $flags, $matchesAll, $wasMatched, $phpVersions), $this->isSubjectOptional($wasMatched, $matchesAll), ); @@ -288,8 +286,8 @@ private function buildArrayType( foreach ($captureGroups as $captureGroup) { $isTrailingOptional = $i >= $countGroups - $trailingOptionals; $isLastGroup = $i === $countGroups - 1; - $groupValueType = $this->createGroupValueType($captureGroup, $wasMatched, $flags, $isTrailingOptional, $isLastGroup, $matchesAll); - $optional = $this->isGroupOptional($captureGroup, $wasMatched, $flags, $isTrailingOptional, $matchesAll); + $groupValueType = $this->createGroupValueType($captureGroup, $wasMatched, $flags, $isTrailingOptional, $isLastGroup, $matchesAll, $phpVersions); + $optional = $this->isGroupOptional($captureGroup, $wasMatched, $flags, $isTrailingOptional, $matchesAll, $phpVersions); if ($captureGroup->isNamed()) { $forceList = false; @@ -350,12 +348,12 @@ private function isSubjectOptional(TrinaryLogic $wasMatched, bool $matchesAll): /** * @param Type $baseType A string type (or string variant) representing the subject of the match */ - private function createSubjectValueType(Type $baseType, int $flags, bool $matchesAll, TrinaryLogic $wasMatched): Type + private function createSubjectValueType(Type $baseType, int $flags, bool $matchesAll, TrinaryLogic $wasMatched, PhpVersions $phpVersions): Type { - $subjectValueType = TypeCombinator::removeNull($this->getValueType($baseType, $flags, $matchesAll)); + $subjectValueType = TypeCombinator::removeNull($this->getValueType($baseType, $flags, $matchesAll, $phpVersions)); if ($matchesAll) { - $subjectValueType = TypeCombinator::removeNull($this->getValueType(new StringType(), $flags, $matchesAll)); + $subjectValueType = TypeCombinator::removeNull($this->getValueType(new StringType(), $flags, $matchesAll, $phpVersions)); if ($this->containsPatternOrder($flags)) { $accessoryTypes = [ @@ -373,10 +371,10 @@ private function createSubjectValueType(Type $baseType, int $flags, bool $matche return $subjectValueType; } - private function isGroupOptional(RegexCapturingGroup $captureGroup, TrinaryLogic $wasMatched, int $flags, bool $isTrailingOptional, bool $matchesAll): bool + private function isGroupOptional(RegexCapturingGroup $captureGroup, TrinaryLogic $wasMatched, int $flags, bool $isTrailingOptional, bool $matchesAll, PhpVersions $phpVersions): bool { if ($matchesAll) { - if ($isTrailingOptional && !$this->containsUnmatchedAsNull($flags, $matchesAll) && $this->containsSetOrder($flags)) { + if ($isTrailingOptional && !$this->containsUnmatchedAsNull($flags, $matchesAll, $phpVersions) && $this->containsSetOrder($flags)) { return true; } @@ -388,7 +386,7 @@ private function isGroupOptional(RegexCapturingGroup $captureGroup, TrinaryLogic } else { if (!$isTrailingOptional) { $optional = false; - } elseif ($this->containsUnmatchedAsNull($flags, $matchesAll)) { + } elseif ($this->containsUnmatchedAsNull($flags, $matchesAll, $phpVersions)) { $optional = false; } else { $optional = $captureGroup->isOptional(); @@ -398,19 +396,19 @@ private function isGroupOptional(RegexCapturingGroup $captureGroup, TrinaryLogic return $optional; } - private function createGroupValueType(RegexCapturingGroup $captureGroup, TrinaryLogic $wasMatched, int $flags, bool $isTrailingOptional, bool $isLastGroup, bool $matchesAll): Type + private function createGroupValueType(RegexCapturingGroup $captureGroup, TrinaryLogic $wasMatched, int $flags, bool $isTrailingOptional, bool $isLastGroup, bool $matchesAll, PhpVersions $phpVersions): Type { if ($matchesAll) { if ( ( !$this->containsSetOrder($flags) - && !$this->containsUnmatchedAsNull($flags, $matchesAll) + && !$this->containsUnmatchedAsNull($flags, $matchesAll, $phpVersions) && $captureGroup->isOptional() ) || ( $this->containsSetOrder($flags) - && !$this->containsUnmatchedAsNull($flags, $matchesAll) + && !$this->containsUnmatchedAsNull($flags, $matchesAll, $phpVersions) && $captureGroup->isOptional() && !$isTrailingOptional ) @@ -419,13 +417,14 @@ private function createGroupValueType(RegexCapturingGroup $captureGroup, Trinary TypeCombinator::union($captureGroup->getType(), new ConstantStringType('')), $flags, $matchesAll, + $phpVersions, ); $groupValueType = TypeCombinator::removeNull($groupValueType); } else { - $groupValueType = $this->getValueType($captureGroup->getType(), $flags, $matchesAll); + $groupValueType = $this->getValueType($captureGroup->getType(), $flags, $matchesAll, $phpVersions); } - if (!$isTrailingOptional && $this->containsUnmatchedAsNull($flags, $matchesAll) && !$captureGroup->isOptional()) { + if (!$isTrailingOptional && $this->containsUnmatchedAsNull($flags, $matchesAll, $phpVersions) && !$captureGroup->isOptional()) { $groupValueType = TypeCombinator::removeNull($groupValueType); } @@ -441,18 +440,19 @@ private function createGroupValueType(RegexCapturingGroup $captureGroup, Trinary return $groupValueType; } - if (!$isLastGroup && !$this->containsUnmatchedAsNull($flags, $matchesAll) && $captureGroup->isOptional()) { + if (!$isLastGroup && !$this->containsUnmatchedAsNull($flags, $matchesAll, $phpVersions) && $captureGroup->isOptional()) { $groupValueType = $this->getValueType( TypeCombinator::union($captureGroup->getType(), new ConstantStringType('')), $flags, $matchesAll, + $phpVersions, ); } else { - $groupValueType = $this->getValueType($captureGroup->getType(), $flags, $matchesAll); + $groupValueType = $this->getValueType($captureGroup->getType(), $flags, $matchesAll, $phpVersions); } if ($wasMatched->yes()) { - if (!$isTrailingOptional && $this->containsUnmatchedAsNull($flags, $matchesAll) && !$captureGroup->isOptional()) { + if (!$isTrailingOptional && $this->containsUnmatchedAsNull($flags, $matchesAll, $phpVersions) && !$captureGroup->isOptional()) { $groupValueType = TypeCombinator::removeNull($groupValueType); } } @@ -476,7 +476,7 @@ private function containsSetOrder(int $flags): bool return ($flags & PREG_SET_ORDER) !== 0; } - private function containsUnmatchedAsNull(int $flags, bool $matchesAll): bool + private function containsUnmatchedAsNull(int $flags, bool $matchesAll, PhpVersions $phpVersions): bool { if ($matchesAll) { // preg_match_all() with PREG_UNMATCHED_AS_NULL works consistently across php-versions @@ -484,7 +484,9 @@ private function containsUnmatchedAsNull(int $flags, bool $matchesAll): bool return ($flags & PREG_UNMATCHED_AS_NULL) !== 0; } - return ($flags & PREG_UNMATCHED_AS_NULL) !== 0 && ($flags & self::PREG_UNMATCHED_AS_NULL_ON_72_73) !== 0; + // an analysed range that still reaches PHP 7.4 has to assume the documented behaviour, + // because inferring a string where the group can be null would hide a real null + return ($flags & PREG_UNMATCHED_AS_NULL) !== 0 && (($flags & self::PREG_UNMATCHED_AS_NULL_ON_72_73) !== 0 || !$phpVersions->supportsPregUnmatchedAsNull()->no()); } private function getKeyType(int|string $key): Type @@ -496,13 +498,13 @@ private function getKeyType(int|string $key): Type return new ConstantIntegerType($key); } - private function getValueType(Type $baseType, int $flags, bool $matchesAll): Type + private function getValueType(Type $baseType, int $flags, bool $matchesAll, PhpVersions $phpVersions): Type { $valueType = $baseType; // unmatched groups return -1 as offset $offsetType = IntegerRangeType::fromInterval(-1, null); - if ($this->containsUnmatchedAsNull($flags, $matchesAll)) { + if ($this->containsUnmatchedAsNull($flags, $matchesAll, $phpVersions)) { $valueType = TypeCombinator::addNull($valueType); } diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleConfigPhpTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleConfigPhpTest.php new file mode 100644 index 00000000000..c597bc3c7a1 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleConfigPhpTest.php @@ -0,0 +1,64 @@ + + */ +class CallToFunctionParametersRuleConfigPhpTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $broker = self::createReflectionProvider(); + return new CallToFunctionParametersRule( + $broker, + new FunctionCallParametersCheck( + new RuleLevelHelper( + $broker, + checkNullables: true, + checkThisOnly: false, + checkUnionTypes: true, + checkExplicitMixed: false, + checkImplicitMixed: false, + checkBenevolentUnionTypes: false, + discoveringSymbolsTip: true, + ), + new NullsafeCheck(), + new UnresolvableTypeHelper(), + new PropertyReflectionFinder(), + $broker, + checkArgumentTypes: true, + checkArgumentsPassedByReference: true, + checkExtraArguments: true, + checkMissingTypehints: true, + ), + ); + } + + public function testPregUnmatchedAsNullInPhpVersionRange(): void + { + $this->analyse([__DIR__ . '/data/preg-unmatched-as-null-php-versions.php'], [ + [ + 'Parameter #1 $s of function PregUnmatchedAsNullPhpVersions\needsString expects string, string|null given.', + 12, + ], + ]); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/data/preg-unmatched-as-null-php-version.neon', + ]; + } + +} diff --git a/tests/PHPStan/Rules/Functions/data/preg-unmatched-as-null-php-version.neon b/tests/PHPStan/Rules/Functions/data/preg-unmatched-as-null-php-version.neon new file mode 100644 index 00000000000..dfce7af1e04 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/preg-unmatched-as-null-php-version.neon @@ -0,0 +1,4 @@ +parameters: + phpVersion: + min: 70200 + max: 80500 diff --git a/tests/PHPStan/Rules/Functions/data/preg-unmatched-as-null-php-versions.php b/tests/PHPStan/Rules/Functions/data/preg-unmatched-as-null-php-versions.php new file mode 100644 index 00000000000..57721591021 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/preg-unmatched-as-null-php-versions.php @@ -0,0 +1,14 @@ + Date: Tue, 22 Sep 2026 15:28:23 +0000 Subject: [PATCH 12/21] Do not infer null from version_compare() with a valid constant operator version_compare() returns null only for an invalid operator, so the fallback return type has to be decided after the operator argument has been read, not before. Picking it up front meant an uncertain throwsValueErrorForInternalFunctions() added a null the call cannot produce - e2e/composer-max-version pins "php": "<=8.3", whose range straddles PHP 8.0, and version_compare(PHP_VERSION, '7.0.0', '<') started inferring bool|null there. The "the operator might be invalid" test is now shared with VersionCompareFunctionDynamicThrowTypeExtension, which already had it. Co-Authored-By: Claude Opus 5 --- ...pareFunctionDynamicReturnTypeExtension.php | 27 ++++++++++++++++--- ...mpareFunctionDynamicThrowTypeExtension.php | 11 +------- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/src/Type/Php/VersionCompareFunctionDynamicReturnTypeExtension.php b/src/Type/Php/VersionCompareFunctionDynamicReturnTypeExtension.php index d454e3630a2..5f6d818b822 100644 --- a/src/Type/Php/VersionCompareFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/VersionCompareFunctionDynamicReturnTypeExtension.php @@ -77,9 +77,9 @@ public function getTypeFromFunctionCall( if (isset($args[2])) { $operatorStrings = $scope->getType($args[2]->value)->getConstantStrings(); $counts[] = count($operatorStrings); - $returnType = $throwsValueError->yes() - ? 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), @@ -128,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 3e93128d05b..96ae2961c44 100644 --- a/src/Type/Php/VersionCompareFunctionDynamicThrowTypeExtension.php +++ b/src/Type/Php/VersionCompareFunctionDynamicThrowTypeExtension.php @@ -8,8 +8,6 @@ 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 @@ -36,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; } From 80619fa8975a6fcf91cf0644f7f6e3bf6f3190b7 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Tue, 22 Sep 2026 15:28:31 +0000 Subject: [PATCH 13/21] Detect DateTime::modify() failure from its return value, not from the host Both extensions probe the string by running the real function in the analyser process. DateTime::modify() and DateInterval::createFromDateString() only throw since PHP 8.3; before that they emit a warning and return false. While the version came from DI it always matched the runtime, so catching Throwable was enough. Now that it comes from Scope::getPhpVersion(), the analysed version can be 8.3+ while this process runs on an older one: the guard passes, the call warns instead of throwing and the extension concludes the call cannot fail. Suppress the warning and read the return value instead, the way the matching return-type extensions already do. Co-Authored-By: Claude Opus 5 --- ...ateIntervalCreateFromDateStringThrowTypeExtension.php | 9 ++++++++- src/Type/Php/DateTimeModifyMethodThrowTypeExtension.php | 9 ++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/Type/Php/DateIntervalCreateFromDateStringThrowTypeExtension.php b/src/Type/Php/DateIntervalCreateFromDateStringThrowTypeExtension.php index aea6f4e43b0..7cbb046d2b6 100644 --- a/src/Type/Php/DateIntervalCreateFromDateStringThrowTypeExtension.php +++ b/src/Type/Php/DateIntervalCreateFromDateStringThrowTypeExtension.php @@ -38,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/DateTimeModifyMethodThrowTypeExtension.php b/src/Type/Php/DateTimeModifyMethodThrowTypeExtension.php index e61bb177215..f3a2ccfdf0f 100644 --- a/src/Type/Php/DateTimeModifyMethodThrowTypeExtension.php +++ b/src/Type/Php/DateTimeModifyMethodThrowTypeExtension.php @@ -40,10 +40,17 @@ 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) { + $result = false; + } + + if ($result === false) { return $this->exceptionType($scope); } From 6d24a23cca7cda53e6541cc7205a64c762955345 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Tue, 22 Sep 2026 15:28:31 +0000 Subject: [PATCH 14/21] Strip modifiers the host PCRE may not know before compiling the pattern RegexGroupParser validates a pattern by compiling it in the analyser process, but decides captureOnlyNamed from the analysed version. The n modifier only exists in the PCRE bundled with PHP 8.2+, so a pattern targeting 8.2 fails validation on an older host and loses its array shape entirely, long before the analysed version is consulted. PHPStan implements the modifier itself, so removing it for the validation compile makes that step give the same answer on every host. Co-Authored-By: Claude Opus 5 --- src/Type/Regex/RegexGroupParser.php | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/src/Type/Regex/RegexGroupParser.php b/src/Type/Regex/RegexGroupParser.php index 32b38211a8d..2086ebbee75 100644 --- a/src/Type/Regex/RegexGroupParser.php +++ b/src/Type/Regex/RegexGroupParser.php @@ -45,6 +45,16 @@ final class RegexGroupParser 'J', // rare modifier too complicated to support ]; + /** + * Modifiers that only exist in the PCRE library bundled with newer PHP versions. + * The analysed version can support them while this process runs on an older one, + * so they are stripped before the pattern is compiled here. PHPStan implements their + * effect itself, so removing them does not change the inferred shape. + */ + private const MODIFIERS_NOT_COMPILABLE_ON_EVERY_RUNTIME = [ + 'n', // PHP 8.2+, https://php.watch/versions/8.2/preg-n-no-capture-modifier + ]; + // upper bound on the number of constant string literals enumerated from a group, // to avoid combinatorial explosion from nested optional/bounded quantifications private const LITERALS_LIMIT = 100; @@ -65,22 +75,21 @@ public function parseGroups(string $regex, PhpVersions $phpVersions): ?RegexAstW /** @throws void */ self::$parser ??= Llk::load(new Read(__DIR__ . '/../../../resources/RegexGrammar.pp')); + $modifiers = $this->regexExpressionHelper->getPatternModifiers($regex) ?? ''; + if (array_key_exists($regex, self::$parsedAst)) { $ast = self::$parsedAst[$regex]; if ($ast === null) { return null; } - - $modifiers = $this->regexExpressionHelper->getPatternModifiers($regex) ?? ''; } else { try { - Strings::match('', $regex); + Strings::match('', $this->removeModifiersNotCompilableOnEveryRuntime($regex, $modifiers)); } catch (RegexpException) { // pattern is invalid, so let the RegularExpressionPatternRule report it return self::$parsedAst[$regex] = null; } - $modifiers = $this->regexExpressionHelper->getPatternModifiers($regex) ?? ''; foreach (self::NOT_SUPPORTED_MODIFIERS as $notSupportedModifier) { if (str_contains($modifiers, $notSupportedModifier)) { return self::$parsedAst[$regex] = null; @@ -149,6 +158,16 @@ private function createEmptyTokenTreeNode(TreeNode $parentAst): TreeNode return new TreeNode('token', ['token' => 'literal', 'value' => '', 'namespace' => 'default'], parent: $parentAst); } + private function removeModifiersNotCompilableOnEveryRuntime(string $regex, string $modifiers): string + { + $compilableModifiers = str_replace(self::MODIFIERS_NOT_COMPILABLE_ON_EVERY_RUNTIME, '', $modifiers); + if ($compilableModifiers === $modifiers) { + return $regex; + } + + return substr($regex, 0, strlen($regex) - strlen($modifiers)) . $compilableModifiers; + } + private function updateAlternationAstRemoveVerticalBarsAndAddEmptyToken(TreeNode $ast): void { $children = $ast->getChildren(); From b2db47673fd3385551d73cdcf31ca3610e67e253 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Tue, 22 Sep 2026 15:28:37 +0000 Subject: [PATCH 15/21] Test return-type extensions against a configured PHP version range The nsrt file only covers narrowing through PHP_VERSION_ID, which makes the analysed version certain. Pin the uncertain case too: with phpVersion min 70400 / max 80500 every version check inside the converted extensions is Maybe, and the inferred type has to stay wide enough for both ends. Covers the four expressions that widen at a range - substr(), str_split(''), mb_substitute_character() with null and with 0 - and the version_compare() regression, which fails here without the previous fix. Co-Authored-By: Claude Opus 5 --- ...hpVersionRangeReturnTypeExtensionsTest.php | 36 +++++++++++++++++++ ...p-version-range-return-type-extensions.php | 28 +++++++++++++++ .../Analyser/scopePhpVersionRange.neon | 4 +++ 3 files changed, 68 insertions(+) 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/scopePhpVersionRange.neon 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..17ce112ed4d --- /dev/null +++ b/tests/PHPStan/Analyser/data/scope-php-version-range-return-type-extensions.php @@ -0,0 +1,28 @@ + Date: Tue, 22 Sep 2026 19:49:58 +0200 Subject: [PATCH 16/21] fix baseline --- phpstan-baseline.neon | 6 ++++++ src/Type/Php/PDOConnectReturnTypeExtension.php | 1 - 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 5a96ce5c15e..560c2f35a6b 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1548,6 +1548,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/Type/Php/PDOConnectReturnTypeExtension.php b/src/Type/Php/PDOConnectReturnTypeExtension.php index 344a08da016..82ceb6118ee 100644 --- a/src/Type/Php/PDOConnectReturnTypeExtension.php +++ b/src/Type/Php/PDOConnectReturnTypeExtension.php @@ -18,7 +18,6 @@ * @see https://wiki.php.net/rfc/pdo_driver_specific_subclasses * @see https://github.com/php/php-src/pull/12804 */ -// @phpstan-ignore phpstanBuild.phpVersionInExtension (this rule requires php-src builtin classes, only available in PHP 8.4+) #[AutowiredService] final class PDOConnectReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension { From 3565169a55b0226805e2c72f3b6d230a3d83737c Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 22 Sep 2026 20:15:25 +0200 Subject: [PATCH 17/21] Update scope-php-version-range-return-type-extensions.php --- .../data/scope-php-version-range-return-type-extensions.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 17ce112ed4d..1f439ee74b8 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 @@ -16,7 +16,7 @@ function bothVersionsPossible(string $s): void assertType('bool', mb_substitute_character(0)); } -function validOperatorNeverReturnsNull(string $a, string $b): void +function validOperatorNeverReturnsNull(string $a, string $b, string $s): void { // version_compare() only returns null for an invalid operator, so a constant valid one // rules null out no matter which version of the range is analysed. From ddc2b6fee18d7c3dc43030e3622ad4f827347340 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Wed, 23 Sep 2026 06:58:59 +0000 Subject: [PATCH 18/21] Move the src/Type/Php extension changes to their own pull request The conversion of the return-type, throw-type and type-specifying extensions under src/Type/Php (with their helpers, the build rule that guards them and their tests) now lives on create-pull-request/type-php-extensions-scope-php-version. Co-Authored-By: Claude Opus 5.5 --- ...tedPhpVersionInScopeAwareExtensionRule.php | 130 -------------- build/phpstan.neon | 1 - phpstan-baseline.neon | 6 - src/Php/PhpVersions.php | 70 -------- .../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 | 167 ------------------ .../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, 458 insertions(+), 1206 deletions(-) delete mode 100644 build/PHPStan/Build/NoInjectedPhpVersionInScopeAwareExtensionRule.php delete mode 100644 tests/PHPStan/Analyser/ScopePhpVersionRangeReturnTypeExtensionsTest.php delete mode 100644 tests/PHPStan/Analyser/data/scope-php-version-range-return-type-extensions.php delete mode 100644 tests/PHPStan/Analyser/nsrt/scope-php-version-return-type-extensions.php delete mode 100644 tests/PHPStan/Analyser/scopePhpVersionRange.neon delete mode 100644 tests/PHPStan/Build/NoInjectedPhpVersionInScopeAwareExtensionRuleTest.php delete mode 100644 tests/PHPStan/Build/data/php-version-in-extension.php delete mode 100644 tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleConfigPhpTest.php delete mode 100644 tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleGetClassConfigPhpTest.php delete mode 100644 tests/PHPStan/Rules/Exceptions/data/get-class-throw-type-php-version.neon delete mode 100644 tests/PHPStan/Rules/Exceptions/data/get-class-throw-type-php-versions.php delete mode 100644 tests/PHPStan/Rules/Exceptions/data/throw-type-php-version.neon delete 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 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; - } - -} 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 560c2f35a6b..5a96ce5c15e 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1548,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/src/Php/PhpVersions.php b/src/Php/PhpVersions.php index 188728fd5fc..07bf6eb0ca7 100644 --- a/src/Php/PhpVersions.php +++ b/src/Php/PhpVersions.php @@ -188,76 +188,16 @@ 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 supportsPregCaptureOnlyNamedGroups(): TrinaryLogic { // https://php.watch/versions/8.2/preg-n-no-capture-modifier @@ -271,16 +211,6 @@ public function supportsPregUnmatchedAsNull(): TrinaryLogic return IntegerRangeType::fromInterval(70400, null)->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; - } - /** * PHPStan's BetterReflection adapters keep their narrowed native return types only on PHP 8+; * the downgraded PHP 7 build widens them back to the core Reflection ones. diff --git a/src/Type/Php/ArrayChunkFunctionReturnTypeExtension.php b/src/Type/Php/ArrayChunkFunctionReturnTypeExtension.php index a1c1e2721c1..cfd69286ecf 100644 --- a/src/Type/Php/ArrayChunkFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayChunkFunctionReturnTypeExtension.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\Constant\ConstantBooleanType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; @@ -18,6 +19,10 @@ final class ArrayChunkFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + public function __construct(private PhpVersion $phpVersion) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'array_chunk'; @@ -32,21 +37,13 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $arrayType = $scope->getType($args[0]->value); if ($arrayType->isArray()->no()) { - if ($scope->getPhpVersion()->arrayFunctionsReturnNullWithNonArray()->no()) { - return new NeverType(); - } - - return new NullType(); + return $this->phpVersion->arrayFunctionsReturnNullWithNonArray() ? new NullType() : new NeverType(); } $lengthType = $scope->getType($args[1]->value); $negativeOrZero = IntegerRangeType::fromInterval(null, 0); if ($negativeOrZero->isSuperTypeOf($lengthType)->yes()) { - if ($scope->getPhpVersion()->throwsValueErrorForInternalFunctions()->yes()) { - return new NeverType(); - } - - return new NullType(); + return $this->phpVersion->throwsValueErrorForInternalFunctions() ? new NeverType() : 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 03e7d0ee1bc..97e8a4361f5 100644 --- a/src/Type/Php/ArrayChunkFunctionThrowTypeExtension.php +++ b/src/Type/Php/ArrayChunkFunctionThrowTypeExtension.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\DynamicFunctionThrowTypeExtension; use PHPStan\Type\IntegerRangeType; @@ -19,6 +20,10 @@ final class ArrayChunkFunctionThrowTypeExtension implements DynamicFunctionThrowTypeExtension { + public function __construct(private PhpVersion $phpVersion) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'array_chunk'; @@ -26,7 +31,7 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getThrowTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $funcCall, Scope $scope): ?Type { - if ($scope->getPhpVersion()->throwsValueErrorForInternalFunctions()->no()) { + if (!$this->phpVersion->throwsValueErrorForInternalFunctions()) { return new VoidType(); } diff --git a/src/Type/Php/ArrayColumnHelper.php b/src/Type/Php/ArrayColumnHelper.php index c2121f50f4d..aeb414875f0 100644 --- a/src/Type/Php/ArrayColumnHelper.php +++ b/src/Type/Php/ArrayColumnHelper.php @@ -4,6 +4,7 @@ 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; @@ -20,6 +21,12 @@ final class ArrayColumnHelper { + public function __construct( + private PhpVersion $phpVersion, + ) + { + } + /** * @return array{Type, TrinaryLogic} */ @@ -67,7 +74,7 @@ public function handleAnyArray(Type $arrayType, Type $columnType, Type $indexTyp } $returnKeyType = $this->getReturnIndexType($arrayType, $indexType, $scope); - $returnType = new ArrayType($this->castToArrayKeyType($returnKeyType, $scope), $returnValueType); + $returnType = new ArrayType($this->castToArrayKeyType($returnKeyType), $returnValueType); if ($iterableAtLeastOnce->yes()) { $returnType = TypeCombinator::intersect($returnType, new NonEmptyArrayType()); @@ -106,7 +113,7 @@ public function handleConstantArray(ConstantArrayType $arrayType, Type $columnTy } if ($keyType !== null) { - $keyType = $this->castToArrayKeyType($keyType, $scope); + $keyType = $this->castToArrayKeyType($keyType); } $builder->setOffsetValueType($keyType, $valueType, $arrayType->isOptionalKey($i)); } @@ -124,9 +131,9 @@ public function handleConstantArray(ConstantArrayType $arrayType, Type $columnTy if ($unsealedKeyFromIndex instanceof NeverType) { $unsealedKey = $unsealedTypes[0]; } elseif ($unsealedKeyCertainty->yes()) { - $unsealedKey = $this->castToArrayKeyType($unsealedKeyFromIndex, $scope); + $unsealedKey = $this->castToArrayKeyType($unsealedKeyFromIndex); } else { - $unsealedKey = $this->castToArrayKeyType(TypeCombinator::union($unsealedKeyFromIndex, new IntegerType()), $scope); + $unsealedKey = $this->castToArrayKeyType(TypeCombinator::union($unsealedKeyFromIndex, new IntegerType())); } } else { // `null` indexType keeps integer-keyed list semantics — @@ -214,23 +221,18 @@ private function getOffsetOrProperty(Type $type, Type $offsetOrProperty, Scope $ return [TypeCombinator::union(...$returnTypes), $certainty]; } - private function castToArrayKeyType(Type $type, Scope $scope): Type + private function castToArrayKeyType(Type $type): Type { - $throwsTypeError = $scope->getPhpVersion()->throwsTypeErrorForInternalFunctions(); $isArray = $type->isArray(); if ($isArray->yes()) { - if ($throwsTypeError->yes()) { - return new NeverType(); - } - - return new IntegerType(); + return $this->phpVersion->throwsTypeErrorForInternalFunctions() ? new NeverType() : new IntegerType(); } if ($isArray->no()) { return $type->toArrayKey(); } $withoutArrayType = TypeCombinator::remove($type, new ArrayType(new MixedType(), new MixedType())); $keyType = $withoutArrayType->toArrayKey(); - if ($throwsTypeError->yes()) { + if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { return $keyType; } return TypeCombinator::union($keyType, new IntegerType()); diff --git a/src/Type/Php/ArrayCombineFunctionReturnTypeExtension.php b/src/Type/Php/ArrayCombineFunctionReturnTypeExtension.php index ba2c1e0a0fb..d33a56f2fca 100644 --- a/src/Type/Php/ArrayCombineFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayCombineFunctionReturnTypeExtension.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\Constant\ConstantBooleanType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; @@ -19,6 +20,7 @@ final class ArrayCombineFunctionReturnTypeExtension implements DynamicFunctionRe public function __construct( private ArrayCombineHelper $arrayCombineHelper, + private PhpVersion $phpVersion, ) { } @@ -43,16 +45,15 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return $returnType; } - $throwsValueError = $scope->getPhpVersion()->throwsValueErrorForInternalFunctions(); if ($hasValueError->yes()) { - if ($throwsValueError->yes()) { + if ($this->phpVersion->throwsValueErrorForInternalFunctions()) { return new NeverType(); } return new ConstantBooleanType(false); } - if ($throwsValueError->yes()) { + if ($this->phpVersion->throwsValueErrorForInternalFunctions()) { return $returnType; } diff --git a/src/Type/Php/ArrayFillFunctionReturnTypeExtension.php b/src/Type/Php/ArrayFillFunctionReturnTypeExtension.php index a8a0270120f..fd3fde84d5a 100644 --- a/src/Type/Php/ArrayFillFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayFillFunctionReturnTypeExtension.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\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\NonEmptyArrayType; @@ -27,6 +28,10 @@ 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'; @@ -41,14 +46,12 @@ 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 ($throwsValueError->yes()) { + if ($this->phpVersion->throwsValueErrorForInternalFunctions()) { return new NeverType(); } - return new ConstantBooleanType(false); } @@ -93,7 +96,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $resultType = TypeCombinator::intersect($resultType, new NonEmptyArrayType()); } - if (!$isValidNumberType->yes() && !$throwsValueError->yes()) { + if (!$isValidNumberType->yes() && !$this->phpVersion->throwsValueErrorForInternalFunctions()) { $resultType = TypeCombinator::union($resultType, new ConstantBooleanType(false)); } diff --git a/src/Type/Php/ArrayFillKeysFunctionReturnTypeExtension.php b/src/Type/Php/ArrayFillKeysFunctionReturnTypeExtension.php index f1486d274b9..7a9474f5c67 100644 --- a/src/Type/Php/ArrayFillKeysFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayFillKeysFunctionReturnTypeExtension.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\Accessory\NonEmptyArrayType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; @@ -18,6 +19,10 @@ final class ArrayFillKeysFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + public function __construct(private PhpVersion $phpVersion) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'array_fill_keys'; @@ -32,11 +37,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $keysType = $scope->getType($args[0]->value); if ($keysType->isArray()->no()) { - if ($scope->getPhpVersion()->arrayFunctionsReturnNullWithNonArray()->no()) { - return new NeverType(); - } - - return new NullType(); + return $this->phpVersion->arrayFunctionsReturnNullWithNonArray() ? new NullType() : new NeverType(); } $filled = $keysType->fillKeysArray($scope->getType($args[1]->value)); diff --git a/src/Type/Php/ArrayFilterFunctionReturnTypeHelper.php b/src/Type/Php/ArrayFilterFunctionReturnTypeHelper.php index b5affda6003..83de6b7f622 100644 --- a/src/Type/Php/ArrayFilterFunctionReturnTypeHelper.php +++ b/src/Type/Php/ArrayFilterFunctionReturnTypeHelper.php @@ -16,6 +16,7 @@ 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; @@ -48,6 +49,7 @@ final class ArrayFilterFunctionReturnTypeHelper public function __construct( private ReflectionProvider $reflectionProvider, + private PhpVersion $phpVersion, ) { } @@ -68,7 +70,7 @@ public function getType(Scope $scope, ?Expr $arrayArg, ?Expr $callbackArg, ?Expr } if ($arrayArgType instanceof MixedType) { - if ($scope->getPhpVersion()->throwsValueErrorForInternalFunctions()->yes()) { + if ($this->phpVersion->throwsValueErrorForInternalFunctions()) { return new ArrayType(new MixedType(), new MixedType()); } diff --git a/src/Type/Php/ArrayFlipFunctionReturnTypeExtension.php b/src/Type/Php/ArrayFlipFunctionReturnTypeExtension.php index f1b66321416..f21e9dc294d 100644 --- a/src/Type/Php/ArrayFlipFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayFlipFunctionReturnTypeExtension.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\Accessory\NonEmptyArrayType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; @@ -18,6 +19,10 @@ final class ArrayFlipFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + public function __construct(private PhpVersion $phpVersion) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'array_flip'; @@ -32,11 +37,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $arrayType = $scope->getType($args[0]->value); if ($arrayType->isArray()->no()) { - if ($scope->getPhpVersion()->arrayFunctionsReturnNullWithNonArray()->no()) { - return new NeverType(); - } - - return new NullType(); + return $this->phpVersion->arrayFunctionsReturnNullWithNonArray() ? new NullType() : new NeverType(); } $flipped = $arrayType->flipArray(); diff --git a/src/Type/Php/ArrayIntersectKeyFunctionReturnTypeExtension.php b/src/Type/Php/ArrayIntersectKeyFunctionReturnTypeExtension.php index cc09f9c735c..c6212c02cc0 100644 --- a/src/Type/Php/ArrayIntersectKeyFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayIntersectKeyFunctionReturnTypeExtension.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\DynamicFunctionReturnTypeExtension; use PHPStan\Type\NeverType; @@ -18,6 +19,10 @@ final class ArrayIntersectKeyFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + public function __construct(private PhpVersion $phpVersion) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'array_intersect_key'; @@ -46,11 +51,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $onlyOneArrayGiven = count($argTypes) === 1; if ($firstArrayType->isArray()->no() || (!$onlyOneArrayGiven && $otherArraysType->isArray()->no())) { - if ($scope->getPhpVersion()->arrayFunctionsReturnNullWithNonArray()->no()) { - return new NeverType(); - } - - return new NullType(); + return $this->phpVersion->arrayFunctionsReturnNullWithNonArray() ? new NullType() : new NeverType(); } if ($onlyOneArrayGiven) { diff --git a/src/Type/Php/ArrayKeyExistsFunctionTypeSpecifyingExtension.php b/src/Type/Php/ArrayKeyExistsFunctionTypeSpecifyingExtension.php index c9d437c87ff..6782f408112 100644 --- a/src/Type/Php/ArrayKeyExistsFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/ArrayKeyExistsFunctionTypeSpecifyingExtension.php @@ -10,6 +10,7 @@ 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; @@ -29,6 +30,12 @@ final class ArrayKeyExistsFunctionTypeSpecifyingExtension implements FunctionTyp private TypeSpecifier $typeSpecifier; + public function __construct( + private PhpVersion $phpVersion, + ) + { + } + public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void { $this->typeSpecifier = $typeSpecifier; @@ -116,7 +123,7 @@ public function specifyTypes( new ArrayType(new MixedType(), new MixedType()), new HasOffsetType($keyType), ]); - } elseif ($scope->getPhpVersion()->throwsValueErrorForInternalFunctions()->yes()) { + } elseif ($this->phpVersion->throwsValueErrorForInternalFunctions()) { $specifiedTypes = $this->typeSpecifier->create( $array, new HasOffsetType($keyType), diff --git a/src/Type/Php/ArrayKeysFunctionDynamicReturnTypeExtension.php b/src/Type/Php/ArrayKeysFunctionDynamicReturnTypeExtension.php index 881e58d7d6c..bb7a7bea880 100644 --- a/src/Type/Php/ArrayKeysFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArrayKeysFunctionDynamicReturnTypeExtension.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\TrinaryLogic; use PHPStan\Type\DynamicFunctionReturnTypeExtension; @@ -18,6 +19,10 @@ final class ArrayKeysFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + public function __construct(private PhpVersion $phpVersion) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return strtolower($functionReflection->getName()) === 'array_keys'; @@ -32,11 +37,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $arrayType = $scope->getType($args[0]->value); if ($arrayType->isArray()->no()) { - if ($scope->getPhpVersion()->arrayFunctionsReturnNullWithNonArray()->no()) { - return new NeverType(); - } - - return new NullType(); + return $this->phpVersion->arrayFunctionsReturnNullWithNonArray() ? new NullType() : new NeverType(); } if (count($args) >= 2) { diff --git a/src/Type/Php/ArrayReverseFunctionReturnTypeExtension.php b/src/Type/Php/ArrayReverseFunctionReturnTypeExtension.php index 9cce71b4051..7f4f2a754fc 100644 --- a/src/Type/Php/ArrayReverseFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayReverseFunctionReturnTypeExtension.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\Constant\ConstantBooleanType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; @@ -16,6 +17,10 @@ final class ArrayReverseFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + public function __construct(private PhpVersion $phpVersion) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'array_reverse'; @@ -30,11 +35,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $type = $scope->getType($args[0]->value); if ($type->isArray()->no()) { - if ($scope->getPhpVersion()->arrayFunctionsReturnNullWithNonArray()->no()) { - return new NeverType(); - } - - return new NullType(); + return $this->phpVersion->arrayFunctionsReturnNullWithNonArray() ? new NullType() : new NeverType(); } $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 c1ec70bf746..87810f8fb02 100644 --- a/src/Type/Php/ArraySearchFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArraySearchFunctionDynamicReturnTypeExtension.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\Constant\ConstantBooleanType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; @@ -17,6 +18,10 @@ final class ArraySearchFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + public function __construct(private PhpVersion $phpVersion) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'array_search'; @@ -32,11 +37,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $haystackArgType = $scope->getType($args[1]->value); if ($haystackArgType->isArray()->no()) { - if ($scope->getPhpVersion()->arrayFunctionsReturnNullWithNonArray()->no()) { - return new NeverType(); - } - - return new NullType(); + return $this->phpVersion->arrayFunctionsReturnNullWithNonArray() ? new NullType() : new NeverType(); } if ($argsCount < 3) { diff --git a/src/Type/Php/ArraySliceFunctionReturnTypeExtension.php b/src/Type/Php/ArraySliceFunctionReturnTypeExtension.php index 42f20f7d61f..5ac2ba46063 100644 --- a/src/Type/Php/ArraySliceFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArraySliceFunctionReturnTypeExtension.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\Constant\ConstantBooleanType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; @@ -17,6 +18,10 @@ final class ArraySliceFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + public function __construct(private PhpVersion $phpVersion) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'array_slice'; @@ -31,11 +36,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $arrayType = $scope->getType($args[0]->value); if ($arrayType->isArray()->no()) { - if ($scope->getPhpVersion()->arrayFunctionsReturnNullWithNonArray()->no()) { - return new NeverType(); - } - - return new NullType(); + return $this->phpVersion->arrayFunctionsReturnNullWithNonArray() ? new NullType() : new NeverType(); } $offsetType = $scope->getType($args[1]->value); diff --git a/src/Type/Php/ArraySpliceFunctionReturnTypeExtension.php b/src/Type/Php/ArraySpliceFunctionReturnTypeExtension.php index c9959b1a935..5dd3c941744 100644 --- a/src/Type/Php/ArraySpliceFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArraySpliceFunctionReturnTypeExtension.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\TrinaryLogic; use PHPStan\Type\DynamicFunctionReturnTypeExtension; @@ -17,6 +18,10 @@ final class ArraySpliceFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + public function __construct(private PhpVersion $phpVersion) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'array_splice'; @@ -35,11 +40,7 @@ public function getTypeFromFunctionCall( $arrayType = $scope->getType($args[0]->value); if ($arrayType->isArray()->no()) { - if ($scope->getPhpVersion()->arrayFunctionsReturnNullWithNonArray()->no()) { - return new NeverType(); - } - - return new NullType(); + return $this->phpVersion->arrayFunctionsReturnNullWithNonArray() ? new NullType() : new NeverType(); } $offsetType = $scope->getType($args[1]->value); diff --git a/src/Type/Php/ArrayValuesFunctionDynamicReturnTypeExtension.php b/src/Type/Php/ArrayValuesFunctionDynamicReturnTypeExtension.php index 594551a3e34..0015c4e1268 100644 --- a/src/Type/Php/ArrayValuesFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArrayValuesFunctionDynamicReturnTypeExtension.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\DynamicFunctionReturnTypeExtension; use PHPStan\Type\NeverType; @@ -17,6 +18,10 @@ final class ArrayValuesFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + public function __construct(private PhpVersion $phpVersion) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return strtolower($functionReflection->getName()) === 'array_values'; @@ -31,11 +36,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $arrayType = $scope->getType($args[0]->value); if ($arrayType->isArray()->no()) { - if ($scope->getPhpVersion()->arrayFunctionsReturnNullWithNonArray()->no()) { - return new NeverType(); - } - - return new NullType(); + return $this->phpVersion->arrayFunctionsReturnNullWithNonArray() ? new NullType() : new NeverType(); } return $arrayType->getValuesArray(); diff --git a/src/Type/Php/BcMathStringOrNullReturnTypeExtension.php b/src/Type/Php/BcMathStringOrNullReturnTypeExtension.php index 8ade43ae493..97ea61c70a6 100644 --- a/src/Type/Php/BcMathStringOrNullReturnTypeExtension.php +++ b/src/Type/Php/BcMathStringOrNullReturnTypeExtension.php @@ -6,6 +6,7 @@ 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; @@ -25,6 +26,10 @@ 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); @@ -40,19 +45,18 @@ 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 ($throwsTypeError->yes()) { + if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { return new NeverType(); } return new NullType(); } - if ($throwsTypeError->yes()) { + if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { $defaultReturnType = $stringAndNumericStringType; } else { $defaultReturnType = new UnionType([$stringAndNumericStringType, new NullType()]); @@ -62,7 +66,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 ($throwsTypeError->yes()) { + if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { return new NeverType(); } @@ -91,14 +95,14 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } if ($thirdArgument instanceof ConstantScalarType && !is_numeric($thirdArgument->getValue())) { - if ($throwsTypeError->yes()) { + if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { return new NeverType(); } return new NullType(); } - if ($throwsTypeError->yes() && $thirdArgumentIsNegative) { + if ($this->phpVersion->throwsTypeErrorForInternalFunctions() && $thirdArgumentIsNegative) { return new NeverType(); } @@ -117,9 +121,8 @@ 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 ($throwsTypeError->yes()) { + if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { $defaultReturnType = $stringAndNumericStringType; } else { $defaultReturnType = new UnionType([$stringAndNumericStringType, new NullType()]); @@ -127,7 +130,7 @@ private function getTypeForBcSqrt(FuncCall $functionCall, Scope $scope): Type $args = $functionCall->getArgs(); if (isset($args[0]) === false) { - if ($throwsTypeError->yes()) { + if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { return new NeverType(); } @@ -140,7 +143,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 ($throwsTypeError->yes()) { + if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { return new NeverType(); } @@ -161,7 +164,7 @@ private function getTypeForBcSqrt(FuncCall $functionCall, Scope $scope): Type $secondArgumentIsNegative = $secondArgument instanceof ConstantScalarType && is_numeric($secondArgument->getValue()) && $secondArgument->getValue() < 0; if ($secondArgumentIsNonNumeric) { - if ($throwsTypeError->yes()) { + if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { return new NeverType(); } @@ -169,7 +172,7 @@ private function getTypeForBcSqrt(FuncCall $functionCall, Scope $scope): Type } if ($secondArgument instanceof UnaryMinus || $secondArgumentIsNegative) { - if ($throwsTypeError->yes()) { + if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { return new NeverType(); } } @@ -188,16 +191,15 @@ private function getTypeForBcSqrt(FuncCall $functionCall, Scope $scope): Type */ private function getTypeForBcPowMod(FuncCall $functionCall, Scope $scope): Type { - $throwsTypeError = $scope->getPhpVersion()->throwsTypeErrorForInternalFunctions(); $args = $functionCall->getArgs(); - if ($throwsTypeError->yes() && isset($args[0]) === false) { + if ($this->phpVersion->throwsTypeErrorForInternalFunctions() && isset($args[0]) === false) { return new NeverType(); } $stringAndNumericStringType = new IntersectionType([new StringType(), new AccessoryNumericStringType()]); if (isset($args[1]) === false) { - if ($throwsTypeError->yes()) { + if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { return new NeverType(); } @@ -207,7 +209,7 @@ private function getTypeForBcPowMod(FuncCall $functionCall, Scope $scope): Type $exponent = $scope->getType($args[1]->value); // Expontent is non numeric - if ($throwsTypeError->yes() + if ($this->phpVersion->throwsTypeErrorForInternalFunctions() && $exponent instanceof ConstantScalarType && !is_numeric($exponent->getValue()) ) { return new NeverType(); @@ -220,7 +222,7 @@ private function getTypeForBcPowMod(FuncCall $functionCall, Scope $scope): Type } if ($exponentIsNegative) { - if ($throwsTypeError->yes()) { + if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { return new NeverType(); } @@ -233,7 +235,7 @@ private function getTypeForBcPowMod(FuncCall $functionCall, Scope $scope): Type $modulusIsNonNumeric = $modulus instanceof ConstantScalarType && !is_numeric($modulus->getValue()); if ($modulusIsZero || $modulusIsNonNumeric) { - if ($throwsTypeError->yes()) { + if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { return new NeverType(); } @@ -244,12 +246,12 @@ private function getTypeForBcPowMod(FuncCall $functionCall, Scope $scope): Type return $stringAndNumericStringType; } } else { - if ($throwsTypeError->yes()) { + if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { return new NeverType(); } } - if ($throwsTypeError->yes()) { + if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { return $stringAndNumericStringType; } diff --git a/src/Type/Php/CountCharsFunctionDynamicReturnTypeExtension.php b/src/Type/Php/CountCharsFunctionDynamicReturnTypeExtension.php index cac46a05dad..70d08ba9e00 100644 --- a/src/Type/Php/CountCharsFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/CountCharsFunctionDynamicReturnTypeExtension.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\ArrayType; use PHPStan\Type\Constant\ConstantBooleanType; @@ -22,6 +23,10 @@ final class CountCharsFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + public function __construct(private PhpVersion $phpVersion) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'count_chars'; @@ -40,19 +45,18 @@ 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 $throwsValueError->yes() + return $this->phpVersion->throwsValueErrorForInternalFunctions() ? $arrayType : TypeUtils::toBenevolentUnion(new UnionType([$arrayType, new ConstantBooleanType(false)])); } $stringType = new StringType(); - return $throwsValueError->yes() + return $this->phpVersion->throwsValueErrorForInternalFunctions() ? $stringType : TypeUtils::toBenevolentUnion(new UnionType([$stringType, new ConstantBooleanType(false)])); } diff --git a/src/Type/Php/DateIntervalConstructorThrowTypeExtension.php b/src/Type/Php/DateIntervalConstructorThrowTypeExtension.php index 0d3487bd1c5..da3cc191234 100644 --- a/src/Type/Php/DateIntervalConstructorThrowTypeExtension.php +++ b/src/Type/Php/DateIntervalConstructorThrowTypeExtension.php @@ -6,6 +6,7 @@ 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,6 +20,10 @@ 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; @@ -37,22 +42,22 @@ public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflect try { new DateInterval($constantString->getValue()); } catch (Throwable) { - return $this->exceptionType($scope); + return $this->exceptionType(); } $valueType = TypeCombinator::remove($valueType, $constantString); } if (!$valueType instanceof NeverType) { - return $this->exceptionType($scope); + return $this->exceptionType(); } return null; } - private function exceptionType(Scope $scope): Type + private function exceptionType(): Type { - if ($scope->getPhpVersion()->hasDateTimeExceptions()->yes()) { + if ($this->phpVersion->hasDateTimeExceptions()) { return new ObjectType('DateMalformedIntervalStringException'); } diff --git a/src/Type/Php/DateIntervalCreateFromDateStringThrowTypeExtension.php b/src/Type/Php/DateIntervalCreateFromDateStringThrowTypeExtension.php index 7cbb046d2b6..2a1c10b29f9 100644 --- a/src/Type/Php/DateIntervalCreateFromDateStringThrowTypeExtension.php +++ b/src/Type/Php/DateIntervalCreateFromDateStringThrowTypeExtension.php @@ -6,6 +6,7 @@ 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; @@ -18,6 +19,10 @@ final class DateIntervalCreateFromDateStringThrowTypeExtension implements DynamicStaticMethodThrowTypeExtension { + public function __construct(private PhpVersion $phpVersion) + { + } + public function isStaticMethodSupported(MethodReflection $methodReflection): bool { return $methodReflection->getName() === 'createFromDateString' @@ -30,7 +35,7 @@ public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflect return null; } - if ($scope->getPhpVersion()->hasDateTimeExceptions()->no()) { + if (!$this->phpVersion->hasDateTimeExceptions()) { return null; } @@ -38,16 +43,9 @@ 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 { - $result = @DateInterval::createFromDateString($constantString->getValue()); + @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 a2d126fc0d4..bc57c6b7e24 100644 --- a/src/Type/Php/DateIntervalDynamicReturnTypeExtension.php +++ b/src/Type/Php/DateIntervalDynamicReturnTypeExtension.php @@ -6,6 +6,7 @@ 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; @@ -18,6 +19,10 @@ final class DateIntervalDynamicReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension { + public function __construct(private PhpVersion $phpVersion) + { + } + public function getClass(): string { return DateInterval::class; @@ -56,7 +61,7 @@ public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, if ($hasFalse) { if (!$hasDateInterval) { - if ($scope->getPhpVersion()->hasDateTimeExceptions()->yes()) { + if ($this->phpVersion->hasDateTimeExceptions()) { return new NeverType(); } diff --git a/src/Type/Php/DateTimeConstructorThrowTypeExtension.php b/src/Type/Php/DateTimeConstructorThrowTypeExtension.php index 84cecacd475..45cdb606a3a 100644 --- a/src/Type/Php/DateTimeConstructorThrowTypeExtension.php +++ b/src/Type/Php/DateTimeConstructorThrowTypeExtension.php @@ -7,6 +7,7 @@ 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; @@ -21,6 +22,10 @@ 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); @@ -39,22 +44,22 @@ public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflect try { new DateTime($constantString->getValue()); } catch (Throwable) { - return $this->exceptionType($scope); + return $this->exceptionType(); } $valueType = TypeCombinator::remove($valueType, $constantString); } if (!$valueType instanceof NeverType) { - return $this->exceptionType($scope); + return $this->exceptionType(); } return null; } - private function exceptionType(Scope $scope): Type + private function exceptionType(): Type { - if ($scope->getPhpVersion()->hasDateTimeExceptions()->yes()) { + if ($this->phpVersion->hasDateTimeExceptions()) { return new ObjectType('DateMalformedStringException'); } diff --git a/src/Type/Php/DateTimeModifyMethodThrowTypeExtension.php b/src/Type/Php/DateTimeModifyMethodThrowTypeExtension.php index f3a2ccfdf0f..f3b91b8b358 100644 --- a/src/Type/Php/DateTimeModifyMethodThrowTypeExtension.php +++ b/src/Type/Php/DateTimeModifyMethodThrowTypeExtension.php @@ -7,6 +7,7 @@ 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; @@ -21,6 +22,10 @@ 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); @@ -32,7 +37,7 @@ public function getThrowTypeFromMethodCall(MethodReflection $methodReflection, M return null; } - if ($scope->getPhpVersion()->hasDateTimeExceptions()->no()) { + if (!$this->phpVersion->hasDateTimeExceptions()) { return null; } @@ -40,33 +45,26 @@ 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(); - $result = @$dateTime->modify($constantString->getValue()); + $dateTime->modify($constantString->getValue()); } catch (Throwable) { - $result = false; - } - - if ($result === false) { - return $this->exceptionType($scope); + return $this->exceptionType(); } $valueType = TypeCombinator::remove($valueType, $constantString); } if (!$valueType instanceof NeverType) { - return $this->exceptionType($scope); + return $this->exceptionType(); } return null; } - private function exceptionType(Scope $scope): Type + private function exceptionType(): Type { - if ($scope->getPhpVersion()->hasDateTimeExceptions()->yes()) { + if ($this->phpVersion->hasDateTimeExceptions()) { return new ObjectType('DateMalformedStringException'); } diff --git a/src/Type/Php/DateTimeModifyReturnTypeExtension.php b/src/Type/Php/DateTimeModifyReturnTypeExtension.php index 91238c8ceb3..a5a3223dc62 100644 --- a/src/Type/Php/DateTimeModifyReturnTypeExtension.php +++ b/src/Type/Php/DateTimeModifyReturnTypeExtension.php @@ -6,6 +6,7 @@ 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; @@ -23,6 +24,7 @@ final class DateTimeModifyReturnTypeExtension implements DynamicMethodReturnType /** @param class-string $dateTimeClass */ public function __construct( + private PhpVersion $phpVersion, private string $dateTimeClass, ) { @@ -73,7 +75,7 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method if ($hasFalse) { if (!$hasDateTime) { - if ($scope->getPhpVersion()->hasDateTimeExceptions()->yes()) { + if ($this->phpVersion->hasDateTimeExceptions()) { return new NeverType(); } diff --git a/src/Type/Php/DateTimeSubMethodThrowTypeExtension.php b/src/Type/Php/DateTimeSubMethodThrowTypeExtension.php index 48c2784122d..fc92e2b91d5 100644 --- a/src/Type/Php/DateTimeSubMethodThrowTypeExtension.php +++ b/src/Type/Php/DateTimeSubMethodThrowTypeExtension.php @@ -7,6 +7,7 @@ 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; @@ -18,6 +19,10 @@ final class DateTimeSubMethodThrowTypeExtension implements DynamicMethodThrowTypeExtension { + public function __construct(private PhpVersion $phpVersion) + { + } + public function isMethodSupported(MethodReflection $methodReflection): bool { return $methodReflection->getName() === 'sub' @@ -30,7 +35,7 @@ public function getThrowTypeFromMethodCall(MethodReflection $methodReflection, M return null; } - if ($scope->getPhpVersion()->hasDateTimeExceptions()->no()) { + if (!$this->phpVersion->hasDateTimeExceptions()) { return null; } diff --git a/src/Type/Php/DateTimeZoneConstructorThrowTypeExtension.php b/src/Type/Php/DateTimeZoneConstructorThrowTypeExtension.php index c14eba4d010..d58e183886a 100644 --- a/src/Type/Php/DateTimeZoneConstructorThrowTypeExtension.php +++ b/src/Type/Php/DateTimeZoneConstructorThrowTypeExtension.php @@ -6,6 +6,7 @@ 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,6 +20,10 @@ 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; @@ -37,22 +42,22 @@ public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflect try { new DateTimeZone($constantString->getValue()); } catch (Throwable) { - return $this->exceptionType($scope); + return $this->exceptionType(); } $valueType = TypeCombinator::remove($valueType, $constantString); } if (!$valueType instanceof NeverType) { - return $this->exceptionType($scope); + return $this->exceptionType(); } return null; } - private function exceptionType(Scope $scope): Type + private function exceptionType(): Type { - if ($scope->getPhpVersion()->hasDateTimeExceptions()->yes()) { + if ($this->phpVersion->hasDateTimeExceptions()) { return new ObjectType('DateInvalidTimeZoneException'); } diff --git a/src/Type/Php/FilterFunctionReturnTypeHelper.php b/src/Type/Php/FilterFunctionReturnTypeHelper.php index af128a42272..484738fa9fc 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\PhpVersions; +use PHPStan\Php\PhpVersion; 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) + public function __construct(private ReflectionProvider $reflectionProvider, private PhpVersion $phpVersion) { $this->flagsString = new ConstantStringType('flags'); } - private function getOffsetValueType(Type $inputType, Type $offsetType, ?Type $filterType, ?Type $flagsType, PhpVersions $phpVersions): Type + private function getOffsetValueType(Type $inputType, Type $offsetType, ?Type $filterType, ?Type $flagsType): Type { $hasNullOnFailure = $this->hasFlag('FILTER_NULL_ON_FAILURE', $flagsType); - if ($this->hasThrowOnFailureFlag($flagsType, $phpVersions)->yes()) { + if ($this->hasThrowOnFailureFlag($flagsType)->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, $phpVersions); + $filteredType = $this->getType($inputType->getOffsetValueType($offsetType), $filterType, $flagsType); return $hasOffsetValueType->maybe() ? TypeCombinator::union($filteredType, $inexistentOffsetType) : $filteredType; } - public function getInputType(Type $typeType, Type $varNameType, ?Type $filterType, ?Type $flagsType, PhpVersions $phpVersions): Type + public function getInputType(Type $typeType, Type $varNameType, ?Type $filterType, ?Type $flagsType): 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 ($phpVersions->throwsTypeErrorForInternalFunctions()->yes()) { + if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { 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, $phpVersions); + return $this->getOffsetValueType($inputType, $varNameType, $filterType, $flagsType); } - public function getType(Type $inputType, ?Type $filterType, ?Type $flagsType, PhpVersions $phpVersions): Type + public function getType(Type $inputType, ?Type $filterType, ?Type $flagsType): Type { $mixedType = new MixedType(); @@ -154,7 +154,7 @@ public function getType(Type $inputType, ?Type $filterType, ?Type $flagsType, Ph $inputIsArray = $inputType->isArray(); $hasRequireArrayFlag = $this->hasFlag('FILTER_REQUIRE_ARRAY', $flagsType); - $hasThrowOnFailureFlag = $this->hasThrowOnFailureFlag($flagsType, $phpVersions); + $hasThrowOnFailureFlag = $this->hasThrowOnFailureFlag($flagsType); 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, PhpVersions $phpVersions): TrinaryLogic + private function hasThrowOnFailureFlag(?Type $flagsType): TrinaryLogic { - return $phpVersions->hasFilterThrowOnFailureConstant()->no() - ? TrinaryLogic::createNo() - : $this->hasFlag('FILTER_THROW_ON_FAILURE', $flagsType); + return $this->phpVersion->hasFilterThrowOnFailureConstant() + ? $this->hasFlag('FILTER_THROW_ON_FAILURE', $flagsType) + : TrinaryLogic::createNo(); } private function getFlagsValue(Type $exprType): Type diff --git a/src/Type/Php/FilterFunctionsThrowTypeExtension.php b/src/Type/Php/FilterFunctionsThrowTypeExtension.php index 6d5d78610be..cb1649ae103 100644 --- a/src/Type/Php/FilterFunctionsThrowTypeExtension.php +++ b/src/Type/Php/FilterFunctionsThrowTypeExtension.php @@ -6,6 +6,7 @@ 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; @@ -18,6 +19,7 @@ final class FilterFunctionsThrowTypeExtension implements DynamicFunctionThrowTyp public function __construct( private ReflectionProvider $reflectionProvider, + private PhpVersion $phpVersion, private FilterFunctionReturnTypeHelper $filterFunctionReturnTypeHelper, private FilterFunctionFlagsHelper $filterFunctionFlagsHelper, ) @@ -38,7 +40,7 @@ public function getThrowTypeFromFunctionCall( ): ?Type { if ( - $scope->getPhpVersion()->hasFilterThrowOnFailureConstant()->no() + !$this->phpVersion->hasFilterThrowOnFailureConstant() || !$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 a693b674e9f..79448c97682 100644 --- a/src/Type/Php/FilterInputDynamicReturnTypeExtension.php +++ b/src/Type/Php/FilterInputDynamicReturnTypeExtension.php @@ -35,7 +35,6 @@ 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 3315035d340..3be499415ca 100644 --- a/src/Type/Php/FilterVarArrayDynamicReturnTypeExtension.php +++ b/src/Type/Php/FilterVarArrayDynamicReturnTypeExtension.php @@ -88,7 +88,6 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $inputArgType->getIterableValueType(), $filterArgType, null, - $scope->getPhpVersion(), ); $arrayType = new ArrayType($inputArgType->getIterableKeyType(), $valueType); @@ -113,7 +112,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } elseif ($filterConstantArrayType === null) { if ($inputConstantArrayType === null) { $isList = $inputArgType->isList()->yes(); - $valueType = $this->filterFunctionReturnTypeHelper->getType($inputArgType, $filterArgType, null, $scope->getPhpVersion()); + $valueType = $this->filterFunctionReturnTypeHelper->getType($inputArgType, $filterArgType, null); $arrayType = new ArrayType( $inputArgType->getIterableKeyType(), @@ -161,7 +160,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } [$filterType, $flagsType] = $this->fetchFilter($filterTypesMap[$key] ?? new MixedType()); - $valueType = $this->filterFunctionReturnTypeHelper->getType($inputType, $filterType, $flagsType, $scope->getPhpVersion()); + $valueType = $this->filterFunctionReturnTypeHelper->getType($inputType, $filterType, $flagsType); if (in_array($key, $optionalKeys, true)) { if ($addEmpty) { @@ -187,7 +186,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } else { [$unsealedFilter, $unsealedFlags] = $this->fetchFilter(new MixedType()); } - $unsealedValueType = $this->filterFunctionReturnTypeHelper->getType($unsealedTypes[1], $unsealedFilter, $unsealedFlags, $scope->getPhpVersion()); + $unsealedValueType = $this->filterFunctionReturnTypeHelper->getType($unsealedTypes[1], $unsealedFilter, $unsealedFlags); if ($addEmpty) { $unsealedValueType = TypeCombinator::addNull($unsealedValueType); } diff --git a/src/Type/Php/FilterVarDynamicReturnTypeExtension.php b/src/Type/Php/FilterVarDynamicReturnTypeExtension.php index 81e2ba13eff..7248566e843 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, $scope->getPhpVersion()); + return $this->filterFunctionReturnTypeHelper->getType($inputType, $filterType, $flagsType); } } diff --git a/src/Type/Php/GetClassDynamicReturnTypeExtension.php b/src/Type/Php/GetClassDynamicReturnTypeExtension.php index 46b5bbccb86..8b8ae644401 100644 --- a/src/Type/Php/GetClassDynamicReturnTypeExtension.php +++ b/src/Type/Php/GetClassDynamicReturnTypeExtension.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\ClassStringType; use PHPStan\Type\Constant\ConstantBooleanType; @@ -12,7 +13,6 @@ use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\NeverType; use PHPStan\Type\Type; -use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; use function count; @@ -20,6 +20,10 @@ final class GetClassDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + public function __construct(private PhpVersion $phpVersion) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'get_class'; @@ -40,21 +44,15 @@ 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. - $throwsValueError = $scope->getPhpVersion()->throwsValueErrorForInternalFunctions(); - $types = []; - if (!$throwsValueError->no()) { + if ($this->phpVersion->throwsValueErrorForInternalFunctions()) { if ($scope->getFunction() !== null && !$scope->isInAnonymousFunction()) { - $types[] = new NeverType(true); - } else { - $types[] = new ClassStringType(); + return new NeverType(true); } - } - if (!$throwsValueError->yes()) { - $types[] = new ConstantBooleanType(false); + return new ClassStringType(); } - return TypeCombinator::union(...$types); + return new ConstantBooleanType(false); } $argType = $scope->getType($args[0]->value); diff --git a/src/Type/Php/GetClassFunctionThrowTypeExtension.php b/src/Type/Php/GetClassFunctionThrowTypeExtension.php index a1409df7243..997fd53bc96 100644 --- a/src/Type/Php/GetClassFunctionThrowTypeExtension.php +++ b/src/Type/Php/GetClassFunctionThrowTypeExtension.php @@ -6,6 +6,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\DynamicFunctionThrowTypeExtension; use PHPStan\Type\ObjectType; @@ -22,6 +23,10 @@ final class GetClassFunctionThrowTypeExtension implements DynamicFunctionThrowTypeExtension { + public function __construct(private PhpVersion $phpVersion) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'get_class'; @@ -29,7 +34,7 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getThrowTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $funcCall, Scope $scope): ?Type { - if ($scope->getPhpVersion()->throwsValueErrorForInternalFunctions()->no()) { + if (!$this->phpVersion->throwsValueErrorForInternalFunctions()) { return new VoidType(); } diff --git a/src/Type/Php/HashFunctionsReturnTypeExtension.php b/src/Type/Php/HashFunctionsReturnTypeExtension.php index 0e484ce78cf..d0c13ef4d90 100644 --- a/src/Type/Php/HashFunctionsReturnTypeExtension.php +++ b/src/Type/Php/HashFunctionsReturnTypeExtension.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\Type\Accessory\AccessoryLowercaseStringType; @@ -85,6 +86,10 @@ 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()); @@ -120,11 +125,10 @@ 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'] || !$throwsValueError->yes()) { + if ($functionData['possiblyFalse'] || !$this->phpVersion->throwsValueErrorForInternalFunctions()) { return new BenevolentUnionType([$stringReturnType, new ConstantBooleanType(false)]); } @@ -133,11 +137,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $neverType = new NeverType(); $falseType = new ConstantBooleanType(false); - if ($throwsValueError->yes()) { - $invalidAlgorithmType = $neverType; - } else { - $invalidAlgorithmType = $falseType; - } + $invalidAlgorithmType = $this->phpVersion->throwsValueErrorForInternalFunctions() ? $neverType : $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 209bb42c530..71f8e6643fe 100644 --- a/src/Type/Php/HighlightStringDynamicReturnTypeExtension.php +++ b/src/Type/Php/HighlightStringDynamicReturnTypeExtension.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; @@ -17,6 +18,10 @@ final class HighlightStringDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + public function __construct(private PhpVersion $phpVersion) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'highlight_string'; @@ -24,16 +29,21 @@ 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) { - $returnType = $scope->getType($args[1]->value); - if ($returnType->isTrue()->yes()) { - return new StringType(); + if (count($args) < 2) { + if ($this->phpVersion->highlightStringDoesNotReturnFalse()) { + return new ConstantBooleanType(true); } + + return new BooleanType(); + } + + $returnType = $scope->getType($args[1]->value); + if ($returnType->isTrue()->yes()) { + return new StringType(); } - if ($doesNotReturnFalse->yes()) { + if ($this->phpVersion->highlightStringDoesNotReturnFalse()) { return new ConstantBooleanType(true); } diff --git a/src/Type/Php/MbConvertEncodingFunctionReturnTypeExtension.php b/src/Type/Php/MbConvertEncodingFunctionReturnTypeExtension.php index e38ad1ffd95..11c7e86fbe6 100644 --- a/src/Type/Php/MbConvertEncodingFunctionReturnTypeExtension.php +++ b/src/Type/Php/MbConvertEncodingFunctionReturnTypeExtension.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\Accessory\AccessoryArrayListType; @@ -26,6 +27,10 @@ final class MbConvertEncodingFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + public function __construct(private PhpVersion $phpVersion) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'mb_convert_encoding'; @@ -55,7 +60,7 @@ public function getTypeFromFunctionCall( $result = $initialReturnType; } - if ($scope->getPhpVersion()->throwsValueErrorForInternalFunctions()->yes()) { + if ($this->phpVersion->throwsValueErrorForInternalFunctions()) { 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 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/MinMaxFunctionReturnTypeExtension.php b/src/Type/Php/MinMaxFunctionReturnTypeExtension.php index cf068f4a0d6..a142784318d 100644 --- a/src/Type/Php/MinMaxFunctionReturnTypeExtension.php +++ b/src/Type/Php/MinMaxFunctionReturnTypeExtension.php @@ -8,6 +8,7 @@ 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; @@ -24,6 +25,12 @@ 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); @@ -42,7 +49,6 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return $this->processArrayType( $functionReflection->getName(), $argType, - $scope, ); } @@ -102,20 +108,19 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, ); } - private function processArrayType(string $functionName, Type $argType, Scope $scope): Type + private function processArrayType(string $functionName, Type $argType): Type { - $throwsValueError = $scope->getPhpVersion()->throwsValueErrorForInternalFunctions(); $constArrayTypes = $argType->getConstantArrays(); if (count($constArrayTypes) > 0) { $resultTypes = []; foreach ($constArrayTypes as $constArrayType) { $isIterable = $constArrayType->isIterableAtLeastOnce(); - if ($isIterable->no() && !$throwsValueError->yes()) { + if ($isIterable->no() && !$this->phpVersion->throwsValueErrorForInternalFunctions()) { $resultTypes[] = new ConstantBooleanType(false); continue; } $argumentTypes = []; - if (!$isIterable->yes() && !$throwsValueError->yes()) { + if (!$isIterable->yes() && !$this->phpVersion->throwsValueErrorForInternalFunctions()) { $argumentTypes[] = new ConstantBooleanType(false); } @@ -138,12 +143,12 @@ private function processArrayType(string $functionName, Type $argType, Scope $sc } $isIterable = $argType->isIterableAtLeastOnce(); - if ($isIterable->no() && !$throwsValueError->yes()) { + if ($isIterable->no() && !$this->phpVersion->throwsValueErrorForInternalFunctions()) { return new ConstantBooleanType(false); } $iterableValueType = $argType->getIterableValueType(); $argumentTypes = []; - if (!$isIterable->yes() && !$throwsValueError->yes()) { + if (!$isIterable->yes() && !$this->phpVersion->throwsValueErrorForInternalFunctions()) { $argumentTypes[] = new ConstantBooleanType(false); } diff --git a/src/Type/Php/MinMaxFunctionThrowTypeExtension.php b/src/Type/Php/MinMaxFunctionThrowTypeExtension.php index 0ec781780de..1dac8af4385 100644 --- a/src/Type/Php/MinMaxFunctionThrowTypeExtension.php +++ b/src/Type/Php/MinMaxFunctionThrowTypeExtension.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\DynamicFunctionThrowTypeExtension; @@ -21,6 +22,10 @@ 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); @@ -33,7 +38,7 @@ public function getThrowTypeFromFunctionCall(FunctionReflection $functionReflect return $functionReflection->getThrowType(); } - if (!$scope->getPhpVersion()->throwsValueErrorForInternalFunctions()->yes()) { + if (!$this->phpVersion->throwsValueErrorForInternalFunctions()) { return new VoidType(); } diff --git a/src/Type/Php/OpensslCipherFunctionsReturnTypeExtension.php b/src/Type/Php/OpensslCipherFunctionsReturnTypeExtension.php index 2339c44f8f4..c59e80ac87f 100644 --- a/src/Type/Php/OpensslCipherFunctionsReturnTypeExtension.php +++ b/src/Type/Php/OpensslCipherFunctionsReturnTypeExtension.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\Constant\ConstantBooleanType; @@ -22,6 +23,7 @@ final class OpensslCipherFunctionsReturnTypeExtension implements DynamicFunction { public function __construct( + private PhpVersion $phpVersion, private OpenSslCipherMethodsProvider $cipherMethodsProvider, ) { @@ -34,7 +36,7 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type { - if (!$scope->getPhpVersion()->throwsValueErrorForInternalFunctions()->yes()) { + if (!$this->phpVersion->throwsValueErrorForInternalFunctions()) { return null; } diff --git a/src/Type/Php/PDOConnectReturnTypeExtension.php b/src/Type/Php/PDOConnectReturnTypeExtension.php index 82ceb6118ee..9aa71ccd275 100644 --- a/src/Type/Php/PDOConnectReturnTypeExtension.php +++ b/src/Type/Php/PDOConnectReturnTypeExtension.php @@ -22,9 +22,7 @@ final class PDOConnectReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension { - public function __construct( - private PhpVersion $phpVersion, - ) + public function __construct(private PhpVersion $phpVersion) { } @@ -35,8 +33,7 @@ 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 c7ee32d4d71..83a6456eae9 100644 --- a/src/Type/Php/PrintfFunctionThrowTypeExtension.php +++ b/src/Type/Php/PrintfFunctionThrowTypeExtension.php @@ -8,6 +8,7 @@ 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; @@ -42,6 +43,7 @@ final class PrintfFunctionThrowTypeExtension implements DynamicFunctionThrowType private const NEGATIVE_PRECISION_SPECIFIERS = ['g', 'G', 'h', 'H']; public function __construct( + private PhpVersion $phpVersion, private PrintfFormatParser $formatParser, ) { @@ -54,7 +56,7 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getThrowTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $funcCall, Scope $scope): ?Type { - if ($scope->getPhpVersion()->throwsValueErrorForInternalFunctions()->no()) { + if (!$this->phpVersion->throwsValueErrorForInternalFunctions()) { return new VoidType(); } diff --git a/src/Type/Php/RoundFunctionReturnTypeExtension.php b/src/Type/Php/RoundFunctionReturnTypeExtension.php index 7adafbf76d5..2bc17560339 100644 --- a/src/Type/Php/RoundFunctionReturnTypeExtension.php +++ b/src/Type/Php/RoundFunctionReturnTypeExtension.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\Accessory\AccessoryNumericStringType; use PHPStan\Type\BooleanType; @@ -26,6 +27,10 @@ final class RoundFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + public function __construct(private PhpVersion $phpVersion) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return in_array( @@ -45,15 +50,16 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, // PHP 8 can either return a float or fatal. $defaultReturnType = null; - $hasStricterRoundFunctions = $scope->getPhpVersion()->hasStricterRoundFunctions(); + 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(); + } if (count($functionCall->getArgs()) < 1) { - // PHP 8 fatals with a missing parameter, PHP 7 returns null. - if ($hasStricterRoundFunctions->yes()) { - return new NeverType(true); - } - - return new NullType(); + return $noArgsReturnType; } $firstArgType = $scope->getType($functionCall->getArgs()[0]->value); @@ -62,7 +68,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return $defaultReturnType; } - if ($hasStricterRoundFunctions->yes()) { + if ($this->phpVersion->hasStricterRoundFunctions()) { if (!$scope->isDeclareStrictTypes()) { $allowed = new UnionType([ new IntegerType(), diff --git a/src/Type/Php/RoundFunctionThrowTypeExtension.php b/src/Type/Php/RoundFunctionThrowTypeExtension.php index 2f3af9c16ce..7a79ec7b2b1 100644 --- a/src/Type/Php/RoundFunctionThrowTypeExtension.php +++ b/src/Type/Php/RoundFunctionThrowTypeExtension.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\DynamicFunctionThrowTypeExtension; use PHPStan\Type\IntegerRangeType; @@ -22,6 +23,10 @@ final class RoundFunctionThrowTypeExtension implements DynamicFunctionThrowTypeExtension { + public function __construct(private PhpVersion $phpVersion) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'round'; @@ -29,7 +34,7 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getThrowTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $funcCall, Scope $scope): ?Type { - if ($scope->getPhpVersion()->throwsValueErrorForInvalidRoundingMode()->no()) { + if (!$this->phpVersion->throwsValueErrorForInvalidRoundingMode()) { return new VoidType(); } 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/src/Type/Php/StrSplitFunctionThrowTypeExtension.php b/src/Type/Php/StrSplitFunctionThrowTypeExtension.php index 10891dbc717..bcc1815efad 100644 --- a/src/Type/Php/StrSplitFunctionThrowTypeExtension.php +++ b/src/Type/Php/StrSplitFunctionThrowTypeExtension.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\DynamicFunctionThrowTypeExtension; use PHPStan\Type\IntegerRangeType; @@ -19,6 +20,10 @@ final class StrSplitFunctionThrowTypeExtension implements DynamicFunctionThrowTypeExtension { + public function __construct(private PhpVersion $phpVersion) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'str_split'; @@ -26,7 +31,7 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getThrowTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $funcCall, Scope $scope): ?Type { - if ($scope->getPhpVersion()->throwsValueErrorForInternalFunctions()->no()) { + if (!$this->phpVersion->throwsValueErrorForInternalFunctions()) { return new VoidType(); } diff --git a/src/Type/Php/SubstrDynamicReturnTypeExtension.php b/src/Type/Php/SubstrDynamicReturnTypeExtension.php index 0e92a4835f6..be4ec43a315 100644 --- a/src/Type/Php/SubstrDynamicReturnTypeExtension.php +++ b/src/Type/Php/SubstrDynamicReturnTypeExtension.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\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; @@ -30,6 +31,10 @@ 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); @@ -46,7 +51,6 @@ public function getTypeFromFunctionCall( return null; } - $returnsFalseInsteadOfEmptyString = $scope->getPhpVersion()->substrReturnFalseInsteadOfEmptyString(); $string = $scope->getType($args[0]->value); $offset = $scope->getType($args[1]->value); @@ -86,12 +90,9 @@ public function getTypeFromFunctionCall( ? $this->substrOrFalse($constantString->getValue(), $offset->getValue(), $length->getValue()) : $this->substrOrFalse($constantString->getValue(), $offset->getValue()); if ($substr === false) { - if (!$returnsFalseInsteadOfEmptyString->no()) { - $results[] = new ConstantBooleanType(false); - } - if (!$returnsFalseInsteadOfEmptyString->yes()) { - $results[] = new ConstantStringType(''); - } + $results[] = $this->phpVersion->substrReturnFalseInsteadOfEmptyString() + ? new ConstantBooleanType(false) + : new ConstantStringType(''); } else { $results[] = new ConstantStringType($substr); } @@ -119,7 +120,7 @@ public function getTypeFromFunctionCall( if (count($accessoryTypes) > 0) { $accessoryTypes[] = new StringType(); - if (!$isNotEmpty && !$returnsFalseInsteadOfEmptyString->no()) { + if (!$isNotEmpty && $this->phpVersion->substrReturnFalseInsteadOfEmptyString()) { return new UnionType([ new ConstantBooleanType(false), new IntersectionType($accessoryTypes), diff --git a/src/Type/Php/TriggerErrorDynamicReturnTypeExtension.php b/src/Type/Php/TriggerErrorDynamicReturnTypeExtension.php index fcc0aad7f1b..3f3e09242df 100644 --- a/src/Type/Php/TriggerErrorDynamicReturnTypeExtension.php +++ b/src/Type/Php/TriggerErrorDynamicReturnTypeExtension.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\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; @@ -22,6 +23,10 @@ final class TriggerErrorDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + public function __construct(private PhpVersion $phpVersion) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'trigger_error'; @@ -49,7 +54,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } if (!in_array($errorLevel, [E_USER_WARNING, E_USER_NOTICE, E_USER_DEPRECATED], true)) { - if ($scope->getPhpVersion()->throwsValueErrorForInternalFunctions()->yes()) { + if ($this->phpVersion->throwsValueErrorForInternalFunctions()) { return new NeverType(true); } diff --git a/src/Type/Php/TriggerErrorFunctionThrowTypeExtension.php b/src/Type/Php/TriggerErrorFunctionThrowTypeExtension.php index 422c6782ac7..49c61fc2642 100644 --- a/src/Type/Php/TriggerErrorFunctionThrowTypeExtension.php +++ b/src/Type/Php/TriggerErrorFunctionThrowTypeExtension.php @@ -6,6 +6,7 @@ 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; @@ -34,6 +35,7 @@ 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, ) @@ -63,7 +65,7 @@ public function getThrowTypeFromFunctionCall(FunctionReflection $functionReflect return $errorHandlerThrowType; } - return $this->getInvalidErrorLevelThrowType($scope); + return $this->getInvalidErrorLevelThrowType(); } $throwTypes = []; @@ -80,7 +82,7 @@ public function getThrowTypeFromFunctionCall(FunctionReflection $functionReflect continue; } - $invalidErrorLevelThrowType = $this->getInvalidErrorLevelThrowType($scope); + $invalidErrorLevelThrowType = $this->getInvalidErrorLevelThrowType(); if ($invalidErrorLevelThrowType === null) { continue; } @@ -95,9 +97,9 @@ public function getThrowTypeFromFunctionCall(FunctionReflection $functionReflect return TypeCombinator::union(...$throwTypes); } - private function getInvalidErrorLevelThrowType(Scope $scope): ?Type + private function getInvalidErrorLevelThrowType(): ?Type { - if ($scope->getPhpVersion()->throwsValueErrorForInternalFunctions()->no()) { + if (!$this->phpVersion->throwsValueErrorForInternalFunctions()) { return null; } diff --git a/src/Type/Php/UnserializeFunctionThrowTypeExtension.php b/src/Type/Php/UnserializeFunctionThrowTypeExtension.php index 56cfb20301e..3a5a95c8384 100644 --- a/src/Type/Php/UnserializeFunctionThrowTypeExtension.php +++ b/src/Type/Php/UnserializeFunctionThrowTypeExtension.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\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantStringType; @@ -26,6 +27,10 @@ final class UnserializeFunctionThrowTypeExtension implements DynamicFunctionThrowTypeExtension { + public function __construct(private PhpVersion $phpVersion) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'unserialize'; @@ -33,7 +38,7 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getThrowTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $funcCall, Scope $scope): ?Type { - if ($scope->getPhpVersion()->throwsValueErrorForInternalFunctions()->no()) { + if (!$this->phpVersion->throwsValueErrorForInternalFunctions()) { return new VoidType(); } diff --git a/src/Type/Php/VersionCompareFunctionDynamicReturnTypeExtension.php b/src/Type/Php/VersionCompareFunctionDynamicReturnTypeExtension.php index 5f6d818b822..f6a8d4a7282 100644 --- a/src/Type/Php/VersionCompareFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/VersionCompareFunctionDynamicReturnTypeExtension.php @@ -7,6 +7,7 @@ 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; @@ -46,6 +47,7 @@ final class VersionCompareFunctionDynamicReturnTypeExtension implements DynamicF public function __construct( private ConfiguredPhpVersionRangeHelper $phpVersionRangeHelper, + private PhpVersion $phpVersion, ) { } @@ -66,7 +68,6 @@ public function getTypeFromFunctionCall( return null; } - $throwsValueError = $scope->getPhpVersion()->throwsValueErrorForInternalFunctions(); $version1Strings = $this->getVersionStrings($args[0]->value, $scope); $version2Strings = $this->getVersionStrings($args[1]->value, $scope); $counts = [ @@ -77,9 +78,9 @@ public function getTypeFromFunctionCall( if (isset($args[2])) { $operatorStrings = $scope->getType($args[2]->value)->getConstantStrings(); $counts[] = count($operatorStrings); - $returnType = !$throwsValueError->yes() && self::mightBeInvalidOperator($operatorStrings) - ? new BenevolentUnionType([new BooleanType(), new NullType()]) - : new BooleanType(); + $returnType = $this->phpVersion->throwsValueErrorForInternalFunctions() + ? new BooleanType() + : new BenevolentUnionType([new BooleanType(), new NullType()]); } else { $returnType = new UnionType([ new ConstantIntegerType(-1), @@ -104,7 +105,7 @@ public function getTypeFromFunctionCall( foreach ($operatorStrings as $operatorString) { $operatorValue = $operatorString->getValue(); if (!in_array($operatorValue, self::VALID_OPERATORS, true)) { - if (!$throwsValueError->yes()) { + if (!$this->phpVersion->throwsValueErrorForInternalFunctions()) { $canBeNull = true; } @@ -128,27 +129,6 @@ 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 96ae2961c44..e4c4f92b34d 100644 --- a/src/Type/Php/VersionCompareFunctionDynamicThrowTypeExtension.php +++ b/src/Type/Php/VersionCompareFunctionDynamicThrowTypeExtension.php @@ -5,14 +5,23 @@ 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'; @@ -24,7 +33,7 @@ public function getThrowTypeFromFunctionCall( Scope $scope, ): ?Type { - if ($scope->getPhpVersion()->throwsValueErrorForInternalFunctions()->no()) { + if (!$this->phpVersion->throwsValueErrorForInternalFunctions()) { return null; } @@ -34,10 +43,17 @@ public function getThrowTypeFromFunctionCall( } $operatorStrings = $scope->getType($args[2]->value)->getConstantStrings(); - if (VersionCompareFunctionDynamicReturnTypeExtension::mightBeInvalidOperator($operatorStrings)) { + if (count($operatorStrings) === 0) { 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 deleted file mode 100644 index d16c74f2b39..00000000000 --- a/tests/PHPStan/Analyser/ScopePhpVersionRangeReturnTypeExtensionsTest.php +++ /dev/null @@ -1,36 +0,0 @@ -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 deleted file mode 100644 index 1f439ee74b8..00000000000 --- a/tests/PHPStan/Analyser/data/scope-php-version-range-return-type-extensions.php +++ /dev/null @@ -1,28 +0,0 @@ -= 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 pregMatchUnmatchedAsNull(string $s): void -{ - if (PHP_VERSION_ID >= 70400) { - if (preg_match('/(a)(b)?/', $s, $matches, PREG_UNMATCHED_AS_NULL) === 1) { - assertType('array{non-falsy-string, \'a\', \'b\'|null}', $matches); - } - } else { - if (preg_match('/(a)(b)?/', $s, $matches, PREG_UNMATCHED_AS_NULL) === 1) { - assertType('array{0: non-falsy-string, 1: \'a\', 2?: \'b\'}', $matches); - } - } -} - -function pregCaptureOnlyNamedGroups(string $s): void -{ - if (PHP_VERSION_ID >= 80200) { - if (preg_match('/(?a)(b)/n', $s, $named) === 1) { - assertType('array{0: non-falsy-string, name: \'a\', 1: \'a\'}', $named); - } - } else { - if (preg_match('/(?a)(b)/n', $s, $named) === 1) { - assertType('array{0: non-falsy-string, name: \'a\', 1: \'a\', 2: \'b\'}', $named); - } - } -} - -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 deleted file mode 100644 index 7bfac80ba3b..00000000000 --- a/tests/PHPStan/Analyser/scopePhpVersionRange.neon +++ /dev/null @@ -1,4 +0,0 @@ -parameters: - phpVersion: - min: 70400 - max: 80500 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 []; - } - -} diff --git a/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleConfigPhpTest.php b/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleConfigPhpTest.php deleted file mode 100644 index 85cbc4a08e4..00000000000 --- a/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleConfigPhpTest.php +++ /dev/null @@ -1,42 +0,0 @@ - - */ -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 deleted file mode 100644 index af9dbc277b8..00000000000 --- a/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleGetClassConfigPhpTest.php +++ /dev/null @@ -1,42 +0,0 @@ - - */ -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 deleted file mode 100644 index e6683ba7e70..00000000000 --- a/tests/PHPStan/Rules/Exceptions/data/get-class-throw-type-php-version.neon +++ /dev/null @@ -1,4 +0,0 @@ -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 deleted file mode 100644 index 4522057d202..00000000000 --- a/tests/PHPStan/Rules/Exceptions/data/get-class-throw-type-php-versions.php +++ /dev/null @@ -1,31 +0,0 @@ -= 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 deleted file mode 100644 index 1768b64451f..00000000000 --- a/tests/PHPStan/Rules/Exceptions/data/throw-type-php-version.neon +++ /dev/null @@ -1,4 +0,0 @@ -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 deleted file mode 100644 index f3f83eedf7d..00000000000 --- a/tests/PHPStan/Rules/Exceptions/data/throw-type-php-versions.php +++ /dev/null @@ -1,54 +0,0 @@ -= 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 0d8d59963ef07abb9ec2b5ef6c775efd5dd6aaed Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Wed, 23 Sep 2026 06:59:15 +0000 Subject: [PATCH 19/21] Leave the Regex* classes out of this pull request RegexArrayShapeMatcher and RegexGroupParser will read the analysed PHP version from the Scope in a dedicated change. Co-Authored-By: Claude Opus 5.5 --- src/Php/PhpVersions.php | 13 --- src/Type/Php/RegexArrayShapeMatcher.php | 79 +++++++++---------- src/Type/Regex/RegexGroupParser.php | 34 ++------ ...lToFunctionParametersRuleConfigPhpTest.php | 64 --------------- .../preg-unmatched-as-null-php-version.neon | 4 - .../preg-unmatched-as-null-php-versions.php | 14 ---- 6 files changed, 44 insertions(+), 164 deletions(-) delete mode 100644 tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleConfigPhpTest.php delete mode 100644 tests/PHPStan/Rules/Functions/data/preg-unmatched-as-null-php-version.neon delete mode 100644 tests/PHPStan/Rules/Functions/data/preg-unmatched-as-null-php-versions.php diff --git a/src/Php/PhpVersions.php b/src/Php/PhpVersions.php index 07bf6eb0ca7..a942d45234a 100644 --- a/src/Php/PhpVersions.php +++ b/src/Php/PhpVersions.php @@ -198,19 +198,6 @@ public function supportsHhPrintfSpecifier(): TrinaryLogic return IntegerRangeType::fromInterval(80000, null)->isSuperTypeOf($this->phpVersions)->result; } - public function supportsPregCaptureOnlyNamedGroups(): TrinaryLogic - { - // https://php.watch/versions/8.2/preg-n-no-capture-modifier - return IntegerRangeType::fromInterval(80200, null)->isSuperTypeOf($this->phpVersions)->result; - } - - public function supportsPregUnmatchedAsNull(): TrinaryLogic - { - // while PREG_UNMATCHED_AS_NULL is defined in php-src since 7.2.x it starts working as expected with 7.4.x - // https://3v4l.org/v3HE4 - return IntegerRangeType::fromInterval(70400, null)->isSuperTypeOf($this->phpVersions)->result; - } - /** * PHPStan's BetterReflection adapters keep their narrowed native return types only on PHP 8+; * the downgraded PHP 7 build widens them back to the core Reflection ones. diff --git a/src/Type/Php/RegexArrayShapeMatcher.php b/src/Type/Php/RegexArrayShapeMatcher.php index 13ddfb86f0d..4badd12420f 100644 --- a/src/Type/Php/RegexArrayShapeMatcher.php +++ b/src/Type/Php/RegexArrayShapeMatcher.php @@ -5,7 +5,7 @@ use PhpParser\Node\Expr; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Php\PhpVersions; +use PHPStan\Php\PhpVersion; use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\NonEmptyArrayType; @@ -47,18 +47,19 @@ final class RegexArrayShapeMatcher public function __construct( private RegexGroupParser $regexGroupParser, private RegexExpressionHelper $regexExpressionHelper, + private PhpVersion $phpVersion, ) { } public function matchAllExpr(Expr $patternExpr, ?Type $flagsType, TrinaryLogic $wasMatched, Scope $scope): ?Type { - return $this->matchPatternType($this->getPatternType($patternExpr, $scope), $flagsType, $wasMatched, true, $scope->getPhpVersion()); + return $this->matchPatternType($this->getPatternType($patternExpr, $scope), $flagsType, $wasMatched, true); } public function matchExpr(Expr $patternExpr, ?Type $flagsType, TrinaryLogic $wasMatched, Scope $scope): ?Type { - return $this->matchPatternType($this->getPatternType($patternExpr, $scope), $flagsType, $wasMatched, false, $scope->getPhpVersion()); + return $this->matchPatternType($this->getPatternType($patternExpr, $scope), $flagsType, $wasMatched, false); } public function matchSubjectExpr(Expr $patternExpr, Scope $scope): ?Type @@ -71,7 +72,7 @@ public function matchSubjectExpr(Expr $patternExpr, Scope $scope): ?Type $subjectTypes = []; foreach ($constantStrings as $constantString) { - $astWalkResult = $this->regexGroupParser->parseGroups($constantString->getValue(), $scope->getPhpVersion()); + $astWalkResult = $this->regexGroupParser->parseGroups($constantString->getValue()); if ($astWalkResult === null) { return null; } @@ -82,7 +83,7 @@ public function matchSubjectExpr(Expr $patternExpr, Scope $scope): ?Type return TypeCombinator::union(...$subjectTypes); } - private function matchPatternType(Type $patternType, ?Type $flagsType, TrinaryLogic $wasMatched, bool $matchesAll, PhpVersions $phpVersions): ?Type + private function matchPatternType(Type $patternType, ?Type $flagsType, TrinaryLogic $wasMatched, bool $matchesAll): ?Type { if ($wasMatched->no()) { return ConstantArrayTypeBuilder::createEmpty()->getArray(); @@ -110,7 +111,7 @@ private function matchPatternType(Type $patternType, ?Type $flagsType, TrinaryLo $matchedTypes = []; foreach ($constantStrings as $constantString) { - $matched = $this->matchRegex($constantString->getValue(), $flags, $wasMatched, $matchesAll, $phpVersions); + $matched = $this->matchRegex($constantString->getValue(), $flags, $wasMatched, $matchesAll); if ($matched === null) { return null; } @@ -128,9 +129,9 @@ private function matchPatternType(Type $patternType, ?Type $flagsType, TrinaryLo /** * @param int-mask|null $flags */ - private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched, bool $matchesAll, PhpVersions $phpVersions): ?Type + private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched, bool $matchesAll): ?Type { - $astWalkResult = $this->regexGroupParser->parseGroups($regex, $phpVersions); + $astWalkResult = $this->regexGroupParser->parseGroups($regex); if ($astWalkResult === null) { // regex could not be parsed by Hoa/Regex return null; @@ -165,13 +166,12 @@ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched $flags, $markVerbs, $matchesAll, - $phpVersions, ); - if (!$this->containsUnmatchedAsNull($flags, $matchesAll, $phpVersions)) { + if (!$this->containsUnmatchedAsNull($flags, $matchesAll)) { // positive match has a subject but not any capturing group $builder = ConstantArrayTypeBuilder::createEmpty(); - $builder->setOffsetValueType(new ConstantIntegerType(0), $this->createSubjectValueType($subjectBaseType, $flags, $matchesAll, $wasMatched, $phpVersions)); + $builder->setOffsetValueType(new ConstantIntegerType(0), $this->createSubjectValueType($subjectBaseType, $flags, $matchesAll, $wasMatched)); $combiType = TypeCombinator::union( $builder->getArray(), @@ -202,11 +202,11 @@ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched } elseif ($beforeCurrentCombo && !$group->resetsGroupCounter()) { $comboList = $comboList->forceGroupTypeAndNonOptional( $group, - $this->containsUnmatchedAsNull($flags, $matchesAll, $phpVersions) ? new NullType() : new ConstantStringType(''), + $this->containsUnmatchedAsNull($flags, $matchesAll) ? new NullType() : new ConstantStringType(''), ); } elseif ( $group->getAlternationId() === $onlyTopLevelAlternation->getId() - && !$this->containsUnmatchedAsNull($flags, $matchesAll, $phpVersions) + && !$this->containsUnmatchedAsNull($flags, $matchesAll) ) { $comboList = $comboList->removeGroup($group); } @@ -220,14 +220,13 @@ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched $flags, $markVerbs, $matchesAll, - $phpVersions, ); $combiTypes[] = $combiType; } if ( - !$this->containsUnmatchedAsNull($flags, $matchesAll, $phpVersions) + !$this->containsUnmatchedAsNull($flags, $matchesAll) && ( $onlyTopLevelAlternation->getAlternationsCount() !== count($onlyTopLevelAlternation->getGroupCombinations()) || $isOptionalAlternation @@ -235,7 +234,7 @@ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched ) { // positive match has a subject but not any capturing group $builder = ConstantArrayTypeBuilder::createEmpty(); - $builder->setOffsetValueType(new ConstantIntegerType(0), $this->createSubjectValueType($subjectBaseType, $flags, $matchesAll, $wasMatched, $phpVersions)); + $builder->setOffsetValueType(new ConstantIntegerType(0), $this->createSubjectValueType($subjectBaseType, $flags, $matchesAll, $wasMatched)); $combiTypes[] = $builder->getArray(); } @@ -253,7 +252,6 @@ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched $flags, $markVerbs, $matchesAll, - $phpVersions, ); } @@ -268,7 +266,6 @@ private function buildArrayType( int $flags, array $markVerbs, bool $matchesAll, - PhpVersions $phpVersions, ): Type { $forceList = count($markVerbs) === 0; @@ -277,7 +274,7 @@ private function buildArrayType( // first item in matches contains the overall match. $builder->setOffsetValueType( $this->getKeyType(0), - $this->createSubjectValueType($subjectBaseType, $flags, $matchesAll, $wasMatched, $phpVersions), + $this->createSubjectValueType($subjectBaseType, $flags, $matchesAll, $wasMatched), $this->isSubjectOptional($wasMatched, $matchesAll), ); @@ -286,8 +283,8 @@ private function buildArrayType( foreach ($captureGroups as $captureGroup) { $isTrailingOptional = $i >= $countGroups - $trailingOptionals; $isLastGroup = $i === $countGroups - 1; - $groupValueType = $this->createGroupValueType($captureGroup, $wasMatched, $flags, $isTrailingOptional, $isLastGroup, $matchesAll, $phpVersions); - $optional = $this->isGroupOptional($captureGroup, $wasMatched, $flags, $isTrailingOptional, $matchesAll, $phpVersions); + $groupValueType = $this->createGroupValueType($captureGroup, $wasMatched, $flags, $isTrailingOptional, $isLastGroup, $matchesAll); + $optional = $this->isGroupOptional($captureGroup, $wasMatched, $flags, $isTrailingOptional, $matchesAll); if ($captureGroup->isNamed()) { $forceList = false; @@ -348,12 +345,12 @@ private function isSubjectOptional(TrinaryLogic $wasMatched, bool $matchesAll): /** * @param Type $baseType A string type (or string variant) representing the subject of the match */ - private function createSubjectValueType(Type $baseType, int $flags, bool $matchesAll, TrinaryLogic $wasMatched, PhpVersions $phpVersions): Type + private function createSubjectValueType(Type $baseType, int $flags, bool $matchesAll, TrinaryLogic $wasMatched): Type { - $subjectValueType = TypeCombinator::removeNull($this->getValueType($baseType, $flags, $matchesAll, $phpVersions)); + $subjectValueType = TypeCombinator::removeNull($this->getValueType($baseType, $flags, $matchesAll)); if ($matchesAll) { - $subjectValueType = TypeCombinator::removeNull($this->getValueType(new StringType(), $flags, $matchesAll, $phpVersions)); + $subjectValueType = TypeCombinator::removeNull($this->getValueType(new StringType(), $flags, $matchesAll)); if ($this->containsPatternOrder($flags)) { $accessoryTypes = [ @@ -371,10 +368,10 @@ private function createSubjectValueType(Type $baseType, int $flags, bool $matche return $subjectValueType; } - private function isGroupOptional(RegexCapturingGroup $captureGroup, TrinaryLogic $wasMatched, int $flags, bool $isTrailingOptional, bool $matchesAll, PhpVersions $phpVersions): bool + private function isGroupOptional(RegexCapturingGroup $captureGroup, TrinaryLogic $wasMatched, int $flags, bool $isTrailingOptional, bool $matchesAll): bool { if ($matchesAll) { - if ($isTrailingOptional && !$this->containsUnmatchedAsNull($flags, $matchesAll, $phpVersions) && $this->containsSetOrder($flags)) { + if ($isTrailingOptional && !$this->containsUnmatchedAsNull($flags, $matchesAll) && $this->containsSetOrder($flags)) { return true; } @@ -386,7 +383,7 @@ private function isGroupOptional(RegexCapturingGroup $captureGroup, TrinaryLogic } else { if (!$isTrailingOptional) { $optional = false; - } elseif ($this->containsUnmatchedAsNull($flags, $matchesAll, $phpVersions)) { + } elseif ($this->containsUnmatchedAsNull($flags, $matchesAll)) { $optional = false; } else { $optional = $captureGroup->isOptional(); @@ -396,19 +393,19 @@ private function isGroupOptional(RegexCapturingGroup $captureGroup, TrinaryLogic return $optional; } - private function createGroupValueType(RegexCapturingGroup $captureGroup, TrinaryLogic $wasMatched, int $flags, bool $isTrailingOptional, bool $isLastGroup, bool $matchesAll, PhpVersions $phpVersions): Type + private function createGroupValueType(RegexCapturingGroup $captureGroup, TrinaryLogic $wasMatched, int $flags, bool $isTrailingOptional, bool $isLastGroup, bool $matchesAll): Type { if ($matchesAll) { if ( ( !$this->containsSetOrder($flags) - && !$this->containsUnmatchedAsNull($flags, $matchesAll, $phpVersions) + && !$this->containsUnmatchedAsNull($flags, $matchesAll) && $captureGroup->isOptional() ) || ( $this->containsSetOrder($flags) - && !$this->containsUnmatchedAsNull($flags, $matchesAll, $phpVersions) + && !$this->containsUnmatchedAsNull($flags, $matchesAll) && $captureGroup->isOptional() && !$isTrailingOptional ) @@ -417,14 +414,13 @@ private function createGroupValueType(RegexCapturingGroup $captureGroup, Trinary TypeCombinator::union($captureGroup->getType(), new ConstantStringType('')), $flags, $matchesAll, - $phpVersions, ); $groupValueType = TypeCombinator::removeNull($groupValueType); } else { - $groupValueType = $this->getValueType($captureGroup->getType(), $flags, $matchesAll, $phpVersions); + $groupValueType = $this->getValueType($captureGroup->getType(), $flags, $matchesAll); } - if (!$isTrailingOptional && $this->containsUnmatchedAsNull($flags, $matchesAll, $phpVersions) && !$captureGroup->isOptional()) { + if (!$isTrailingOptional && $this->containsUnmatchedAsNull($flags, $matchesAll) && !$captureGroup->isOptional()) { $groupValueType = TypeCombinator::removeNull($groupValueType); } @@ -440,19 +436,18 @@ private function createGroupValueType(RegexCapturingGroup $captureGroup, Trinary return $groupValueType; } - if (!$isLastGroup && !$this->containsUnmatchedAsNull($flags, $matchesAll, $phpVersions) && $captureGroup->isOptional()) { + if (!$isLastGroup && !$this->containsUnmatchedAsNull($flags, $matchesAll) && $captureGroup->isOptional()) { $groupValueType = $this->getValueType( TypeCombinator::union($captureGroup->getType(), new ConstantStringType('')), $flags, $matchesAll, - $phpVersions, ); } else { - $groupValueType = $this->getValueType($captureGroup->getType(), $flags, $matchesAll, $phpVersions); + $groupValueType = $this->getValueType($captureGroup->getType(), $flags, $matchesAll); } if ($wasMatched->yes()) { - if (!$isTrailingOptional && $this->containsUnmatchedAsNull($flags, $matchesAll, $phpVersions) && !$captureGroup->isOptional()) { + if (!$isTrailingOptional && $this->containsUnmatchedAsNull($flags, $matchesAll) && !$captureGroup->isOptional()) { $groupValueType = TypeCombinator::removeNull($groupValueType); } } @@ -476,7 +471,7 @@ private function containsSetOrder(int $flags): bool return ($flags & PREG_SET_ORDER) !== 0; } - private function containsUnmatchedAsNull(int $flags, bool $matchesAll, PhpVersions $phpVersions): bool + private function containsUnmatchedAsNull(int $flags, bool $matchesAll): bool { if ($matchesAll) { // preg_match_all() with PREG_UNMATCHED_AS_NULL works consistently across php-versions @@ -484,9 +479,7 @@ private function containsUnmatchedAsNull(int $flags, bool $matchesAll, PhpVersio return ($flags & PREG_UNMATCHED_AS_NULL) !== 0; } - // an analysed range that still reaches PHP 7.4 has to assume the documented behaviour, - // because inferring a string where the group can be null would hide a real null - return ($flags & PREG_UNMATCHED_AS_NULL) !== 0 && (($flags & self::PREG_UNMATCHED_AS_NULL_ON_72_73) !== 0 || !$phpVersions->supportsPregUnmatchedAsNull()->no()); + return ($flags & PREG_UNMATCHED_AS_NULL) !== 0 && (($flags & self::PREG_UNMATCHED_AS_NULL_ON_72_73) !== 0 || $this->phpVersion->supportsPregUnmatchedAsNull()); } private function getKeyType(int|string $key): Type @@ -498,13 +491,13 @@ private function getKeyType(int|string $key): Type return new ConstantIntegerType($key); } - private function getValueType(Type $baseType, int $flags, bool $matchesAll, PhpVersions $phpVersions): Type + private function getValueType(Type $baseType, int $flags, bool $matchesAll): Type { $valueType = $baseType; // unmatched groups return -1 as offset $offsetType = IntegerRangeType::fromInterval(-1, null); - if ($this->containsUnmatchedAsNull($flags, $matchesAll, $phpVersions)) { + if ($this->containsUnmatchedAsNull($flags, $matchesAll)) { $valueType = TypeCombinator::addNull($valueType); } diff --git a/src/Type/Regex/RegexGroupParser.php b/src/Type/Regex/RegexGroupParser.php index 2086ebbee75..048b2a3beb3 100644 --- a/src/Type/Regex/RegexGroupParser.php +++ b/src/Type/Regex/RegexGroupParser.php @@ -10,7 +10,7 @@ use Nette\Utils\RegexpException; use Nette\Utils\Strings; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Php\PhpVersions; +use PHPStan\Php\PhpVersion; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; @@ -45,16 +45,6 @@ final class RegexGroupParser 'J', // rare modifier too complicated to support ]; - /** - * Modifiers that only exist in the PCRE library bundled with newer PHP versions. - * The analysed version can support them while this process runs on an older one, - * so they are stripped before the pattern is compiled here. PHPStan implements their - * effect itself, so removing them does not change the inferred shape. - */ - private const MODIFIERS_NOT_COMPILABLE_ON_EVERY_RUNTIME = [ - 'n', // PHP 8.2+, https://php.watch/versions/8.2/preg-n-no-capture-modifier - ]; - // upper bound on the number of constant string literals enumerated from a group, // to avoid combinatorial explosion from nested optional/bounded quantifications private const LITERALS_LIMIT = 100; @@ -65,31 +55,33 @@ final class RegexGroupParser private static array $parsedAst = []; public function __construct( + private PhpVersion $phpVersion, private RegexExpressionHelper $regexExpressionHelper, ) { } - public function parseGroups(string $regex, PhpVersions $phpVersions): ?RegexAstWalkResult + public function parseGroups(string $regex): ?RegexAstWalkResult { /** @throws void */ self::$parser ??= Llk::load(new Read(__DIR__ . '/../../../resources/RegexGrammar.pp')); - $modifiers = $this->regexExpressionHelper->getPatternModifiers($regex) ?? ''; - if (array_key_exists($regex, self::$parsedAst)) { $ast = self::$parsedAst[$regex]; if ($ast === null) { return null; } + + $modifiers = $this->regexExpressionHelper->getPatternModifiers($regex) ?? ''; } else { try { - Strings::match('', $this->removeModifiersNotCompilableOnEveryRuntime($regex, $modifiers)); + Strings::match('', $regex); } catch (RegexpException) { // pattern is invalid, so let the RegularExpressionPatternRule report it return self::$parsedAst[$regex] = null; } + $modifiers = $this->regexExpressionHelper->getPatternModifiers($regex) ?? ''; foreach (self::NOT_SUPPORTED_MODIFIERS as $notSupportedModifier) { if (str_contains($modifiers, $notSupportedModifier)) { return self::$parsedAst[$regex] = null; @@ -114,7 +106,7 @@ public function parseGroups(string $regex, PhpVersions $phpVersions): ?RegexAstW $this->updateCapturingAstAddEmptyToken($ast); $captureOnlyNamed = false; - if ($phpVersions->supportsPregCaptureOnlyNamedGroups()->yes()) { + if ($this->phpVersion->supportsPregCaptureOnlyNamedGroups()) { $captureOnlyNamed = str_contains($modifiers, 'n'); } @@ -158,16 +150,6 @@ private function createEmptyTokenTreeNode(TreeNode $parentAst): TreeNode return new TreeNode('token', ['token' => 'literal', 'value' => '', 'namespace' => 'default'], parent: $parentAst); } - private function removeModifiersNotCompilableOnEveryRuntime(string $regex, string $modifiers): string - { - $compilableModifiers = str_replace(self::MODIFIERS_NOT_COMPILABLE_ON_EVERY_RUNTIME, '', $modifiers); - if ($compilableModifiers === $modifiers) { - return $regex; - } - - return substr($regex, 0, strlen($regex) - strlen($modifiers)) . $compilableModifiers; - } - private function updateAlternationAstRemoveVerticalBarsAndAddEmptyToken(TreeNode $ast): void { $children = $ast->getChildren(); diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleConfigPhpTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleConfigPhpTest.php deleted file mode 100644 index c597bc3c7a1..00000000000 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleConfigPhpTest.php +++ /dev/null @@ -1,64 +0,0 @@ - - */ -class CallToFunctionParametersRuleConfigPhpTest extends RuleTestCase -{ - - protected function getRule(): Rule - { - $broker = self::createReflectionProvider(); - return new CallToFunctionParametersRule( - $broker, - new FunctionCallParametersCheck( - new RuleLevelHelper( - $broker, - checkNullables: true, - checkThisOnly: false, - checkUnionTypes: true, - checkExplicitMixed: false, - checkImplicitMixed: false, - checkBenevolentUnionTypes: false, - discoveringSymbolsTip: true, - ), - new NullsafeCheck(), - new UnresolvableTypeHelper(), - new PropertyReflectionFinder(), - $broker, - checkArgumentTypes: true, - checkArgumentsPassedByReference: true, - checkExtraArguments: true, - checkMissingTypehints: true, - ), - ); - } - - public function testPregUnmatchedAsNullInPhpVersionRange(): void - { - $this->analyse([__DIR__ . '/data/preg-unmatched-as-null-php-versions.php'], [ - [ - 'Parameter #1 $s of function PregUnmatchedAsNullPhpVersions\needsString expects string, string|null given.', - 12, - ], - ]); - } - - public static function getAdditionalConfigFiles(): array - { - return [ - __DIR__ . '/data/preg-unmatched-as-null-php-version.neon', - ]; - } - -} diff --git a/tests/PHPStan/Rules/Functions/data/preg-unmatched-as-null-php-version.neon b/tests/PHPStan/Rules/Functions/data/preg-unmatched-as-null-php-version.neon deleted file mode 100644 index dfce7af1e04..00000000000 --- a/tests/PHPStan/Rules/Functions/data/preg-unmatched-as-null-php-version.neon +++ /dev/null @@ -1,4 +0,0 @@ -parameters: - phpVersion: - min: 70200 - max: 80500 diff --git a/tests/PHPStan/Rules/Functions/data/preg-unmatched-as-null-php-versions.php b/tests/PHPStan/Rules/Functions/data/preg-unmatched-as-null-php-versions.php deleted file mode 100644 index 57721591021..00000000000 --- a/tests/PHPStan/Rules/Functions/data/preg-unmatched-as-null-php-versions.php +++ /dev/null @@ -1,14 +0,0 @@ - Date: Wed, 23 Sep 2026 06:59:15 +0000 Subject: [PATCH 20/21] Test that FilterVarRule follows PHP_VERSION_ID narrowing Co-Authored-By: Claude Opus 5.5 --- tests/PHPStan/Rules/Functions/FilterVarRuleTest.php | 8 ++++++++ .../data/filter_var_null_and_throw_php_version.php | 11 +++++++++++ 2 files changed, 19 insertions(+) create mode 100644 tests/PHPStan/Rules/Functions/data/filter_var_null_and_throw_php_version.php diff --git a/tests/PHPStan/Rules/Functions/FilterVarRuleTest.php b/tests/PHPStan/Rules/Functions/FilterVarRuleTest.php index a84798b849f..3810ab0a33d 100644 --- a/tests/PHPStan/Rules/Functions/FilterVarRuleTest.php +++ b/tests/PHPStan/Rules/Functions/FilterVarRuleTest.php @@ -43,6 +43,14 @@ public function testFilterFunctions(): void ]); } + #[RequiresPhp('>= 8.5.0')] + public function testRuleNarrowedByPhpVersionId(): void + { + $this->analyse([__DIR__ . '/data/filter_var_null_and_throw_php_version.php'], [ + ['Cannot use both FILTER_NULL_ON_FAILURE and FILTER_THROW_ON_FAILURE.', 7], + ]); + } + #[RequiresPhp('>= 8.2.0')] public function testRuleWithGlobalRange(): void { diff --git a/tests/PHPStan/Rules/Functions/data/filter_var_null_and_throw_php_version.php b/tests/PHPStan/Rules/Functions/data/filter_var_null_and_throw_php_version.php new file mode 100644 index 00000000000..d879aea3b5c --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/filter_var_null_and_throw_php_version.php @@ -0,0 +1,11 @@ += 8.5 + +namespace FilterVarNullAndThrowPhpVersion; + +function (string $s): void { + if (PHP_VERSION_ID >= 80500) { + filter_var($s, FILTER_VALIDATE_INT, FILTER_THROW_ON_FAILURE|FILTER_NULL_ON_FAILURE); + } else { + filter_var($s, FILTER_VALIDATE_INT, FILTER_THROW_ON_FAILURE|FILTER_NULL_ON_FAILURE); + } +}; From 8cd67337c3868ca3eaf4d3a505230ec7a91f7af7 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 23 Sep 2026 13:57:32 +0200 Subject: [PATCH 21/21] restore phpversions --- src/Php/PhpVersions.php | 71 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 66 insertions(+), 5 deletions(-) diff --git a/src/Php/PhpVersions.php b/src/Php/PhpVersions.php index a942d45234a..f6c8bfc2f18 100644 --- a/src/Php/PhpVersions.php +++ b/src/Php/PhpVersions.php @@ -188,23 +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; } - /** - * PHPStan's BetterReflection adapters keep their narrowed native return types only on PHP 8+; - * the downgraded PHP 7 build widens them back to the core Reflection ones. - */ - public function supportsNativeReflectionAdapterReturnTypes(): TrinaryLogic + 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; + } + }