From 60db01a49bcbb9250574202d61316e41a87ce2e2 Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Wed, 8 Jul 2026 02:13:28 +0900 Subject: [PATCH 01/29] Parse @pure-unless-parameter-passed PHPDoc tag Resolve the tag into per-parameter flags and merge them across parent PHPDocs. Until phpstan/phpdoc-parser#259 is merged the parser does not understand the tag natively, so parse it from the generic tag value and whitelist the @phpstan- alias in InvalidPHPStanDocTagRule. --- src/PhpDoc/PhpDocNodeResolver.php | 43 ++++++++++++++++++ src/PhpDoc/ResolvedPhpDocBlock.php | 45 +++++++++++++++++++ src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php | 3 ++ 3 files changed, 91 insertions(+) diff --git a/src/PhpDoc/PhpDocNodeResolver.php b/src/PhpDoc/PhpDocNodeResolver.php index 2304edc1490..9dff372d533 100644 --- a/src/PhpDoc/PhpDocNodeResolver.php +++ b/src/PhpDoc/PhpDocNodeResolver.php @@ -28,6 +28,7 @@ use PHPStan\PhpDoc\Tag\UsesTag; use PHPStan\PhpDoc\Tag\VarTag; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprNullNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\GenericTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\MixinTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode; use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode; @@ -47,8 +48,10 @@ use function array_reverse; use function count; use function in_array; +use function ltrim; use function method_exists; use function str_starts_with; +use function strpos; use function substr; #[AutowiredService] @@ -403,6 +406,46 @@ public function resolveParamPureUnlessCallableIsImpure(PhpDocNode $phpDocNode): return $parameters; } + /** + * @return array + */ + public function resolveParamPureUnlessParameterPassed(PhpDocNode $phpDocNode): array + { + $parameters = []; + // TODO: replace this generic-tag parsing with + // $phpDocNode->getPureUnlessParameterIsPassedTagValues() once + // phpstan/phpdoc-parser#259 is merged and the parser understands the tag. + foreach (['@pure-unless-parameter-passed', '@phpstan-pure-unless-parameter-passed'] as $tagName) { + foreach ($phpDocNode->getTags() as $tag) { + if ($tag->name !== $tagName) { + continue; + } + if (!$tag->value instanceof GenericTagValueNode) { + continue; + } + + $value = ltrim($tag->value->value); + if ($value === '' || $value[0] !== '$') { + continue; + } + + $parameterName = substr($value, 1); + $spacePosition = strpos($parameterName, ' '); + if ($spacePosition !== false) { + $parameterName = substr($parameterName, 0, $spacePosition); + } + + if ($parameterName === '') { + continue; + } + + $parameters[$parameterName] = true; + } + } + + return $parameters; + } + /** * @return array */ diff --git a/src/PhpDoc/ResolvedPhpDocBlock.php b/src/PhpDoc/ResolvedPhpDocBlock.php index 1284486d9af..e1026770cef 100644 --- a/src/PhpDoc/ResolvedPhpDocBlock.php +++ b/src/PhpDoc/ResolvedPhpDocBlock.php @@ -102,6 +102,9 @@ final class ResolvedPhpDocBlock /** @var array|false */ private array|false $paramsPureUnlessCallableIsImpure = false; + /** @var array|false */ + private array|false $paramsPureUnlessParameterPassed = false; + /** @var array|false */ private array|false $paramClosureThisTags = false; @@ -227,6 +230,7 @@ public static function createEmpty(): self $self->paramOutTags = []; $self->paramsImmediatelyInvokedCallable = []; $self->paramsPureUnlessCallableIsImpure = []; + $self->paramsPureUnlessParameterPassed = []; $self->paramClosureThisTags = []; $self->returnTag = null; $self->throwsTag = null; @@ -284,6 +288,7 @@ public function merge(ResolvedPhpDocBlock $parent, InheritedPhpDocParameterMappi $result->paramOutTags = self::mergeParamOutTags($this->getParamOutTags(), $parent, $parameterMapping, $parentClass); $result->paramsImmediatelyInvokedCallable = self::mergeParamsImmediatelyInvokedCallable($this->getParamsImmediatelyInvokedCallable(), $parent, $parameterMapping); $result->paramsPureUnlessCallableIsImpure = self::mergeParamsPureUnlessCallableIsImpure($this->getParamsPureUnlessCallableIsImpure(), $parent, $parameterMapping); + $result->paramsPureUnlessParameterPassed = self::mergeParamsPureUnlessParameterPassed($this->getParamsPureUnlessParameterPassed(), $parent, $parameterMapping); $result->paramClosureThisTags = self::mergeParamClosureThisTags($this->getParamClosureThisTags(), $parent, $parameterMapping, $parentClass); $result->returnTag = self::mergeReturnTags($this->getReturnTag(), $declaringClass, $parent, $parameterMapping, $parentClass); $result->throwsTag = self::mergeThrowsTags($this->getThrowsTag(), $parent, $parameterMapping); @@ -608,6 +613,18 @@ public function getParamsPureUnlessCallableIsImpure(): array return $this->paramsPureUnlessCallableIsImpure; } + /** + * @return array + */ + public function getParamsPureUnlessParameterPassed(): array + { + if ($this->paramsPureUnlessParameterPassed === false) { + $this->paramsPureUnlessParameterPassed = $this->phpDocNodeResolver->resolveParamPureUnlessParameterPassed($this->phpDocNode); + } + + return $this->paramsPureUnlessParameterPassed; + } + /** * @return array */ @@ -1144,6 +1161,34 @@ private static function mergeOneParentParamPureUnlessCallableIsImpure(array $par return $paramsPureUnlessCallableIsImpure; } + /** + * @param array $paramsPureUnlessParameterPassed + * @return array + */ + private static function mergeParamsPureUnlessParameterPassed(array $paramsPureUnlessParameterPassed, self $parent, InheritedPhpDocParameterMapping $parameterMapping): array + { + return self::mergeOneParentParamPureUnlessParameterPassed($paramsPureUnlessParameterPassed, $parent, $parameterMapping); + } + + /** + * @param array $paramsPureUnlessParameterPassed + * @return array + */ + private static function mergeOneParentParamPureUnlessParameterPassed(array $paramsPureUnlessParameterPassed, self $parent, InheritedPhpDocParameterMapping $parameterMapping): array + { + $parentPureUnlessParameterPassed = $parameterMapping->transformArrayKeysWithParameterNameMapping($parent->getParamsPureUnlessParameterPassed()); + + foreach ($parentPureUnlessParameterPassed as $name => $parentIsPureUnlessParameterPassed) { + if (array_key_exists($name, $paramsPureUnlessParameterPassed)) { + continue; + } + + $paramsPureUnlessParameterPassed[$name] = $parentIsPureUnlessParameterPassed; + } + + return $paramsPureUnlessParameterPassed; + } + /** * @param array $paramsClosureThisTags * @return array diff --git a/src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php b/src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php index 05d42bcdc67..5ce4a370fb8 100644 --- a/src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php +++ b/src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php @@ -64,6 +64,9 @@ final class InvalidPHPStanDocTagRule implements MultipleNodeTypesRule '@phpstan-param-immediately-invoked-callable', '@phpstan-param-later-invoked-callable', '@phpstan-param-closure-this', + // TODO: drop this once phpstan/phpdoc-parser#259 is merged and the parser + // recognizes the tag natively (like @phpstan-pure-unless-callable-is-impure). + '@phpstan-pure-unless-parameter-passed', '@phpstan-all-methods-pure', '@phpstan-all-methods-impure', ]; From fdfad4f0ada5095682c32d0c5502510d1bbed58c Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Wed, 8 Jul 2026 02:13:39 +0900 Subject: [PATCH 02/29] Thread @pure-unless-parameter-passed through the reflection layer Add isPureUnlessParameterPassedParameter(): TrinaryLogic on parameter reflections, populated from PHPDoc and function metadata, mirroring the already-merged @pure-unless-callable-is-impure threading (same TrinaryLogic typing from the start, so combineAcceptors() merges it with equals-or-Maybe instead of ->or(), giving correct behavior on union method variants). --- .../AnnotationsMethodParameterReflection.php | 5 + .../BetterReflectionProvider.php | 10 ++ .../ExtendedParameterReflection.php | 2 + src/Reflection/FunctionReflectionFactory.php | 2 + .../GenericParametersAcceptorResolver.php | 1 + .../ExtendedNativeParameterReflection.php | 6 + src/Reflection/ParametersAcceptorSelector.php | 8 ++ .../Php/ClosureCallMethodReflection.php | 5 +- src/Reflection/Php/ExitFunctionReflection.php | 1 + src/Reflection/Php/ExtendedDummyParameter.php | 6 + .../Php/PhpClassReflectionExtension.php | 15 ++- .../PhpFunctionFromParserNodeReflection.php | 4 + src/Reflection/Php/PhpFunctionReflection.php | 3 + .../Php/PhpMethodFromParserNodeReflection.php | 2 + src/Reflection/Php/PhpMethodReflection.php | 5 + .../Php/PhpMethodReflectionFactory.php | 2 + .../PhpParameterFromParserNodeReflection.php | 6 + src/Reflection/Php/PhpParameterReflection.php | 6 + .../ResolvedFunctionVariantWithOriginal.php | 1 + .../NativeFunctionReflectionProvider.php | 11 +- .../SignatureMap/SignatureMapProvider.php | 4 +- ...ackUnresolvedMethodPrototypeReflection.php | 1 + ...ypeUnresolvedMethodPrototypeReflection.php | 1 + .../WrappedExtendedMethodReflection.php | 1 + .../ParametersAcceptorSelectorTest.php | 2 + turbo-ext/src/ExtendedDummyParameter.cpp | 22 ++-- .../src/ExtendedNativeParameterReflection.cpp | 21 ++-- turbo-ext/src/ParameterValues.h | 6 +- turbo-ext/src/ParametersAcceptorSelector.cpp | 39 ++++-- turbo-ext/src/PhpClassReflectionExtension.cpp | 35 +++++- turbo-ext/src/PhpParameterReflection.cpp | 14 ++- .../ResolvedFunctionVariantWithOriginal.cpp | 12 +- turbo-ext/src/ResolvedPhpDocBlockAccess.cpp | 1 + turbo-ext/src/TypeTraits.cpp | 7 +- .../src/generated/ExtendedDummyParameter.h | 79 ++++++------ .../ExtendedNativeParameterReflection.h | 113 +++++++++-------- .../src/generated/PhpParameterReflection.h | 117 ++++++++++-------- turbo-ext/src/support.h | 2 + .../tests/php-class-reflection-family.php | 1 + turbo-ext/tests/type-family.php | 41 +++--- 40 files changed, 402 insertions(+), 218 deletions(-) diff --git a/src/Reflection/Annotations/AnnotationsMethodParameterReflection.php b/src/Reflection/Annotations/AnnotationsMethodParameterReflection.php index 60849ddc98e..7becf4b758a 100644 --- a/src/Reflection/Annotations/AnnotationsMethodParameterReflection.php +++ b/src/Reflection/Annotations/AnnotationsMethodParameterReflection.php @@ -97,4 +97,9 @@ public function isPureUnlessCallableIsImpureParameter(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isPureUnlessParameterPassedParameter(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + } diff --git a/src/Reflection/BetterReflection/BetterReflectionProvider.php b/src/Reflection/BetterReflection/BetterReflectionProvider.php index 23fa4425fa2..391e293a2ec 100644 --- a/src/Reflection/BetterReflection/BetterReflectionProvider.php +++ b/src/Reflection/BetterReflection/BetterReflectionProvider.php @@ -305,6 +305,7 @@ private function getCustomFunction(string $functionName): PhpFunctionReflection $phpDocParameterImmediatelyInvokedCallable = []; $phpDocParameterClosureThisTypeTags = []; $phpDocParameterPureUnlessCallableIsImpure = []; + $phpDocParameterPureUnlessParameterPassed = []; $resolvedPhpDoc = $this->stubPhpDocProvider->findFunctionPhpDoc($reflectionFunction->getName(), array_map(static fn (ReflectionParameter $parameter): string => $parameter->getName(), $reflectionFunction->getParameters())); if ($resolvedPhpDoc === null && $reflectionFunction->getFileName() !== false && $reflectionFunction->getDocComment() !== false) { @@ -332,6 +333,7 @@ private function getCustomFunction(string $functionName): PhpFunctionReflection $phpDocParameterImmediatelyInvokedCallable = $resolvedPhpDoc->getParamsImmediatelyInvokedCallable(); $phpDocParameterClosureThisTypeTags = $resolvedPhpDoc->getParamClosureThisTags(); $phpDocParameterPureUnlessCallableIsImpure = $resolvedPhpDoc->getParamsPureUnlessCallableIsImpure(); + $phpDocParameterPureUnlessParameterPassed = $resolvedPhpDoc->getParamsPureUnlessParameterPassed(); } $lowerCasedFunctionName = strtolower($reflectionFunction->getName()); @@ -344,6 +346,13 @@ private function getCustomFunction(string $functionName): PhpFunctionReflection $phpDocParameterPureUnlessCallableIsImpure[$parameterName] = $isPureUnlessCallableIsImpure; } + foreach ($functionMetadata['pureUnlessParameterPassedParameters'] ?? [] as $parameterName => $isPureUnlessParameterPassed) { + if (($phpDocParameterPureUnlessParameterPassed[$parameterName] ?? false) === true) { + continue; + } + + $phpDocParameterPureUnlessParameterPassed[$parameterName] = $isPureUnlessParameterPassed; + } } return $this->functionReflectionFactory->create( @@ -365,6 +374,7 @@ private function getCustomFunction(string $functionName): PhpFunctionReflection array_map(static fn (ParamClosureThisTag $tag): Type => $tag->getType(), $phpDocParameterClosureThisTypeTags), $this->attributeReflectionFactory->fromNativeReflection($reflectionFunction->getAttributes(), InitializerExprContext::fromFunction($reflectionFunction->getName(), $reflectionFunction->getFileName() !== false ? $reflectionFunction->getFileName() : null)), $phpDocParameterPureUnlessCallableIsImpure, + $phpDocParameterPureUnlessParameterPassed, ); } diff --git a/src/Reflection/ExtendedParameterReflection.php b/src/Reflection/ExtendedParameterReflection.php index f7ea424da2b..f25d065ff2b 100644 --- a/src/Reflection/ExtendedParameterReflection.php +++ b/src/Reflection/ExtendedParameterReflection.php @@ -40,4 +40,6 @@ public function checkAllowedConstants(array $constants): AllowedConstantsResult; public function isPureUnlessCallableIsImpureParameter(): TrinaryLogic; + public function isPureUnlessParameterPassedParameter(): TrinaryLogic; + } diff --git a/src/Reflection/FunctionReflectionFactory.php b/src/Reflection/FunctionReflectionFactory.php index 58224858714..3ff2adcbbbe 100644 --- a/src/Reflection/FunctionReflectionFactory.php +++ b/src/Reflection/FunctionReflectionFactory.php @@ -17,6 +17,7 @@ interface FunctionReflectionFactory * @param array $phpDocParameterClosureThisTypes * @param list $attributes * @param array $phpDocParameterPureUnlessCallableIsImpure + * @param array $phpDocParameterPureUnlessParameterPassed */ public function create( ReflectionFunction $reflection, @@ -37,6 +38,7 @@ public function create( array $phpDocParameterClosureThisTypes, array $attributes, array $phpDocParameterPureUnlessCallableIsImpure, + array $phpDocParameterPureUnlessParameterPassed, ): PhpFunctionReflection; } diff --git a/src/Reflection/GenericParametersAcceptorResolver.php b/src/Reflection/GenericParametersAcceptorResolver.php index 9b88fd38b08..f6434612be8 100644 --- a/src/Reflection/GenericParametersAcceptorResolver.php +++ b/src/Reflection/GenericParametersAcceptorResolver.php @@ -135,6 +135,7 @@ public static function resolve(array $argTypes, ParametersAcceptor $parametersAc [], null, TrinaryLogic::createNo(), + TrinaryLogic::createNo(), ), $parameters), $parametersAcceptor->isVariadic(), $returnType, diff --git a/src/Reflection/Native/ExtendedNativeParameterReflection.php b/src/Reflection/Native/ExtendedNativeParameterReflection.php index 3c38d75f9ac..f28e9e45aca 100644 --- a/src/Reflection/Native/ExtendedNativeParameterReflection.php +++ b/src/Reflection/Native/ExtendedNativeParameterReflection.php @@ -34,6 +34,7 @@ public function __construct( private array $attributes, private ?ParameterAllowedConstants $allowedConstants, private TrinaryLogic $pureUnlessCallableIsImpureParameter, + private TrinaryLogic $pureUnlessParameterPassedParameter, ) { } @@ -122,4 +123,9 @@ public function isPureUnlessCallableIsImpureParameter(): TrinaryLogic return $this->pureUnlessCallableIsImpureParameter; } + public function isPureUnlessParameterPassedParameter(): TrinaryLogic + { + return $this->pureUnlessParameterPassedParameter; + } + } diff --git a/src/Reflection/ParametersAcceptorSelector.php b/src/Reflection/ParametersAcceptorSelector.php index 50481e5b43d..22c137ede82 100644 --- a/src/Reflection/ParametersAcceptorSelector.php +++ b/src/Reflection/ParametersAcceptorSelector.php @@ -843,6 +843,7 @@ public static function combineAcceptors(array $acceptors): ExtendedParametersAcc $parameter instanceof ExtendedParameterReflection ? $parameter->getAttributes() : [], $parameter instanceof ExtendedParameterReflection ? $parameter->getAllowedConstants() : null, $parameter instanceof ExtendedParameterReflection ? $parameter->isPureUnlessCallableIsImpureParameter() : TrinaryLogic::createNo(), + $parameter instanceof ExtendedParameterReflection ? $parameter->isPureUnlessParameterPassedParameter() : TrinaryLogic::createNo(), ); continue; } @@ -921,6 +922,10 @@ public static function combineAcceptors(array $acceptors): ExtendedParametersAcc $rightPureUnless = $parameter instanceof ExtendedParameterReflection ? $parameter->isPureUnlessCallableIsImpureParameter() : TrinaryLogic::createNo(); $pureUnlessCallableIsImpureParameter = $leftPureUnless->equals($rightPureUnless) ? $leftPureUnless : TrinaryLogic::createMaybe(); + $leftPureUnlessParameterPassed = $parameters[$i]->isPureUnlessParameterPassedParameter(); + $rightPureUnlessParameterPassed = $parameter instanceof ExtendedParameterReflection ? $parameter->isPureUnlessParameterPassedParameter() : TrinaryLogic::createNo(); + $pureUnlessParameterPassedParameter = $leftPureUnlessParameterPassed->equals($rightPureUnlessParameterPassed) ? $leftPureUnlessParameterPassed : TrinaryLogic::createMaybe(); + $parameters[$i] = new ExtendedDummyParameter( $parameters[$i]->getName() !== $parameter->getName() ? sprintf('%s|%s', $parameters[$i]->getName(), $parameter->getName()) : $parameter->getName(), $type, @@ -936,6 +941,7 @@ public static function combineAcceptors(array $acceptors): ExtendedParametersAcc $attributes, $allowedConstants, $pureUnlessCallableIsImpureParameter, + $pureUnlessParameterPassedParameter, ); if ($isParameterVariadic) { @@ -1038,6 +1044,7 @@ private static function wrapParameter(ParameterReflection $parameter): ExtendedP [], null, TrinaryLogic::createNo(), + TrinaryLogic::createNo(), ); } @@ -1396,6 +1403,7 @@ private static function overrideParameterType(ParameterReflection $original, Typ $wrapped->getAttributes(), $wrapped->getAllowedConstants(), $wrapped->isPureUnlessCallableIsImpureParameter(), + $wrapped->isPureUnlessParameterPassedParameter(), ); } diff --git a/src/Reflection/Php/ClosureCallMethodReflection.php b/src/Reflection/Php/ClosureCallMethodReflection.php index d42361970e6..e031576c097 100644 --- a/src/Reflection/Php/ClosureCallMethodReflection.php +++ b/src/Reflection/Php/ClosureCallMethodReflection.php @@ -99,8 +99,9 @@ public function getVariants(): array null, [], null, - // pure-unless-callable-is-impure is not threaded here: a closure's own - // parameters cannot carry the tag. + // pure-unless-callable-is-impure and pure-unless-parameter-passed are not + // threaded here: a closure's own parameters cannot carry either tag. + TrinaryLogic::createNo(), TrinaryLogic::createNo(), ), $parameters), $this->closureType->isVariadic(), diff --git a/src/Reflection/Php/ExitFunctionReflection.php b/src/Reflection/Php/ExitFunctionReflection.php index 9b771e11bd0..a3609465e4a 100644 --- a/src/Reflection/Php/ExitFunctionReflection.php +++ b/src/Reflection/Php/ExitFunctionReflection.php @@ -61,6 +61,7 @@ public function getVariants(): array [], null, TrinaryLogic::createNo(), + TrinaryLogic::createNo(), ), ], false, diff --git a/src/Reflection/Php/ExtendedDummyParameter.php b/src/Reflection/Php/ExtendedDummyParameter.php index 7ef97b508c0..efd3d70b9eb 100644 --- a/src/Reflection/Php/ExtendedDummyParameter.php +++ b/src/Reflection/Php/ExtendedDummyParameter.php @@ -34,6 +34,7 @@ public function __construct( private array $attributes, private ?ParameterAllowedConstants $allowedConstants, private TrinaryLogic $pureUnlessCallableIsImpureParameter, + private TrinaryLogic $pureUnlessParameterPassedParameter, ) { parent::__construct($name, $type, $optional, $passedByReference, $variadic, $defaultValue); @@ -93,4 +94,9 @@ public function isPureUnlessCallableIsImpureParameter(): TrinaryLogic return $this->pureUnlessCallableIsImpureParameter; } + public function isPureUnlessParameterPassedParameter(): TrinaryLogic + { + return $this->pureUnlessParameterPassedParameter; + } + } diff --git a/src/Reflection/Php/PhpClassReflectionExtension.php b/src/Reflection/Php/PhpClassReflectionExtension.php index 694ae002e76..20f6500bc82 100644 --- a/src/Reflection/Php/PhpClassReflectionExtension.php +++ b/src/Reflection/Php/PhpClassReflectionExtension.php @@ -903,6 +903,7 @@ public function createUserlandMethodReflection(ClassReflection $fileDeclaringCla $isPure = null; $pureUnlessCallableIsImpureParameters = []; + $pureUnlessParameterPassedParameters = []; if ($actualDeclaringClass->isBuiltin() || $actualDeclaringClass->isEnum()) { foreach (array_keys($actualDeclaringClass->getAncestors()) as $className) { if ($this->signatureMapProvider->hasMethodMetadata($className, $methodReflection->getName())) { @@ -910,6 +911,7 @@ public function createUserlandMethodReflection(ClassReflection $fileDeclaringCla $hasSideEffects = $methodMetadata['hasSideEffects'] ?? true; $isPure = !$hasSideEffects; $pureUnlessCallableIsImpureParameters += $methodMetadata['pureUnlessCallableIsImpureParameters'] ?? []; + $pureUnlessParameterPassedParameters += $methodMetadata['pureUnlessParameterPassedParameters'] ?? []; break; } @@ -935,6 +937,9 @@ public function createUserlandMethodReflection(ClassReflection $fileDeclaringCla foreach ($resolvedPhpDoc->getParamsPureUnlessCallableIsImpure() as $paramName => $isPureUnlessCallableIsImpure) { $pureUnlessCallableIsImpureParameters[$paramName] = $isPureUnlessCallableIsImpure; } + foreach ($resolvedPhpDoc->getParamsPureUnlessParameterPassed() as $paramName => $isPureUnlessParameterPassed) { + $pureUnlessParameterPassedParameters[$paramName] = $isPureUnlessParameterPassed; + } $phpDocReturnType = $this->getPhpDocReturnType($phpDocBlockClassReflection, $resolvedPhpDoc, $nativeReturnType); $phpDocThrowType = $resolvedPhpDoc->getThrowsTag() !== null ? $resolvedPhpDoc->getThrowsTag()->getType() : null; foreach ($resolvedPhpDoc->getParamTags() as $paramName => $paramTag) { @@ -1015,6 +1020,7 @@ public function createUserlandMethodReflection(ClassReflection $fileDeclaringCla $acceptsNamedArguments, $this->attributeReflectionFactory->fromNativeReflection($methodReflection->getAttributes(), InitializerExprContext::fromClassMethod($actualDeclaringClass->getName(), $declaringTraitName, $methodReflection->getName(), $actualDeclaringClass->getFileName())), $pureUnlessCallableIsImpureParameters, + $pureUnlessParameterPassedParameters, ); } @@ -1083,8 +1089,10 @@ private function createNativeMethodVariant( $closureThisType, [], $this->allowedConstantsMapProvider->getForMethodParameter($declaringClassName, $methodName, $parameterSignature->getName()), - // pure-unless-callable-is-impure is not threaded here because no built-in method - // carries it (there are no Class::method entries in functionMetadata.php). + // pure-unless-callable-is-impure and pure-unless-parameter-passed are not + // threaded here because no built-in method carries either tag (there are no + // Class::method entries in functionMetadata.php). + TrinaryLogic::createNo(), TrinaryLogic::createNo(), ); } @@ -1193,7 +1201,7 @@ private function inferAndCachePropertyTypes( $classScope = $classScope->enterNamespace($namespace); } $classScope = $classScope->enterClass($declaringClass); - [$templateTypeMap, $phpDocParameterTypes, $phpDocImmediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, , $phpDocComment, $asserts, $selfOutType, $phpDocParameterOutTypes, , , , $phpDocPureUnlessCallableIsImpureParameters] = $this->phpDocsResolver->getPhpDocs($classScope, $methodNode); + [$templateTypeMap, $phpDocParameterTypes, $phpDocImmediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, , $phpDocComment, $asserts, $selfOutType, $phpDocParameterOutTypes, , , , $phpDocPureUnlessCallableIsImpureParameters, $phpDocPureUnlessParameterPassedParameters] = $this->phpDocsResolver->getPhpDocs($classScope, $methodNode); $methodScope = $classScope->enterClassMethod( $methodNode, $templateTypeMap, @@ -1215,6 +1223,7 @@ private function inferAndCachePropertyTypes( false, null, $phpDocPureUnlessCallableIsImpureParameters, + $phpDocPureUnlessParameterPassedParameters, ); $propertyTypes = []; diff --git a/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php b/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php index 557cc125785..cbbf67e1c7e 100644 --- a/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php +++ b/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php @@ -52,6 +52,7 @@ class PhpFunctionFromParserNodeReflection implements FunctionReflection, Extende * @param array $phpDocClosureThisTypeParameters * @param list $attributes * @param array $pureUnlessCallableIsImpureParameters + * @param array $pureUnlessParameterPassedParameters */ public function __construct( FunctionLike $functionLike, @@ -76,6 +77,7 @@ public function __construct( private array $phpDocClosureThisTypeParameters, private array $attributes, private array $pureUnlessCallableIsImpureParameters, + private array $pureUnlessParameterPassedParameters, ) { $this->functionLike = $functionLike; @@ -183,6 +185,7 @@ public function getParameters(): array } $pureUnlessCallableIsImpureParameter = TrinaryLogic::createFromBoolean($this->pureUnlessCallableIsImpureParameters[$parameter->var->name] ?? false); + $pureUnlessParameterPassedParameter = TrinaryLogic::createFromBoolean($this->pureUnlessParameterPassedParameters[$parameter->var->name] ?? false); $parameters[] = new PhpParameterFromParserNodeReflection( $parameter->var->name, @@ -199,6 +202,7 @@ public function getParameters(): array $closureThisType, $this->parameterAttributes[$parameter->var->name] ?? [], $pureUnlessCallableIsImpureParameter, + $pureUnlessParameterPassedParameter, ); } diff --git a/src/Reflection/Php/PhpFunctionReflection.php b/src/Reflection/Php/PhpFunctionReflection.php index 15afca5bd98..f1fb979dd45 100644 --- a/src/Reflection/Php/PhpFunctionReflection.php +++ b/src/Reflection/Php/PhpFunctionReflection.php @@ -41,6 +41,7 @@ final class PhpFunctionReflection implements FunctionReflection * @param array $phpDocParameterClosureThisTypes * @param list $attributes * @param array $phpDocParameterPureUnlessCallableIsImpure + * @param array $phpDocParameterPureUnlessParameterPassed */ public function __construct( private InitializerExprTypeResolver $initializerExprTypeResolver, @@ -64,6 +65,7 @@ public function __construct( private array $phpDocParameterClosureThisTypes, private array $attributes, private array $phpDocParameterPureUnlessCallableIsImpure, + private array $phpDocParameterPureUnlessParameterPassed, ) { } @@ -133,6 +135,7 @@ private function getParameters(): array $this->attributeReflectionFactory->fromNativeReflection($reflection->getAttributes(), InitializerExprContext::fromReflectionParameter($reflection)), $this->allowedConstantsMapProvider->getForFunctionParameter(strtolower($this->reflection->getName()), $reflection->getName()), TrinaryLogic::createFromBoolean($this->phpDocParameterPureUnlessCallableIsImpure[$reflection->getName()] ?? false), + TrinaryLogic::createFromBoolean($this->phpDocParameterPureUnlessParameterPassed[$reflection->getName()] ?? false), ); }, $this->reflection->getParameters()); } diff --git a/src/Reflection/Php/PhpMethodFromParserNodeReflection.php b/src/Reflection/Php/PhpMethodFromParserNodeReflection.php index 8a5a3ebc2ff..3d8ca7bbbbc 100644 --- a/src/Reflection/Php/PhpMethodFromParserNodeReflection.php +++ b/src/Reflection/Php/PhpMethodFromParserNodeReflection.php @@ -75,6 +75,7 @@ public function __construct( private bool $isConstructor, array $attributes, array $pureUnlessCallableIsImpureParameters, + array $pureUnlessParameterPassedParameters, ) { if ($this->classMethod instanceof Node\PropertyHook) { @@ -142,6 +143,7 @@ public function __construct( $phpDocClosureThisTypeParameters, $attributes, $pureUnlessCallableIsImpureParameters, + $pureUnlessParameterPassedParameters, ); } diff --git a/src/Reflection/Php/PhpMethodReflection.php b/src/Reflection/Php/PhpMethodReflection.php index c460f0e6bbd..1b394f5fb4b 100644 --- a/src/Reflection/Php/PhpMethodReflection.php +++ b/src/Reflection/Php/PhpMethodReflection.php @@ -68,6 +68,7 @@ final class PhpMethodReflection implements ExtendedMethodReflection * @param array $phpDocClosureThisTypeParameters * @param list $attributes * @param array $pureUnlessCallableIsImpureParameters + * @param array $pureUnlessParameterPassedParameters */ public function __construct( private InitializerExprTypeResolver $initializerExprTypeResolver, @@ -96,6 +97,7 @@ public function __construct( private array $phpDocClosureThisTypeParameters, private array $attributes, private array $pureUnlessCallableIsImpureParameters, + private array $pureUnlessParameterPassedParameters, ) { } @@ -240,6 +242,7 @@ private function getParameters(): array $this->attributeReflectionFactory->fromNativeReflection($reflection->getAttributes(), InitializerExprContext::fromReflectionParameter($reflection)), $this->allowedConstantsMapProvider->getForMethodParameter($this->declaringClass->getName(), $this->reflection->getName(), $reflection->getName()), TrinaryLogic::createFromBoolean($this->pureUnlessCallableIsImpureParameters[$reflection->getName()] ?? false), + TrinaryLogic::createFromBoolean($this->pureUnlessParameterPassedParameters[$reflection->getName()] ?? false), ), $this->reflection->getParameters()); } @@ -453,6 +456,7 @@ public function changePropertyGetHookPhpDocType(Type $phpDocType): self $this->phpDocClosureThisTypeParameters, $this->attributes, $this->pureUnlessCallableIsImpureParameters, + $this->pureUnlessParameterPassedParameters, ); } @@ -488,6 +492,7 @@ public function changePropertySetHookPhpDocType(string $parameterName, Type $php $this->phpDocClosureThisTypeParameters, $this->attributes, $this->pureUnlessCallableIsImpureParameters, + $this->pureUnlessParameterPassedParameters, ); } diff --git a/src/Reflection/Php/PhpMethodReflectionFactory.php b/src/Reflection/Php/PhpMethodReflectionFactory.php index c2978529d86..583d2cb4d28 100644 --- a/src/Reflection/Php/PhpMethodReflectionFactory.php +++ b/src/Reflection/Php/PhpMethodReflectionFactory.php @@ -21,6 +21,7 @@ interface PhpMethodReflectionFactory * @param array $phpDocClosureThisTypeParameters * @param list $attributes * @param array $pureUnlessCallableIsImpureParameters + * @param array $pureUnlessParameterPassedParameters */ public function create( ClassReflection $declaringClass, @@ -45,6 +46,7 @@ public function create( bool $acceptsNamedArguments, array $attributes, array $pureUnlessCallableIsImpureParameters, + array $pureUnlessParameterPassedParameters, ): PhpMethodReflection; } diff --git a/src/Reflection/Php/PhpParameterFromParserNodeReflection.php b/src/Reflection/Php/PhpParameterFromParserNodeReflection.php index 754f6622c2a..9535b0c7f7c 100644 --- a/src/Reflection/Php/PhpParameterFromParserNodeReflection.php +++ b/src/Reflection/Php/PhpParameterFromParserNodeReflection.php @@ -34,6 +34,7 @@ public function __construct( private ?Type $closureThisType, private array $attributes, private TrinaryLogic $pureUnlessCallableIsImpureParameter, + private TrinaryLogic $pureUnlessParameterPassedParameter, ) { } @@ -131,4 +132,9 @@ public function isPureUnlessCallableIsImpureParameter(): TrinaryLogic return $this->pureUnlessCallableIsImpureParameter; } + public function isPureUnlessParameterPassedParameter(): TrinaryLogic + { + return $this->pureUnlessParameterPassedParameter; + } + } diff --git a/src/Reflection/Php/PhpParameterReflection.php b/src/Reflection/Php/PhpParameterReflection.php index 8f386ada408..968a3e38a7e 100644 --- a/src/Reflection/Php/PhpParameterReflection.php +++ b/src/Reflection/Php/PhpParameterReflection.php @@ -40,6 +40,7 @@ public function __construct( private array $attributes, private ?ParameterAllowedConstants $allowedConstants, private TrinaryLogic $pureUnlessCallableIsImpureParameter, + private TrinaryLogic $pureUnlessParameterPassedParameter, ) { } @@ -168,4 +169,9 @@ public function isPureUnlessCallableIsImpureParameter(): TrinaryLogic return $this->pureUnlessCallableIsImpureParameter; } + public function isPureUnlessParameterPassedParameter(): TrinaryLogic + { + return $this->pureUnlessParameterPassedParameter; + } + } diff --git a/src/Reflection/ResolvedFunctionVariantWithOriginal.php b/src/Reflection/ResolvedFunctionVariantWithOriginal.php index 0bdb5133be1..af85439f365 100644 --- a/src/Reflection/ResolvedFunctionVariantWithOriginal.php +++ b/src/Reflection/ResolvedFunctionVariantWithOriginal.php @@ -139,6 +139,7 @@ function (ExtendedParameterReflection $param): ExtendedParameterReflection { $param->getAttributes(), $param->getAllowedConstants(), $param->isPureUnlessCallableIsImpureParameter(), + $param->isPureUnlessParameterPassedParameter(), ); }, $this->parametersAcceptor->getParameters(), diff --git a/src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php b/src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php index 6b40422c1d4..6a868d39971 100644 --- a/src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php +++ b/src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php @@ -111,11 +111,15 @@ public function findFunctionReflection(string $functionName): ?NativeFunctionRef $allowedConstantsMapProvider = $this->allowedConstantsMapProvider; $pureUnlessCallableIsImpureParameters = []; + $pureUnlessParameterPassedParameters = []; if ($this->signatureMapProvider->hasFunctionMetadata($lowerCasedFunctionName)) { $functionMetadata = $this->signatureMapProvider->getFunctionMetadata($lowerCasedFunctionName); if (isset($functionMetadata['pureUnlessCallableIsImpureParameters'])) { $pureUnlessCallableIsImpureParameters = $functionMetadata['pureUnlessCallableIsImpureParameters']; } + if (isset($functionMetadata['pureUnlessParameterPassedParameters'])) { + $pureUnlessParameterPassedParameters = $functionMetadata['pureUnlessParameterPassedParameters']; + } } else { $functionMetadata = null; } @@ -126,7 +130,7 @@ public function findFunctionReflection(string $functionName): ?NativeFunctionRef $variantsByType[$signatureType][] = new ExtendedFunctionVariant( TemplateTypeMap::createEmpty(), null, - array_map(static function (ParameterSignature $parameterSignature) use ($phpDoc, $lowerCasedFunctionName, $allowedConstantsMapProvider, $pureUnlessCallableIsImpureParameters): ExtendedNativeParameterReflection { + array_map(static function (ParameterSignature $parameterSignature) use ($phpDoc, $lowerCasedFunctionName, $allowedConstantsMapProvider, $pureUnlessCallableIsImpureParameters, $pureUnlessParameterPassedParameters): ExtendedNativeParameterReflection { $name = $parameterSignature->getName(); $type = $parameterSignature->getType(); @@ -134,6 +138,7 @@ public function findFunctionReflection(string $functionName): ?NativeFunctionRef $immediatelyInvokedCallable = TrinaryLogic::createMaybe(); $closureThisType = null; $pureUnlessCallableIsImpureParameter = TrinaryLogic::createFromBoolean($pureUnlessCallableIsImpureParameters[$name] ?? false); + $pureUnlessParameterPassedParameter = TrinaryLogic::createFromBoolean($pureUnlessParameterPassedParameters[$name] ?? false); if ($phpDoc !== null) { if (array_key_exists($parameterSignature->getName(), $phpDoc->getParamTags())) { $phpDocType = $phpDoc->getParamTags()[$parameterSignature->getName()]->getType(); @@ -147,6 +152,9 @@ public function findFunctionReflection(string $functionName): ?NativeFunctionRef if (($phpDoc->getParamsPureUnlessCallableIsImpure()[$parameterSignature->getName()] ?? false) === true) { $pureUnlessCallableIsImpureParameter = TrinaryLogic::createYes(); } + if (($phpDoc->getParamsPureUnlessParameterPassed()[$parameterSignature->getName()] ?? false) === true) { + $pureUnlessParameterPassedParameter = TrinaryLogic::createYes(); + } } return new ExtendedNativeParameterReflection( @@ -164,6 +172,7 @@ public function findFunctionReflection(string $functionName): ?NativeFunctionRef [], $allowedConstantsMapProvider->getForFunctionParameter($lowerCasedFunctionName, $parameterSignature->getName()), $pureUnlessCallableIsImpureParameter, + $pureUnlessParameterPassedParameter, ); }, $functionSignature->getParameters()), $functionSignature->isVariadic(), diff --git a/src/Reflection/SignatureMap/SignatureMapProvider.php b/src/Reflection/SignatureMap/SignatureMapProvider.php index 8afcf9b1b1a..2ccb642ccee 100644 --- a/src/Reflection/SignatureMap/SignatureMapProvider.php +++ b/src/Reflection/SignatureMap/SignatureMapProvider.php @@ -26,12 +26,12 @@ public function hasMethodMetadata(string $className, string $methodName): bool; public function hasFunctionMetadata(string $name): bool; /** - * @return array{hasSideEffects?: bool, pureUnlessCallableIsImpureParameters?: array} + * @return array{hasSideEffects?: bool, pureUnlessCallableIsImpureParameters?: array, pureUnlessParameterPassedParameters?: array} */ public function getMethodMetadata(string $className, string $methodName): array; /** - * @return array{hasSideEffects?: bool, pureUnlessCallableIsImpureParameters?: array} + * @return array{hasSideEffects?: bool, pureUnlessCallableIsImpureParameters?: array, pureUnlessParameterPassedParameters?: array} */ public function getFunctionMetadata(string $functionName): array; diff --git a/src/Reflection/Type/CallbackUnresolvedMethodPrototypeReflection.php b/src/Reflection/Type/CallbackUnresolvedMethodPrototypeReflection.php index 33860cff17d..f0e0254fd40 100644 --- a/src/Reflection/Type/CallbackUnresolvedMethodPrototypeReflection.php +++ b/src/Reflection/Type/CallbackUnresolvedMethodPrototypeReflection.php @@ -122,6 +122,7 @@ function (ExtendedParameterReflection $parameter): ExtendedParameterReflection { $parameter->getAttributes(), $parameter->getAllowedConstants(), $parameter->isPureUnlessCallableIsImpureParameter(), + $parameter->isPureUnlessParameterPassedParameter(), ); }, $acceptor->getParameters(), diff --git a/src/Reflection/Type/CalledOnTypeUnresolvedMethodPrototypeReflection.php b/src/Reflection/Type/CalledOnTypeUnresolvedMethodPrototypeReflection.php index e8e9da50d9c..63b888ebe9d 100644 --- a/src/Reflection/Type/CalledOnTypeUnresolvedMethodPrototypeReflection.php +++ b/src/Reflection/Type/CalledOnTypeUnresolvedMethodPrototypeReflection.php @@ -109,6 +109,7 @@ private function transformMethodWithStaticType(ClassReflection $declaringClass, $parameter->getAttributes(), $parameter->getAllowedConstants(), $parameter->isPureUnlessCallableIsImpureParameter(), + $parameter->isPureUnlessParameterPassedParameter(), ), $acceptor->getParameters(), ), diff --git a/src/Reflection/WrappedExtendedMethodReflection.php b/src/Reflection/WrappedExtendedMethodReflection.php index 04db875d3ef..1c613c116aa 100644 --- a/src/Reflection/WrappedExtendedMethodReflection.php +++ b/src/Reflection/WrappedExtendedMethodReflection.php @@ -81,6 +81,7 @@ public function getVariants(): array [], null, TrinaryLogic::createNo(), + TrinaryLogic::createNo(), ), $variant->getParameters()), $variant->isVariadic(), $variant->getReturnType(), diff --git a/tests/PHPStan/Reflection/ParametersAcceptorSelectorTest.php b/tests/PHPStan/Reflection/ParametersAcceptorSelectorTest.php index 91a7c18d8c2..efd36e19a0a 100644 --- a/tests/PHPStan/Reflection/ParametersAcceptorSelectorTest.php +++ b/tests/PHPStan/Reflection/ParametersAcceptorSelectorTest.php @@ -94,6 +94,7 @@ public static function dataSelectFromTypes(): Generator $parameter->getAttributes(), $parameter->getAllowedConstants(), $parameter->isPureUnlessCallableIsImpureParameter(), + $parameter->isPureUnlessParameterPassedParameter(), ), $datePeriodConstructorVariants[0]->getParameters()), false, new VoidType(), @@ -127,6 +128,7 @@ public static function dataSelectFromTypes(): Generator $parameter->getAttributes(), $parameter->getAllowedConstants(), $parameter->isPureUnlessCallableIsImpureParameter(), + $parameter->isPureUnlessParameterPassedParameter(), ), $datePeriodConstructorVariants[1]->getParameters()), false, new VoidType(), diff --git a/turbo-ext/src/ExtendedDummyParameter.cpp b/turbo-ext/src/ExtendedDummyParameter.cpp index 5e63f47b403..e6802d20f31 100644 --- a/turbo-ext/src/ExtendedDummyParameter.cpp +++ b/turbo-ext/src/ExtendedDummyParameter.cpp @@ -2,7 +2,7 @@ * PHPStanTurbo\ExtendedDummyParameter — native implementation of * PHPStan\Reflection\Php\ExtendedDummyParameter. * - * Final, extending the shadowing DummyParameter: its eight promoted slots + * Final, extending the shadowing DummyParameter: its nine promoted slots * follow the parent's six (generated declarations), the constructor writes * them and runs the parent's constructor body directly * (pt_dummy_parameter_construct() — the parent method is native, nothing can @@ -30,9 +30,9 @@ class ExtendedDummyParameter explicit ExtendedDummyParameter(zend_object *self) : self(self) {} /* the promoted slots in parameter order, then parent::__construct() - * (argv: the constructor's fourteen arguments, NULL / IS_NULL for the + * (argv: the constructor's fifteen arguments, NULL / IS_NULL for the * nullable nulls); false = pending exception */ - [[nodiscard]] bool construct(zend_string *name, zval *type, bool optional, zval *passedByReference, bool variadic, zval *defaultValue, zval *nativeType, zval *phpDocType, zval *outType, zval *immediatelyInvokedCallable, zval *closureThisType, zval *attributes, zval *allowedConstants, zval *pureUnlessCallableIsImpureParameter) const + [[nodiscard]] bool construct(zend_string *name, zval *type, bool optional, zval *passedByReference, bool variadic, zval *defaultValue, zval *nativeType, zval *phpDocType, zval *outType, zval *immediatelyInvokedCallable, zval *closureThisType, zval *attributes, zval *allowedConstants, zval *pureUnlessCallableIsImpureParameter, zval *pureUnlessParameterPassedParameter) const { write(slots::nativeType, nativeType); write(slots::phpDocType, phpDocType); @@ -42,6 +42,7 @@ class ExtendedDummyParameter write(slots::attributes, attributes); write(slots::allowedConstants, allowedConstants); write(slots::pureUnlessCallableIsImpureParameter, pureUnlessCallableIsImpureParameter); + write(slots::pureUnlessParameterPassedParameter, pureUnlessParameterPassedParameter); return pt_dummy_parameter_construct(self, name, type, optional, passedByReference, variadic, defaultValue); } @@ -53,6 +54,7 @@ class ExtendedDummyParameter zv::Val getAttributes() const { return copyOf(slots::attributes, "attributes"); } zv::Val getAllowedConstants() const { return copyOf(slots::allowedConstants, "allowedConstants"); } zv::Val isPureUnlessCallableIsImpureParameter() const { return copyOf(slots::pureUnlessCallableIsImpureParameter, "pureUnlessCallableIsImpureParameter"); } + zv::Val isPureUnlessParameterPassedParameter() const { return copyOf(slots::pureUnlessParameterPassedParameter, "pureUnlessParameterPassedParameter"); } /* !$this->nativeType instanceof MixedType || $this->nativeType->isExplicitMixed(); * false = pending exception */ @@ -136,18 +138,18 @@ using phpstanturbo::ExtendedDummyParameter; zv::Val pt_extended_dummy_parameter_new(uint32_t argc, zval *argv) { - if (EXPECTED(argc == 14)) { + if (EXPECTED(argc == 15)) { zval *name = &argv[0], *optional = &argv[2], *passedByReference = &argv[3], *variadic = &argv[4], *defaultValue = &argv[5]; zval *outType = &argv[8], *closureThisType = &argv[10], *attributes = &argv[11], *allowedConstants = &argv[12]; auto isBool = [](zval *value) { return Z_TYPE_P(value) == IS_TRUE || Z_TYPE_P(value) == IS_FALSE; }; auto isObjectOrNull = [](zval *value) { return Z_TYPE_P(value) == IS_OBJECT || Z_TYPE_P(value) == IS_NULL; }; if (EXPECTED(Z_TYPE_P(name) == IS_STRING && Z_TYPE(argv[1]) == IS_OBJECT && isBool(optional) && isObjectOrNull(passedByReference) && isBool(variadic) && isObjectOrNull(defaultValue) && Z_TYPE(argv[6]) == IS_OBJECT && Z_TYPE(argv[7]) == IS_OBJECT && isObjectOrNull(outType) && Z_TYPE(argv[9]) == IS_OBJECT && isObjectOrNull(closureThisType) - && Z_TYPE_P(attributes) == IS_ARRAY && isObjectOrNull(allowedConstants) && Z_TYPE(argv[13]) == IS_OBJECT)) { + && Z_TYPE_P(attributes) == IS_ARRAY && isObjectOrNull(allowedConstants) && Z_TYPE(argv[13]) == IS_OBJECT && Z_TYPE(argv[14]) == IS_OBJECT)) { zval object; if (UNEXPECTED(object_init_ex(&object, pt_ce_extended_dummy_parameter) != SUCCESS)) return zv::Val(); zv::Val result = zv::Val::adopt(object); - if (UNEXPECTED(!ExtendedDummyParameter(Z_OBJ_P(result.raw())).construct(Z_STR_P(name), &argv[1], Z_TYPE_P(optional) == IS_TRUE, Z_TYPE_P(passedByReference) == IS_NULL ? NULL : passedByReference, Z_TYPE_P(variadic) == IS_TRUE, Z_TYPE_P(defaultValue) == IS_NULL ? NULL : defaultValue, &argv[6], &argv[7], outType, &argv[9], closureThisType, attributes, allowedConstants, &argv[13]))) return zv::Val(); + if (UNEXPECTED(!ExtendedDummyParameter(Z_OBJ_P(result.raw())).construct(Z_STR_P(name), &argv[1], Z_TYPE_P(optional) == IS_TRUE, Z_TYPE_P(passedByReference) == IS_NULL ? NULL : passedByReference, Z_TYPE_P(variadic) == IS_TRUE, Z_TYPE_P(defaultValue) == IS_NULL ? NULL : defaultValue, &argv[6], &argv[7], outType, &argv[9], closureThisType, attributes, allowedConstants, &argv[13], &argv[14]))) return zv::Val(); return result; } } @@ -172,9 +174,9 @@ PT_MINIT_REGISTRATION(pt_register_extended_dummy_parameter) cls.method(sigs::__construct, [](INTERNAL_FUNCTION_PARAMETERS) { zend_string *name; - zval *type, *passedByReference = NULL, *defaultValue = NULL, *nativeType, *phpDocType, *outType = NULL, *immediatelyInvokedCallable, *closureThisType = NULL, *attributes, *allowedConstants = NULL, *pureUnlessCallableIsImpureParameter; + zval *type, *passedByReference = NULL, *defaultValue = NULL, *nativeType, *phpDocType, *outType = NULL, *immediatelyInvokedCallable, *closureThisType = NULL, *attributes, *allowedConstants = NULL, *pureUnlessCallableIsImpureParameter, *pureUnlessParameterPassedParameter; bool optional, variadic; - ZEND_PARSE_PARAMETERS_START(14, 14) + ZEND_PARSE_PARAMETERS_START(15, 15) Z_PARAM_STR(name) Z_PARAM_OBJECT(type) Z_PARAM_BOOL(optional) @@ -189,8 +191,9 @@ PT_MINIT_REGISTRATION(pt_register_extended_dummy_parameter) Z_PARAM_ARRAY(attributes) Z_PARAM_OBJECT_OR_NULL(allowedConstants) Z_PARAM_OBJECT(pureUnlessCallableIsImpureParameter) + Z_PARAM_OBJECT(pureUnlessParameterPassedParameter) ZEND_PARSE_PARAMETERS_END(); - if (UNEXPECTED(!PT_EDP_THIS.construct(name, type, optional, passedByReference, variadic, defaultValue, nativeType, phpDocType, outType, immediatelyInvokedCallable, closureThisType, attributes, allowedConstants, pureUnlessCallableIsImpureParameter))) RETURN_THROWS(); + if (UNEXPECTED(!PT_EDP_THIS.construct(name, type, optional, passedByReference, variadic, defaultValue, nativeType, phpDocType, outType, immediatelyInvokedCallable, closureThisType, attributes, allowedConstants, pureUnlessCallableIsImpureParameter, pureUnlessParameterPassedParameter))) RETURN_THROWS(); }); cls.method<&ExtendedDummyParameter::getPhpDocType>(sigs::getPhpDocType); @@ -211,6 +214,7 @@ PT_MINIT_REGISTRATION(pt_register_extended_dummy_parameter) }); cls.method<&ExtendedDummyParameter::isPureUnlessCallableIsImpureParameter>(sigs::isPureUnlessCallableIsImpureParameter); + cls.method<&ExtendedDummyParameter::isPureUnlessParameterPassedParameter>(sigs::isPureUnlessParameterPassedParameter); cls.shadow(&pt_ce_extended_dummy_parameter); } diff --git a/turbo-ext/src/ExtendedNativeParameterReflection.cpp b/turbo-ext/src/ExtendedNativeParameterReflection.cpp index affed69015b..5bb91e62362 100644 --- a/turbo-ext/src/ExtendedNativeParameterReflection.cpp +++ b/turbo-ext/src/ExtendedNativeParameterReflection.cpp @@ -2,7 +2,7 @@ * PHPStanTurbo\ExtendedNativeParameterReflection — native implementation of * PHPStan\Reflection\Native\ExtendedNativeParameterReflection. * - * A final value class over its fourteen promoted slots in the twin's order: + * A final value class over its fifteen promoted slots in the twin's order: * every getter returns its slot, hasNativeType() asks the native type * whether it is an explicit mixed, checkAllowedConstants() delegates to the * PHP ParameterAllowedConstants. Native callers read the getters through @@ -56,7 +56,7 @@ class ExtendedNativeParameterReflection /* the promoted slots in parameter order (borrowed; NULL for a nullable * null) */ - void construct(zend_string *name, bool optional, zval *type, zval *phpDocType, zval *nativeType, zval *passedByReference, bool variadic, zval *defaultValue, zval *outType, zval *immediatelyInvokedCallable, zval *closureThisType, zval *attributes, zval *allowedConstants, zval *pureUnlessCallableIsImpureParameter) const + void construct(zend_string *name, bool optional, zval *type, zval *phpDocType, zval *nativeType, zval *passedByReference, bool variadic, zval *defaultValue, zval *outType, zval *immediatelyInvokedCallable, zval *closureThisType, zval *attributes, zval *allowedConstants, zval *pureUnlessCallableIsImpureParameter, zval *pureUnlessParameterPassedParameter) const { zval value; ZVAL_STR(&value, name); @@ -76,6 +76,7 @@ class ExtendedNativeParameterReflection pt_write_slot(self, slots::attributes, attributes); writeNullable(slots::allowedConstants, allowedConstants); pt_write_slot(self, slots::pureUnlessCallableIsImpureParameter, pureUnlessCallableIsImpureParameter); + pt_write_slot(self, slots::pureUnlessParameterPassedParameter, pureUnlessParameterPassedParameter); } zv::Val getName() const { return copyOf(slots::name, "name"); } @@ -126,6 +127,7 @@ class ExtendedNativeParameterReflection } zv::Val isPureUnlessCallableIsImpureParameter() const { return copyOf(slots::pureUnlessCallableIsImpureParameter, "pureUnlessCallableIsImpureParameter"); } + zv::Val isPureUnlessParameterPassedParameter() const { return copyOf(slots::pureUnlessParameterPassedParameter, "pureUnlessParameterPassedParameter"); } private: zend_object *self; @@ -159,16 +161,16 @@ using phpstanturbo::ExtendedNativeParameterReflection; zv::Val pt_extended_native_parameter_reflection_new(uint32_t argc, zval *argv) { - if (EXPECTED(argc == 14)) { + if (EXPECTED(argc == 15)) { auto isBool = [](zval *value) { return Z_TYPE_P(value) == IS_TRUE || Z_TYPE_P(value) == IS_FALSE; }; auto isObjectOrNull = [](zval *value) { return Z_TYPE_P(value) == IS_OBJECT || Z_TYPE_P(value) == IS_NULL; }; if (EXPECTED(Z_TYPE(argv[0]) == IS_STRING && isBool(&argv[1]) && Z_TYPE(argv[2]) == IS_OBJECT && Z_TYPE(argv[3]) == IS_OBJECT && Z_TYPE(argv[4]) == IS_OBJECT && Z_TYPE(argv[5]) == IS_OBJECT && isBool(&argv[6]) && isObjectOrNull(&argv[7]) && isObjectOrNull(&argv[8]) && Z_TYPE(argv[9]) == IS_OBJECT - && isObjectOrNull(&argv[10]) && Z_TYPE(argv[11]) == IS_ARRAY && isObjectOrNull(&argv[12]) && Z_TYPE(argv[13]) == IS_OBJECT)) { + && isObjectOrNull(&argv[10]) && Z_TYPE(argv[11]) == IS_ARRAY && isObjectOrNull(&argv[12]) && Z_TYPE(argv[13]) == IS_OBJECT && Z_TYPE(argv[14]) == IS_OBJECT)) { zval object; if (UNEXPECTED(object_init_ex(&object, pt_ce_extended_native_parameter_reflection) != SUCCESS)) return zv::Val(); auto orNull = [](zval *value) { return Z_TYPE_P(value) == IS_NULL ? NULL : value; }; - ExtendedNativeParameterReflection(Z_OBJ(object)).construct(Z_STR(argv[0]), Z_TYPE(argv[1]) == IS_TRUE, &argv[2], &argv[3], &argv[4], &argv[5], Z_TYPE(argv[6]) == IS_TRUE, orNull(&argv[7]), orNull(&argv[8]), &argv[9], orNull(&argv[10]), &argv[11], orNull(&argv[12]), &argv[13]); + ExtendedNativeParameterReflection(Z_OBJ(object)).construct(Z_STR(argv[0]), Z_TYPE(argv[1]) == IS_TRUE, &argv[2], &argv[3], &argv[4], &argv[5], Z_TYPE(argv[6]) == IS_TRUE, orNull(&argv[7]), orNull(&argv[8]), &argv[9], orNull(&argv[10]), &argv[11], orNull(&argv[12]), &argv[13], &argv[14]); return zv::Val::adopt(object); } } @@ -200,6 +202,7 @@ zv::Val pt_extended_native_parameter_reflection_call(zend_object *parameter, pt_ case PT_PR_GET_ATTRIBUTES: return reflection.getAttributes(); case PT_PR_GET_ALLOWED_CONSTANTS: return reflection.getAllowedConstants(); case PT_PR_IS_PURE_UNLESS_CALLABLE_IS_IMPURE_PARAMETER: return reflection.isPureUnlessCallableIsImpureParameter(); + case PT_PR_IS_PURE_UNLESS_PARAMETER_PASSED_PARAMETER: return reflection.isPureUnlessParameterPassedParameter(); case PT_PR_MEMBER_COUNT: break; } ZEND_UNREACHABLE(); @@ -223,8 +226,8 @@ PT_MINIT_REGISTRATION(pt_register_extended_native_parameter_reflection) cls.method(sigs::__construct, [](INTERNAL_FUNCTION_PARAMETERS) { zend_string *name; bool optional, variadic; - zval *type, *phpDocType, *nativeType, *passedByReference, *defaultValue = NULL, *outType = NULL, *immediatelyInvokedCallable, *closureThisType = NULL, *attributes, *allowedConstants = NULL, *pureUnlessCallableIsImpureParameter; - ZEND_PARSE_PARAMETERS_START(14, 14) + zval *type, *phpDocType, *nativeType, *passedByReference, *defaultValue = NULL, *outType = NULL, *immediatelyInvokedCallable, *closureThisType = NULL, *attributes, *allowedConstants = NULL, *pureUnlessCallableIsImpureParameter, *pureUnlessParameterPassedParameter; + ZEND_PARSE_PARAMETERS_START(15, 15) Z_PARAM_STR(name) Z_PARAM_BOOL(optional) Z_PARAM_OBJECT(type) @@ -239,8 +242,9 @@ PT_MINIT_REGISTRATION(pt_register_extended_native_parameter_reflection) Z_PARAM_ARRAY(attributes) Z_PARAM_OBJECT_OR_NULL(allowedConstants) Z_PARAM_OBJECT(pureUnlessCallableIsImpureParameter) + Z_PARAM_OBJECT(pureUnlessParameterPassedParameter) ZEND_PARSE_PARAMETERS_END(); - PT_ENPR_THIS.construct(name, optional, type, phpDocType, nativeType, passedByReference, variadic, defaultValue, outType, immediatelyInvokedCallable, closureThisType, attributes, allowedConstants, pureUnlessCallableIsImpureParameter); + PT_ENPR_THIS.construct(name, optional, type, phpDocType, nativeType, passedByReference, variadic, defaultValue, outType, immediatelyInvokedCallable, closureThisType, attributes, allowedConstants, pureUnlessCallableIsImpureParameter, pureUnlessParameterPassedParameter); }); cls.method<&ExtendedNativeParameterReflection::getName>(sigs::getName); @@ -273,6 +277,7 @@ PT_MINIT_REGISTRATION(pt_register_extended_native_parameter_reflection) }); cls.method<&ExtendedNativeParameterReflection::isPureUnlessCallableIsImpureParameter>(sigs::isPureUnlessCallableIsImpureParameter); + cls.method<&ExtendedNativeParameterReflection::isPureUnlessParameterPassedParameter>(sigs::isPureUnlessParameterPassedParameter); cls.shadow(&pt_ce_extended_native_parameter_reflection); } diff --git a/turbo-ext/src/ParameterValues.h b/turbo-ext/src/ParameterValues.h index 70fb843ca2b..ac98305ad68 100644 --- a/turbo-ext/src/ParameterValues.h +++ b/turbo-ext/src/ParameterValues.h @@ -70,7 +70,7 @@ inline constexpr int8_t dummySlots[PT_PR_MEMBER_COUNT] = { /* PT_PR_PASSED_BY_REFERENCE */ (int8_t) ptdecl::DummyParameter::slot::passedByReference, /* PT_PR_IS_VARIADIC */ (int8_t) ptdecl::DummyParameter::slot::variadic, /* PT_PR_GET_DEFAULT_VALUE */ (int8_t) ptdecl::DummyParameter::slot::defaultValue, - PT_PV_NONE, PT_PV_NONE, PT_PV_NONE, PT_PV_NONE, PT_PV_NONE, PT_PV_NONE, PT_PV_NONE, PT_PV_NONE, PT_PV_NONE, + PT_PV_NONE, PT_PV_NONE, PT_PV_NONE, PT_PV_NONE, PT_PV_NONE, PT_PV_NONE, PT_PV_NONE, PT_PV_NONE, PT_PV_NONE, PT_PV_NONE, }; inline constexpr int8_t extendedDummySlots[PT_PR_MEMBER_COUNT] = { @@ -89,6 +89,7 @@ inline constexpr int8_t extendedDummySlots[PT_PR_MEMBER_COUNT] = { /* PT_PR_GET_ATTRIBUTES */ (int8_t) ptdecl::ExtendedDummyParameter::slot::attributes, /* PT_PR_GET_ALLOWED_CONSTANTS */ (int8_t) ptdecl::ExtendedDummyParameter::slot::allowedConstants, /* PT_PR_IS_PURE_UNLESS_CALLABLE_IS_IMPURE_PARAMETER */ (int8_t) ptdecl::ExtendedDummyParameter::slot::pureUnlessCallableIsImpureParameter, + /* PT_PR_IS_PURE_UNLESS_PARAMETER_PASSED_PARAMETER */ (int8_t) ptdecl::ExtendedDummyParameter::slot::pureUnlessParameterPassedParameter, }; inline constexpr int8_t nativeSlots[PT_PR_MEMBER_COUNT] = { @@ -98,7 +99,7 @@ inline constexpr int8_t nativeSlots[PT_PR_MEMBER_COUNT] = { (int8_t) ptdecl::NativeParameterReflection::slot::passedByReference, (int8_t) ptdecl::NativeParameterReflection::slot::variadic, (int8_t) ptdecl::NativeParameterReflection::slot::defaultValue, - PT_PV_NONE, PT_PV_NONE, PT_PV_NONE, PT_PV_NONE, PT_PV_NONE, PT_PV_NONE, PT_PV_NONE, PT_PV_NONE, PT_PV_NONE, + PT_PV_NONE, PT_PV_NONE, PT_PV_NONE, PT_PV_NONE, PT_PV_NONE, PT_PV_NONE, PT_PV_NONE, PT_PV_NONE, PT_PV_NONE, PT_PV_NONE, }; inline constexpr int8_t extendedNativeSlots[PT_PR_MEMBER_COUNT] = { @@ -117,6 +118,7 @@ inline constexpr int8_t extendedNativeSlots[PT_PR_MEMBER_COUNT] = { (int8_t) ptdecl::ExtendedNativeParameterReflection::slot::attributes, (int8_t) ptdecl::ExtendedNativeParameterReflection::slot::allowedConstants, (int8_t) ptdecl::ExtendedNativeParameterReflection::slot::pureUnlessCallableIsImpureParameter, + (int8_t) ptdecl::ExtendedNativeParameterReflection::slot::pureUnlessParameterPassedParameter, }; #undef PT_PV_NONE diff --git a/turbo-ext/src/ParametersAcceptorSelector.cpp b/turbo-ext/src/ParametersAcceptorSelector.cpp index 837891e7c14..c1869f628bd 100644 --- a/turbo-ext/src/ParametersAcceptorSelector.cpp +++ b/turbo-ext/src/ParametersAcceptorSelector.cpp @@ -252,6 +252,7 @@ enum ParameterGetter PG_ATTRIBUTES, PG_ALLOWED_CONSTANTS, PG_PURE_UNLESS_CALLABLE_IS_IMPURE, + PG_PURE_UNLESS_PARAMETER_PASSED, PG_COUNT, }; @@ -271,6 +272,7 @@ const pt_parameter_reflection_member pt_pas_parameter_members[PG_COUNT] = { PT_PR_GET_ATTRIBUTES, PT_PR_GET_ALLOWED_CONSTANTS, PT_PR_IS_PURE_UNLESS_CALLABLE_IS_IMPURE_PARAMETER, + PT_PR_IS_PURE_UNLESS_PARAMETER_PASSED_PARAMETER, }; /* $parameter->getX() (ParameterValues.h) */ @@ -1128,14 +1130,14 @@ class ParametersAcceptorSelector return zv::Val(std::move(values)); } - /* new ExtendedDummyParameter(...$argv) over fourteen owned values */ + /* new ExtendedDummyParameter(...$argv) over fifteen owned values */ static zv::Val newExtendedDummyParameter(zv::Val *values) { - zval argv[14]; - for (int k = 0; k < 14; k++) { + zval argv[15]; + for (int k = 0; k < 15; k++) { ZVAL_COPY_VALUE(&argv[k], values[k].raw()); } - return pt_extended_dummy_parameter_new(14, argv); + return pt_extended_dummy_parameter_new(15, argv); } /* `$parameter instanceof ExtendedParameterReflection ? $parameter->getX() @@ -1156,7 +1158,7 @@ class ParametersAcceptorSelector * over its getters ($i + 1 > $minimumNumberOfParameters as optional) */ static zv::Val firstCombinedParameter(zval *parameter, bool isExtended, bool optional) { - zv::Val values[14]; + zv::Val values[15]; values[0] = parameterGet(parameter, PG_NAME); if (UNEXPECTED(values[0].isUndef())) return zv::Val(); values[1] = parameterGet(parameter, PG_TYPE); @@ -1172,7 +1174,7 @@ class ParametersAcceptorSelector { ParameterGetter getter; zv::Val (*fallback)(); - } extended[8] = { + } extended[9] = { { PG_NATIVE_TYPE, mixedDefault }, { PG_PHPDOC_TYPE, mixedDefault }, { PG_OUT_TYPE, nullDefault }, @@ -1181,8 +1183,9 @@ class ParametersAcceptorSelector { PG_ATTRIBUTES, emptyArrayDefault }, { PG_ALLOWED_CONSTANTS, nullDefault }, { PG_PURE_UNLESS_CALLABLE_IS_IMPURE, noDefault }, + { PG_PURE_UNLESS_PARAMETER_PASSED, noDefault }, }; - for (int k = 0; k < 8; k++) { + for (int k = 0; k < 9; k++) { values[6 + k] = extendedOr(parameter, isExtended, extended[k].getter, extended[k].fallback); if (UNEXPECTED(values[6 + k].isUndef())) return zv::Val(); } @@ -1350,7 +1353,17 @@ class ParametersAcceptorSelector if (UNEXPECTED(rightPure < 0)) return zv::Val(); zv::Val pureUnlessCallableIsImpureParameter = leftPure == rightPure ? std::move(leftPureUnless) : trinary(PT_TRI_MAYBE); - zv::Val values[14]; + zv::Val leftPureUnlessParameterPassed = parameterGet(existing, PG_PURE_UNLESS_PARAMETER_PASSED); + if (UNEXPECTED(leftPureUnlessParameterPassed.isUndef())) return zv::Val(); + zv::Val rightPureUnlessParameterPassed = isExtended ? parameterGet(parameter, PG_PURE_UNLESS_PARAMETER_PASSED) : trinary(PT_TRI_NO); + if (UNEXPECTED(rightPureUnlessParameterPassed.isUndef())) return zv::Val(); + zend_long leftPassed = trinaryOf(leftPureUnlessParameterPassed); + if (UNEXPECTED(leftPassed < 0)) return zv::Val(); + zend_long rightPassed = trinaryOf(rightPureUnlessParameterPassed); + if (UNEXPECTED(rightPassed < 0)) return zv::Val(); + zv::Val pureUnlessParameterPassedParameter = leftPassed == rightPassed ? std::move(leftPureUnlessParameterPassed) : trinary(PT_TRI_MAYBE); + + zv::Val values[15]; { zv::Val existingName = parameterGet(existing, PG_NAME); if (UNEXPECTED(existingName.isUndef())) return zv::Val(); @@ -1391,6 +1404,7 @@ class ParametersAcceptorSelector values[11] = std::move(attributes); values[12] = std::move(allowedConstants); values[13] = std::move(pureUnlessCallableIsImpureParameter); + values[14] = std::move(pureUnlessParameterPassedParameter); return newExtendedDummyParameter(values); } @@ -1460,7 +1474,7 @@ class ParametersAcceptorSelector if (UNEXPECTED(!isA(parameter, PT_CLASS_EXTENDED_PARAMETER_REFLECTION, isExtended))) return zv::Val(); if (isExtended) return zv::Val::copyOf(zv::Ref(parameter)); - zv::Val values[14]; + zv::Val values[15]; static const ParameterGetter getters[6] = { PG_NAME, PG_TYPE, PG_OPTIONAL, PG_PASSED_BY_REFERENCE, PG_VARIADIC, PG_DEFAULT_VALUE }; for (int k = 0; k < 6; k++) { values[k] = parameterGet(parameter, getters[k]); @@ -1476,6 +1490,7 @@ class ParametersAcceptorSelector values[11] = zv::Val(zv::Arr::empty()); values[12] = zv::Val::null(); values[13] = trinary(PT_TRI_NO); + values[14] = trinary(PT_TRI_NO); return newExtendedDummyParameter(values); } @@ -1484,7 +1499,7 @@ class ParametersAcceptorSelector { zv::Val wrapped = wrapParameter(original); if (UNEXPECTED(wrapped.isUndef())) return zv::Val(); - zv::Val values[14]; + zv::Val values[15]; values[0] = parameterGet(wrapped.raw(), PG_NAME); if (UNEXPECTED(values[0].isUndef())) return zv::Val(); values[1] = zv::Val::copyOf(zv::Ref(type)); @@ -1492,10 +1507,10 @@ class ParametersAcceptorSelector { int index; ParameterGetter getter; - } getters[10] = { + } getters[11] = { { 2, PG_OPTIONAL }, { 3, PG_PASSED_BY_REFERENCE }, { 4, PG_VARIADIC }, { 5, PG_DEFAULT_VALUE }, { 8, PG_OUT_TYPE }, { 9, PG_IMMEDIATELY_INVOKED_CALLABLE }, { 10, PG_CLOSURE_THIS_TYPE }, { 11, PG_ATTRIBUTES }, - { 12, PG_ALLOWED_CONSTANTS }, { 13, PG_PURE_UNLESS_CALLABLE_IS_IMPURE }, + { 12, PG_ALLOWED_CONSTANTS }, { 13, PG_PURE_UNLESS_CALLABLE_IS_IMPURE }, { 14, PG_PURE_UNLESS_PARAMETER_PASSED }, }; values[6] = zv::Val::copyOf(zv::Ref(nativeType)); values[7] = zv::Val::copyOf(zv::Ref(type)); diff --git a/turbo-ext/src/PhpClassReflectionExtension.cpp b/turbo-ext/src/PhpClassReflectionExtension.cpp index 1143525d7ee..754c16ec6f8 100644 --- a/turbo-ext/src/PhpClassReflectionExtension.cpp +++ b/turbo-ext/src/PhpClassReflectionExtension.cpp @@ -1986,13 +1986,15 @@ class PhpClassReflectionExtension if (UNEXPECTED(allowedConstants.isUndef())) return zv::Val(); zv::Val pureUnlessCallableIsImpure = trinaryNo(); if (UNEXPECTED(pureUnlessCallableIsImpure.isUndef())) return zv::Val(); + zv::Val pureUnlessParameterPassed = trinaryNo(); + if (UNEXPECTED(pureUnlessParameterPassed.isUndef())) return zv::Val(); zv::Val mixedPhpDocType; if (phpDocType.isUndef()) { mixedPhpDocType = mixedType(); if (UNEXPECTED(mixedPhpDocType.isUndef())) return zv::Val(); } - zval args[14]; + zval args[15]; if (usePhpDocParameterNames) { ZVAL_STR(&args[0], phpDocParameterName.get()); } else { @@ -2011,7 +2013,8 @@ class PhpClassReflectionExtension ZVAL_EMPTY_ARRAY(&args[11]); ZVAL_COPY_VALUE(&args[12], allowedConstants.raw()); ZVAL_COPY_VALUE(&args[13], pureUnlessCallableIsImpure.raw()); - zv::Val parameter = pt_extended_native_parameter_reflection_new(14, args); + ZVAL_COPY_VALUE(&args[14], pureUnlessParameterPassed.raw()); + zv::Val parameter = pt_extended_native_parameter_reflection_new(15, args); if (UNEXPECTED(parameter.isUndef())) return zv::Val(); parameters.push(std::move(parameter)); } @@ -2209,6 +2212,7 @@ class PhpClassReflectionExtension int isPure = -1; zv::Arr pureUnlessCallableIsImpureParameters = zv::Arr::create(0); + zv::Arr pureUnlessParameterPassedParameters = zv::Arr::create(0); bool isBuiltin = callBool(actualDeclaringClass, PT_LC("isbuiltin"), 0, NULL, ok); if (UNEXPECTED(!ok)) return zv::Val(); bool actualIsEnum = false; @@ -2251,6 +2255,19 @@ class PhpClassReflectionExtension } } } + zval *pureUnlessPassed = Z_TYPE_P(metadata.raw()) == IS_ARRAY ? zend_hash_str_find(Z_ARRVAL_P(metadata.raw()), PT_LC("pureUnlessParameterPassedParameters")) : NULL; + if (pureUnlessPassed != NULL) { + ZVAL_DEREF(pureUnlessPassed); + if (Z_TYPE_P(pureUnlessPassed) == IS_ARRAY) { + for (zv::ArrayEntry passedEntry : zv::ArrRef(pureUnlessPassed)) { + zend_string *key = passedEntry.stringKeyOrNull(); + if (key == NULL) continue; + if (!zend_symtable_exists(pureUnlessParameterPassedParameters.table(), key)) { + pureUnlessParameterPassedParameters.set(key, zv::Val::copyOf(passedEntry.value())); + } + } + } + } break; } } @@ -2290,6 +2307,15 @@ class PhpClassReflectionExtension pureUnlessCallableIsImpureParameters.set(key, zv::Val::copyOf(pureEntry.value())); } } + zv::Val pureUnlessPassed = pt_resolved_php_doc_block_call(resolvedPhpDoc.raw(), PT_RPD_GET_PARAMS_PURE_UNLESS_PARAMETER_PASSED); + if (UNEXPECTED(pureUnlessPassed.isUndef())) return zv::Val(); + if (Z_TYPE_P(pureUnlessPassed.raw()) == IS_ARRAY) { + for (zv::ArrayEntry passedEntry : zv::ArrRef(pureUnlessPassed.raw())) { + zend_string *key = passedEntry.stringKeyOrNull(); + if (key == NULL) continue; + pureUnlessParameterPassedParameters.set(key, zv::Val::copyOf(passedEntry.value())); + } + } phpDocReturnType = getPhpDocReturnType(phpDocBlockClassReflection.raw(), resolvedPhpDoc.raw(), nativeReturnType.raw()); if (UNEXPECTED(phpDocReturnType.isUndef())) return zv::Val(); @@ -2442,7 +2468,7 @@ class PhpClassReflectionExtension zv::Val attributes = attributesOf(methodReflection, context.raw()); if (UNEXPECTED(attributes.isUndef())) return zv::Val(); - zval args[22]; + zval args[23]; ZVAL_COPY_VALUE(&args[0], actualDeclaringClass); ZVAL_COPY_VALUE(&args[1], declaringTrait.raw()); ZVAL_COPY_VALUE(&args[2], methodReflection); @@ -2469,7 +2495,8 @@ class PhpClassReflectionExtension ZVAL_BOOL(&args[19], acceptsNamedArguments); ZVAL_COPY_VALUE(&args[20], attributes.raw()); ZVAL_COPY_VALUE(&args[21], pureUnlessCallableIsImpureParameters.raw()); - return call(slot(slots::methodReflectionFactory), PT_LC("create"), 22, args); + ZVAL_COPY_VALUE(&args[22], pureUnlessParameterPassedParameters.raw()); + return call(slot(slots::methodReflectionFactory), PT_LC("create"), 23, args); } /* Mirrors getPhpDocReturnType(). */ diff --git a/turbo-ext/src/PhpParameterReflection.cpp b/turbo-ext/src/PhpParameterReflection.cpp index 0abf2c2a415..8e66fe4e2cb 100644 --- a/turbo-ext/src/PhpParameterReflection.cpp +++ b/turbo-ext/src/PhpParameterReflection.cpp @@ -79,7 +79,7 @@ class PhpParameterReflection /* the promoted slots in parameter order (borrowed; NULL for a nullable * null) */ - void construct(zval *initializerExprTypeResolver, zval *reflection, zval *phpDocType, zval *declaringClass, zval *outType, zval *immediatelyInvokedCallable, zval *closureThisType, zval *attributes, zval *allowedConstants, zval *pureUnlessCallableIsImpureParameter) const + void construct(zval *initializerExprTypeResolver, zval *reflection, zval *phpDocType, zval *declaringClass, zval *outType, zval *immediatelyInvokedCallable, zval *closureThisType, zval *attributes, zval *allowedConstants, zval *pureUnlessCallableIsImpureParameter, zval *pureUnlessParameterPassedParameter) const { pt_write_slot(self, slots::initializerExprTypeResolver, initializerExprTypeResolver); pt_write_slot(self, slots::reflection, reflection); @@ -91,6 +91,7 @@ class PhpParameterReflection pt_write_slot(self, slots::attributes, attributes); writeNullable(slots::allowedConstants, allowedConstants); pt_write_slot(self, slots::pureUnlessCallableIsImpureParameter, pureUnlessCallableIsImpureParameter); + pt_write_slot(self, slots::pureUnlessParameterPassedParameter, pureUnlessParameterPassedParameter); } /* $this->reflection->isOptional() */ @@ -249,6 +250,7 @@ class PhpParameterReflection } zv::Val isPureUnlessCallableIsImpureParameter() const { return copyOf(slots::pureUnlessCallableIsImpureParameter, "pureUnlessCallableIsImpureParameter"); } + zv::Val isPureUnlessParameterPassedParameter() const { return copyOf(slots::pureUnlessParameterPassedParameter, "pureUnlessParameterPassedParameter"); } private: zend_object *self; @@ -330,6 +332,7 @@ const MemberName memberNames[PT_PR_MEMBER_COUNT] = { /* PT_PR_GET_ATTRIBUTES */ PT_PPR_MEMBER("getattributes", "getAttributes"), /* PT_PR_GET_ALLOWED_CONSTANTS */ PT_PPR_MEMBER("getallowedconstants", "getAllowedConstants"), /* PT_PR_IS_PURE_UNLESS_CALLABLE_IS_IMPURE_PARAMETER */ PT_PPR_MEMBER("ispureunlesscallableisimpureparameter", "isPureUnlessCallableIsImpureParameter"), + /* PT_PR_IS_PURE_UNLESS_PARAMETER_PASSED_PARAMETER */ PT_PPR_MEMBER("ispureunlessparameterpassedparameter", "isPureUnlessParameterPassedParameter"), }; #undef PT_PPR_MEMBER @@ -374,6 +377,7 @@ zv::Val pt_php_parameter_reflection_call(zend_object *parameter, pt_parameter_re case PT_PR_GET_ATTRIBUTES: return reflection.getAttributes(); case PT_PR_GET_ALLOWED_CONSTANTS: return reflection.getAllowedConstants(); case PT_PR_IS_PURE_UNLESS_CALLABLE_IS_IMPURE_PARAMETER: return reflection.isPureUnlessCallableIsImpureParameter(); + case PT_PR_IS_PURE_UNLESS_PARAMETER_PASSED_PARAMETER: return reflection.isPureUnlessParameterPassedParameter(); case PT_PR_MEMBER_COUNT: break; } ZEND_UNREACHABLE(); @@ -395,8 +399,8 @@ PT_MINIT_REGISTRATION(pt_register_php_parameter_reflection) ptdecl::PhpParameterReflection::declareProperties(cls); cls.method(sigs::__construct, [](INTERNAL_FUNCTION_PARAMETERS) { - zval *initializerExprTypeResolver, *reflection, *phpDocType = NULL, *declaringClass = NULL, *outType = NULL, *immediatelyInvokedCallable, *closureThisType = NULL, *attributes, *allowedConstants = NULL, *pureUnlessCallableIsImpureParameter; - ZEND_PARSE_PARAMETERS_START(10, 10) + zval *initializerExprTypeResolver, *reflection, *phpDocType = NULL, *declaringClass = NULL, *outType = NULL, *immediatelyInvokedCallable, *closureThisType = NULL, *attributes, *allowedConstants = NULL, *pureUnlessCallableIsImpureParameter, *pureUnlessParameterPassedParameter; + ZEND_PARSE_PARAMETERS_START(11, 11) Z_PARAM_OBJECT(initializerExprTypeResolver) Z_PARAM_OBJECT(reflection) Z_PARAM_OBJECT_OR_NULL(phpDocType) @@ -407,8 +411,9 @@ PT_MINIT_REGISTRATION(pt_register_php_parameter_reflection) Z_PARAM_ARRAY(attributes) Z_PARAM_OBJECT_OR_NULL(allowedConstants) Z_PARAM_OBJECT(pureUnlessCallableIsImpureParameter) + Z_PARAM_OBJECT(pureUnlessParameterPassedParameter) ZEND_PARSE_PARAMETERS_END(); - PT_PPR_THIS.construct(initializerExprTypeResolver, reflection, phpDocType, declaringClass, outType, immediatelyInvokedCallable, closureThisType, attributes, allowedConstants, pureUnlessCallableIsImpureParameter); + PT_PPR_THIS.construct(initializerExprTypeResolver, reflection, phpDocType, declaringClass, outType, immediatelyInvokedCallable, closureThisType, attributes, allowedConstants, pureUnlessCallableIsImpureParameter, pureUnlessParameterPassedParameter); }); cls.method<&PhpParameterReflection::isOptional>(sigs::isOptional); @@ -441,6 +446,7 @@ PT_MINIT_REGISTRATION(pt_register_php_parameter_reflection) }); cls.method<&PhpParameterReflection::isPureUnlessCallableIsImpureParameter>(sigs::isPureUnlessCallableIsImpureParameter); + cls.method<&PhpParameterReflection::isPureUnlessParameterPassedParameter>(sigs::isPureUnlessParameterPassedParameter); cls.shadow(&pt_ce_php_parameter_reflection); } diff --git a/turbo-ext/src/ResolvedFunctionVariantWithOriginal.cpp b/turbo-ext/src/ResolvedFunctionVariantWithOriginal.cpp index b3b46b16bbf..920f4d630f7 100644 --- a/turbo-ext/src/ResolvedFunctionVariantWithOriginal.cpp +++ b/turbo-ext/src/ResolvedFunctionVariantWithOriginal.cpp @@ -572,17 +572,18 @@ class ResolvedFunctionVariantWithOriginal /* new ExtendedDummyParameter($param->getName(), $paramType, ...) — the * getters in argument order */ - static constexpr pt_parameter_reflection_member getters[11] = { + static constexpr pt_parameter_reflection_member getters[12] = { PT_PR_GET_NAME, PT_PR_IS_OPTIONAL, PT_PR_PASSED_BY_REFERENCE, PT_PR_IS_VARIADIC, PT_PR_GET_DEFAULT_VALUE, PT_PR_GET_NATIVE_TYPE, PT_PR_GET_PHPDOC_TYPE, PT_PR_IS_IMMEDIATELY_INVOKED_CALLABLE, PT_PR_GET_ATTRIBUTES, PT_PR_GET_ALLOWED_CONSTANTS, PT_PR_IS_PURE_UNLESS_CALLABLE_IS_IMPURE_PARAMETER, + PT_PR_IS_PURE_UNLESS_PARAMETER_PASSED_PARAMETER, }; - zv::Val values[11]; - for (int i = 0; i < 11; i++) { + zv::Val values[12]; + for (int i = 0; i < 12; i++) { values[i] = pt_parameter_reflection_call(param, getters[i]); if (UNEXPECTED(values[i].isUndef())) return zv::Val(); } - zval args[14]; + zval args[15]; ZVAL_COPY_VALUE(&args[0], values[0].raw()); ZVAL_COPY_VALUE(&args[1], paramType.raw()); ZVAL_COPY_VALUE(&args[2], values[1].raw()); @@ -597,7 +598,8 @@ class ResolvedFunctionVariantWithOriginal ZVAL_COPY_VALUE(&args[11], values[8].raw()); ZVAL_COPY_VALUE(&args[12], values[9].raw()); ZVAL_COPY_VALUE(&args[13], values[10].raw()); - return pt_extended_dummy_parameter_new(14, args); + ZVAL_COPY_VALUE(&args[14], values[11].raw()); + return pt_extended_dummy_parameter_new(15, args); } /* TypeUtils::resolveLateResolvableTypes(TemplateTypeHelper::resolveTemplateTypes( diff --git a/turbo-ext/src/ResolvedPhpDocBlockAccess.cpp b/turbo-ext/src/ResolvedPhpDocBlockAccess.cpp index d4aeb96d0ae..ebd87bc163b 100644 --- a/turbo-ext/src/ResolvedPhpDocBlockAccess.cpp +++ b/turbo-ext/src/ResolvedPhpDocBlockAccess.cpp @@ -54,6 +54,7 @@ const MemberInfo pt_rpd_members[PT_RPD_MEMBER_COUNT] = { /* PT_RPD_GET_PARAM_OUT_TAGS */ {"paramOutTags", PT_LC("getparamouttags"), "getParamOutTags", PT_RPD_ARRAY_FALSE}, /* PT_RPD_GET_PARAMS_IMMEDIATELY_INVOKED_CALLABLE */ {"paramsImmediatelyInvokedCallable", PT_LC("getparamsimmediatelyinvokedcallable"), "getParamsImmediatelyInvokedCallable", PT_RPD_ARRAY_FALSE}, /* PT_RPD_GET_PARAMS_PURE_UNLESS_CALLABLE_IS_IMPURE */ {"paramsPureUnlessCallableIsImpure", PT_LC("getparamspureunlesscallableisimpure"), "getParamsPureUnlessCallableIsImpure", PT_RPD_ARRAY_FALSE}, + /* PT_RPD_GET_PARAMS_PURE_UNLESS_PARAMETER_PASSED */ {"paramsPureUnlessParameterPassed", PT_LC("getparamspureunlessparameterpassed"), "getParamsPureUnlessParameterPassed", PT_RPD_ARRAY_FALSE}, /* PT_RPD_GET_PARAM_CLOSURE_THIS_TAGS */ {"paramClosureThisTags", PT_LC("getparamclosurethistags"), "getParamClosureThisTags", PT_RPD_ARRAY_FALSE}, /* PT_RPD_GET_RETURN_TAG */ {"returnTag", PT_LC("getreturntag"), "getReturnTag", PT_RPD_TAG_BOOL}, /* PT_RPD_GET_THROWS_TAG */ {"throwsTag", PT_LC("getthrowstag"), "getThrowsTag", PT_RPD_TAG_BOOL}, diff --git a/turbo-ext/src/TypeTraits.cpp b/turbo-ext/src/TypeTraits.cpp index 84f34fcc7af..4a331146d7e 100644 --- a/turbo-ext/src/TypeTraits.cpp +++ b/turbo-ext/src/TypeTraits.cpp @@ -5245,7 +5245,9 @@ zv::Val protoParameter(PrototypeKind kind, const PrototypeTransformer &transform if (UNEXPECTED(allowedConstants.isUndef())) return zv::Val(); zv::Val pureUnlessCallableIsImpure = protoParameterCall(parameter, PT_PR_IS_PURE_UNLESS_CALLABLE_IS_IMPURE_PARAMETER, "ispureunlesscallableisimpureparameter", PROTO_ANY); if (UNEXPECTED(pureUnlessCallableIsImpure.isUndef())) return zv::Val(); - zval args[14]; + zv::Val pureUnlessParameterPassed = protoParameterCall(parameter, PT_PR_IS_PURE_UNLESS_PARAMETER_PASSED_PARAMETER, "ispureunlessparameterpassedparameter", PROTO_ANY); + if (UNEXPECTED(pureUnlessParameterPassed.isUndef())) return zv::Val(); + zval args[15]; ZVAL_COPY_VALUE(&args[0], name.raw()); ZVAL_COPY_VALUE(&args[1], transformedType.raw()); ZVAL_COPY_VALUE(&args[2], optional.raw()); @@ -5260,7 +5262,8 @@ zv::Val protoParameter(PrototypeKind kind, const PrototypeTransformer &transform ZVAL_COPY_VALUE(&args[11], attributes.raw()); ZVAL_COPY_VALUE(&args[12], allowedConstants.raw()); ZVAL_COPY_VALUE(&args[13], pureUnlessCallableIsImpure.raw()); - return pt_extended_dummy_parameter_new(14, args); + ZVAL_COPY_VALUE(&args[14], pureUnlessParameterPassed.raw()); + return pt_extended_dummy_parameter_new(15, args); } /* the twins' $variantFn: new ExtendedFunctionVariant(...) over $acceptor; diff --git a/turbo-ext/src/generated/ExtendedDummyParameter.h b/turbo-ext/src/generated/ExtendedDummyParameter.h index 9607caa68b8..79546e54a5c 100644 --- a/turbo-ext/src/generated/ExtendedDummyParameter.h +++ b/turbo-ext/src/generated/ExtendedDummyParameter.h @@ -18,6 +18,7 @@ inline constexpr uint32_t closureThisType = 10; inline constexpr uint32_t attributes = 11; inline constexpr uint32_t allowedConstants = 12; inline constexpr uint32_t pureUnlessCallableIsImpureParameter = 13; +inline constexpr uint32_t pureUnlessParameterPassedParameter = 14; } // namespace slot inline void declareClass(reg::Class &cls) @@ -38,6 +39,7 @@ inline void declareProperties(reg::Class &cls) cls.property("attributes", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, MAY_BE_ARRAY); cls.property("allowedConstants", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, MAY_BE_NULL, "PHPStan\\Reflection\\ParameterAllowedConstants"); cls.property("pureUnlessCallableIsImpureParameter", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, "PHPStan\\TrinaryLogic"); + cls.property("pureUnlessParameterPassedParameter", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, "PHPStan\\TrinaryLogic"); } /* the string and parameter tables the signatures below index into (see reg::Sig) */ @@ -61,20 +63,22 @@ inline constexpr char strings[] = "allowedConstants\0" /* 219 */ "PHPStan\\Reflection\\ParameterAllowedConstants\0" /* 236 */ "pureUnlessCallableIsImpureParameter\0" /* 281 */ - "__construct\0" /* 317 */ - "\0" /* 329 */ - "getPhpDocType\0" /* 330 */ - "hasNativeType\0" /* 344 */ - "getNativeType\0" /* 358 */ - "getOutType\0" /* 372 */ - "isImmediatelyInvokedCallable\0" /* 383 */ - "getClosureThisType\0" /* 412 */ - "getAttributes\0" /* 431 */ - "getAllowedConstants\0" /* 445 */ - "constants\0" /* 465 */ - "PHPStan\\Reflection\\AllowedConstantsResult\0" /* 475 */ - "checkAllowedConstants\0" /* 517 */ - "isPureUnlessCallableIsImpureParameter"; /* 539 */ + "pureUnlessParameterPassedParameter\0" /* 317 */ + "__construct\0" /* 352 */ + "\0" /* 364 */ + "getPhpDocType\0" /* 365 */ + "hasNativeType\0" /* 379 */ + "getNativeType\0" /* 393 */ + "getOutType\0" /* 407 */ + "isImmediatelyInvokedCallable\0" /* 418 */ + "getClosureThisType\0" /* 447 */ + "getAttributes\0" /* 466 */ + "getAllowedConstants\0" /* 480 */ + "constants\0" /* 500 */ + "PHPStan\\Reflection\\AllowedConstantsResult\0" /* 510 */ + "checkAllowedConstants\0" /* 552 */ + "isPureUnlessCallableIsImpureParameter\0" /* 574 */ + "isPureUnlessParameterPassedParameter"; /* 612 */ inline constexpr reg::PackedArg args[] = { reg::packed(0, MAY_BE_STRING), /* __construct $name */ reg::packed(5, 0, 10), /* __construct $type */ @@ -90,34 +94,37 @@ inline constexpr reg::PackedArg args[] = { reg::packed(208, MAY_BE_ARRAY), /* __construct $attributes */ reg::packed(219, MAY_BE_NULL, 236), /* __construct $allowedConstants */ reg::packed(281, 0, 171), /* __construct $pureUnlessCallableIsImpureParameter */ - reg::packed(329, 0, 10), /* getPhpDocType return */ - reg::packed(329, MAY_BE_BOOL), /* hasNativeType return */ - reg::packed(329, 0, 10), /* getNativeType return */ - reg::packed(329, MAY_BE_NULL, 10), /* getOutType return */ - reg::packed(329, 0, 171), /* isImmediatelyInvokedCallable return */ - reg::packed(329, MAY_BE_NULL, 10), /* getClosureThisType return */ - reg::packed(329, MAY_BE_ARRAY), /* getAttributes return */ - reg::packed(329, MAY_BE_NULL, 236), /* getAllowedConstants return */ - reg::packed(465, MAY_BE_ARRAY), /* checkAllowedConstants $constants */ - reg::packed(329, 0, 475), /* checkAllowedConstants return */ - reg::packed(329, 0, 171), /* isPureUnlessCallableIsImpureParameter return */ + reg::packed(317, 0, 171), /* __construct $pureUnlessParameterPassedParameter */ + reg::packed(364, 0, 10), /* getPhpDocType return */ + reg::packed(364, MAY_BE_BOOL), /* hasNativeType return */ + reg::packed(364, 0, 10), /* getNativeType return */ + reg::packed(364, MAY_BE_NULL, 10), /* getOutType return */ + reg::packed(364, 0, 171), /* isImmediatelyInvokedCallable return */ + reg::packed(364, MAY_BE_NULL, 10), /* getClosureThisType return */ + reg::packed(364, MAY_BE_ARRAY), /* getAttributes return */ + reg::packed(364, MAY_BE_NULL, 236), /* getAllowedConstants return */ + reg::packed(500, MAY_BE_ARRAY), /* checkAllowedConstants $constants */ + reg::packed(364, 0, 510), /* checkAllowedConstants return */ + reg::packed(364, 0, 171), /* isPureUnlessCallableIsImpureParameter return */ + reg::packed(364, 0, 171), /* isPureUnlessParameterPassedParameter return */ }; using Sig = reg::Sig; } // namespace sigtab /* the signatures of the methods the class declares itself (a used trait's are in the trait's header) */ namespace sig { -inline constexpr sigtab::Sig __construct = { { 317 /* __construct */, 14, 0, 14, reg::NoArg, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getPhpDocType = { { 330 /* getPhpDocType */, 0, 14, 0, 14, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig hasNativeType = { { 344 /* hasNativeType */, 0, 15, 0, 15, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getNativeType = { { 358 /* getNativeType */, 0, 16, 0, 16, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getOutType = { { 372 /* getOutType */, 0, 17, 0, 17, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig isImmediatelyInvokedCallable = { { 383 /* isImmediatelyInvokedCallable */, 0, 18, 0, 18, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getClosureThisType = { { 412 /* getClosureThisType */, 0, 19, 0, 19, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getAttributes = { { 431 /* getAttributes */, 0, 20, 0, 20, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getAllowedConstants = { { 445 /* getAllowedConstants */, 0, 21, 0, 21, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig checkAllowedConstants = { { 517 /* checkAllowedConstants */, 1, 22, 1, 23, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig isPureUnlessCallableIsImpureParameter = { { 539 /* isPureUnlessCallableIsImpureParameter */, 0, 24, 0, 24, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig __construct = { { 352 /* __construct */, 15, 0, 15, reg::NoArg, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getPhpDocType = { { 365 /* getPhpDocType */, 0, 15, 0, 15, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig hasNativeType = { { 379 /* hasNativeType */, 0, 16, 0, 16, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getNativeType = { { 393 /* getNativeType */, 0, 17, 0, 17, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getOutType = { { 407 /* getOutType */, 0, 18, 0, 18, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig isImmediatelyInvokedCallable = { { 418 /* isImmediatelyInvokedCallable */, 0, 19, 0, 19, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getClosureThisType = { { 447 /* getClosureThisType */, 0, 20, 0, 20, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getAttributes = { { 466 /* getAttributes */, 0, 21, 0, 21, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getAllowedConstants = { { 480 /* getAllowedConstants */, 0, 22, 0, 22, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig checkAllowedConstants = { { 552 /* checkAllowedConstants */, 1, 23, 1, 24, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig isPureUnlessCallableIsImpureParameter = { { 574 /* isPureUnlessCallableIsImpureParameter */, 0, 25, 0, 25, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig isPureUnlessParameterPassedParameter = { { 612 /* isPureUnlessParameterPassedParameter */, 0, 26, 0, 26, ZEND_ACC_PUBLIC } }; } // namespace sig } // namespace ptdecl::ExtendedDummyParameter diff --git a/turbo-ext/src/generated/ExtendedNativeParameterReflection.h b/turbo-ext/src/generated/ExtendedNativeParameterReflection.h index 5fe64228b87..133894afabe 100644 --- a/turbo-ext/src/generated/ExtendedNativeParameterReflection.h +++ b/turbo-ext/src/generated/ExtendedNativeParameterReflection.h @@ -24,6 +24,7 @@ inline constexpr uint32_t closureThisType = 10; inline constexpr uint32_t attributes = 11; inline constexpr uint32_t allowedConstants = 12; inline constexpr uint32_t pureUnlessCallableIsImpureParameter = 13; +inline constexpr uint32_t pureUnlessParameterPassedParameter = 14; } // namespace slot inline void declareClass(reg::Class &cls) @@ -49,6 +50,7 @@ inline void declareProperties(reg::Class &cls) cls.property("attributes", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, MAY_BE_ARRAY); cls.property("allowedConstants", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, MAY_BE_NULL, "PHPStan\\Reflection\\ParameterAllowedConstants"); cls.property("pureUnlessCallableIsImpureParameter", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, "PHPStan\\TrinaryLogic"); + cls.property("pureUnlessParameterPassedParameter", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, "PHPStan\\TrinaryLogic"); } /* the string and parameter tables the signatures below index into (see reg::Sig) */ @@ -72,25 +74,27 @@ inline constexpr char strings[] = "allowedConstants\0" /* 219 */ "PHPStan\\Reflection\\ParameterAllowedConstants\0" /* 236 */ "pureUnlessCallableIsImpureParameter\0" /* 281 */ - "__construct\0" /* 317 */ - "\0" /* 329 */ - "getName\0" /* 330 */ - "isOptional\0" /* 338 */ - "getType\0" /* 349 */ - "getPhpDocType\0" /* 357 */ - "hasNativeType\0" /* 371 */ - "getNativeType\0" /* 385 */ - "isVariadic\0" /* 399 */ - "getDefaultValue\0" /* 410 */ - "getOutType\0" /* 426 */ - "isImmediatelyInvokedCallable\0" /* 437 */ - "getClosureThisType\0" /* 466 */ - "getAttributes\0" /* 485 */ - "getAllowedConstants\0" /* 499 */ - "constants\0" /* 519 */ - "PHPStan\\Reflection\\AllowedConstantsResult\0" /* 529 */ - "checkAllowedConstants\0" /* 571 */ - "isPureUnlessCallableIsImpureParameter"; /* 593 */ + "pureUnlessParameterPassedParameter\0" /* 317 */ + "__construct\0" /* 352 */ + "\0" /* 364 */ + "getName\0" /* 365 */ + "isOptional\0" /* 373 */ + "getType\0" /* 384 */ + "getPhpDocType\0" /* 392 */ + "hasNativeType\0" /* 406 */ + "getNativeType\0" /* 420 */ + "isVariadic\0" /* 434 */ + "getDefaultValue\0" /* 445 */ + "getOutType\0" /* 461 */ + "isImmediatelyInvokedCallable\0" /* 472 */ + "getClosureThisType\0" /* 501 */ + "getAttributes\0" /* 520 */ + "getAllowedConstants\0" /* 534 */ + "constants\0" /* 554 */ + "PHPStan\\Reflection\\AllowedConstantsResult\0" /* 564 */ + "checkAllowedConstants\0" /* 606 */ + "isPureUnlessCallableIsImpureParameter\0" /* 628 */ + "isPureUnlessParameterPassedParameter"; /* 666 */ inline constexpr reg::PackedArg args[] = { reg::packed(0, MAY_BE_STRING), /* __construct $name */ reg::packed(5, MAY_BE_BOOL), /* __construct $optional */ @@ -106,46 +110,49 @@ inline constexpr reg::PackedArg args[] = { reg::packed(208, MAY_BE_ARRAY), /* __construct $attributes */ reg::packed(219, MAY_BE_NULL, 236), /* __construct $allowedConstants */ reg::packed(281, 0, 171), /* __construct $pureUnlessCallableIsImpureParameter */ - reg::packed(329, MAY_BE_STRING), /* getName return */ - reg::packed(329, MAY_BE_BOOL), /* isOptional return */ - reg::packed(329, 0, 19), /* getType return */ - reg::packed(329, 0, 19), /* getPhpDocType return */ - reg::packed(329, MAY_BE_BOOL), /* hasNativeType return */ - reg::packed(329, 0, 19), /* getNativeType return */ - reg::packed(329, 0, 77), /* passedByReference return */ - reg::packed(329, MAY_BE_BOOL), /* isVariadic return */ - reg::packed(329, MAY_BE_NULL, 19), /* getDefaultValue return */ - reg::packed(329, MAY_BE_NULL, 19), /* getOutType return */ - reg::packed(329, 0, 171), /* isImmediatelyInvokedCallable return */ - reg::packed(329, MAY_BE_NULL, 19), /* getClosureThisType return */ - reg::packed(329, MAY_BE_ARRAY), /* getAttributes return */ - reg::packed(329, MAY_BE_NULL, 236), /* getAllowedConstants return */ - reg::packed(519, MAY_BE_ARRAY), /* checkAllowedConstants $constants */ - reg::packed(329, 0, 529), /* checkAllowedConstants return */ - reg::packed(329, 0, 171), /* isPureUnlessCallableIsImpureParameter return */ + reg::packed(317, 0, 171), /* __construct $pureUnlessParameterPassedParameter */ + reg::packed(364, MAY_BE_STRING), /* getName return */ + reg::packed(364, MAY_BE_BOOL), /* isOptional return */ + reg::packed(364, 0, 19), /* getType return */ + reg::packed(364, 0, 19), /* getPhpDocType return */ + reg::packed(364, MAY_BE_BOOL), /* hasNativeType return */ + reg::packed(364, 0, 19), /* getNativeType return */ + reg::packed(364, 0, 77), /* passedByReference return */ + reg::packed(364, MAY_BE_BOOL), /* isVariadic return */ + reg::packed(364, MAY_BE_NULL, 19), /* getDefaultValue return */ + reg::packed(364, MAY_BE_NULL, 19), /* getOutType return */ + reg::packed(364, 0, 171), /* isImmediatelyInvokedCallable return */ + reg::packed(364, MAY_BE_NULL, 19), /* getClosureThisType return */ + reg::packed(364, MAY_BE_ARRAY), /* getAttributes return */ + reg::packed(364, MAY_BE_NULL, 236), /* getAllowedConstants return */ + reg::packed(554, MAY_BE_ARRAY), /* checkAllowedConstants $constants */ + reg::packed(364, 0, 564), /* checkAllowedConstants return */ + reg::packed(364, 0, 171), /* isPureUnlessCallableIsImpureParameter return */ + reg::packed(364, 0, 171), /* isPureUnlessParameterPassedParameter return */ }; using Sig = reg::Sig; } // namespace sigtab /* the signatures of the methods the class declares itself (a used trait's are in the trait's header) */ namespace sig { -inline constexpr sigtab::Sig __construct = { { 317 /* __construct */, 14, 0, 14, reg::NoArg, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getName = { { 330 /* getName */, 0, 14, 0, 14, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig isOptional = { { 338 /* isOptional */, 0, 15, 0, 15, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getType = { { 349 /* getType */, 0, 16, 0, 16, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getPhpDocType = { { 357 /* getPhpDocType */, 0, 17, 0, 17, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig hasNativeType = { { 371 /* hasNativeType */, 0, 18, 0, 18, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getNativeType = { { 385 /* getNativeType */, 0, 19, 0, 19, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig passedByReference = { { 59 /* passedByReference */, 0, 20, 0, 20, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig isVariadic = { { 399 /* isVariadic */, 0, 21, 0, 21, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getDefaultValue = { { 410 /* getDefaultValue */, 0, 22, 0, 22, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getOutType = { { 426 /* getOutType */, 0, 23, 0, 23, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig isImmediatelyInvokedCallable = { { 437 /* isImmediatelyInvokedCallable */, 0, 24, 0, 24, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getClosureThisType = { { 466 /* getClosureThisType */, 0, 25, 0, 25, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getAttributes = { { 485 /* getAttributes */, 0, 26, 0, 26, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getAllowedConstants = { { 499 /* getAllowedConstants */, 0, 27, 0, 27, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig checkAllowedConstants = { { 571 /* checkAllowedConstants */, 1, 28, 1, 29, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig isPureUnlessCallableIsImpureParameter = { { 593 /* isPureUnlessCallableIsImpureParameter */, 0, 30, 0, 30, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig __construct = { { 352 /* __construct */, 15, 0, 15, reg::NoArg, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getName = { { 365 /* getName */, 0, 15, 0, 15, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig isOptional = { { 373 /* isOptional */, 0, 16, 0, 16, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getType = { { 384 /* getType */, 0, 17, 0, 17, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getPhpDocType = { { 392 /* getPhpDocType */, 0, 18, 0, 18, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig hasNativeType = { { 406 /* hasNativeType */, 0, 19, 0, 19, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getNativeType = { { 420 /* getNativeType */, 0, 20, 0, 20, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig passedByReference = { { 59 /* passedByReference */, 0, 21, 0, 21, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig isVariadic = { { 434 /* isVariadic */, 0, 22, 0, 22, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getDefaultValue = { { 445 /* getDefaultValue */, 0, 23, 0, 23, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getOutType = { { 461 /* getOutType */, 0, 24, 0, 24, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig isImmediatelyInvokedCallable = { { 472 /* isImmediatelyInvokedCallable */, 0, 25, 0, 25, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getClosureThisType = { { 501 /* getClosureThisType */, 0, 26, 0, 26, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getAttributes = { { 520 /* getAttributes */, 0, 27, 0, 27, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getAllowedConstants = { { 534 /* getAllowedConstants */, 0, 28, 0, 28, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig checkAllowedConstants = { { 606 /* checkAllowedConstants */, 1, 29, 1, 30, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig isPureUnlessCallableIsImpureParameter = { { 628 /* isPureUnlessCallableIsImpureParameter */, 0, 31, 0, 31, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig isPureUnlessParameterPassedParameter = { { 666 /* isPureUnlessParameterPassedParameter */, 0, 32, 0, 32, ZEND_ACC_PUBLIC } }; } // namespace sig } // namespace ptdecl::ExtendedNativeParameterReflection diff --git a/turbo-ext/src/generated/PhpParameterReflection.h b/turbo-ext/src/generated/PhpParameterReflection.h index d6d325cc792..cc0e4389a55 100644 --- a/turbo-ext/src/generated/PhpParameterReflection.h +++ b/turbo-ext/src/generated/PhpParameterReflection.h @@ -22,6 +22,7 @@ inline constexpr uint32_t closureThisType = 8; inline constexpr uint32_t attributes = 9; inline constexpr uint32_t allowedConstants = 10; inline constexpr uint32_t pureUnlessCallableIsImpureParameter = 11; +inline constexpr uint32_t pureUnlessParameterPassedParameter = 12; } // namespace slot inline void declareClass(reg::Class &cls) @@ -45,6 +46,7 @@ inline void declareProperties(reg::Class &cls) cls.property("attributes", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, MAY_BE_ARRAY); cls.property("allowedConstants", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, MAY_BE_NULL, "PHPStan\\Reflection\\ParameterAllowedConstants"); cls.property("pureUnlessCallableIsImpureParameter", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, "PHPStan\\TrinaryLogic"); + cls.property("pureUnlessParameterPassedParameter", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, "PHPStan\\TrinaryLogic"); } /* the string and parameter tables the signatures below index into (see reg::Sig) */ @@ -66,27 +68,29 @@ inline constexpr char strings[] = "allowedConstants\0" /* 312 */ "PHPStan\\Reflection\\ParameterAllowedConstants\0" /* 329 */ "pureUnlessCallableIsImpureParameter\0" /* 374 */ - "__construct\0" /* 410 */ - "\0" /* 422 */ - "isOptional\0" /* 423 */ - "getName\0" /* 434 */ - "getType\0" /* 442 */ - "PHPStan\\Reflection\\PassedByReference\0" /* 450 */ - "passedByReference\0" /* 487 */ - "isVariadic\0" /* 505 */ - "getPhpDocType\0" /* 516 */ - "hasNativeType\0" /* 530 */ - "getNativeType\0" /* 544 */ - "getDefaultValue\0" /* 558 */ - "getOutType\0" /* 574 */ - "isImmediatelyInvokedCallable\0" /* 585 */ - "getClosureThisType\0" /* 614 */ - "getAttributes\0" /* 633 */ - "getAllowedConstants\0" /* 647 */ - "constants\0" /* 667 */ - "PHPStan\\Reflection\\AllowedConstantsResult\0" /* 677 */ - "checkAllowedConstants\0" /* 719 */ - "isPureUnlessCallableIsImpureParameter"; /* 741 */ + "pureUnlessParameterPassedParameter\0" /* 410 */ + "__construct\0" /* 445 */ + "\0" /* 457 */ + "isOptional\0" /* 458 */ + "getName\0" /* 469 */ + "getType\0" /* 477 */ + "PHPStan\\Reflection\\PassedByReference\0" /* 485 */ + "passedByReference\0" /* 522 */ + "isVariadic\0" /* 540 */ + "getPhpDocType\0" /* 551 */ + "hasNativeType\0" /* 565 */ + "getNativeType\0" /* 579 */ + "getDefaultValue\0" /* 593 */ + "getOutType\0" /* 609 */ + "isImmediatelyInvokedCallable\0" /* 620 */ + "getClosureThisType\0" /* 649 */ + "getAttributes\0" /* 668 */ + "getAllowedConstants\0" /* 682 */ + "constants\0" /* 702 */ + "PHPStan\\Reflection\\AllowedConstantsResult\0" /* 712 */ + "checkAllowedConstants\0" /* 754 */ + "isPureUnlessCallableIsImpureParameter\0" /* 776 */ + "isPureUnlessParameterPassedParameter"; /* 814 */ inline constexpr reg::PackedArg args[] = { reg::packed(0, 0, 28), /* __construct $initializerExprTypeResolver */ reg::packed(75, 0, 86), /* __construct $reflection */ @@ -98,46 +102,49 @@ inline constexpr reg::PackedArg args[] = { reg::packed(301, MAY_BE_ARRAY), /* __construct $attributes */ reg::packed(312, MAY_BE_NULL, 329), /* __construct $allowedConstants */ reg::packed(374, 0, 264), /* __construct $pureUnlessCallableIsImpureParameter */ - reg::packed(422, MAY_BE_BOOL), /* isOptional return */ - reg::packed(422, MAY_BE_STRING), /* getName return */ - reg::packed(422, 0, 161), /* getType return */ - reg::packed(422, 0, 450), /* passedByReference return */ - reg::packed(422, MAY_BE_BOOL), /* isVariadic return */ - reg::packed(422, 0, 161), /* getPhpDocType return */ - reg::packed(422, MAY_BE_BOOL), /* hasNativeType return */ - reg::packed(422, 0, 161), /* getNativeType return */ - reg::packed(422, MAY_BE_NULL, 161), /* getDefaultValue return */ - reg::packed(422, MAY_BE_NULL, 161), /* getOutType return */ - reg::packed(422, 0, 264), /* isImmediatelyInvokedCallable return */ - reg::packed(422, MAY_BE_NULL, 161), /* getClosureThisType return */ - reg::packed(422, MAY_BE_ARRAY), /* getAttributes return */ - reg::packed(422, MAY_BE_NULL, 329), /* getAllowedConstants return */ - reg::packed(667, MAY_BE_ARRAY), /* checkAllowedConstants $constants */ - reg::packed(422, 0, 677), /* checkAllowedConstants return */ - reg::packed(422, 0, 264), /* isPureUnlessCallableIsImpureParameter return */ + reg::packed(410, 0, 264), /* __construct $pureUnlessParameterPassedParameter */ + reg::packed(457, MAY_BE_BOOL), /* isOptional return */ + reg::packed(457, MAY_BE_STRING), /* getName return */ + reg::packed(457, 0, 161), /* getType return */ + reg::packed(457, 0, 485), /* passedByReference return */ + reg::packed(457, MAY_BE_BOOL), /* isVariadic return */ + reg::packed(457, 0, 161), /* getPhpDocType return */ + reg::packed(457, MAY_BE_BOOL), /* hasNativeType return */ + reg::packed(457, 0, 161), /* getNativeType return */ + reg::packed(457, MAY_BE_NULL, 161), /* getDefaultValue return */ + reg::packed(457, MAY_BE_NULL, 161), /* getOutType return */ + reg::packed(457, 0, 264), /* isImmediatelyInvokedCallable return */ + reg::packed(457, MAY_BE_NULL, 161), /* getClosureThisType return */ + reg::packed(457, MAY_BE_ARRAY), /* getAttributes return */ + reg::packed(457, MAY_BE_NULL, 329), /* getAllowedConstants return */ + reg::packed(702, MAY_BE_ARRAY), /* checkAllowedConstants $constants */ + reg::packed(457, 0, 712), /* checkAllowedConstants return */ + reg::packed(457, 0, 264), /* isPureUnlessCallableIsImpureParameter return */ + reg::packed(457, 0, 264), /* isPureUnlessParameterPassedParameter return */ }; using Sig = reg::Sig; } // namespace sigtab /* the signatures of the methods the class declares itself (a used trait's are in the trait's header) */ namespace sig { -inline constexpr sigtab::Sig __construct = { { 410 /* __construct */, 10, 0, 10, reg::NoArg, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig isOptional = { { 423 /* isOptional */, 0, 10, 0, 10, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getName = { { 434 /* getName */, 0, 11, 0, 11, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getType = { { 442 /* getType */, 0, 12, 0, 12, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig passedByReference = { { 487 /* passedByReference */, 0, 13, 0, 13, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig isVariadic = { { 505 /* isVariadic */, 0, 14, 0, 14, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getPhpDocType = { { 516 /* getPhpDocType */, 0, 15, 0, 15, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig hasNativeType = { { 530 /* hasNativeType */, 0, 16, 0, 16, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getNativeType = { { 544 /* getNativeType */, 0, 17, 0, 17, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getDefaultValue = { { 558 /* getDefaultValue */, 0, 18, 0, 18, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getOutType = { { 574 /* getOutType */, 0, 19, 0, 19, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig isImmediatelyInvokedCallable = { { 585 /* isImmediatelyInvokedCallable */, 0, 20, 0, 20, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getClosureThisType = { { 614 /* getClosureThisType */, 0, 21, 0, 21, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getAttributes = { { 633 /* getAttributes */, 0, 22, 0, 22, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getAllowedConstants = { { 647 /* getAllowedConstants */, 0, 23, 0, 23, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig checkAllowedConstants = { { 719 /* checkAllowedConstants */, 1, 24, 1, 25, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig isPureUnlessCallableIsImpureParameter = { { 741 /* isPureUnlessCallableIsImpureParameter */, 0, 26, 0, 26, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig __construct = { { 445 /* __construct */, 11, 0, 11, reg::NoArg, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig isOptional = { { 458 /* isOptional */, 0, 11, 0, 11, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getName = { { 469 /* getName */, 0, 12, 0, 12, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getType = { { 477 /* getType */, 0, 13, 0, 13, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig passedByReference = { { 522 /* passedByReference */, 0, 14, 0, 14, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig isVariadic = { { 540 /* isVariadic */, 0, 15, 0, 15, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getPhpDocType = { { 551 /* getPhpDocType */, 0, 16, 0, 16, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig hasNativeType = { { 565 /* hasNativeType */, 0, 17, 0, 17, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getNativeType = { { 579 /* getNativeType */, 0, 18, 0, 18, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getDefaultValue = { { 593 /* getDefaultValue */, 0, 19, 0, 19, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getOutType = { { 609 /* getOutType */, 0, 20, 0, 20, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig isImmediatelyInvokedCallable = { { 620 /* isImmediatelyInvokedCallable */, 0, 21, 0, 21, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getClosureThisType = { { 649 /* getClosureThisType */, 0, 22, 0, 22, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getAttributes = { { 668 /* getAttributes */, 0, 23, 0, 23, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getAllowedConstants = { { 682 /* getAllowedConstants */, 0, 24, 0, 24, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig checkAllowedConstants = { { 754 /* checkAllowedConstants */, 1, 25, 1, 26, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig isPureUnlessCallableIsImpureParameter = { { 776 /* isPureUnlessCallableIsImpureParameter */, 0, 27, 0, 27, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig isPureUnlessParameterPassedParameter = { { 814 /* isPureUnlessParameterPassedParameter */, 0, 28, 0, 28, ZEND_ACC_PUBLIC } }; } // namespace sig } // namespace ptdecl::PhpParameterReflection diff --git a/turbo-ext/src/support.h b/turbo-ext/src/support.h index a239becd051..ecb5ffecd60 100644 --- a/turbo-ext/src/support.h +++ b/turbo-ext/src/support.h @@ -3735,6 +3735,7 @@ enum pt_parameter_reflection_member PT_PR_GET_ATTRIBUTES, PT_PR_GET_ALLOWED_CONSTANTS, PT_PR_IS_PURE_UNLESS_CALLABLE_IS_IMPURE_PARAMETER, + PT_PR_IS_PURE_UNLESS_PARAMETER_PASSED_PARAMETER, PT_PR_MEMBER_COUNT }; /* $parameter->() of any parameter reflection (borrowed): callers @@ -4310,6 +4311,7 @@ enum pt_resolved_php_doc_member PT_RPD_GET_PARAM_OUT_TAGS, PT_RPD_GET_PARAMS_IMMEDIATELY_INVOKED_CALLABLE, PT_RPD_GET_PARAMS_PURE_UNLESS_CALLABLE_IS_IMPURE, + PT_RPD_GET_PARAMS_PURE_UNLESS_PARAMETER_PASSED, PT_RPD_GET_PARAM_CLOSURE_THIS_TAGS, PT_RPD_GET_RETURN_TAG, PT_RPD_GET_THROWS_TAG, diff --git a/turbo-ext/tests/php-class-reflection-family.php b/turbo-ext/tests/php-class-reflection-family.php index 1f51c2b61dd..a52a06a04c9 100644 --- a/turbo-ext/tests/php-class-reflection-family.php +++ b/turbo-ext/tests/php-class-reflection-family.php @@ -249,6 +249,7 @@ public function describeParameter(\PHPStan\Reflection\ExtendedParameterReflectio 'attributes' => $this->describe($parameter->getAttributes(), $depth + 1), 'allowedConstants' => $parameter->getAllowedConstants() === null ? null : 'allowedConstants', 'pureUnlessCallableIsImpure' => $parameter->isPureUnlessCallableIsImpureParameter()->describe(), + 'pureUnlessParameterPassed' => $parameter->isPureUnlessParameterPassedParameter()->describe(), ]; } diff --git a/turbo-ext/tests/type-family.php b/turbo-ext/tests/type-family.php index 23bd678eee8..142258f5c84 100644 --- a/turbo-ext/tests/type-family.php +++ b/turbo-ext/tests/type-family.php @@ -6439,7 +6439,7 @@ protected function getResult(): \PHPStan\Type\Type $byRef = $p->passedByReference(); $r = [get_class($p), $p->getName(), $p->isOptional(), $view($p->getType()), [$byRef->no(), $byRef->yes(), $byRef->createsNewVariable()], $p->isVariadic(), $view($p->getDefaultValue())]; if ($p instanceof \PHPStan\Reflection\ExtendedParameterReflection) { - $r[] = [$view($p->getNativeType()), $view($p->getPhpDocType()), $view($p->getOutType()), $view($p->isImmediatelyInvokedCallable()), $view($p->getClosureThisType()), count($p->getAttributes()), $p->getAllowedConstants() === null ? null : get_class($p->getAllowedConstants()), $view($p->isPureUnlessCallableIsImpureParameter()), $p->hasNativeType()]; + $r[] = [$view($p->getNativeType()), $view($p->getPhpDocType()), $view($p->getOutType()), $view($p->isImmediatelyInvokedCallable()), $view($p->getClosureThisType()), count($p->getAttributes()), $p->getAllowedConstants() === null ? null : get_class($p->getAllowedConstants()), $view($p->isPureUnlessCallableIsImpureParameter()), $view($p->isPureUnlessParameterPassedParameter()), $p->hasNativeType()]; } return $r; }; @@ -7009,9 +7009,9 @@ public function getResolvedPhpDoc(): ?\PHPStan\PhpDoc\ResolvedPhpDocBlock { retu 'plain' => new \PHPStan\Reflection\Php\DummyParameter('a', $int, false, null, false, null), 'byRef' => new \PHPStan\Reflection\Php\DummyParameter('b', $string, true, $modes['creates'], true, new \PHPStan\Type\Constant\ConstantStringType('x')), 'named' => new \PHPStan\Reflection\Php\DummyParameter(defaultValue: null, variadic: false, passedByReference: null, optional: true, type: new \PHPStan\Type\MixedType(), name: 'n'), - 'extended' => new \PHPStan\Reflection\Php\ExtendedDummyParameter('e', $int, false, $modes['reads'], false, null, new \PHPStan\Type\MixedType(), $int, $string, \PHPStan\TrinaryLogic::createYes(), new \PHPStan\Type\ObjectType(\stdClass::class), [], null, \PHPStan\TrinaryLogic::createMaybe()), - 'extendedExplicitMixed' => new \PHPStan\Reflection\Php\ExtendedDummyParameter('x', $int, true, null, true, $int, new \PHPStan\Type\MixedType(true), $int, null, \PHPStan\TrinaryLogic::createNo(), null, [], $allowed, \PHPStan\TrinaryLogic::createNo()), - 'extendedNative' => new \PHPStan\Reflection\Php\ExtendedDummyParameter(pureUnlessCallableIsImpureParameter: \PHPStan\TrinaryLogic::createYes(), allowedConstants: null, attributes: [], closureThisType: null, immediatelyInvokedCallable: \PHPStan\TrinaryLogic::createMaybe(), outType: null, phpDocType: $string, nativeType: $string, defaultValue: null, variadic: false, passedByReference: $modes['no'], optional: false, type: $string, name: 's'), + 'extended' => new \PHPStan\Reflection\Php\ExtendedDummyParameter('e', $int, false, $modes['reads'], false, null, new \PHPStan\Type\MixedType(), $int, $string, \PHPStan\TrinaryLogic::createYes(), new \PHPStan\Type\ObjectType(\stdClass::class), [], null, \PHPStan\TrinaryLogic::createMaybe(), \PHPStan\TrinaryLogic::createYes()), + 'extendedExplicitMixed' => new \PHPStan\Reflection\Php\ExtendedDummyParameter('x', $int, true, null, true, $int, new \PHPStan\Type\MixedType(true), $int, null, \PHPStan\TrinaryLogic::createNo(), null, [], $allowed, \PHPStan\TrinaryLogic::createNo(), \PHPStan\TrinaryLogic::createMaybe()), + 'extendedNative' => new \PHPStan\Reflection\Php\ExtendedDummyParameter(pureUnlessParameterPassedParameter: \PHPStan\TrinaryLogic::createNo(), pureUnlessCallableIsImpureParameter: \PHPStan\TrinaryLogic::createYes(), allowedConstants: null, attributes: [], closureThisType: null, immediatelyInvokedCallable: \PHPStan\TrinaryLogic::createMaybe(), outType: null, phpDocType: $string, nativeType: $string, defaultValue: null, variadic: false, passedByReference: $modes['no'], optional: false, type: $string, name: 's'), 'subclass' => new \PHPStanTurboTests\DummyParameterSubclass('sub', $int, false, null, false, null), ]; foreach ($dummies as $name => $dummy) { @@ -7022,14 +7022,14 @@ public function getResolvedPhpDoc(): ?\PHPStan\PhpDoc\ResolvedPhpDocBlock { retu $result = $dummies[$name]->checkAllowedConstants([]); $r["dummy $name checkAllowedConstants"] = [get_class($result), $result->isOk(), $result->isBitmaskNotAllowed(), $result->getDisallowedConstants(), $result->getViolatedExclusiveGroups()]; } - foreach ([\PHPStan\Reflection\Php\DummyParameter::class => ['getName', 'isOptional', 'getType', 'passedByReference', 'isVariadic', 'getDefaultValue'], \PHPStan\Reflection\Php\ExtendedDummyParameter::class => ['getName', 'getPhpDocType', 'hasNativeType', 'getNativeType', 'getOutType', 'isImmediatelyInvokedCallable', 'getClosureThisType', 'getAttributes', 'getAllowedConstants', 'isPureUnlessCallableIsImpureParameter']] as $class => $methods) { + foreach ([\PHPStan\Reflection\Php\DummyParameter::class => ['getName', 'isOptional', 'getType', 'passedByReference', 'isVariadic', 'getDefaultValue'], \PHPStan\Reflection\Php\ExtendedDummyParameter::class => ['getName', 'getPhpDocType', 'hasNativeType', 'getNativeType', 'getOutType', 'isImmediatelyInvokedCallable', 'getClosureThisType', 'getAttributes', 'getAllowedConstants', 'isPureUnlessCallableIsImpureParameter', 'isPureUnlessParameterPassedParameter']] as $class => $methods) { $raw = (new \ReflectionClass($class))->newInstanceWithoutConstructor(); foreach ($methods as $method) { $r["unconstructed $class $method"] = $error(static fn () => $raw->$method()); } } $r['dummy wrong name'] = $error(static fn () => new \PHPStan\Reflection\Php\DummyParameter([], $int, false, null, false, null)); - $r['extended wrong attributes'] = $error(static fn () => new \PHPStan\Reflection\Php\ExtendedDummyParameter('e', $int, false, null, false, null, $int, $int, null, \PHPStan\TrinaryLogic::createYes(), null, 'x', null, \PHPStan\TrinaryLogic::createNo())); + $r['extended wrong attributes'] = $error(static fn () => new \PHPStan\Reflection\Php\ExtendedDummyParameter('e', $int, false, null, false, null, $int, $int, null, \PHPStan\TrinaryLogic::createYes(), null, 'x', null, \PHPStan\TrinaryLogic::createNo(), \PHPStan\TrinaryLogic::createNo())); foreach ($r as $key => $value) { $observations["parameter values $key"] = $value; } @@ -7155,7 +7155,7 @@ public function getResolvedPhpDoc(): ?\PHPStan\PhpDoc\ResolvedPhpDocBlock { retu $template = \PHPStan\Type\Generic\TemplateTypeFactory::create(\PHPStan\Type\Generic\TemplateTypeScope::createWithFunction('foo'), 'T', null, \PHPStan\Type\Generic\TemplateTypeVariance::createInvariant()); $emptyMap = \PHPStan\Type\Generic\TemplateTypeMap::createEmpty(); $param = static fn (string $name, \PHPStan\Type\Type $type, bool $optional = false, bool $variadic = false, ?\PHPStan\Type\Type $default = null, ?\PHPStan\Reflection\PassedByReference $byRef = null): \PHPStan\Reflection\Php\DummyParameter => new \PHPStan\Reflection\Php\DummyParameter($name, $type, $optional, $byRef, $variadic, $default); - $extParam = static fn (string $name, \PHPStan\Type\Type $type, bool $optional = false, bool $variadic = false, ?\PHPStan\Type\Type $out = null, ?\PHPStan\Type\Type $closureThis = null, ?\PHPStan\Reflection\ParameterAllowedConstants $allowed = null, ?\PHPStan\TrinaryLogic $immediately = null, ?\PHPStan\TrinaryLogic $pure = null): \PHPStan\Reflection\Php\ExtendedDummyParameter => new \PHPStan\Reflection\Php\ExtendedDummyParameter($name, $type, $optional, \PHPStan\Reflection\PassedByReference::createReadsArgument(), $variadic, $optional ? $type : null, $type, $type, $out, $immediately ?? \PHPStan\TrinaryLogic::createMaybe(), $closureThis, [], $allowed, $pure ?? \PHPStan\TrinaryLogic::createNo()); + $extParam = static fn (string $name, \PHPStan\Type\Type $type, bool $optional = false, bool $variadic = false, ?\PHPStan\Type\Type $out = null, ?\PHPStan\Type\Type $closureThis = null, ?\PHPStan\Reflection\ParameterAllowedConstants $allowed = null, ?\PHPStan\TrinaryLogic $immediately = null, ?\PHPStan\TrinaryLogic $pure = null, ?\PHPStan\TrinaryLogic $passed = null): \PHPStan\Reflection\Php\ExtendedDummyParameter => new \PHPStan\Reflection\Php\ExtendedDummyParameter($name, $type, $optional, \PHPStan\Reflection\PassedByReference::createReadsArgument(), $variadic, $optional ? $type : null, $type, $type, $out, $immediately ?? \PHPStan\TrinaryLogic::createMaybe(), $closureThis, [], $allowed, $pure ?? \PHPStan\TrinaryLogic::createNo(), $passed ?? \PHPStan\TrinaryLogic::createNo()); $variant = static fn (array $parameters, bool $variadic = false, ?\PHPStan\Type\Type $return = null): \PHPStan\Reflection\FunctionVariant => new \PHPStan\Reflection\FunctionVariant($emptyMap, null, $parameters, $variadic, $return ?? $int); $extVariant = static fn (array $parameters, bool $variadic = false, ?\PHPStan\Type\Type $return = null): \PHPStan\Reflection\ExtendedFunctionVariant => new \PHPStan\Reflection\ExtendedFunctionVariant($emptyMap, null, $parameters, $variadic, $return ?? $int, $return ?? $int, $mixed); $allowedA = new \PHPStan\Reflection\ParameterAllowedConstants('list', [], []); @@ -7167,6 +7167,7 @@ public function getResolvedPhpDoc(): ?\PHPStan\PhpDoc\ResolvedPhpDocBlock { retu 'mixedParams' => [$variant([$param('a', $mixed)]), $variant([$param('b', $mixed)])], 'extended' => [$extVariant([$extParam('a', $int, false, false, $int, $int, $allowedA, \PHPStan\TrinaryLogic::createYes())]), $extVariant([$extParam('b', $string, true, false, $string, null, $allowedA), $extParam('c', $int, true, true)], true)], 'extendedAllowed' => [$extVariant([$extParam('a', $int, false, false, null, null, $allowedA)]), $extVariant([$extParam('a', $int, false, false, null, null, $allowedB, null, \PHPStan\TrinaryLogic::createYes())])], + 'extendedPassed' => [$extVariant([$extParam('a', $int, true, false, null, null, null, null, null, \PHPStan\TrinaryLogic::createYes())]), $extVariant([$extParam('a', $int, true)])], 'mixedKinds' => [$variant([$param('x', $int, false, false, $int, \PHPStan\Reflection\PassedByReference::createCreatesNewVariable())]), $extVariant([$extParam('y', $string, true)])], 'template' => [$variant([$param('a', $template)], false, $template)], 'closure' => [new \PHPStan\Type\ClosureType([$param('a', $int)], $int, false), new \PHPStan\Type\ClosureType([$param('a', $string), $param('b', $int)], $string, false)], @@ -7284,17 +7285,17 @@ public function getResolvedPhpDoc(): ?\PHPStan\PhpDoc\ResolvedPhpDocBlock { retu foreach ($adapterParameters as $k => $adapterParameter) { foreach (['none' => null, 'string' => $string, 'nullable' => new \PHPStan\Type\UnionType([$string, new \PHPStan\Type\NullType()])] as $phpDocName => $phpDocType) { foreach (['noClass' => null, 'class' => $signatureFixture] as $className => $declaringClass) { - $constructed = new \PHPStan\Reflection\Php\PhpParameterReflection($initializerExprTypeResolver, $adapterParameter, $phpDocType, $declaringClass, $phpDocName === 'string' ? $int : null, \PHPStan\TrinaryLogic::createMaybe(), $phpDocName === 'nullable' ? new \PHPStan\Type\ObjectType(\stdClass::class) : null, [], $phpDocName === 'string' ? new \PHPStan\Reflection\ParameterAllowedConstants('list', [], []) : null, \PHPStan\TrinaryLogic::createNo()); + $constructed = new \PHPStan\Reflection\Php\PhpParameterReflection($initializerExprTypeResolver, $adapterParameter, $phpDocType, $declaringClass, $phpDocName === 'string' ? $int : null, \PHPStan\TrinaryLogic::createMaybe(), $phpDocName === 'nullable' ? new \PHPStan\Type\ObjectType(\stdClass::class) : null, [], $phpDocName === 'string' ? new \PHPStan\Reflection\ParameterAllowedConstants('list', [], []) : null, \PHPStan\TrinaryLogic::createNo(), $phpDocName === 'string' ? \PHPStan\TrinaryLogic::createYes() : \PHPStan\TrinaryLogic::createNo()); $r["constructed $k $phpDocName $className"] = $catching(static fn () => $viewFullParameter($constructed)); } } } - $named = new \PHPStan\Reflection\Php\PhpParameterReflection(pureUnlessCallableIsImpureParameter: \PHPStan\TrinaryLogic::createYes(), allowedConstants: null, attributes: [], closureThisType: null, immediatelyInvokedCallable: \PHPStan\TrinaryLogic::createYes(), outType: null, declaringClass: null, phpDocType: $int, reflection: $adapterParameters[2], initializerExprTypeResolver: $initializerExprTypeResolver); + $named = new \PHPStan\Reflection\Php\PhpParameterReflection(pureUnlessParameterPassedParameter: \PHPStan\TrinaryLogic::createMaybe(), pureUnlessCallableIsImpureParameter: \PHPStan\TrinaryLogic::createYes(), allowedConstants: null, attributes: [], closureThisType: null, immediatelyInvokedCallable: \PHPStan\TrinaryLogic::createYes(), outType: null, declaringClass: null, phpDocType: $int, reflection: $adapterParameters[2], initializerExprTypeResolver: $initializerExprTypeResolver); $r['constructed named'] = $catching(static fn () => $viewFullParameter($named)); // the order of the memoized reads: getNativeType() before getType() - $nativeFirst = new \PHPStan\Reflection\Php\PhpParameterReflection($initializerExprTypeResolver, $adapterParameters[1], $string, $signatureFixture, null, \PHPStan\TrinaryLogic::createNo(), null, [], null, \PHPStan\TrinaryLogic::createNo()); + $nativeFirst = new \PHPStan\Reflection\Php\PhpParameterReflection($initializerExprTypeResolver, $adapterParameters[1], $string, $signatureFixture, null, \PHPStan\TrinaryLogic::createNo(), null, [], null, \PHPStan\TrinaryLogic::createNo(), \PHPStan\TrinaryLogic::createYes()); $r['native first'] = [$view($nativeFirst->getNativeType()), $view($nativeFirst->getType()), $nativeFirst->getNativeType() === $nativeFirst->getNativeType()]; - $r['constructor wrong attributes'] = $catching(static fn () => new \PHPStan\Reflection\Php\PhpParameterReflection($initializerExprTypeResolver, $adapterParameters[0], null, null, null, \PHPStan\TrinaryLogic::createNo(), null, 'x', null, \PHPStan\TrinaryLogic::createNo())); + $r['constructor wrong attributes'] = $catching(static fn () => new \PHPStan\Reflection\Php\PhpParameterReflection($initializerExprTypeResolver, $adapterParameters[0], null, null, null, \PHPStan\TrinaryLogic::createNo(), null, 'x', null, \PHPStan\TrinaryLogic::createNo(), \PHPStan\TrinaryLogic::createNo())); // the built-in functions' parameters foreach (['array_map', 'str_replace', 'preg_match', 'sprintf', 'json_decode', 'array_filter', 'usort', 'strlen', 'htmlspecialchars', 'array_walk'] as $functionName) { @@ -7307,16 +7308,16 @@ public function getResolvedPhpDoc(): ?\PHPStan\PhpDoc\ResolvedPhpDocBlock { retu } $allowed = new \PHPStan\Reflection\ParameterAllowedConstants('bitmask', [], []); $nativeParameters = [ - 'plain' => new \PHPStan\Reflection\Native\ExtendedNativeParameterReflection('a', false, $int, $int, $int, $modes['no'], false, null, null, \PHPStan\TrinaryLogic::createNo(), null, [], null, \PHPStan\TrinaryLogic::createNo()), - 'implicitMixed' => new \PHPStan\Reflection\Native\ExtendedNativeParameterReflection('b', true, new \PHPStan\Type\MixedType(), new \PHPStan\Type\MixedType(), new \PHPStan\Type\MixedType(), $modes['creates'], true, new \PHPStan\Type\Constant\ConstantIntegerType(1), $string, \PHPStan\TrinaryLogic::createYes(), new \PHPStan\Type\ObjectType(\stdClass::class), [], $allowed, \PHPStan\TrinaryLogic::createMaybe()), - 'explicitMixed' => new \PHPStan\Reflection\Native\ExtendedNativeParameterReflection('c', false, $string, $string, new \PHPStan\Type\MixedType(true), $modes['reads'], false, null, null, \PHPStan\TrinaryLogic::createMaybe(), null, [], null, \PHPStan\TrinaryLogic::createYes()), - 'named' => new \PHPStan\Reflection\Native\ExtendedNativeParameterReflection(pureUnlessCallableIsImpureParameter: \PHPStan\TrinaryLogic::createNo(), allowedConstants: $allowed, attributes: [], closureThisType: null, immediatelyInvokedCallable: \PHPStan\TrinaryLogic::createNo(), outType: $int, defaultValue: null, variadic: false, passedByReference: $modes['no'], nativeType: $string, phpDocType: $int, type: $int, optional: true, name: 'n'), + 'plain' => new \PHPStan\Reflection\Native\ExtendedNativeParameterReflection('a', false, $int, $int, $int, $modes['no'], false, null, null, \PHPStan\TrinaryLogic::createNo(), null, [], null, \PHPStan\TrinaryLogic::createNo(), \PHPStan\TrinaryLogic::createYes()), + 'implicitMixed' => new \PHPStan\Reflection\Native\ExtendedNativeParameterReflection('b', true, new \PHPStan\Type\MixedType(), new \PHPStan\Type\MixedType(), new \PHPStan\Type\MixedType(), $modes['creates'], true, new \PHPStan\Type\Constant\ConstantIntegerType(1), $string, \PHPStan\TrinaryLogic::createYes(), new \PHPStan\Type\ObjectType(\stdClass::class), [], $allowed, \PHPStan\TrinaryLogic::createMaybe(), \PHPStan\TrinaryLogic::createNo()), + 'explicitMixed' => new \PHPStan\Reflection\Native\ExtendedNativeParameterReflection('c', false, $string, $string, new \PHPStan\Type\MixedType(true), $modes['reads'], false, null, null, \PHPStan\TrinaryLogic::createMaybe(), null, [], null, \PHPStan\TrinaryLogic::createYes(), \PHPStan\TrinaryLogic::createMaybe()), + 'named' => new \PHPStan\Reflection\Native\ExtendedNativeParameterReflection(pureUnlessParameterPassedParameter: \PHPStan\TrinaryLogic::createYes(), pureUnlessCallableIsImpureParameter: \PHPStan\TrinaryLogic::createNo(), allowedConstants: $allowed, attributes: [], closureThisType: null, immediatelyInvokedCallable: \PHPStan\TrinaryLogic::createNo(), outType: $int, defaultValue: null, variadic: false, passedByReference: $modes['no'], nativeType: $string, phpDocType: $int, type: $int, optional: true, name: 'n'), ]; foreach ($nativeParameters as $name => $parameter) { $r["native constructed $name"] = $catching(static fn () => $viewFullParameter($parameter)); } - $r['native wrong attributes'] = $catching(static fn () => new \PHPStan\Reflection\Native\ExtendedNativeParameterReflection('a', false, $int, $int, $int, $modes['no'], false, null, null, \PHPStan\TrinaryLogic::createNo(), null, 'x', null, \PHPStan\TrinaryLogic::createNo())); - $r['native wrong name'] = $catching(static fn () => new \PHPStan\Reflection\Native\ExtendedNativeParameterReflection([], false, $int, $int, $int, $modes['no'], false, null, null, \PHPStan\TrinaryLogic::createNo(), null, [], null, \PHPStan\TrinaryLogic::createNo())); + $r['native wrong attributes'] = $catching(static fn () => new \PHPStan\Reflection\Native\ExtendedNativeParameterReflection('a', false, $int, $int, $int, $modes['no'], false, null, null, \PHPStan\TrinaryLogic::createNo(), null, 'x', null, \PHPStan\TrinaryLogic::createNo(), \PHPStan\TrinaryLogic::createNo())); + $r['native wrong name'] = $catching(static fn () => new \PHPStan\Reflection\Native\ExtendedNativeParameterReflection([], false, $int, $int, $int, $modes['no'], false, null, null, \PHPStan\TrinaryLogic::createNo(), null, [], null, \PHPStan\TrinaryLogic::createNo(), \PHPStan\TrinaryLogic::createNo())); // the native dispatch: ParametersAcceptorSelector over variants whose // parameters are these reflections @@ -7327,7 +7328,7 @@ public function getResolvedPhpDoc(): ?\PHPStan\PhpDoc\ResolvedPhpDocBlock { retu foreach ([\PHPStan\Reflection\Php\PhpParameterReflection::class, \PHPStan\Reflection\Native\ExtendedNativeParameterReflection::class] as $class) { $raw = (new \ReflectionClass($class))->newInstanceWithoutConstructor(); - foreach (['getName', 'isOptional', 'getType', 'passedByReference', 'isVariadic', 'getDefaultValue', 'getPhpDocType', 'hasNativeType', 'getNativeType', 'getOutType', 'isImmediatelyInvokedCallable', 'getClosureThisType', 'getAttributes', 'getAllowedConstants', 'isPureUnlessCallableIsImpureParameter'] as $method) { + foreach (['getName', 'isOptional', 'getType', 'passedByReference', 'isVariadic', 'getDefaultValue', 'getPhpDocType', 'hasNativeType', 'getNativeType', 'getOutType', 'isImmediatelyInvokedCallable', 'getClosureThisType', 'getAttributes', 'getAllowedConstants', 'isPureUnlessCallableIsImpureParameter', 'isPureUnlessParameterPassedParameter'] as $method) { $r["unconstructed $class $method"] = $catching(static fn () => $view($raw->$method())); } $r["unconstructed $class checkAllowedConstants"] = $catching(static fn () => get_class($raw->checkAllowedConstants([]))); @@ -7381,7 +7382,7 @@ public function getResolvedPhpDoc(): ?\PHPStan\PhpDoc\ResolvedPhpDocBlock { retu $int = new \PHPStan\Type\IntegerType(); $string = new \PHPStan\Type\StringType(); $dummy = new \PHPStan\Reflection\Php\DummyParameter('d', $int, false, null, false, null); - $extendedDummy = new \PHPStan\Reflection\Php\ExtendedDummyParameter('e', $string, true, null, true, null, $string, $string, $int, \PHPStan\TrinaryLogic::createYes(), null, [], null, \PHPStan\TrinaryLogic::createNo()); + $extendedDummy = new \PHPStan\Reflection\Php\ExtendedDummyParameter('e', $string, true, null, true, null, $string, $string, $int, \PHPStan\TrinaryLogic::createYes(), null, [], null, \PHPStan\TrinaryLogic::createNo(), \PHPStan\TrinaryLogic::createMaybe()); $variances = new \PHPStan\Type\Generic\TemplateTypeVarianceMap(['T' => \PHPStan\Type\Generic\TemplateTypeVariance::createCovariant()]); $assertions = $stringReflectionProvider->getClass(\PHPStanTurboTests\SignatureFixture::class)->getNativeMethod('asserting')->getAsserts(); $acceptors = [ @@ -7396,7 +7397,7 @@ public function getResolvedPhpDoc(): ?\PHPStan\PhpDoc\ResolvedPhpDocBlock { retu 'trivial' => new \PHPStan\Reflection\TrivialParametersAcceptor(), 'trivial named' => new \PHPStan\Reflection\TrivialParametersAcceptor(callableName: 'Closure'), 'fv subclass' => new \PHPStanTurboTests\FunctionVariantSubclass($int), - 'efv subclass' => new \PHPStanTurboTests\ExtendedFunctionVariantSubclass($emptyMap, null, [$extendedDummy, new \PHPStan\Reflection\Php\ExtendedDummyParameter('f', $int, false, null, false, null, $int, $int, null, \PHPStan\TrinaryLogic::createNo(), null, [], null, \PHPStan\TrinaryLogic::createNo())], false, $int, $int, $int), + 'efv subclass' => new \PHPStanTurboTests\ExtendedFunctionVariantSubclass($emptyMap, null, [$extendedDummy, new \PHPStan\Reflection\Php\ExtendedDummyParameter('f', $int, false, null, false, null, $int, $int, null, \PHPStan\TrinaryLogic::createNo(), null, [], null, \PHPStan\TrinaryLogic::createNo(), \PHPStan\TrinaryLogic::createNo())], false, $int, $int, $int), ]; foreach ($acceptors as $name => $acceptor) { $r["acceptor $name"] = $viewAcceptorFull($acceptor); From dd125239e402159444772f164daa3d5aec3da633 Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Wed, 8 Jul 2026 02:13:47 +0900 Subject: [PATCH 03/29] Wire @pure-unless-parameter-passed into the analyser Thread the raw parameter flags through enterClassMethod()/enterFunction() and getPhpDocs(), and consult them in SimpleImpurePoint::resolvePureUnlessParameterPassedVerdict(): a call to a function flagged with @pure-unless-parameter-passed stays pure as long as none of those by-ref parameters received an argument. --- src/Analyser/MutatingScope.php | 7 + src/Analyser/PhpDocsResolver.php | 6 +- .../StmtHandler/ClassMethodHandler.php | 3 +- src/Analyser/StmtHandler/FunctionHandler.php | 3 +- .../Callables/SimpleImpurePoint.php | 62 + turbo-ext/src/ClassMethodHandler.cpp | 8 +- turbo-ext/src/FunctionHandler.cpp | 8 +- turbo-ext/src/MutatingScope.cpp | 41 +- turbo-ext/src/PhpClassReflectionExtension.cpp | 10 +- turbo-ext/src/PhpDocsResolver.cpp | 9 +- turbo-ext/src/SimpleImpurePoint.cpp | 95 +- turbo-ext/src/generated/MutatingScope.h | 1080 +++++++++-------- turbo-ext/src/generated/SimpleImpurePoint.h | 17 +- turbo-ext/src/support.h | 5 +- turbo-ext/tests/scope-family.php | 2 + turbo-ext/tests/type-family.php | 13 +- 16 files changed, 787 insertions(+), 582 deletions(-) diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index f5c6472d413..4546aa56d75 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -2039,6 +2039,7 @@ public function enterTrait(ClassReflection $traitReflection): self * @param array $immediatelyInvokedCallableParameters * @param array $phpDocClosureThisTypeParameters * @param array $phpDocPureUnlessCallableIsImpureParameters + * @param array $phpDocPureUnlessParameterPassedParameters */ public function enterClassMethod( Node\Stmt\ClassMethod $classMethod, @@ -2061,6 +2062,7 @@ public function enterClassMethod( bool $isConstructor = false, ?ResolvedPhpDocBlock $resolvedPhpDocBlock = null, array $phpDocPureUnlessCallableIsImpureParameters = [], + array $phpDocPureUnlessParameterPassedParameters = [], ): self { if (!$this->isInClass()) { @@ -2097,6 +2099,7 @@ public function enterClassMethod( $isConstructor, $this->attributeReflectionFactory->fromAttrGroups($classMethod->attrGroups, InitializerExprContext::fromStubParameter($this->getClassReflection()->getName(), $this->getFile(), $classMethod)), $phpDocPureUnlessCallableIsImpureParameters, + $phpDocPureUnlessParameterPassedParameters, ), !$classMethod->isStatic(), ); @@ -2188,6 +2191,7 @@ public function enterPropertyHook( false, $this->attributeReflectionFactory->fromAttrGroups($hook->attrGroups, InitializerExprContext::fromStubParameter($this->getClassReflection()->getName(), $this->getFile(), $hook)), [], + [], ), true, ); @@ -2265,6 +2269,7 @@ private function getParameterAttributes(ClassMethod|Function_|PropertyHook $func * @param array $immediatelyInvokedCallableParameters * @param array $phpDocClosureThisTypeParameters * @param array $pureUnlessCallableIsImpureParameters + * @param array $pureUnlessParameterPassedParameters */ public function enterFunction( Node\Stmt\Function_ $function, @@ -2283,6 +2288,7 @@ public function enterFunction( array $immediatelyInvokedCallableParameters = [], array $phpDocClosureThisTypeParameters = [], array $pureUnlessCallableIsImpureParameters = [], + array $pureUnlessParameterPassedParameters = [], ): self { return $this->enterFunctionLike( @@ -2309,6 +2315,7 @@ public function enterFunction( $phpDocClosureThisTypeParameters, $this->attributeReflectionFactory->fromAttrGroups($function->attrGroups, InitializerExprContext::fromStubParameter(null, $this->getFile(), $function)), $pureUnlessCallableIsImpureParameters, + $pureUnlessParameterPassedParameters, ), false, ); diff --git a/src/Analyser/PhpDocsResolver.php b/src/Analyser/PhpDocsResolver.php index 601d0a64c05..2bb8fe85215 100644 --- a/src/Analyser/PhpDocsResolver.php +++ b/src/Analyser/PhpDocsResolver.php @@ -42,7 +42,7 @@ public function __construct( } /** - * @return array{TemplateTypeMap, array, array, array, ?Type, ?Type, ?string, bool, bool, bool, bool|null, bool, bool, string|null, Assertions, ?Type, array, array<(string|int), VarTag>, bool, ?ResolvedPhpDocBlock, array} + * @return array{TemplateTypeMap, array, array, array, ?Type, ?Type, ?string, bool, bool, bool, bool|null, bool, bool, string|null, Assertions, ?Type, array, array<(string|int), VarTag>, bool, ?ResolvedPhpDocBlock, array, array} */ public function getPhpDocs(Scope $scope, Node\FunctionLike|Node\Stmt\Property $node): array { @@ -73,6 +73,7 @@ public function getPhpDocs(Scope $scope, Node\FunctionLike|Node\Stmt\Property $n $functionName = null; $phpDocParameterOutTypes = []; $phpDocPureUnlessCallableIsImpureParameters = []; + $phpDocPureUnlessParameterPassedParameters = []; if ($node instanceof Node\Stmt\ClassMethod) { if (!$scope->isInClass()) { @@ -207,6 +208,7 @@ public function getPhpDocs(Scope $scope, Node\FunctionLike|Node\Stmt\Property $n $selfOutType = $resolvedPhpDoc->getSelfOutTag() !== null ? $resolvedPhpDoc->getSelfOutTag()->getType() : null; $varTags = $resolvedPhpDoc->getVarTags(); $phpDocPureUnlessCallableIsImpureParameters = $resolvedPhpDoc->getParamsPureUnlessCallableIsImpure(); + $phpDocPureUnlessParameterPassedParameters = $resolvedPhpDoc->getParamsPureUnlessParameterPassed(); } if ($acceptsNamedArguments && $scope->isInClass()) { @@ -234,7 +236,7 @@ public function getPhpDocs(Scope $scope, Node\FunctionLike|Node\Stmt\Property $n } } - return [$templateTypeMap, $phpDocParameterTypes, $phpDocImmediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, $isReadOnly, $docComment, $asserts, $selfOutType, $phpDocParameterOutTypes, $varTags, $isAllowedPrivateMutation, $resolvedPhpDoc, $phpDocPureUnlessCallableIsImpureParameters]; + return [$templateTypeMap, $phpDocParameterTypes, $phpDocImmediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, $isReadOnly, $docComment, $asserts, $selfOutType, $phpDocParameterOutTypes, $varTags, $isAllowedPrivateMutation, $resolvedPhpDoc, $phpDocPureUnlessCallableIsImpureParameters, $phpDocPureUnlessParameterPassedParameters]; } private function getPhpDocReturnType(ResolvedPhpDocBlock $resolvedPhpDoc, Type $nativeReturnType): ?Type diff --git a/src/Analyser/StmtHandler/ClassMethodHandler.php b/src/Analyser/StmtHandler/ClassMethodHandler.php index aa6357523d2..1d07acea04c 100644 --- a/src/Analyser/StmtHandler/ClassMethodHandler.php +++ b/src/Analyser/StmtHandler/ClassMethodHandler.php @@ -76,7 +76,7 @@ public function processStmt( { $dependencies = []; $dependencies[] = $this->attributesHandler->processAttributeGroups($nodeScopeResolver, $stmt, $stmt->attrGroups, $scope, $storage, $nodeCallback); - [$templateTypeMap, $phpDocParameterTypes, $phpDocImmediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, $isReadOnly, $phpDocComment, $asserts, $selfOutType, $phpDocParameterOutTypes, , , , $pureUnlessCallableIsImpureParameters] = $this->phpDocsResolver->getPhpDocs($scope, $stmt); + [$templateTypeMap, $phpDocParameterTypes, $phpDocImmediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, $isReadOnly, $phpDocComment, $asserts, $selfOutType, $phpDocParameterOutTypes, , , , $pureUnlessCallableIsImpureParameters, $pureUnlessParameterPassedParameters] = $this->phpDocsResolver->getPhpDocs($scope, $stmt); $dependencies[] = $this->parametersProcessor->processParams($nodeScopeResolver, $stmt, $stmt->params, $scope, $storage, $nodeCallback); @@ -112,6 +112,7 @@ public function processStmt( $isConstructor, null, $pureUnlessCallableIsImpureParameters, + $pureUnlessParameterPassedParameters, ); if (!$scope->isInClass()) { diff --git a/src/Analyser/StmtHandler/FunctionHandler.php b/src/Analyser/StmtHandler/FunctionHandler.php index 7c6e0d44b9b..1c1611ee03f 100644 --- a/src/Analyser/StmtHandler/FunctionHandler.php +++ b/src/Analyser/StmtHandler/FunctionHandler.php @@ -67,7 +67,7 @@ public function processStmt( { $dependencies = []; $dependencies[] = $this->attributesHandler->processAttributeGroups($nodeScopeResolver, $stmt, $stmt->attrGroups, $scope, $storage, $nodeCallback); - [$templateTypeMap, $phpDocParameterTypes, $phpDocImmediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, , $isPure, $acceptsNamedArguments, , $phpDocComment, $asserts,, $phpDocParameterOutTypes, , , , $pureUnlessCallableIsImpureParameters] = $this->phpDocsResolver->getPhpDocs($scope, $stmt); + [$templateTypeMap, $phpDocParameterTypes, $phpDocImmediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, , $isPure, $acceptsNamedArguments, , $phpDocComment, $asserts,, $phpDocParameterOutTypes, , , , $pureUnlessCallableIsImpureParameters, $pureUnlessParameterPassedParameters] = $this->phpDocsResolver->getPhpDocs($scope, $stmt); $dependencies[] = $this->parametersProcessor->processParams($nodeScopeResolver, $stmt, $stmt->params, $scope, $storage, $nodeCallback); @@ -96,6 +96,7 @@ public function processStmt( $phpDocImmediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, $pureUnlessCallableIsImpureParameters, + $pureUnlessParameterPassedParameters, ); $functionReflection = $functionScope->getFunction(); if (!$functionReflection instanceof PhpFunctionFromParserNodeReflection) { diff --git a/src/Reflection/Callables/SimpleImpurePoint.php b/src/Reflection/Callables/SimpleImpurePoint.php index d120c17e169..f19ec1a6dd7 100644 --- a/src/Reflection/Callables/SimpleImpurePoint.php +++ b/src/Reflection/Callables/SimpleImpurePoint.php @@ -76,6 +76,15 @@ public static function createFromVariant(FunctionReflection|ExtendedMethodReflec } } + if (!$certain && $scope !== null && $variant !== null) { + $passedVerdict = self::resolvePureUnlessParameterPassedVerdict($variant, $args); + if ($passedVerdict !== null && $passedVerdict->yes()) { + // None of the @pure-unless-parameter-passed by-ref parameters + // received an argument, so the call is pure. + return null; + } + } + if ($function instanceof FunctionReflection) { if (isset(self::SIDE_EFFECT_FLIP_PARAMETERS[$function->getName()]) && $scope !== null) { [ @@ -205,6 +214,59 @@ public static function resolvePureUnlessCallableIsImpureVerdict(ParametersAccept return $verdict; } + /** + * Purity verdict for parameters flagged with @pure-unless-parameter-passed: + * the call stays pure as long as none of those (by-ref out) parameters + * received an argument. Returns Yes when no flagged parameter was passed, + * No when at least one was, and null when the variant has no such parameters + * (so the caller keeps its current behavior). + * + * @param Arg[] $args + */ + public static function resolvePureUnlessParameterPassedVerdict(ParametersAcceptor $variant, array $args): ?TrinaryLogic + { + $parameters = $variant->getParameters(); + $verdict = null; + + foreach ($parameters as $parameterIndex => $parameter) { + if (!$parameter instanceof ExtendedParameterReflection) { + continue; + } + if ($parameter->isPureUnlessParameterPassedParameter()->no()) { + continue; + } + + $verdict ??= TrinaryLogic::createYes(); + + $matchedArg = null; + $hasNamedParameter = false; + foreach ($args as $i => $arg) { + if ($arg->name !== null) { + $hasNamedParameter = true; + if ($arg->name->name === $parameter->getName()) { + $matchedArg = $arg; + break; + } + + continue; + } + + if (!$hasNamedParameter && $i === $parameterIndex) { + $matchedArg = $arg; + break; + } + } + + if ($matchedArg === null) { + continue; + } + + $verdict = $verdict->and(TrinaryLogic::createNo()); + } + + return $verdict; + } + /** @return ImpurePointIdentifier */ public function getIdentifier(): string { diff --git a/turbo-ext/src/ClassMethodHandler.cpp b/turbo-ext/src/ClassMethodHandler.cpp index 63e08cd21ff..6a00411b6f9 100644 --- a/turbo-ext/src/ClassMethodHandler.cpp +++ b/turbo-ext/src/ClassMethodHandler.cpp @@ -198,9 +198,9 @@ class ClassMethodHandler zv::Val attributeDependencies = ptsh::processAttributeGroups(OBJ_PROP_NUM(self, slots::attributesHandler), nodeScopeResolver, stmt, attrGroups, scope, storage, nodeCallback); if (UNEXPECTED(attributeDependencies.isUndef())) return zv::Val(); collect(dependencies, attributeDependencies.raw()); - /* [$templateTypeMap, ..., $phpDocParameterOutTypes, , , , $pureUnlessCallableIsImpureParameters] */ + /* [$templateTypeMap, ..., $phpDocParameterOutTypes, , , , $pureUnlessCallableIsImpureParameters, $pureUnlessParameterPassedParameters] */ pt_php_docs phpDocs; - if (UNEXPECTED(!ptsh::getPhpDocs(OBJ_PROP_NUM(self, slots::phpDocsResolver), scope, stmt, 0x1FFFFu | (1u << 20), phpDocs))) return zv::Val(); + if (UNEXPECTED(!ptsh::getPhpDocs(OBJ_PROP_NUM(self, slots::phpDocsResolver), scope, stmt, 0x1FFFFu | (1u << 20) | (1u << 21), phpDocs))) return zv::Val(); zval *docs[PT_PHP_DOCS_COUNT]; for (uint32_t index = 0; index < PT_PHP_DOCS_COUNT; index++) docs[index] = &phpDocs.items[index]; zval *templateTypeMap = docs[0]; @@ -221,6 +221,7 @@ class ClassMethodHandler zval *selfOutType = docs[15]; zval *phpDocParameterOutTypes = docs[16]; zval *pureUnlessCallableIsImpureParameters = docs[20]; + zval *pureUnlessParameterPassedParameters = docs[21]; zval *params = ptsh::readNodeProperty(pt_cmh_params_site, stmt, PT_LC("params")); if (UNEXPECTED(params == NULL)) return zv::Val(); @@ -260,7 +261,7 @@ class ClassMethodHandler isConstructor = Z_TYPE_P(identifier) == IS_STRING && zend_string_equals_literal_ci(Z_STR_P(identifier), "__construct"); } - zval enterArgv[20]; + zval enterArgv[21]; ZVAL_COPY_VALUE(&enterArgv[0], stmt); ZVAL_COPY_VALUE(&enterArgv[1], templateTypeMap); ZVAL_COPY_VALUE(&enterArgv[2], phpDocParameterTypes); @@ -281,6 +282,7 @@ class ClassMethodHandler ZVAL_BOOL(&enterArgv[17], isConstructor); ZVAL_NULL(&enterArgv[18]); ZVAL_COPY_VALUE(&enterArgv[19], pureUnlessCallableIsImpureParameters); + ZVAL_COPY_VALUE(&enterArgv[20], pureUnlessParameterPassedParameters); zv::Val methodScope = pt_mutating_scope_enter_class_method(Z_OBJ_P(scope), enterArgv); if (UNEXPECTED(methodScope.isUndef())) return zv::Val(); diff --git a/turbo-ext/src/FunctionHandler.cpp b/turbo-ext/src/FunctionHandler.cpp index 81b5b4ec8a7..a45072e6cf7 100644 --- a/turbo-ext/src/FunctionHandler.cpp +++ b/turbo-ext/src/FunctionHandler.cpp @@ -119,8 +119,9 @@ class FunctionHandler if (UNEXPECTED(attributeDependencies.isUndef())) return zv::Val(); collect(dependencies, attributeDependencies.raw()); /* [$templateTypeMap, ..., $isInternal, , $isPure, $acceptsNamedArguments, , - * $phpDocComment, $asserts,, $phpDocParameterOutTypes, , , , $pureUnlessCallableIsImpureParameters] */ - static constexpr uint32_t listIndexes[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 13, 14, 16, 20 }; + * $phpDocComment, $asserts,, $phpDocParameterOutTypes, , , , $pureUnlessCallableIsImpureParameters, + * $pureUnlessParameterPassedParameters] */ + static constexpr uint32_t listIndexes[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 13, 14, 16, 20, 21 }; uint32_t destructured = 0; for (uint32_t index : listIndexes) destructured |= 1u << index; pt_php_docs phpDocs; @@ -150,7 +151,7 @@ class FunctionHandler deprecatedDescription = attributeDeprecatedDescription.raw(); } - zval enterArgv[16]; + zval enterArgv[17]; ZVAL_COPY_VALUE(&enterArgv[0], stmt); ZVAL_COPY_VALUE(&enterArgv[1], docs[0]); ZVAL_COPY_VALUE(&enterArgv[2], docs[1]); @@ -167,6 +168,7 @@ class FunctionHandler ZVAL_COPY_VALUE(&enterArgv[13], docs[2]); ZVAL_COPY_VALUE(&enterArgv[14], docs[3]); ZVAL_COPY_VALUE(&enterArgv[15], docs[20]); + ZVAL_COPY_VALUE(&enterArgv[16], docs[21]); zv::Val functionScope = pt_mutating_scope_enter_function(Z_OBJ_P(scope), enterArgv); if (UNEXPECTED(functionScope.isUndef())) return zv::Val(); zv::Val functionReflection = pt_mutating_scope_get_function(Z_OBJ_P(functionScope.raw())); diff --git a/turbo-ext/src/MutatingScope.cpp b/turbo-ext/src/MutatingScope.cpp index ce4c0efd70d..88b2c9a8f4a 100644 --- a/turbo-ext/src/MutatingScope.cpp +++ b/turbo-ext/src/MutatingScope.cpp @@ -4326,7 +4326,7 @@ class MutatingScope static zv::Val emptyAssertions() { return pt_assertions_create_empty(); } /** @api (twin 2012) */ - zv::Val enterClassMethod(zval *classMethod, zval *templateTypeMap, zval *phpDocParameterTypes, zval *phpDocReturnType, zval *throwType, zval *deprecatedDescription, bool isDeprecated, bool isInternal, bool isFinal, zval *isPure, bool acceptsNamedArguments, zval *asserts, zval *selfOutType, zval *phpDocComment, zval *parameterOutTypes, zval *immediatelyInvokedCallableParameters, zval *phpDocClosureThisTypeParameters, bool isConstructor, zval *resolvedPhpDocBlock, zval *phpDocPureUnlessCallableIsImpureParameters) + zv::Val enterClassMethod(zval *classMethod, zval *templateTypeMap, zval *phpDocParameterTypes, zval *phpDocReturnType, zval *throwType, zval *deprecatedDescription, bool isDeprecated, bool isInternal, bool isFinal, zval *isPure, bool acceptsNamedArguments, zval *asserts, zval *selfOutType, zval *phpDocComment, zval *parameterOutTypes, zval *immediatelyInvokedCallableParameters, zval *phpDocClosureThisTypeParameters, bool isConstructor, zval *resolvedPhpDocBlock, zval *phpDocPureUnlessCallableIsImpureParameters, zval *phpDocPureUnlessParameterPassedParameters) { bool inClass; if (UNEXPECTED(!thisIsInClass(inClass))) return zv::Val(); @@ -4336,7 +4336,7 @@ class MutatingScope } zend_object *classMethodObject = Z_OBJ_P(classMethod); - Args<28> a; + Args<29> a; PT_MS_ARG_OWNED(a, thisGetClassReflection()); a.add(zv::Ref(classMethod)); a.addNull(); @@ -4387,6 +4387,7 @@ class MutatingScope PT_MS_ARG_OWNED(a, attributesFromAttrGroups(attrGroups.deref().raw(), className.raw(), classMethodObject)); } a.add(zv::Ref(phpDocPureUnlessCallableIsImpureParameters)); + a.add(zv::Ref(phpDocPureUnlessParameterPassedParameters)); zv::Val reflection = pt_type_new(PT_CLASS_PHP_METHOD_FROM_PARSER_NODE_REFLECTION, a.count, a.argv); if (UNEXPECTED(reflection.isUndef())) return zv::Val(); @@ -4502,7 +4503,7 @@ class MutatingScope zv::Val realParameterTypes = getRealParameterTypes(hookObject); if (UNEXPECTED(realParameterTypes.isUndef())) return zv::Val(); - Args<28> a; + Args<29> a; PT_MS_ARG_OWNED(a, thisGetClassReflection()); { zval hookZv; @@ -4551,6 +4552,7 @@ class MutatingScope PT_MS_ARG_OWNED(a, attributesFromAttrGroups(attrGroups.deref().raw(), className.raw(), hookObject)); } a.addEmptyArray(); + a.addEmptyArray(); zv::Val reflection = pt_type_new(PT_CLASS_PHP_METHOD_FROM_PARSER_NODE_REFLECTION, a.count, a.argv); if (UNEXPECTED(reflection.isUndef())) return zv::Val(); @@ -4558,7 +4560,7 @@ class MutatingScope } /** @api (twin 2237) */ - zv::Val enterFunction(zval *function, zval *templateTypeMap, zval *phpDocParameterTypes, zval *phpDocReturnType, zval *throwType, zval *deprecatedDescription, bool isDeprecated, bool isInternal, zval *isPure, bool acceptsNamedArguments, zval *asserts, zval *phpDocComment, zval *parameterOutTypes, zval *immediatelyInvokedCallableParameters, zval *phpDocClosureThisTypeParameters, zval *pureUnlessCallableIsImpureParameters) + zv::Val enterFunction(zval *function, zval *templateTypeMap, zval *phpDocParameterTypes, zval *phpDocReturnType, zval *throwType, zval *deprecatedDescription, bool isDeprecated, bool isInternal, zval *isPure, bool acceptsNamedArguments, zval *asserts, zval *phpDocComment, zval *parameterOutTypes, zval *immediatelyInvokedCallableParameters, zval *phpDocClosureThisTypeParameters, zval *pureUnlessCallableIsImpureParameters, zval *pureUnlessParameterPassedParameters) { zend_object *functionObject = Z_OBJ_P(function); Args<28> a; @@ -4602,6 +4604,7 @@ class MutatingScope PT_MS_ARG_OWNED(a, attributesFromAttrGroups(attrGroups.deref().raw(), &nullClassName, functionObject)); } a.add(zv::Ref(pureUnlessCallableIsImpureParameters)); + a.add(zv::Ref(pureUnlessParameterPassedParameters)); zv::Val reflection = pt_type_new(PT_CLASS_PHP_FUNCTION_FROM_PARSER_NODE_REFLECTION, a.count, a.argv); if (UNEXPECTED(reflection.isUndef())) return zv::Val(); @@ -11992,10 +11995,10 @@ zv::Val pt_mutating_scope_enter_class_method(zend_object *scope, zval *argv) && msIsBool(&argv[6]) && msIsBool(&argv[7]) && msIsBool(&argv[8]) && msIsBoolOrNull(&argv[9]) && msIsBool(&argv[10]) && msIsObjectOrNull(&argv[11]) && msIsObjectOrNull(&argv[12]) && msIsStringOrNull(&argv[13]) && Z_TYPE(argv[14]) == IS_ARRAY && Z_TYPE(argv[15]) == IS_ARRAY && Z_TYPE(argv[16]) == IS_ARRAY - && msIsBool(&argv[17]) && msIsObjectOrNull(&argv[18]) && Z_TYPE(argv[19]) == IS_ARRAY)) { - return MutatingScope(scope).enterClassMethod(&argv[0], &argv[1], &argv[2], &argv[3], &argv[4], &argv[5], Z_TYPE(argv[6]) == IS_TRUE, Z_TYPE(argv[7]) == IS_TRUE, Z_TYPE(argv[8]) == IS_TRUE, &argv[9], Z_TYPE(argv[10]) == IS_TRUE, &argv[11], &argv[12], &argv[13], &argv[14], &argv[15], &argv[16], Z_TYPE(argv[17]) == IS_TRUE, &argv[18], &argv[19]); + && msIsBool(&argv[17]) && msIsObjectOrNull(&argv[18]) && Z_TYPE(argv[19]) == IS_ARRAY && Z_TYPE(argv[20]) == IS_ARRAY)) { + return MutatingScope(scope).enterClassMethod(&argv[0], &argv[1], &argv[2], &argv[3], &argv[4], &argv[5], Z_TYPE(argv[6]) == IS_TRUE, Z_TYPE(argv[7]) == IS_TRUE, Z_TYPE(argv[8]) == IS_TRUE, &argv[9], Z_TYPE(argv[10]) == IS_TRUE, &argv[11], &argv[12], &argv[13], &argv[14], &argv[15], &argv[16], Z_TYPE(argv[17]) == IS_TRUE, &argv[18], &argv[19], &argv[20]); } - return pt_type_call(scope, PT_LC("enterclassmethod"), 20, argv); + return pt_type_call(scope, PT_LC("enterclassmethod"), 21, argv); } zv::Val pt_mutating_scope_enter_function(zend_object *scope, zval *argv) @@ -12005,10 +12008,10 @@ zv::Val pt_mutating_scope_enter_function(zend_object *scope, zval *argv) && msIsObjectOrNull(&argv[3]) && msIsObjectOrNull(&argv[4]) && msIsStringOrNull(&argv[5]) && msIsBool(&argv[6]) && msIsBool(&argv[7]) && msIsBoolOrNull(&argv[8]) && msIsBool(&argv[9]) && msIsObjectOrNull(&argv[10]) && msIsStringOrNull(&argv[11]) - && Z_TYPE(argv[12]) == IS_ARRAY && Z_TYPE(argv[13]) == IS_ARRAY && Z_TYPE(argv[14]) == IS_ARRAY && Z_TYPE(argv[15]) == IS_ARRAY)) { - return MutatingScope(scope).enterFunction(&argv[0], &argv[1], &argv[2], &argv[3], &argv[4], &argv[5], Z_TYPE(argv[6]) == IS_TRUE, Z_TYPE(argv[7]) == IS_TRUE, &argv[8], Z_TYPE(argv[9]) == IS_TRUE, &argv[10], &argv[11], &argv[12], &argv[13], &argv[14], &argv[15]); + && Z_TYPE(argv[12]) == IS_ARRAY && Z_TYPE(argv[13]) == IS_ARRAY && Z_TYPE(argv[14]) == IS_ARRAY && Z_TYPE(argv[15]) == IS_ARRAY && Z_TYPE(argv[16]) == IS_ARRAY)) { + return MutatingScope(scope).enterFunction(&argv[0], &argv[1], &argv[2], &argv[3], &argv[4], &argv[5], Z_TYPE(argv[6]) == IS_TRUE, Z_TYPE(argv[7]) == IS_TRUE, &argv[8], Z_TYPE(argv[9]) == IS_TRUE, &argv[10], &argv[11], &argv[12], &argv[13], &argv[14], &argv[15], &argv[16]); } - return pt_type_call(scope, PT_LC("enterfunction"), 16, argv); + return pt_type_call(scope, PT_LC("enterfunction"), 17, argv); } zv::Val pt_mutating_scope_enter_class(zend_object *scope, zval *classReflection) @@ -12847,12 +12850,13 @@ PT_MINIT_REGISTRATION(pt_register_mutating_scope) reg::withDefault(reg::boolArg("isConstructor"), "false"), reg::withDefault(reg::obj("resolvedPhpDocBlock", resolvedPhpDocBlock, true), "null"), reg::withDefault(reg::arrayArg("phpDocPureUnlessCallableIsImpureParameters"), "[]"), + reg::withDefault(reg::arrayArg("phpDocPureUnlessParameterPassedParameters"), "[]"), }, [](INTERNAL_FUNCTION_PARAMETERS) { zval *classMethod, *templateTypeMapArg, *phpDocParameterTypes, *phpDocReturnType = NULL, *throwType = NULL, *asserts = NULL, *selfOutType = NULL, *resolvedPhpDocBlock = NULL; - zval *parameterOutTypes = NULL, *immediatelyInvokedCallableParameters = NULL, *phpDocClosureThisTypeParameters = NULL, *phpDocPureUnlessCallableIsImpureParameters = NULL; + zval *parameterOutTypes = NULL, *immediatelyInvokedCallableParameters = NULL, *phpDocClosureThisTypeParameters = NULL, *phpDocPureUnlessCallableIsImpureParameters = NULL, *phpDocPureUnlessParameterPassedParameters = NULL; zend_string *deprecatedDescription = NULL, *phpDocComment = NULL; bool isDeprecated, isInternal, isFinal, isPure = false, isPureIsNull = true, acceptsNamedArguments = true, isConstructor = false; - ZEND_PARSE_PARAMETERS_START(9, 20) + ZEND_PARSE_PARAMETERS_START(9, 21) Z_PARAM_OBJECT(classMethod) Z_PARAM_OBJECT(templateTypeMapArg) Z_PARAM_ARRAY(phpDocParameterTypes) @@ -12874,6 +12878,7 @@ PT_MINIT_REGISTRATION(pt_register_mutating_scope) Z_PARAM_BOOL(isConstructor) Z_PARAM_OBJECT_OR_NULL(resolvedPhpDocBlock) Z_PARAM_ARRAY(phpDocPureUnlessCallableIsImpureParameters) + Z_PARAM_ARRAY(phpDocPureUnlessParameterPassedParameters) ZEND_PARSE_PARAMETERS_END(); PT_MS_OBJ_ZVAL(phpDocReturnType); PT_MS_OBJ_ZVAL(throwType); @@ -12886,13 +12891,14 @@ PT_MINIT_REGISTRATION(pt_register_mutating_scope) PT_MS_ARRAY_ZVAL(immediatelyInvokedCallableParameters); PT_MS_ARRAY_ZVAL(phpDocClosureThisTypeParameters); PT_MS_ARRAY_ZVAL(phpDocPureUnlessCallableIsImpureParameters); + PT_MS_ARRAY_ZVAL(phpDocPureUnlessParameterPassedParameters); zval isPureZv; if (isPureIsNull) { ZVAL_NULL(&isPureZv); } else { ZVAL_BOOL(&isPureZv, isPure); } - PT_RETURN_VAL(PT_THIS.enterClassMethod(classMethod, templateTypeMapArg, phpDocParameterTypes, phpDocReturnType, throwType, &deprecatedDescriptionZv, isDeprecated, isInternal, isFinal, &isPureZv, acceptsNamedArguments, asserts, selfOutType, &phpDocCommentZv, parameterOutTypes, immediatelyInvokedCallableParameters, phpDocClosureThisTypeParameters, isConstructor, resolvedPhpDocBlock, phpDocPureUnlessCallableIsImpureParameters)); + PT_RETURN_VAL(PT_THIS.enterClassMethod(classMethod, templateTypeMapArg, phpDocParameterTypes, phpDocReturnType, throwType, &deprecatedDescriptionZv, isDeprecated, isInternal, isFinal, &isPureZv, acceptsNamedArguments, asserts, selfOutType, &phpDocCommentZv, parameterOutTypes, immediatelyInvokedCallableParameters, phpDocClosureThisTypeParameters, isConstructor, resolvedPhpDocBlock, phpDocPureUnlessCallableIsImpureParameters, phpDocPureUnlessParameterPassedParameters)); }, &returnsSelf); cls.method("enterPropertyHook", reg::Public, 10, { @@ -12957,12 +12963,13 @@ PT_MINIT_REGISTRATION(pt_register_mutating_scope) reg::withDefault(reg::arrayArg("immediatelyInvokedCallableParameters"), "[]"), reg::withDefault(reg::arrayArg("phpDocClosureThisTypeParameters"), "[]"), reg::withDefault(reg::arrayArg("pureUnlessCallableIsImpureParameters"), "[]"), + reg::withDefault(reg::arrayArg("pureUnlessParameterPassedParameters"), "[]"), }, [](INTERNAL_FUNCTION_PARAMETERS) { zval *function = NULL, *templateTypeMapArg = NULL, *phpDocParameterTypes = NULL, *phpDocReturnType = NULL, *throwType = NULL, *asserts = NULL; - zval *parameterOutTypes = NULL, *immediatelyInvokedCallableParameters = NULL, *phpDocClosureThisTypeParameters = NULL, *pureUnlessCallableIsImpureParameters = NULL; + zval *parameterOutTypes = NULL, *immediatelyInvokedCallableParameters = NULL, *phpDocClosureThisTypeParameters = NULL, *pureUnlessCallableIsImpureParameters = NULL, *pureUnlessParameterPassedParameters = NULL; zend_string *deprecatedDescription = NULL, *phpDocComment = NULL; bool isDeprecated = false, isInternal = false, isPure = false, isPureIsNull = true, acceptsNamedArguments = true; - ZEND_PARSE_PARAMETERS_START(8, 16) + ZEND_PARSE_PARAMETERS_START(8, 17) Z_PARAM_OBJECT(function) Z_PARAM_OBJECT(templateTypeMapArg) Z_PARAM_ARRAY(phpDocParameterTypes) @@ -12980,6 +12987,7 @@ PT_MINIT_REGISTRATION(pt_register_mutating_scope) Z_PARAM_ARRAY(immediatelyInvokedCallableParameters) Z_PARAM_ARRAY(phpDocClosureThisTypeParameters) Z_PARAM_ARRAY(pureUnlessCallableIsImpureParameters) + Z_PARAM_ARRAY(pureUnlessParameterPassedParameters) ZEND_PARSE_PARAMETERS_END(); PT_MS_OBJ_ZVAL(phpDocReturnType); PT_MS_OBJ_ZVAL(throwType); @@ -12990,13 +12998,14 @@ PT_MINIT_REGISTRATION(pt_register_mutating_scope) PT_MS_ARRAY_ZVAL(immediatelyInvokedCallableParameters); PT_MS_ARRAY_ZVAL(phpDocClosureThisTypeParameters); PT_MS_ARRAY_ZVAL(pureUnlessCallableIsImpureParameters); + PT_MS_ARRAY_ZVAL(pureUnlessParameterPassedParameters); zval isPureZv; if (isPureIsNull) { ZVAL_NULL(&isPureZv); } else { ZVAL_BOOL(&isPureZv, isPure); } - PT_RETURN_VAL(PT_THIS.enterFunction(function, templateTypeMapArg, phpDocParameterTypes, phpDocReturnType, throwType, &deprecatedDescriptionZv, isDeprecated, isInternal, &isPureZv, acceptsNamedArguments, asserts, &phpDocCommentZv, parameterOutTypes, immediatelyInvokedCallableParameters, phpDocClosureThisTypeParameters, pureUnlessCallableIsImpureParameters)); + PT_RETURN_VAL(PT_THIS.enterFunction(function, templateTypeMapArg, phpDocParameterTypes, phpDocReturnType, throwType, &deprecatedDescriptionZv, isDeprecated, isInternal, &isPureZv, acceptsNamedArguments, asserts, &phpDocCommentZv, parameterOutTypes, immediatelyInvokedCallableParameters, phpDocClosureThisTypeParameters, pureUnlessCallableIsImpureParameters, pureUnlessParameterPassedParameters)); }, &returnsSelf); cls.method<&MutatingScope::enterNamespace, zp::Str>(sigs::enterNamespace); diff --git a/turbo-ext/src/PhpClassReflectionExtension.cpp b/turbo-ext/src/PhpClassReflectionExtension.cpp index 754c16ec6f8..9f2d92b8451 100644 --- a/turbo-ext/src/PhpClassReflectionExtension.cpp +++ b/turbo-ext/src/PhpClassReflectionExtension.cpp @@ -2671,13 +2671,14 @@ class PhpClassReflectionExtension if (UNEXPECTED(classScope.isUndef())) return zv::Val(); /* [$templateTypeMap, ..., $acceptsNamedArguments, , $phpDocComment, ..., - * $phpDocParameterOutTypes, , , , $phpDocPureUnlessCallableIsImpureParameters] */ + * $phpDocParameterOutTypes, , , , $phpDocPureUnlessCallableIsImpureParameters, + * $phpDocPureUnlessParameterPassedParameters] */ pt_php_docs phpDocs; - if (UNEXPECTED(!pt_php_docs_resolver_get_php_docs(slot(slots::phpDocsResolver), classScope.raw(), methodNode.raw(), 0x1EFFFu | (1u << 20), phpDocs))) return zv::Val(); + if (UNEXPECTED(!pt_php_docs_resolver_get_php_docs(slot(slots::phpDocsResolver), classScope.raw(), methodNode.raw(), 0x1EFFFu | (1u << 20) | (1u << 21), phpDocs))) return zv::Val(); zval *docs[PT_PHP_DOCS_COUNT]; for (uint32_t i = 0; i < PT_PHP_DOCS_COUNT; i++) docs[i] = &phpDocs.items[i]; - zval enterArgs[20]; + zval enterArgs[21]; ZVAL_COPY_VALUE(&enterArgs[0], methodNode.raw()); ZVAL_COPY_VALUE(&enterArgs[1], docs[0]); /* templateTypeMap */ ZVAL_COPY_VALUE(&enterArgs[2], docs[1]); /* phpDocParameterTypes */ @@ -2698,7 +2699,8 @@ class PhpClassReflectionExtension ZVAL_FALSE(&enterArgs[17]); ZVAL_NULL(&enterArgs[18]); ZVAL_COPY_VALUE(&enterArgs[19], docs[20]); /* pureUnlessCallableIsImpureParameters */ - zv::Val methodScope = call(classScope.raw(), PT_LC("enterclassmethod"), 20, enterArgs); + ZVAL_COPY_VALUE(&enterArgs[20], docs[21]); /* pureUnlessParameterPassedParameters */ + zv::Val methodScope = call(classScope.raw(), PT_LC("enterclassmethod"), 21, enterArgs); if (UNEXPECTED(methodScope.isUndef())) return zv::Val(); zend_class_entry *expressionStmtCe = pt_class(PT_CLASS_EXPRESSION_STMT); diff --git a/turbo-ext/src/PhpDocsResolver.cpp b/turbo-ext/src/PhpDocsResolver.cpp index 6ad0ba0ca82..039d21f19c9 100644 --- a/turbo-ext/src/PhpDocsResolver.cpp +++ b/turbo-ext/src/PhpDocsResolver.cpp @@ -6,7 +6,7 @@ * arginfo so Nette autowires it. getPhpDocs() — asked by ClassMethodHandler, * FunctionHandler, PropertyHandler, PropertyHooksProcessor and * PhpClassReflectionExtension for every declaration — is exported as - * pt_php_docs_resolver_get_php_docs(), which hands the 21 values over + * pt_php_docs_resolver_get_php_docs(), which hands the 22 values over * without building the twin's array; the PHP method builds it. * * FileTypeMapper, PhpDocInheritanceResolver, ResolvedPhpDocBlock, the PHPDoc @@ -283,6 +283,7 @@ class PhpDocsResolver zv::Val selfOutType = zv::Val::null(); zv::Arr phpDocParameterOutTypes = zv::Arr::empty(); zv::Arr phpDocPureUnlessCallableIsImpureParameters = zv::Arr::empty(); + zv::Arr phpDocPureUnlessParameterPassedParameters = zv::Arr::empty(); zv::Val templateTypeMap; { @@ -467,6 +468,11 @@ class PhpDocsResolver if (UNEXPECTED(value.isUndef())) return false; phpDocPureUnlessCallableIsImpureParameters = zv::Arr::adoptVal(std::move(value)); } + { + zv::Val value = pt_resolved_php_doc_block_call(doc, PT_RPD_GET_PARAMS_PURE_UNLESS_PARAMETER_PASSED); + if (UNEXPECTED(value.isUndef())) return false; + phpDocPureUnlessParameterPassedParameters = zv::Arr::adoptVal(std::move(value)); + } } else { asserts = createEmptyAssertions(); if (UNEXPECTED(asserts.isUndef())) return false; @@ -510,6 +516,7 @@ class PhpDocsResolver moveInto(items[PT_PHP_DOCS_IS_ALLOWED_PRIVATE_MUTATION], isAllowedPrivateMutation); moveInto(items[PT_PHP_DOCS_RESOLVED_PHP_DOC], resolvedPhpDoc); moveInto(items[PT_PHP_DOCS_PURE_UNLESS_CALLABLE_IS_IMPURE_PARAMETERS], phpDocPureUnlessCallableIsImpureParameters); + moveInto(items[PT_PHP_DOCS_PURE_UNLESS_PARAMETER_PASSED_PARAMETERS], phpDocPureUnlessParameterPassedParameters); return true; } diff --git a/turbo-ext/src/SimpleImpurePoint.cpp b/turbo-ext/src/SimpleImpurePoint.cpp index 9a81a76a5b7..e73a67154b0 100644 --- a/turbo-ext/src/SimpleImpurePoint.cpp +++ b/turbo-ext/src/SimpleImpurePoint.cpp @@ -2,10 +2,11 @@ * PHPStanTurbo\SimpleImpurePoint — native implementation of * PHPStan\Reflection\Callables\SimpleImpurePoint. * - * A final value class over the twin's three promoted slots, plus the two + * A final value class over the twin's three promoted slots, plus the * statics every call handler asks: createFromVariant() (the impure point of a - * call to a function or method, null for a pure one) and - * resolvePureUnlessCallableIsImpureVerdict(). Native callers use + * call to a function or method, null for a pure one), + * resolvePureUnlessCallableIsImpureVerdict() and + * resolvePureUnlessParameterPassedVerdict(). Native callers use * pt_simple_impure_point_resolve(), which answers createFromVariant() without * the intermediate object (the method call handler copies its three values * into an ImpurePoint straight away), and pt_simple_impure_point_new(). The @@ -55,6 +56,12 @@ zv::Val parameterIsPureUnlessCallableIsImpure(zval *parameter) return pt_parameter_reflection_call(parameter, PT_PR_IS_PURE_UNLESS_CALLABLE_IS_IMPURE_PARAMETER); } +/* $parameter->isPureUnlessParameterPassedParameter() */ +zv::Val parameterIsPureUnlessParameterPassed(zval *parameter) +{ + return pt_parameter_reflection_call(parameter, PT_PR_IS_PURE_UNLESS_PARAMETER_PASSED_PARAMETER); +} + /* $parameter->getName() */ zv::Val parameterName(zval *parameter) { @@ -225,6 +232,13 @@ class SimpleImpurePoint } } + if (!certain && scope != NULL && variant != NULL) { + zend_long passedVerdict = PT_TRI_YES; + bool hasPassedVerdict = false; + if (UNEXPECTED(!resolvePassedVerdict(variant, args, hasPassedVerdict, passedVerdict))) return false; + if (hasPassedVerdict && passedVerdict == PT_TRI_YES) return true; + } + zend_class_entry *functionReflectionCe = pt_class(PT_CLASS_FUNCTION_REFLECTION); if (UNEXPECTED(functionReflectionCe == NULL)) return false; if (instanceof_function(Z_OBJCE_P(function), functionReflectionCe)) { @@ -377,6 +391,71 @@ class SimpleImpurePoint return true; } + /* Mirrors resolvePureUnlessParameterPassedVerdict(): hasVerdict false + * for the twin's null, verdict the PT_TRI_* value otherwise; false = + * pending exception */ + [[nodiscard]] static bool resolvePassedVerdict(zval *variant, zval *args, bool &hasVerdict, zend_long &verdict) + { + hasVerdict = false; + verdict = PT_TRI_YES; + zv::Val parameters = variantParameters(variant); + if (UNEXPECTED(parameters.isUndef())) return false; + if (UNEXPECTED(Z_TYPE_P(parameters.raw()) != IS_ARRAY)) { + zend_error(E_WARNING, "foreach() argument must be of type array|object, %s given", zend_zval_value_name(parameters.raw())); + return !EG(exception); + } + zend_class_entry *extendedParameterCe = pt_class(PT_CLASS_EXTENDED_PARAMETER_REFLECTION); + if (UNEXPECTED(extendedParameterCe == NULL)) return false; + + for (auto parameterEntry : zv::TableRef(Z_ARRVAL_P(parameters.raw()))) { + zval *parameter = parameterEntry.value().deref().raw(); + if (Z_TYPE_P(parameter) != IS_OBJECT || !instanceof_function(Z_OBJCE_P(parameter), extendedParameterCe)) continue; + zend_long pureUnless = trinaryOf(parameterIsPureUnlessParameterPassed(parameter)); + if (UNEXPECTED(pureUnless < 0)) return false; + if (pureUnless == PT_TRI_NO) continue; + + if (!hasVerdict) { + hasVerdict = true; + verdict = PT_TRI_YES; + } + + zval *matchedArg = NULL; + bool hasNamedParameter = false; + if (EXPECTED(Z_TYPE_P(args) == IS_ARRAY)) { + for (auto argEntry : zv::TableRef(Z_ARRVAL_P(args))) { + zval *arg = argEntry.value().deref().raw(); + zval *argName = nodeProperty(pt_sip_arg_name_site, arg, PT_LC("name")); + if (UNEXPECTED(EG(exception))) return false; + if (Z_TYPE_P(argName) != IS_NULL) { + hasNamedParameter = true; + zval *identifierName = nodeProperty(pt_sip_identifier_name_site, argName, PT_LC("name")); + if (UNEXPECTED(EG(exception))) return false; + zv::Val parameterNameValue = parameterName(parameter); + if (UNEXPECTED(parameterNameValue.isUndef())) return false; + if (zend_is_identical(identifierName, parameterNameValue.raw())) { + matchedArg = arg; + break; + } + continue; + } + if (!hasNamedParameter && identicalKeys(argEntry, parameterEntry)) { + matchedArg = arg; + break; + } + } + } else { + zend_error(E_WARNING, "foreach() argument must be of type array|object, %s given", zend_zval_value_name(args)); + if (UNEXPECTED(EG(exception))) return false; + } + + if (matchedArg == NULL) continue; + + verdict = pt_trinary_and(verdict, PT_TRI_NO); + } + + return true; + } + private: zend_object *self; @@ -518,6 +597,16 @@ PT_MINIT_REGISTRATION(pt_register_simple_impure_point) RETURN_COPY(pt_trinary_singleton(verdict)); }); + cls.method(sigs::resolvePureUnlessParameterPassedVerdict, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *variant, *args; + if (!zp::parse(execute_data, variant, args)) RETURN_THROWS(); + bool hasVerdict = false; + zend_long verdict = PT_TRI_YES; + if (UNEXPECTED(!SimpleImpurePoint::resolvePassedVerdict(variant, args, hasVerdict, verdict))) RETURN_THROWS(); + if (!hasVerdict) RETURN_NULL(); + RETURN_COPY(pt_trinary_singleton(verdict)); + }); + cls.method<&SimpleImpurePoint::getIdentifier>(sigs::getIdentifier); cls.method<&SimpleImpurePoint::getDescription>(sigs::getDescription); cls.method<&SimpleImpurePoint::isCertain>(sigs::isCertain); diff --git a/turbo-ext/src/generated/MutatingScope.h b/turbo-ext/src/generated/MutatingScope.h index d069ac1319f..832eb9a9226 100644 --- a/turbo-ext/src/generated/MutatingScope.h +++ b/turbo-ext/src/generated/MutatingScope.h @@ -303,244 +303,246 @@ inline constexpr char strings[] = "resolvedPhpDocBlock\0" /* 4278 */ "PHPStan\\PhpDoc\\ResolvedPhpDocBlock\0" /* 4298 */ "phpDocPureUnlessCallableIsImpureParameters\0" /* 4333 */ - "enterClassMethod\0" /* 4376 */ - "hook\0" /* 4393 */ - "PhpParser\\Node\\PropertyHook\0" /* 4398 */ - "propertyName\0" /* 4426 */ - "nativePropertyTypeNode\0" /* 4439 */ - "PhpParser\\Node\\Identifier|PhpParser\\Node\\Name|PhpParser\\Node\\ComplexType\0" /* 4462 */ - "phpDocPropertyType\0" /* 4535 */ - "enterPropertyHook\0" /* 4554 */ - "type\0" /* 4572 */ - "transformStaticType\0" /* 4577 */ - "functionLike\0" /* 4597 */ - "PhpParser\\Node\\FunctionLike\0" /* 4610 */ - "getRealParameterTypes\0" /* 4638 */ - "getRealParameterDefaultValues\0" /* 4660 */ - "PhpParser\\Node\\Stmt\\ClassMethod|PhpParser\\Node\\Stmt\\Function_|PhpParser\\Node\\PropertyHook\0" /* 4690 */ - "getParameterAttributes\0" /* 4780 */ - "PhpParser\\Node\\Stmt\\Function_\0" /* 4803 */ - "pureUnlessCallableIsImpureParameters\0" /* 4833 */ - "enterFunction\0" /* 4870 */ - "functionReflection\0" /* 4884 */ - "preserveConstructorScope\0" /* 4903 */ - "enterFunctionLike\0" /* 4928 */ - "namespaceName\0" /* 4946 */ - "enterNamespace\0" /* 4960 */ - "thisType\0" /* 4975 */ - "nativeThisType\0" /* 4984 */ - "scopeClasses\0" /* 4999 */ - "enterClosureBind\0" /* 5012 */ - "originalScope\0" /* 5029 */ - "restoreOriginalScopeAfterClosureBind\0" /* 5043 */ - "restoreThisScope\0" /* 5080 */ - "restoreThis\0" /* 5097 */ - "enterClosureCall\0" /* 5109 */ - "isInClosureBind\0" /* 5126 */ - "withClosureBindScopeClasses\0" /* 5142 */ - "closure\0" /* 5170 */ - "PhpParser\\Node\\Expr\\Closure\0" /* 5178 */ - "callableParameters\0" /* 5206 */ - "nativeCallableParameters\0" /* 5225 */ - "enterAnonymousFunction\0" /* 5250 */ - "enterAnonymousFunctionWithoutReflection\0" /* 5273 */ - "typeHolder\0" /* 5313 */ - "PHPStan\\Analyser\\ExpressionTypeHolder\0" /* 5324 */ - "expressionTypeIsUnchangeable\0" /* 5362 */ - "invalidateStaticExpressions\0" /* 5391 */ - "arrowFunction\0" /* 5419 */ - "PhpParser\\Node\\Expr\\ArrowFunction\0" /* 5433 */ - "enterArrowFunction\0" /* 5467 */ - "enterArrowFunctionWithoutReflection\0" /* 5486 */ - "PhpParser\\Node\\Param\0" /* 5522 */ - "isParameterValueNullable\0" /* 5543 */ - "isNullable\0" /* 5568 */ - "isVariadic\0" /* 5579 */ - "getFunctionType\0" /* 5590 */ - "index\0" /* 5606 */ - "getCallableParameterType\0" /* 5612 */ - "startIndex\0" /* 5637 */ - "buildVariadicArrayTypeFromCallableParameters\0" /* 5648 */ - "nativeType\0" /* 5693 */ - "inferredType\0" /* 5704 */ - "intersectButNotNever\0" /* 5717 */ - "PhpParser\\Node\\Expr\\Match_\0" /* 5738 */ - "condType\0" /* 5765 */ - "condNativeType\0" /* 5774 */ - "enterMatch\0" /* 5789 */ - "iteratee\0" /* 5800 */ - "iterateeType\0" /* 5809 */ - "nativeIterateeType\0" /* 5822 */ - "valueName\0" /* 5841 */ - "keyName\0" /* 5851 */ - "valueByRef\0" /* 5859 */ - "enterForeach\0" /* 5870 */ - "enterForeachKey\0" /* 5883 */ - "catchType\0" /* 5899 */ - "enterCatchType\0" /* 5909 */ - "isPlainWrite\0" /* 5924 */ - "enterExpressionAssign\0" /* 5937 */ - "exitExpressionAssign\0" /* 5959 */ - "isInExpressionAssign\0" /* 5980 */ - "isInWriteExpressionAssign\0" /* 6001 */ - "setAllowedUndefinedExpression\0" /* 6027 */ - "unsetAllowedUndefinedExpression\0" /* 6057 */ - "isUndefinedExpressionAllowed\0" /* 6089 */ - "certainty\0" /* 6118 */ - "intertwinedPropagatedFrom\0" /* 6128 */ - "assignVariable\0" /* 6154 */ - "overwriteExpression\0" /* 6169 */ - "scope\0" /* 6189 */ - "rootType\0" /* 6195 */ - "assignedExpr\0" /* 6204 */ - "rootVariableName\0" /* 6217 */ - "native\0" /* 6234 */ - "resolveIntertwinedAssignedType\0" /* 6241 */ - "dimFetch\0" /* 6272 */ - "PhpParser\\Node\\Expr\\ArrayDimFetch\0" /* 6281 */ - "isDimFetchPathReachable\0" /* 6315 */ - "unsetExpression\0" /* 6339 */ - "getStateType\0" /* 6355 */ - "getScopeStateType\0" /* 6368 */ - "getScopeStateNativeType\0" /* 6386 */ - "resolveScopeStateType\0" /* 6410 */ - "specifyExpressionType\0" /* 6432 */ - "openSpecificationScope\0" /* 6454 */ - "isSpecifyExpressionTypeNoop\0" /* 6477 */ - "specifyExpressionTypeInPlace\0" /* 6505 */ - "assignExpression\0" /* 6534 */ - "fetchedOnType\0" /* 6551 */ - "assignInitializedProperty\0" /* 6565 */ - "expressionToInvalidate\0" /* 6591 */ - "requireMoreCharacters\0" /* 6614 */ - "invalidatingClass\0" /* 6636 */ - "keepPropertyFetches\0" /* 6654 */ - "invalidateExpression\0" /* 6674 */ - "isPrivatePropertyOfDifferentClass\0" /* 6695 */ - "invalidateMethodsOnExpression\0" /* 6729 */ - "setExpressionCertaintyKeepingType\0" /* 6759 */ - "isComplexUnionType\0" /* 6793 */ - "addTypeToExpression\0" /* 6812 */ - "typeToRemove\0" /* 6832 */ - "removeTypeFromExpression\0" /* 6845 */ - "filterByTruthyValue\0" /* 6870 */ - "filterByFalseyValue\0" /* 6890 */ - "specifiedTypes\0" /* 6910 */ - "applySpecifiedTypes\0" /* 6925 */ - "specifiedExpressions\0" /* 6945 */ - "processConditionalExpressionsAfterSpecifying\0" /* 6966 */ - "getConditionalExpressions\0" /* 7011 */ - "conditionalExpressionHolders\0" /* 7037 */ - "addConditionalExpressions\0" /* 7066 */ - "exitFirstLevelStatements\0" /* 7092 */ - "isInFirstLevelStatement\0" /* 7117 */ - "otherScope\0" /* 7141 */ - "preserveVacuousConditionals\0" /* 7152 */ - "mergeWith\0" /* 7180 */ - "ours\0" /* 7190 */ - "theirs\0" /* 7195 */ - "collectAbsorbedClosures\0" /* 7202 */ - "mergeWithVariableState\0" /* 7226 */ - "differingExpressionKeys\0" /* 7249 */ - "ourExpressionTypes\0" /* 7273 */ - "theirExpressionTypes\0" /* 7292 */ - "withoutPreciseClassConstantFetches\0" /* 7313 */ - "currentConditionalExpressions\0" /* 7348 */ - "sourceConditionalExpressions\0" /* 7378 */ - "otherExpressionTypes\0" /* 7407 */ - "preserveVacuousConditionalExpressions\0" /* 7428 */ - "ourConditionalExpressions\0" /* 7466 */ - "theirConditionalExpressions\0" /* 7492 */ - "mergeSameGuardConditionalExpressions\0" /* 7520 */ - "newConditionalExpressions\0" /* 7557 */ - "existingConditionalExpressions\0" /* 7583 */ - "mergeConditionalExpressions\0" /* 7614 */ - "calledMethodScope\0" /* 7642 */ - "mergeInitializedProperties\0" /* 7660 */ - "finallyScope\0" /* 7687 */ - "originalFinallyScope\0" /* 7700 */ - "processFinallyScope\0" /* 7721 */ - "ourVariableTypeHolders\0" /* 7741 */ - "finallyVariableTypeHolders\0" /* 7764 */ - "originalVariableTypeHolders\0" /* 7791 */ - "processFinallyScopeVariableTypeHolders\0" /* 7819 */ - "closureScope\0" /* 7858 */ - "prevScope\0" /* 7871 */ - "byRefUses\0" /* 7881 */ - "processClosureScope\0" /* 7891 */ - "finalScope\0" /* 7911 */ - "processAlwaysIterableForeachScopeWithoutPollute\0" /* 7922 */ - "writableVariableNames\0" /* 7970 */ - "generalizeWith\0" /* 7992 */ - "generalizeWithVariableState\0" /* 8007 */ - "variableTypeHolders\0" /* 8035 */ - "otherVariableTypeHolders\0" /* 8055 */ - "generalizeVariableTypeHolders\0" /* 8080 */ - "flattenUnionForGeneralization\0" /* 8110 */ - "a\0" /* 8140 */ - "b\0" /* 8142 */ - "depth\0" /* 8144 */ - "generalizeType\0" /* 8150 */ - "getArrayDepth\0" /* 8165 */ - "equals\0" /* 8179 */ - "otherConditionalExpressions\0" /* 8186 */ - "compareConditionalExpressions\0" /* 8214 */ - "compareVariableTypeHolders\0" /* 8244 */ - "propertyReflection\0" /* 8271 */ - "PHPStan\\Reflection\\PropertyReflection\0" /* 8290 */ - "canAccessProperty\0" /* 8328 */ - "PHPStan\\Reflection\\ExtendedPropertyReflection\0" /* 8346 */ - "canReadProperty\0" /* 8392 */ - "canWriteProperty\0" /* 8408 */ - "methodReflection\0" /* 8425 */ - "PHPStan\\Reflection\\MethodReflection\0" /* 8442 */ - "canCallMethod\0" /* 8478 */ - "constantReflection\0" /* 8492 */ - "PHPStan\\Reflection\\ClassConstantReflection\0" /* 8511 */ - "canAccessConstant\0" /* 8554 */ - "classMemberReflection\0" /* 8572 */ - "PHPStan\\Reflection\\ClassMemberReflection\0" /* 8594 */ - "canAccessClassMember\0" /* 8635 */ - "debug\0" /* 8656 */ - "typeWithMethod\0" /* 8662 */ - "methodName\0" /* 8677 */ - "filterTypeWithMethod\0" /* 8688 */ - "PHPStan\\Reflection\\ExtendedMethodReflection\0" /* 8709 */ - "getMethodReflection\0" /* 8753 */ - "getNakedMethod\0" /* 8773 */ - "typeWithProperty\0" /* 8788 */ - "getPropertyReflection\0" /* 8805 */ - "getInstancePropertyReflection\0" /* 8827 */ - "getStaticPropertyReflection\0" /* 8857 */ - "typeWithConstant\0" /* 8885 */ - "constantName\0" /* 8902 */ - "getConstantReflection\0" /* 8915 */ - "constantType\0" /* 8937 */ - "getConstantExplicitTypeFromConfig\0" /* 8950 */ - "getConstantTypes\0" /* 8984 */ - "getGlobalConstantType\0" /* 9001 */ - "nameString\0" /* 9023 */ - "createGlobalConstantFetches\0" /* 9034 */ - "getNativeConstantTypes\0" /* 9062 */ - "getIterableKeyType\0" /* 9085 */ - "getIterableValueType\0" /* 9104 */ - "PHPStan\\Php\\PhpVersions\0" /* 9125 */ - "getPhpVersion\0" /* 9149 */ - "isOverallPhpVersionRange\0" /* 9163 */ - "PhpParser\\Node\0" /* 9188 */ - "invokeNodeCallback\0" /* 9203 */ - "collectorType\0" /* 9222 */ - "data\0" /* 9236 */ - "emitCollectedData\0" /* 9241 */ - "extensionClass\0" /* 9259 */ - "trackValueDependency\0" /* 9274 */ - "file\0" /* 9295 */ - "trackFileDependency\0" /* 9300 */ - "directory\0" /* 9320 */ - "pattern\0" /* 9330 */ - "'*'\0" /* 9338 */ - "trackDirectoryDependency\0" /* 9342 */ - "trackClassDependency"; /* 9367 */ + "phpDocPureUnlessParameterPassedParameters\0" /* 4376 */ + "enterClassMethod\0" /* 4418 */ + "hook\0" /* 4435 */ + "PhpParser\\Node\\PropertyHook\0" /* 4440 */ + "propertyName\0" /* 4468 */ + "nativePropertyTypeNode\0" /* 4481 */ + "PhpParser\\Node\\Identifier|PhpParser\\Node\\Name|PhpParser\\Node\\ComplexType\0" /* 4504 */ + "phpDocPropertyType\0" /* 4577 */ + "enterPropertyHook\0" /* 4596 */ + "type\0" /* 4614 */ + "transformStaticType\0" /* 4619 */ + "functionLike\0" /* 4639 */ + "PhpParser\\Node\\FunctionLike\0" /* 4652 */ + "getRealParameterTypes\0" /* 4680 */ + "getRealParameterDefaultValues\0" /* 4702 */ + "PhpParser\\Node\\Stmt\\ClassMethod|PhpParser\\Node\\Stmt\\Function_|PhpParser\\Node\\PropertyHook\0" /* 4732 */ + "getParameterAttributes\0" /* 4822 */ + "PhpParser\\Node\\Stmt\\Function_\0" /* 4845 */ + "pureUnlessCallableIsImpureParameters\0" /* 4875 */ + "pureUnlessParameterPassedParameters\0" /* 4912 */ + "enterFunction\0" /* 4948 */ + "functionReflection\0" /* 4962 */ + "preserveConstructorScope\0" /* 4981 */ + "enterFunctionLike\0" /* 5006 */ + "namespaceName\0" /* 5024 */ + "enterNamespace\0" /* 5038 */ + "thisType\0" /* 5053 */ + "nativeThisType\0" /* 5062 */ + "scopeClasses\0" /* 5077 */ + "enterClosureBind\0" /* 5090 */ + "originalScope\0" /* 5107 */ + "restoreOriginalScopeAfterClosureBind\0" /* 5121 */ + "restoreThisScope\0" /* 5158 */ + "restoreThis\0" /* 5175 */ + "enterClosureCall\0" /* 5187 */ + "isInClosureBind\0" /* 5204 */ + "withClosureBindScopeClasses\0" /* 5220 */ + "closure\0" /* 5248 */ + "PhpParser\\Node\\Expr\\Closure\0" /* 5256 */ + "callableParameters\0" /* 5284 */ + "nativeCallableParameters\0" /* 5303 */ + "enterAnonymousFunction\0" /* 5328 */ + "enterAnonymousFunctionWithoutReflection\0" /* 5351 */ + "typeHolder\0" /* 5391 */ + "PHPStan\\Analyser\\ExpressionTypeHolder\0" /* 5402 */ + "expressionTypeIsUnchangeable\0" /* 5440 */ + "invalidateStaticExpressions\0" /* 5469 */ + "arrowFunction\0" /* 5497 */ + "PhpParser\\Node\\Expr\\ArrowFunction\0" /* 5511 */ + "enterArrowFunction\0" /* 5545 */ + "enterArrowFunctionWithoutReflection\0" /* 5564 */ + "PhpParser\\Node\\Param\0" /* 5600 */ + "isParameterValueNullable\0" /* 5621 */ + "isNullable\0" /* 5646 */ + "isVariadic\0" /* 5657 */ + "getFunctionType\0" /* 5668 */ + "index\0" /* 5684 */ + "getCallableParameterType\0" /* 5690 */ + "startIndex\0" /* 5715 */ + "buildVariadicArrayTypeFromCallableParameters\0" /* 5726 */ + "nativeType\0" /* 5771 */ + "inferredType\0" /* 5782 */ + "intersectButNotNever\0" /* 5795 */ + "PhpParser\\Node\\Expr\\Match_\0" /* 5816 */ + "condType\0" /* 5843 */ + "condNativeType\0" /* 5852 */ + "enterMatch\0" /* 5867 */ + "iteratee\0" /* 5878 */ + "iterateeType\0" /* 5887 */ + "nativeIterateeType\0" /* 5900 */ + "valueName\0" /* 5919 */ + "keyName\0" /* 5929 */ + "valueByRef\0" /* 5937 */ + "enterForeach\0" /* 5948 */ + "enterForeachKey\0" /* 5961 */ + "catchType\0" /* 5977 */ + "enterCatchType\0" /* 5987 */ + "isPlainWrite\0" /* 6002 */ + "enterExpressionAssign\0" /* 6015 */ + "exitExpressionAssign\0" /* 6037 */ + "isInExpressionAssign\0" /* 6058 */ + "isInWriteExpressionAssign\0" /* 6079 */ + "setAllowedUndefinedExpression\0" /* 6105 */ + "unsetAllowedUndefinedExpression\0" /* 6135 */ + "isUndefinedExpressionAllowed\0" /* 6167 */ + "certainty\0" /* 6196 */ + "intertwinedPropagatedFrom\0" /* 6206 */ + "assignVariable\0" /* 6232 */ + "overwriteExpression\0" /* 6247 */ + "scope\0" /* 6267 */ + "rootType\0" /* 6273 */ + "assignedExpr\0" /* 6282 */ + "rootVariableName\0" /* 6295 */ + "native\0" /* 6312 */ + "resolveIntertwinedAssignedType\0" /* 6319 */ + "dimFetch\0" /* 6350 */ + "PhpParser\\Node\\Expr\\ArrayDimFetch\0" /* 6359 */ + "isDimFetchPathReachable\0" /* 6393 */ + "unsetExpression\0" /* 6417 */ + "getStateType\0" /* 6433 */ + "getScopeStateType\0" /* 6446 */ + "getScopeStateNativeType\0" /* 6464 */ + "resolveScopeStateType\0" /* 6488 */ + "specifyExpressionType\0" /* 6510 */ + "openSpecificationScope\0" /* 6532 */ + "isSpecifyExpressionTypeNoop\0" /* 6555 */ + "specifyExpressionTypeInPlace\0" /* 6583 */ + "assignExpression\0" /* 6612 */ + "fetchedOnType\0" /* 6629 */ + "assignInitializedProperty\0" /* 6643 */ + "expressionToInvalidate\0" /* 6669 */ + "requireMoreCharacters\0" /* 6692 */ + "invalidatingClass\0" /* 6714 */ + "keepPropertyFetches\0" /* 6732 */ + "invalidateExpression\0" /* 6752 */ + "isPrivatePropertyOfDifferentClass\0" /* 6773 */ + "invalidateMethodsOnExpression\0" /* 6807 */ + "setExpressionCertaintyKeepingType\0" /* 6837 */ + "isComplexUnionType\0" /* 6871 */ + "addTypeToExpression\0" /* 6890 */ + "typeToRemove\0" /* 6910 */ + "removeTypeFromExpression\0" /* 6923 */ + "filterByTruthyValue\0" /* 6948 */ + "filterByFalseyValue\0" /* 6968 */ + "specifiedTypes\0" /* 6988 */ + "applySpecifiedTypes\0" /* 7003 */ + "specifiedExpressions\0" /* 7023 */ + "processConditionalExpressionsAfterSpecifying\0" /* 7044 */ + "getConditionalExpressions\0" /* 7089 */ + "conditionalExpressionHolders\0" /* 7115 */ + "addConditionalExpressions\0" /* 7144 */ + "exitFirstLevelStatements\0" /* 7170 */ + "isInFirstLevelStatement\0" /* 7195 */ + "otherScope\0" /* 7219 */ + "preserveVacuousConditionals\0" /* 7230 */ + "mergeWith\0" /* 7258 */ + "ours\0" /* 7268 */ + "theirs\0" /* 7273 */ + "collectAbsorbedClosures\0" /* 7280 */ + "mergeWithVariableState\0" /* 7304 */ + "differingExpressionKeys\0" /* 7327 */ + "ourExpressionTypes\0" /* 7351 */ + "theirExpressionTypes\0" /* 7370 */ + "withoutPreciseClassConstantFetches\0" /* 7391 */ + "currentConditionalExpressions\0" /* 7426 */ + "sourceConditionalExpressions\0" /* 7456 */ + "otherExpressionTypes\0" /* 7485 */ + "preserveVacuousConditionalExpressions\0" /* 7506 */ + "ourConditionalExpressions\0" /* 7544 */ + "theirConditionalExpressions\0" /* 7570 */ + "mergeSameGuardConditionalExpressions\0" /* 7598 */ + "newConditionalExpressions\0" /* 7635 */ + "existingConditionalExpressions\0" /* 7661 */ + "mergeConditionalExpressions\0" /* 7692 */ + "calledMethodScope\0" /* 7720 */ + "mergeInitializedProperties\0" /* 7738 */ + "finallyScope\0" /* 7765 */ + "originalFinallyScope\0" /* 7778 */ + "processFinallyScope\0" /* 7799 */ + "ourVariableTypeHolders\0" /* 7819 */ + "finallyVariableTypeHolders\0" /* 7842 */ + "originalVariableTypeHolders\0" /* 7869 */ + "processFinallyScopeVariableTypeHolders\0" /* 7897 */ + "closureScope\0" /* 7936 */ + "prevScope\0" /* 7949 */ + "byRefUses\0" /* 7959 */ + "processClosureScope\0" /* 7969 */ + "finalScope\0" /* 7989 */ + "processAlwaysIterableForeachScopeWithoutPollute\0" /* 8000 */ + "writableVariableNames\0" /* 8048 */ + "generalizeWith\0" /* 8070 */ + "generalizeWithVariableState\0" /* 8085 */ + "variableTypeHolders\0" /* 8113 */ + "otherVariableTypeHolders\0" /* 8133 */ + "generalizeVariableTypeHolders\0" /* 8158 */ + "flattenUnionForGeneralization\0" /* 8188 */ + "a\0" /* 8218 */ + "b\0" /* 8220 */ + "depth\0" /* 8222 */ + "generalizeType\0" /* 8228 */ + "getArrayDepth\0" /* 8243 */ + "equals\0" /* 8257 */ + "otherConditionalExpressions\0" /* 8264 */ + "compareConditionalExpressions\0" /* 8292 */ + "compareVariableTypeHolders\0" /* 8322 */ + "propertyReflection\0" /* 8349 */ + "PHPStan\\Reflection\\PropertyReflection\0" /* 8368 */ + "canAccessProperty\0" /* 8406 */ + "PHPStan\\Reflection\\ExtendedPropertyReflection\0" /* 8424 */ + "canReadProperty\0" /* 8470 */ + "canWriteProperty\0" /* 8486 */ + "methodReflection\0" /* 8503 */ + "PHPStan\\Reflection\\MethodReflection\0" /* 8520 */ + "canCallMethod\0" /* 8556 */ + "constantReflection\0" /* 8570 */ + "PHPStan\\Reflection\\ClassConstantReflection\0" /* 8589 */ + "canAccessConstant\0" /* 8632 */ + "classMemberReflection\0" /* 8650 */ + "PHPStan\\Reflection\\ClassMemberReflection\0" /* 8672 */ + "canAccessClassMember\0" /* 8713 */ + "debug\0" /* 8734 */ + "typeWithMethod\0" /* 8740 */ + "methodName\0" /* 8755 */ + "filterTypeWithMethod\0" /* 8766 */ + "PHPStan\\Reflection\\ExtendedMethodReflection\0" /* 8787 */ + "getMethodReflection\0" /* 8831 */ + "getNakedMethod\0" /* 8851 */ + "typeWithProperty\0" /* 8866 */ + "getPropertyReflection\0" /* 8883 */ + "getInstancePropertyReflection\0" /* 8905 */ + "getStaticPropertyReflection\0" /* 8935 */ + "typeWithConstant\0" /* 8963 */ + "constantName\0" /* 8980 */ + "getConstantReflection\0" /* 8993 */ + "constantType\0" /* 9015 */ + "getConstantExplicitTypeFromConfig\0" /* 9028 */ + "getConstantTypes\0" /* 9062 */ + "getGlobalConstantType\0" /* 9079 */ + "nameString\0" /* 9101 */ + "createGlobalConstantFetches\0" /* 9112 */ + "getNativeConstantTypes\0" /* 9140 */ + "getIterableKeyType\0" /* 9163 */ + "getIterableValueType\0" /* 9182 */ + "PHPStan\\Php\\PhpVersions\0" /* 9203 */ + "getPhpVersion\0" /* 9227 */ + "isOverallPhpVersionRange\0" /* 9241 */ + "PhpParser\\Node\0" /* 9266 */ + "invokeNodeCallback\0" /* 9281 */ + "collectorType\0" /* 9300 */ + "data\0" /* 9314 */ + "emitCollectedData\0" /* 9319 */ + "extensionClass\0" /* 9337 */ + "trackValueDependency\0" /* 9352 */ + "file\0" /* 9373 */ + "trackFileDependency\0" /* 9378 */ + "directory\0" /* 9398 */ + "pattern\0" /* 9408 */ + "'*'\0" /* 9416 */ + "trackDirectoryDependency\0" /* 9420 */ + "trackClassDependency"; /* 9445 */ inline constexpr reg::PackedArg args[] = { reg::packed(0, 0, 10), /* __construct $container */ reg::packed(48, 0, 61), /* __construct $scopeFactory */ @@ -736,11 +738,12 @@ inline constexpr reg::PackedArg args[] = { reg::packed(4264, MAY_BE_BOOL, reg::NoString, false, false, 891), /* enterClassMethod $isConstructor */ reg::packed(4278, MAY_BE_NULL, 4298, false, false, 867), /* enterClassMethod $resolvedPhpDocBlock */ reg::packed(4333, MAY_BE_ARRAY, reg::NoString, false, false, 991), /* enterClassMethod $phpDocPureUnlessCallableIsImpureParameters */ + reg::packed(4376, MAY_BE_ARRAY, reg::NoString, false, false, 991), /* enterClassMethod $phpDocPureUnlessParameterPassedParameters */ reg::packed(1476, 0, 1261), /* enterClassMethod return */ - reg::packed(4393, 0, 4398), /* enterPropertyHook $hook */ - reg::packed(4426, MAY_BE_STRING), /* enterPropertyHook $propertyName */ - reg::packed(4439, MAY_BE_NULL, 4462), /* enterPropertyHook $nativePropertyTypeNode */ - reg::packed(4535, MAY_BE_NULL, 2162), /* enterPropertyHook $phpDocPropertyType */ + reg::packed(4435, 0, 4440), /* enterPropertyHook $hook */ + reg::packed(4468, MAY_BE_STRING), /* enterPropertyHook $propertyName */ + reg::packed(4481, MAY_BE_NULL, 4504), /* enterPropertyHook $nativePropertyTypeNode */ + reg::packed(4577, MAY_BE_NULL, 2162), /* enterPropertyHook $phpDocPropertyType */ reg::packed(3982, MAY_BE_ARRAY), /* enterPropertyHook $phpDocParameterTypes */ reg::packed(4020, MAY_BE_NULL, 2162), /* enterPropertyHook $throwType */ reg::packed(4030, MAY_BE_NULL | MAY_BE_STRING), /* enterPropertyHook $deprecatedDescription */ @@ -749,15 +752,15 @@ inline constexpr reg::PackedArg args[] = { reg::packed(4163, MAY_BE_NULL | MAY_BE_STRING), /* enterPropertyHook $phpDocComment */ reg::packed(4278, MAY_BE_NULL, 4298, false, false, 867), /* enterPropertyHook $resolvedPhpDocBlock */ reg::packed(1476, 0, 1261), /* enterPropertyHook return */ - reg::packed(4572, 0, 2162), /* transformStaticType $type */ + reg::packed(4614, 0, 2162), /* transformStaticType $type */ reg::packed(1476, 0, 2162), /* transformStaticType return */ - reg::packed(4597, 0, 4610), /* getRealParameterTypes $functionLike */ + reg::packed(4639, 0, 4652), /* getRealParameterTypes $functionLike */ reg::packed(1476, MAY_BE_ARRAY), /* getRealParameterTypes return */ - reg::packed(4597, 0, 4610), /* getRealParameterDefaultValues $functionLike */ + reg::packed(4639, 0, 4652), /* getRealParameterDefaultValues $functionLike */ reg::packed(1476, MAY_BE_ARRAY), /* getRealParameterDefaultValues return */ - reg::packed(4597, 0, 4690), /* getParameterAttributes $functionLike */ + reg::packed(4639, 0, 4732), /* getParameterAttributes $functionLike */ reg::packed(1476, MAY_BE_ARRAY), /* getParameterAttributes return */ - reg::packed(897, 0, 4803), /* enterFunction $function */ + reg::packed(897, 0, 4845), /* enterFunction $function */ reg::packed(3929, 0, 3945), /* enterFunction $templateTypeMap */ reg::packed(3982, MAY_BE_ARRAY), /* enterFunction $phpDocParameterTypes */ reg::packed(4003, MAY_BE_NULL, 2162), /* enterFunction $phpDocReturnType */ @@ -772,86 +775,87 @@ inline constexpr reg::PackedArg args[] = { reg::packed(4177, MAY_BE_ARRAY, reg::NoString, false, false, 991), /* enterFunction $parameterOutTypes */ reg::packed(4195, MAY_BE_ARRAY, reg::NoString, false, false, 991), /* enterFunction $immediatelyInvokedCallableParameters */ reg::packed(4232, MAY_BE_ARRAY, reg::NoString, false, false, 991), /* enterFunction $phpDocClosureThisTypeParameters */ - reg::packed(4833, MAY_BE_ARRAY, reg::NoString, false, false, 991), /* enterFunction $pureUnlessCallableIsImpureParameters */ + reg::packed(4875, MAY_BE_ARRAY, reg::NoString, false, false, 991), /* enterFunction $pureUnlessCallableIsImpureParameters */ + reg::packed(4912, MAY_BE_ARRAY, reg::NoString, false, false, 991), /* enterFunction $pureUnlessParameterPassedParameters */ reg::packed(1476, 0, 1261), /* enterFunction return */ - reg::packed(4884, 0, 906), /* enterFunctionLike $functionReflection */ - reg::packed(4903, MAY_BE_BOOL), /* enterFunctionLike $preserveConstructorScope */ + reg::packed(4962, 0, 906), /* enterFunctionLike $functionReflection */ + reg::packed(4981, MAY_BE_BOOL), /* enterFunctionLike $preserveConstructorScope */ reg::packed(1476, 0, 1261), /* enterFunctionLike return */ - reg::packed(4946, MAY_BE_STRING), /* enterNamespace $namespaceName */ + reg::packed(5024, MAY_BE_STRING), /* enterNamespace $namespaceName */ reg::packed(1476, 0, 1261), /* enterNamespace return */ - reg::packed(4975, MAY_BE_NULL, 2162), /* enterClosureBind $thisType */ - reg::packed(4984, MAY_BE_NULL, 2162), /* enterClosureBind $nativeThisType */ - reg::packed(4999, MAY_BE_ARRAY), /* enterClosureBind $scopeClasses */ + reg::packed(5053, MAY_BE_NULL, 2162), /* enterClosureBind $thisType */ + reg::packed(5062, MAY_BE_NULL, 2162), /* enterClosureBind $nativeThisType */ + reg::packed(5077, MAY_BE_ARRAY), /* enterClosureBind $scopeClasses */ reg::packed(1476, 0, 1261), /* enterClosureBind return */ - reg::packed(5029, 0, 1261), /* restoreOriginalScopeAfterClosureBind $originalScope */ + reg::packed(5107, 0, 1261), /* restoreOriginalScopeAfterClosureBind $originalScope */ reg::packed(1476, 0, 1261), /* restoreOriginalScopeAfterClosureBind return */ - reg::packed(5080, 0, 1261), /* restoreThis $restoreThisScope */ + reg::packed(5158, 0, 1261), /* restoreThis $restoreThisScope */ reg::packed(1476, 0, 1261), /* restoreThis return */ - reg::packed(4975, 0, 2162), /* enterClosureCall $thisType */ - reg::packed(4984, 0, 2162), /* enterClosureCall $nativeThisType */ + reg::packed(5053, 0, 2162), /* enterClosureCall $thisType */ + reg::packed(5062, 0, 2162), /* enterClosureCall $nativeThisType */ reg::packed(1476, 0, 1261), /* enterClosureCall return */ reg::packed(1476, MAY_BE_BOOL), /* isInClosureBind return */ - reg::packed(4999, MAY_BE_ARRAY), /* withClosureBindScopeClasses $scopeClasses */ + reg::packed(5077, MAY_BE_ARRAY), /* withClosureBindScopeClasses $scopeClasses */ reg::packed(1476, 0, 1261), /* withClosureBindScopeClasses return */ - reg::packed(5170, 0, 5178), /* enterAnonymousFunction $closure */ - reg::packed(5206, MAY_BE_NULL | MAY_BE_ARRAY), /* enterAnonymousFunction $callableParameters */ - reg::packed(5225, MAY_BE_NULL | MAY_BE_ARRAY, reg::NoString, false, false, 867), /* enterAnonymousFunction $nativeCallableParameters */ + reg::packed(5248, 0, 5256), /* enterAnonymousFunction $closure */ + reg::packed(5284, MAY_BE_NULL | MAY_BE_ARRAY), /* enterAnonymousFunction $callableParameters */ + reg::packed(5303, MAY_BE_NULL | MAY_BE_ARRAY, reg::NoString, false, false, 867), /* enterAnonymousFunction $nativeCallableParameters */ reg::packed(1476, 0, 1261), /* enterAnonymousFunction return */ - reg::packed(5170, 0, 5178), /* enterAnonymousFunctionWithoutReflection $closure */ - reg::packed(5206, MAY_BE_NULL | MAY_BE_ARRAY), /* enterAnonymousFunctionWithoutReflection $callableParameters */ - reg::packed(5225, MAY_BE_NULL | MAY_BE_ARRAY), /* enterAnonymousFunctionWithoutReflection $nativeCallableParameters */ + reg::packed(5248, 0, 5256), /* enterAnonymousFunctionWithoutReflection $closure */ + reg::packed(5284, MAY_BE_NULL | MAY_BE_ARRAY), /* enterAnonymousFunctionWithoutReflection $callableParameters */ + reg::packed(5303, MAY_BE_NULL | MAY_BE_ARRAY), /* enterAnonymousFunctionWithoutReflection $nativeCallableParameters */ reg::packed(1476, 0, 1261), /* enterAnonymousFunctionWithoutReflection return */ - reg::packed(5313, 0, 5324), /* expressionTypeIsUnchangeable $typeHolder */ + reg::packed(5391, 0, 5402), /* expressionTypeIsUnchangeable $typeHolder */ reg::packed(1476, MAY_BE_BOOL), /* expressionTypeIsUnchangeable return */ reg::packed(975, MAY_BE_ARRAY), /* invalidateStaticExpressions $expressionTypes */ reg::packed(1476, MAY_BE_ARRAY), /* invalidateStaticExpressions return */ - reg::packed(5419, 0, 5433), /* enterArrowFunction $arrowFunction */ - reg::packed(5206, MAY_BE_NULL | MAY_BE_ARRAY), /* enterArrowFunction $callableParameters */ - reg::packed(5225, MAY_BE_NULL | MAY_BE_ARRAY, reg::NoString, false, false, 867), /* enterArrowFunction $nativeCallableParameters */ + reg::packed(5497, 0, 5511), /* enterArrowFunction $arrowFunction */ + reg::packed(5284, MAY_BE_NULL | MAY_BE_ARRAY), /* enterArrowFunction $callableParameters */ + reg::packed(5303, MAY_BE_NULL | MAY_BE_ARRAY, reg::NoString, false, false, 867), /* enterArrowFunction $nativeCallableParameters */ reg::packed(1476, 0, 1261), /* enterArrowFunction return */ - reg::packed(5419, 0, 5433), /* enterArrowFunctionWithoutReflection $arrowFunction */ - reg::packed(5206, MAY_BE_NULL | MAY_BE_ARRAY), /* enterArrowFunctionWithoutReflection $callableParameters */ - reg::packed(5225, MAY_BE_NULL | MAY_BE_ARRAY), /* enterArrowFunctionWithoutReflection $nativeCallableParameters */ + reg::packed(5497, 0, 5511), /* enterArrowFunctionWithoutReflection $arrowFunction */ + reg::packed(5284, MAY_BE_NULL | MAY_BE_ARRAY), /* enterArrowFunctionWithoutReflection $callableParameters */ + reg::packed(5303, MAY_BE_NULL | MAY_BE_ARRAY), /* enterArrowFunctionWithoutReflection $nativeCallableParameters */ reg::packed(1476, 0, 1261), /* enterArrowFunctionWithoutReflection return */ - reg::packed(3617, 0, 5522), /* isParameterValueNullable $parameter */ + reg::packed(3617, 0, 5600), /* isParameterValueNullable $parameter */ reg::packed(1476, MAY_BE_BOOL), /* isParameterValueNullable return */ - reg::packed(4572, 0), /* getFunctionType $type */ - reg::packed(5568, MAY_BE_BOOL), /* getFunctionType $isNullable */ - reg::packed(5579, MAY_BE_BOOL), /* getFunctionType $isVariadic */ + reg::packed(4614, 0), /* getFunctionType $type */ + reg::packed(5646, MAY_BE_BOOL), /* getFunctionType $isNullable */ + reg::packed(5657, MAY_BE_BOOL), /* getFunctionType $isVariadic */ reg::packed(1476, 0, 2162), /* getFunctionType return */ - reg::packed(3617, 0, 5522), /* getCallableParameterType $parameter */ - reg::packed(5206, MAY_BE_ARRAY), /* getCallableParameterType $callableParameters */ - reg::packed(5606, MAY_BE_LONG), /* getCallableParameterType $index */ + reg::packed(3617, 0, 5600), /* getCallableParameterType $parameter */ + reg::packed(5284, MAY_BE_ARRAY), /* getCallableParameterType $callableParameters */ + reg::packed(5684, MAY_BE_LONG), /* getCallableParameterType $index */ reg::packed(1476, 0, 2162), /* getCallableParameterType return */ - reg::packed(5206, MAY_BE_ARRAY), /* buildVariadicArrayTypeFromCallableParameters $callableParameters */ - reg::packed(5637, MAY_BE_LONG), /* buildVariadicArrayTypeFromCallableParameters $startIndex */ + reg::packed(5284, MAY_BE_ARRAY), /* buildVariadicArrayTypeFromCallableParameters $callableParameters */ + reg::packed(5715, MAY_BE_LONG), /* buildVariadicArrayTypeFromCallableParameters $startIndex */ reg::packed(1476, 0, 2162), /* buildVariadicArrayTypeFromCallableParameters return */ - reg::packed(5693, 0, 2162), /* intersectButNotNever $nativeType */ - reg::packed(5704, 0, 2162), /* intersectButNotNever $inferredType */ + reg::packed(5771, 0, 2162), /* intersectButNotNever $nativeType */ + reg::packed(5782, 0, 2162), /* intersectButNotNever $inferredType */ reg::packed(1476, 0, 2162), /* intersectButNotNever return */ - reg::packed(1704, 0, 5738), /* enterMatch $expr */ - reg::packed(5765, 0, 2162), /* enterMatch $condType */ - reg::packed(5774, 0, 2162), /* enterMatch $condNativeType */ + reg::packed(1704, 0, 5816), /* enterMatch $expr */ + reg::packed(5843, 0, 2162), /* enterMatch $condType */ + reg::packed(5852, 0, 2162), /* enterMatch $condNativeType */ reg::packed(1476, 0, 1261), /* enterMatch return */ - reg::packed(5029, 0, 1261), /* enterForeach $originalScope */ - reg::packed(5800, 0, 2241), /* enterForeach $iteratee */ - reg::packed(5809, 0, 2162), /* enterForeach $iterateeType */ - reg::packed(5822, 0, 2162), /* enterForeach $nativeIterateeType */ - reg::packed(5841, MAY_BE_STRING), /* enterForeach $valueName */ - reg::packed(5851, MAY_BE_NULL | MAY_BE_STRING), /* enterForeach $keyName */ - reg::packed(5859, MAY_BE_BOOL), /* enterForeach $valueByRef */ + reg::packed(5107, 0, 1261), /* enterForeach $originalScope */ + reg::packed(5878, 0, 2241), /* enterForeach $iteratee */ + reg::packed(5887, 0, 2162), /* enterForeach $iterateeType */ + reg::packed(5900, 0, 2162), /* enterForeach $nativeIterateeType */ + reg::packed(5919, MAY_BE_STRING), /* enterForeach $valueName */ + reg::packed(5929, MAY_BE_NULL | MAY_BE_STRING), /* enterForeach $keyName */ + reg::packed(5937, MAY_BE_BOOL), /* enterForeach $valueByRef */ reg::packed(1476, 0, 1261), /* enterForeach return */ - reg::packed(5029, 0, 1261), /* enterForeachKey $originalScope */ - reg::packed(5800, 0, 2241), /* enterForeachKey $iteratee */ - reg::packed(5809, 0, 2162), /* enterForeachKey $iterateeType */ - reg::packed(5822, 0, 2162), /* enterForeachKey $nativeIterateeType */ - reg::packed(5851, MAY_BE_STRING), /* enterForeachKey $keyName */ + reg::packed(5107, 0, 1261), /* enterForeachKey $originalScope */ + reg::packed(5878, 0, 2241), /* enterForeachKey $iteratee */ + reg::packed(5887, 0, 2162), /* enterForeachKey $iterateeType */ + reg::packed(5900, 0, 2162), /* enterForeachKey $nativeIterateeType */ + reg::packed(5929, MAY_BE_STRING), /* enterForeachKey $keyName */ reg::packed(1476, 0, 1261), /* enterForeachKey return */ - reg::packed(5899, 0, 2162), /* enterCatchType $catchType */ + reg::packed(5977, 0, 2162), /* enterCatchType $catchType */ reg::packed(2112, MAY_BE_NULL | MAY_BE_STRING), /* enterCatchType $variableName */ reg::packed(1476, 0, 1261), /* enterCatchType return */ reg::packed(1704, 0, 2241), /* enterExpressionAssign $expr */ - reg::packed(5924, MAY_BE_BOOL, reg::NoString, false, false, 1140), /* enterExpressionAssign $isPlainWrite */ + reg::packed(6002, MAY_BE_BOOL, reg::NoString, false, false, 1140), /* enterExpressionAssign $isPlainWrite */ reg::packed(1476, 0, 1261), /* enterExpressionAssign return */ reg::packed(1704, 0, 2241), /* exitExpressionAssign $expr */ reg::packed(1476, 0, 1261), /* exitExpressionAssign return */ @@ -866,23 +870,23 @@ inline constexpr reg::PackedArg args[] = { reg::packed(1704, 0, 2241), /* isUndefinedExpressionAllowed $expr */ reg::packed(1476, MAY_BE_BOOL), /* isUndefinedExpressionAllowed return */ reg::packed(2112, MAY_BE_STRING), /* assignVariable $variableName */ - reg::packed(4572, 0, 2162), /* assignVariable $type */ - reg::packed(5693, 0, 2162), /* assignVariable $nativeType */ - reg::packed(6118, 0, 2125), /* assignVariable $certainty */ - reg::packed(6128, MAY_BE_ARRAY, reg::NoString, false, false, 991), /* assignVariable $intertwinedPropagatedFrom */ + reg::packed(4614, 0, 2162), /* assignVariable $type */ + reg::packed(5771, 0, 2162), /* assignVariable $nativeType */ + reg::packed(6196, 0, 2125), /* assignVariable $certainty */ + reg::packed(6206, MAY_BE_ARRAY, reg::NoString, false, false, 991), /* assignVariable $intertwinedPropagatedFrom */ reg::packed(1476, 0, 1261), /* assignVariable return */ reg::packed(1704, 0, 2241), /* overwriteExpression $expr */ - reg::packed(4572, 0, 2162), /* overwriteExpression $type */ - reg::packed(5693, 0, 2162), /* overwriteExpression $nativeType */ + reg::packed(4614, 0, 2162), /* overwriteExpression $type */ + reg::packed(5771, 0, 2162), /* overwriteExpression $nativeType */ reg::packed(1476, 0, 1261), /* overwriteExpression return */ - reg::packed(6189, 0, 1261), /* resolveIntertwinedAssignedType $scope */ - reg::packed(6195, 0, 2162), /* resolveIntertwinedAssignedType $rootType */ - reg::packed(6204, 0, 2241), /* resolveIntertwinedAssignedType $assignedExpr */ - reg::packed(6217, MAY_BE_STRING), /* resolveIntertwinedAssignedType $rootVariableName */ - reg::packed(6234, MAY_BE_BOOL), /* resolveIntertwinedAssignedType $native */ + reg::packed(6267, 0, 1261), /* resolveIntertwinedAssignedType $scope */ + reg::packed(6273, 0, 2162), /* resolveIntertwinedAssignedType $rootType */ + reg::packed(6282, 0, 2241), /* resolveIntertwinedAssignedType $assignedExpr */ + reg::packed(6295, MAY_BE_STRING), /* resolveIntertwinedAssignedType $rootVariableName */ + reg::packed(6312, MAY_BE_BOOL), /* resolveIntertwinedAssignedType $native */ reg::packed(1476, 0, 2162), /* resolveIntertwinedAssignedType return */ - reg::packed(6189, 0, 1261), /* isDimFetchPathReachable $scope */ - reg::packed(6272, 0, 6281), /* isDimFetchPathReachable $dimFetch */ + reg::packed(6267, 0, 1261), /* isDimFetchPathReachable $scope */ + reg::packed(6350, 0, 6359), /* isDimFetchPathReachable $dimFetch */ reg::packed(1476, MAY_BE_BOOL), /* isDimFetchPathReachable return */ reg::packed(1704, 0, 2241), /* unsetExpression $expr */ reg::packed(1476, 0, 1261), /* unsetExpression return */ @@ -893,193 +897,193 @@ inline constexpr reg::PackedArg args[] = { reg::packed(1704, 0, 2241), /* getScopeStateNativeType $expr */ reg::packed(1476, 0, 2162), /* getScopeStateNativeType return */ reg::packed(1704, 0, 2241), /* resolveScopeStateType $expr */ - reg::packed(6234, MAY_BE_BOOL), /* resolveScopeStateType $native */ + reg::packed(6312, MAY_BE_BOOL), /* resolveScopeStateType $native */ reg::packed(1476, 0, 2162), /* resolveScopeStateType return */ reg::packed(1704, 0, 2241), /* specifyExpressionType $expr */ - reg::packed(4572, 0, 2162), /* specifyExpressionType $type */ - reg::packed(5693, 0, 2162), /* specifyExpressionType $nativeType */ - reg::packed(6118, 0, 2125), /* specifyExpressionType $certainty */ + reg::packed(4614, 0, 2162), /* specifyExpressionType $type */ + reg::packed(5771, 0, 2162), /* specifyExpressionType $nativeType */ + reg::packed(6196, 0, 2125), /* specifyExpressionType $certainty */ reg::packed(1476, 0, 1261), /* specifyExpressionType return */ reg::packed(1476, 0, 1261), /* openSpecificationScope return */ reg::packed(1704, 0, 2241), /* isSpecifyExpressionTypeNoop $expr */ - reg::packed(4572, 0, 2162), /* isSpecifyExpressionTypeNoop $type */ + reg::packed(4614, 0, 2162), /* isSpecifyExpressionTypeNoop $type */ reg::packed(1476, MAY_BE_BOOL), /* isSpecifyExpressionTypeNoop return */ reg::packed(1704, 0, 2241), /* specifyExpressionTypeInPlace $expr */ - reg::packed(4572, 0, 2162), /* specifyExpressionTypeInPlace $type */ - reg::packed(5693, 0, 2162), /* specifyExpressionTypeInPlace $nativeType */ - reg::packed(6118, 0, 2125), /* specifyExpressionTypeInPlace $certainty */ + reg::packed(4614, 0, 2162), /* specifyExpressionTypeInPlace $type */ + reg::packed(5771, 0, 2162), /* specifyExpressionTypeInPlace $nativeType */ + reg::packed(6196, 0, 2125), /* specifyExpressionTypeInPlace $certainty */ reg::packed(1476, MAY_BE_VOID), /* specifyExpressionTypeInPlace return */ reg::packed(1704, 0, 2241), /* assignExpression $expr */ - reg::packed(4572, 0, 2162), /* assignExpression $type */ - reg::packed(5693, 0, 2162), /* assignExpression $nativeType */ + reg::packed(4614, 0, 2162), /* assignExpression $type */ + reg::packed(5771, 0, 2162), /* assignExpression $nativeType */ reg::packed(1476, 0, 1261), /* assignExpression return */ - reg::packed(6551, 0, 2162), /* assignInitializedProperty $fetchedOnType */ - reg::packed(4426, MAY_BE_STRING), /* assignInitializedProperty $propertyName */ + reg::packed(6629, 0, 2162), /* assignInitializedProperty $fetchedOnType */ + reg::packed(4468, MAY_BE_STRING), /* assignInitializedProperty $propertyName */ reg::packed(1476, 0, 1261), /* assignInitializedProperty return */ - reg::packed(6591, 0, 2241), /* invalidateExpression $expressionToInvalidate */ - reg::packed(6614, MAY_BE_BOOL, reg::NoString, false, false, 891), /* invalidateExpression $requireMoreCharacters */ - reg::packed(6636, MAY_BE_NULL, 1803, false, false, 867), /* invalidateExpression $invalidatingClass */ - reg::packed(6654, MAY_BE_BOOL, reg::NoString, false, false, 891), /* invalidateExpression $keepPropertyFetches */ + reg::packed(6669, 0, 2241), /* invalidateExpression $expressionToInvalidate */ + reg::packed(6692, MAY_BE_BOOL, reg::NoString, false, false, 891), /* invalidateExpression $requireMoreCharacters */ + reg::packed(6714, MAY_BE_NULL, 1803, false, false, 867), /* invalidateExpression $invalidatingClass */ + reg::packed(6732, MAY_BE_BOOL, reg::NoString, false, false, 891), /* invalidateExpression $keepPropertyFetches */ reg::packed(1476, 0, 1261), /* invalidateExpression return */ reg::packed(1704, 0, 2241), /* isPrivatePropertyOfDifferentClass $expr */ - reg::packed(6636, 0, 1803), /* isPrivatePropertyOfDifferentClass $invalidatingClass */ + reg::packed(6714, 0, 1803), /* isPrivatePropertyOfDifferentClass $invalidatingClass */ reg::packed(1476, MAY_BE_BOOL), /* isPrivatePropertyOfDifferentClass return */ - reg::packed(6591, 0, 2241), /* invalidateMethodsOnExpression $expressionToInvalidate */ + reg::packed(6669, 0, 2241), /* invalidateMethodsOnExpression $expressionToInvalidate */ reg::packed(1476, 0, 1261), /* invalidateMethodsOnExpression return */ reg::packed(1704, 0, 2241), /* setExpressionCertaintyKeepingType $expr */ - reg::packed(6118, 0, 2125), /* setExpressionCertaintyKeepingType $certainty */ + reg::packed(6196, 0, 2125), /* setExpressionCertaintyKeepingType $certainty */ reg::packed(1476, 0, 1261), /* setExpressionCertaintyKeepingType return */ - reg::packed(4572, 0, 2162), /* isComplexUnionType $type */ + reg::packed(4614, 0, 2162), /* isComplexUnionType $type */ reg::packed(1476, MAY_BE_BOOL), /* isComplexUnionType return */ reg::packed(1704, 0, 2241), /* addTypeToExpression $expr */ - reg::packed(4572, 0, 2162), /* addTypeToExpression $type */ + reg::packed(4614, 0, 2162), /* addTypeToExpression $type */ reg::packed(1476, 0, 1261), /* addTypeToExpression return */ reg::packed(1704, 0, 2241), /* removeTypeFromExpression $expr */ - reg::packed(6832, 0, 2162), /* removeTypeFromExpression $typeToRemove */ + reg::packed(6910, 0, 2162), /* removeTypeFromExpression $typeToRemove */ reg::packed(1476, 0, 1261), /* removeTypeFromExpression return */ reg::packed(1704, 0, 2241), /* filterByTruthyValue $expr */ reg::packed(1476, 0, 1261), /* filterByTruthyValue return */ reg::packed(1704, 0, 2241), /* filterByFalseyValue $expr */ reg::packed(1476, 0, 1261), /* filterByFalseyValue return */ - reg::packed(6910, 0, 2775), /* applySpecifiedTypes $specifiedTypes */ + reg::packed(6988, 0, 2775), /* applySpecifiedTypes $specifiedTypes */ reg::packed(1476, 0, 1261), /* applySpecifiedTypes return */ - reg::packed(6945, MAY_BE_ARRAY), /* processConditionalExpressionsAfterSpecifying $specifiedExpressions */ + reg::packed(7023, MAY_BE_ARRAY), /* processConditionalExpressionsAfterSpecifying $specifiedExpressions */ reg::packed(1476, 0, 1261), /* processConditionalExpressionsAfterSpecifying return */ reg::packed(1476, MAY_BE_ARRAY), /* getConditionalExpressions return */ reg::packed(2623, MAY_BE_STRING), /* addConditionalExpressions $exprString */ - reg::packed(7037, MAY_BE_ARRAY), /* addConditionalExpressions $conditionalExpressionHolders */ + reg::packed(7115, MAY_BE_ARRAY), /* addConditionalExpressions $conditionalExpressionHolders */ reg::packed(1476, 0, 1261), /* addConditionalExpressions return */ reg::packed(1476, 0, 1261), /* exitFirstLevelStatements return */ reg::packed(1476, MAY_BE_BOOL), /* isInFirstLevelStatement return */ - reg::packed(7141, MAY_BE_NULL, 1261), /* mergeWith $otherScope */ - reg::packed(7152, MAY_BE_BOOL, reg::NoString, false, false, 891), /* mergeWith $preserveVacuousConditionals */ + reg::packed(7219, MAY_BE_NULL, 1261), /* mergeWith $otherScope */ + reg::packed(7230, MAY_BE_BOOL, reg::NoString, false, false, 891), /* mergeWith $preserveVacuousConditionals */ reg::packed(1476, 0, 1261), /* mergeWith return */ - reg::packed(7190, 0, 1261), /* collectAbsorbedClosures $ours */ - reg::packed(7195, 0, 1261), /* collectAbsorbedClosures $theirs */ + reg::packed(7268, 0, 1261), /* collectAbsorbedClosures $ours */ + reg::packed(7273, 0, 1261), /* collectAbsorbedClosures $theirs */ reg::packed(1476, MAY_BE_NULL, 1410), /* collectAbsorbedClosures return */ - reg::packed(7141, MAY_BE_NULL, 1261), /* mergeWithVariableState $otherScope */ - reg::packed(7152, MAY_BE_BOOL, reg::NoString, false, false, 891), /* mergeWithVariableState $preserveVacuousConditionals */ + reg::packed(7219, MAY_BE_NULL, 1261), /* mergeWithVariableState $otherScope */ + reg::packed(7230, MAY_BE_BOOL, reg::NoString, false, false, 891), /* mergeWithVariableState $preserveVacuousConditionals */ reg::packed(1476, 0, 1261), /* mergeWithVariableState return */ - reg::packed(7249, MAY_BE_ARRAY), /* withoutPreciseClassConstantFetches $differingExpressionKeys */ - reg::packed(7273, MAY_BE_ARRAY), /* withoutPreciseClassConstantFetches $ourExpressionTypes */ - reg::packed(7292, MAY_BE_ARRAY), /* withoutPreciseClassConstantFetches $theirExpressionTypes */ + reg::packed(7327, MAY_BE_ARRAY), /* withoutPreciseClassConstantFetches $differingExpressionKeys */ + reg::packed(7351, MAY_BE_ARRAY), /* withoutPreciseClassConstantFetches $ourExpressionTypes */ + reg::packed(7370, MAY_BE_ARRAY), /* withoutPreciseClassConstantFetches $theirExpressionTypes */ reg::packed(1476, MAY_BE_ARRAY), /* withoutPreciseClassConstantFetches return */ - reg::packed(7348, MAY_BE_ARRAY), /* preserveVacuousConditionalExpressions $currentConditionalExpressions */ - reg::packed(7378, MAY_BE_ARRAY), /* preserveVacuousConditionalExpressions $sourceConditionalExpressions */ - reg::packed(7407, MAY_BE_ARRAY), /* preserveVacuousConditionalExpressions $otherExpressionTypes */ + reg::packed(7426, MAY_BE_ARRAY), /* preserveVacuousConditionalExpressions $currentConditionalExpressions */ + reg::packed(7456, MAY_BE_ARRAY), /* preserveVacuousConditionalExpressions $sourceConditionalExpressions */ + reg::packed(7485, MAY_BE_ARRAY), /* preserveVacuousConditionalExpressions $otherExpressionTypes */ reg::packed(1476, MAY_BE_ARRAY), /* preserveVacuousConditionalExpressions return */ - reg::packed(7348, MAY_BE_ARRAY), /* mergeSameGuardConditionalExpressions $currentConditionalExpressions */ - reg::packed(7466, MAY_BE_ARRAY), /* mergeSameGuardConditionalExpressions $ourConditionalExpressions */ - reg::packed(7492, MAY_BE_ARRAY), /* mergeSameGuardConditionalExpressions $theirConditionalExpressions */ + reg::packed(7426, MAY_BE_ARRAY), /* mergeSameGuardConditionalExpressions $currentConditionalExpressions */ + reg::packed(7544, MAY_BE_ARRAY), /* mergeSameGuardConditionalExpressions $ourConditionalExpressions */ + reg::packed(7570, MAY_BE_ARRAY), /* mergeSameGuardConditionalExpressions $theirConditionalExpressions */ reg::packed(1476, MAY_BE_ARRAY), /* mergeSameGuardConditionalExpressions return */ - reg::packed(7557, MAY_BE_ARRAY), /* mergeConditionalExpressions $newConditionalExpressions */ - reg::packed(7583, MAY_BE_ARRAY), /* mergeConditionalExpressions $existingConditionalExpressions */ + reg::packed(7635, MAY_BE_ARRAY), /* mergeConditionalExpressions $newConditionalExpressions */ + reg::packed(7661, MAY_BE_ARRAY), /* mergeConditionalExpressions $existingConditionalExpressions */ reg::packed(1476, MAY_BE_ARRAY), /* mergeConditionalExpressions return */ - reg::packed(7642, 0, 1261), /* mergeInitializedProperties $calledMethodScope */ + reg::packed(7720, 0, 1261), /* mergeInitializedProperties $calledMethodScope */ reg::packed(1476, 0, 1261), /* mergeInitializedProperties return */ - reg::packed(7687, 0, 1261), /* processFinallyScope $finallyScope */ - reg::packed(7700, 0, 1261), /* processFinallyScope $originalFinallyScope */ + reg::packed(7765, 0, 1261), /* processFinallyScope $finallyScope */ + reg::packed(7778, 0, 1261), /* processFinallyScope $originalFinallyScope */ reg::packed(1476, 0, 1261), /* processFinallyScope return */ - reg::packed(7741, MAY_BE_ARRAY), /* processFinallyScopeVariableTypeHolders $ourVariableTypeHolders */ - reg::packed(7764, MAY_BE_ARRAY), /* processFinallyScopeVariableTypeHolders $finallyVariableTypeHolders */ - reg::packed(7791, MAY_BE_ARRAY), /* processFinallyScopeVariableTypeHolders $originalVariableTypeHolders */ + reg::packed(7819, MAY_BE_ARRAY), /* processFinallyScopeVariableTypeHolders $ourVariableTypeHolders */ + reg::packed(7842, MAY_BE_ARRAY), /* processFinallyScopeVariableTypeHolders $finallyVariableTypeHolders */ + reg::packed(7869, MAY_BE_ARRAY), /* processFinallyScopeVariableTypeHolders $originalVariableTypeHolders */ reg::packed(1476, MAY_BE_ARRAY), /* processFinallyScopeVariableTypeHolders return */ - reg::packed(7858, 0, 1261), /* processClosureScope $closureScope */ - reg::packed(7871, MAY_BE_NULL, 1261), /* processClosureScope $prevScope */ - reg::packed(7881, MAY_BE_ARRAY), /* processClosureScope $byRefUses */ + reg::packed(7936, 0, 1261), /* processClosureScope $closureScope */ + reg::packed(7949, MAY_BE_NULL, 1261), /* processClosureScope $prevScope */ + reg::packed(7959, MAY_BE_ARRAY), /* processClosureScope $byRefUses */ reg::packed(1476, 0, 1261), /* processClosureScope return */ - reg::packed(7911, 0, 1261), /* processAlwaysIterableForeachScopeWithoutPollute $finalScope */ + reg::packed(7989, 0, 1261), /* processAlwaysIterableForeachScopeWithoutPollute $finalScope */ reg::packed(1476, 0, 1261), /* processAlwaysIterableForeachScopeWithoutPollute return */ - reg::packed(7141, 0, 1261), /* generalizeWith $otherScope */ - reg::packed(7970, MAY_BE_NULL | MAY_BE_ARRAY, reg::NoString, false, false, 867), /* generalizeWith $writableVariableNames */ + reg::packed(7219, 0, 1261), /* generalizeWith $otherScope */ + reg::packed(8048, MAY_BE_NULL | MAY_BE_ARRAY, reg::NoString, false, false, 867), /* generalizeWith $writableVariableNames */ reg::packed(1476, 0, 1261), /* generalizeWith return */ - reg::packed(7141, 0, 1261), /* generalizeWithVariableState $otherScope */ - reg::packed(7970, MAY_BE_NULL | MAY_BE_ARRAY), /* generalizeWithVariableState $writableVariableNames */ + reg::packed(7219, 0, 1261), /* generalizeWithVariableState $otherScope */ + reg::packed(8048, MAY_BE_NULL | MAY_BE_ARRAY), /* generalizeWithVariableState $writableVariableNames */ reg::packed(1476, 0, 1261), /* generalizeWithVariableState return */ - reg::packed(8035, MAY_BE_ARRAY), /* generalizeVariableTypeHolders $variableTypeHolders */ - reg::packed(8055, MAY_BE_ARRAY), /* generalizeVariableTypeHolders $otherVariableTypeHolders */ - reg::packed(7970, MAY_BE_NULL | MAY_BE_ARRAY), /* generalizeVariableTypeHolders $writableVariableNames */ + reg::packed(8113, MAY_BE_ARRAY), /* generalizeVariableTypeHolders $variableTypeHolders */ + reg::packed(8133, MAY_BE_ARRAY), /* generalizeVariableTypeHolders $otherVariableTypeHolders */ + reg::packed(8048, MAY_BE_NULL | MAY_BE_ARRAY), /* generalizeVariableTypeHolders $writableVariableNames */ reg::packed(1476, MAY_BE_ARRAY), /* generalizeVariableTypeHolders return */ - reg::packed(4572, 0, 2162), /* flattenUnionForGeneralization $type */ + reg::packed(4614, 0, 2162), /* flattenUnionForGeneralization $type */ reg::packed(1476, MAY_BE_ARRAY), /* flattenUnionForGeneralization return */ - reg::packed(8140, 0, 2162), /* generalizeType $a */ - reg::packed(8142, 0, 2162), /* generalizeType $b */ - reg::packed(8144, MAY_BE_LONG), /* generalizeType $depth */ + reg::packed(8218, 0, 2162), /* generalizeType $a */ + reg::packed(8220, 0, 2162), /* generalizeType $b */ + reg::packed(8222, MAY_BE_LONG), /* generalizeType $depth */ reg::packed(1476, 0, 2162), /* generalizeType return */ - reg::packed(4572, 0, 2162), /* getArrayDepth $type */ + reg::packed(4614, 0, 2162), /* getArrayDepth $type */ reg::packed(1476, MAY_BE_LONG), /* getArrayDepth return */ - reg::packed(7141, 0, 1261), /* equals $otherScope */ + reg::packed(7219, 0, 1261), /* equals $otherScope */ reg::packed(1476, MAY_BE_BOOL), /* equals return */ reg::packed(1016, MAY_BE_ARRAY), /* compareConditionalExpressions $conditionalExpressions */ - reg::packed(8186, MAY_BE_ARRAY), /* compareConditionalExpressions $otherConditionalExpressions */ + reg::packed(8264, MAY_BE_ARRAY), /* compareConditionalExpressions $otherConditionalExpressions */ reg::packed(1476, MAY_BE_BOOL), /* compareConditionalExpressions return */ - reg::packed(8035, MAY_BE_ARRAY), /* compareVariableTypeHolders $variableTypeHolders */ - reg::packed(8055, MAY_BE_ARRAY), /* compareVariableTypeHolders $otherVariableTypeHolders */ + reg::packed(8113, MAY_BE_ARRAY), /* compareVariableTypeHolders $variableTypeHolders */ + reg::packed(8133, MAY_BE_ARRAY), /* compareVariableTypeHolders $otherVariableTypeHolders */ reg::packed(1476, MAY_BE_BOOL), /* compareVariableTypeHolders return */ - reg::packed(8271, 0, 8290), /* canAccessProperty $propertyReflection */ + reg::packed(8349, 0, 8368), /* canAccessProperty $propertyReflection */ reg::packed(1476, MAY_BE_BOOL), /* canAccessProperty return */ - reg::packed(8271, 0, 8346), /* canReadProperty $propertyReflection */ + reg::packed(8349, 0, 8424), /* canReadProperty $propertyReflection */ reg::packed(1476, MAY_BE_BOOL), /* canReadProperty return */ - reg::packed(8271, 0, 8346), /* canWriteProperty $propertyReflection */ + reg::packed(8349, 0, 8424), /* canWriteProperty $propertyReflection */ reg::packed(1476, MAY_BE_BOOL), /* canWriteProperty return */ - reg::packed(8425, 0, 8442), /* canCallMethod $methodReflection */ + reg::packed(8503, 0, 8520), /* canCallMethod $methodReflection */ reg::packed(1476, MAY_BE_BOOL), /* canCallMethod return */ - reg::packed(8492, 0, 8511), /* canAccessConstant $constantReflection */ + reg::packed(8570, 0, 8589), /* canAccessConstant $constantReflection */ reg::packed(1476, MAY_BE_BOOL), /* canAccessConstant return */ - reg::packed(8572, 0, 8594), /* canAccessClassMember $classMemberReflection */ + reg::packed(8650, 0, 8672), /* canAccessClassMember $classMemberReflection */ reg::packed(1476, MAY_BE_BOOL), /* canAccessClassMember return */ reg::packed(1476, MAY_BE_ARRAY), /* debug return */ - reg::packed(8662, 0, 2162), /* filterTypeWithMethod $typeWithMethod */ - reg::packed(8677, MAY_BE_STRING), /* filterTypeWithMethod $methodName */ + reg::packed(8740, 0, 2162), /* filterTypeWithMethod $typeWithMethod */ + reg::packed(8755, MAY_BE_STRING), /* filterTypeWithMethod $methodName */ reg::packed(1476, MAY_BE_NULL, 2162), /* filterTypeWithMethod return */ - reg::packed(8662, 0, 2162), /* getMethodReflection $typeWithMethod */ - reg::packed(8677, MAY_BE_STRING), /* getMethodReflection $methodName */ - reg::packed(1476, MAY_BE_NULL, 8709), /* getMethodReflection return */ - reg::packed(8662, 0, 2162), /* getNakedMethod $typeWithMethod */ - reg::packed(8677, MAY_BE_STRING), /* getNakedMethod $methodName */ - reg::packed(1476, MAY_BE_NULL, 8709), /* getNakedMethod return */ - reg::packed(8788, 0, 2162), /* getPropertyReflection $typeWithProperty */ - reg::packed(4426, MAY_BE_STRING), /* getPropertyReflection $propertyName */ - reg::packed(1476, MAY_BE_NULL, 8346), /* getPropertyReflection return */ - reg::packed(8788, 0, 2162), /* getInstancePropertyReflection $typeWithProperty */ - reg::packed(4426, MAY_BE_STRING), /* getInstancePropertyReflection $propertyName */ - reg::packed(1476, MAY_BE_NULL, 8346), /* getInstancePropertyReflection return */ - reg::packed(8788, 0, 2162), /* getStaticPropertyReflection $typeWithProperty */ - reg::packed(4426, MAY_BE_STRING), /* getStaticPropertyReflection $propertyName */ - reg::packed(1476, MAY_BE_NULL, 8346), /* getStaticPropertyReflection return */ - reg::packed(8885, 0, 2162), /* getConstantReflection $typeWithConstant */ - reg::packed(8902, MAY_BE_STRING), /* getConstantReflection $constantName */ - reg::packed(1476, MAY_BE_NULL, 8511), /* getConstantReflection return */ - reg::packed(8902, MAY_BE_STRING), /* getConstantExplicitTypeFromConfig $constantName */ - reg::packed(8937, 0, 2162), /* getConstantExplicitTypeFromConfig $constantType */ + reg::packed(8740, 0, 2162), /* getMethodReflection $typeWithMethod */ + reg::packed(8755, MAY_BE_STRING), /* getMethodReflection $methodName */ + reg::packed(1476, MAY_BE_NULL, 8787), /* getMethodReflection return */ + reg::packed(8740, 0, 2162), /* getNakedMethod $typeWithMethod */ + reg::packed(8755, MAY_BE_STRING), /* getNakedMethod $methodName */ + reg::packed(1476, MAY_BE_NULL, 8787), /* getNakedMethod return */ + reg::packed(8866, 0, 2162), /* getPropertyReflection $typeWithProperty */ + reg::packed(4468, MAY_BE_STRING), /* getPropertyReflection $propertyName */ + reg::packed(1476, MAY_BE_NULL, 8424), /* getPropertyReflection return */ + reg::packed(8866, 0, 2162), /* getInstancePropertyReflection $typeWithProperty */ + reg::packed(4468, MAY_BE_STRING), /* getInstancePropertyReflection $propertyName */ + reg::packed(1476, MAY_BE_NULL, 8424), /* getInstancePropertyReflection return */ + reg::packed(8866, 0, 2162), /* getStaticPropertyReflection $typeWithProperty */ + reg::packed(4468, MAY_BE_STRING), /* getStaticPropertyReflection $propertyName */ + reg::packed(1476, MAY_BE_NULL, 8424), /* getStaticPropertyReflection return */ + reg::packed(8963, 0, 2162), /* getConstantReflection $typeWithConstant */ + reg::packed(8980, MAY_BE_STRING), /* getConstantReflection $constantName */ + reg::packed(1476, MAY_BE_NULL, 8589), /* getConstantReflection return */ + reg::packed(8980, MAY_BE_STRING), /* getConstantExplicitTypeFromConfig $constantName */ + reg::packed(9015, 0, 2162), /* getConstantExplicitTypeFromConfig $constantType */ reg::packed(1476, 0, 2162), /* getConstantExplicitTypeFromConfig return */ reg::packed(1476, MAY_BE_ARRAY), /* getConstantTypes return */ reg::packed(2313, 0, 2318), /* getGlobalConstantType $name */ reg::packed(1476, MAY_BE_NULL, 2162), /* getGlobalConstantType return */ reg::packed(2313, 0, 2318), /* createGlobalConstantFetches $name */ - reg::packed(9023, MAY_BE_STRING), /* createGlobalConstantFetches $nameString */ + reg::packed(9101, MAY_BE_STRING), /* createGlobalConstantFetches $nameString */ reg::packed(965, MAY_BE_NULL | MAY_BE_STRING), /* createGlobalConstantFetches $namespace */ reg::packed(1476, MAY_BE_ARRAY), /* createGlobalConstantFetches return */ reg::packed(1476, MAY_BE_ARRAY), /* getNativeConstantTypes return */ - reg::packed(5800, 0, 2162), /* getIterableKeyType $iteratee */ + reg::packed(5878, 0, 2162), /* getIterableKeyType $iteratee */ reg::packed(1476, 0, 2162), /* getIterableKeyType return */ - reg::packed(5800, 0, 2162), /* getIterableValueType $iteratee */ + reg::packed(5878, 0, 2162), /* getIterableValueType $iteratee */ reg::packed(1476, 0, 2162), /* getIterableValueType return */ - reg::packed(1476, 0, 9125), /* getPhpVersion return */ - reg::packed(4572, 0, 2162), /* isOverallPhpVersionRange $type */ + reg::packed(1476, 0, 9203), /* getPhpVersion return */ + reg::packed(4614, 0, 2162), /* isOverallPhpVersionRange $type */ reg::packed(1476, MAY_BE_BOOL), /* isOverallPhpVersionRange return */ - reg::packed(2500, 0, 9188), /* invokeNodeCallback $node */ + reg::packed(2500, 0, 9266), /* invokeNodeCallback $node */ reg::packed(1476, MAY_BE_VOID), /* invokeNodeCallback return */ - reg::packed(9222, MAY_BE_STRING), /* emitCollectedData $collectorType */ - reg::packed(9236, MAY_BE_ANY), /* emitCollectedData $data */ + reg::packed(9300, MAY_BE_STRING), /* emitCollectedData $collectorType */ + reg::packed(9314, MAY_BE_ANY), /* emitCollectedData $data */ reg::packed(1476, MAY_BE_VOID), /* emitCollectedData return */ - reg::packed(9259, MAY_BE_STRING), /* trackValueDependency $extensionClass */ + reg::packed(9337, MAY_BE_STRING), /* trackValueDependency $extensionClass */ reg::packed(3280, MAY_BE_STRING), /* trackValueDependency $key */ reg::packed(1476, MAY_BE_VOID), /* trackValueDependency return */ - reg::packed(9295, MAY_BE_STRING), /* trackFileDependency $file */ + reg::packed(9373, MAY_BE_STRING), /* trackFileDependency $file */ reg::packed(1476, MAY_BE_VOID), /* trackFileDependency return */ - reg::packed(9320, MAY_BE_STRING), /* trackDirectoryDependency $directory */ - reg::packed(9330, MAY_BE_STRING, reg::NoString, false, false, 9338), /* trackDirectoryDependency $pattern */ + reg::packed(9398, MAY_BE_STRING), /* trackDirectoryDependency $directory */ + reg::packed(9408, MAY_BE_STRING, reg::NoString, false, false, 9416), /* trackDirectoryDependency $pattern */ reg::packed(1476, MAY_BE_VOID), /* trackDirectoryDependency return */ reg::packed(3717, MAY_BE_STRING), /* trackClassDependency $className */ reg::packed(1476, MAY_BE_VOID), /* trackClassDependency return */ @@ -1171,123 +1175,123 @@ inline constexpr sigtab::Sig getFunctionCallStackWithParameters = { { 3764 /* ge inline constexpr sigtab::Sig isInFunctionExists = { { 3812 /* isInFunctionExists */, 1, 168, 1, 169, ZEND_ACC_PUBLIC } }; inline constexpr sigtab::Sig enterClass = { { 3847 /* enterClass */, 1, 170, 1, 171, ZEND_ACC_PUBLIC } }; inline constexpr sigtab::Sig enterTrait = { { 3874 /* enterTrait */, 1, 172, 1, 173, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig enterClassMethod = { { 4376 /* enterClassMethod */, 9, 174, 20, 194, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig enterPropertyHook = { { 4554 /* enterPropertyHook */, 10, 195, 11, 206, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig transformStaticType = { { 4577 /* transformStaticType */, 1, 207, 1, 208, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig getRealParameterTypes = { { 4638 /* getRealParameterTypes */, 1, 209, 1, 210, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig getRealParameterDefaultValues = { { 4660 /* getRealParameterDefaultValues */, 1, 211, 1, 212, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig getParameterAttributes = { { 4780 /* getParameterAttributes */, 1, 213, 1, 214, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig enterFunction = { { 4870 /* enterFunction */, 8, 215, 16, 231, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig enterFunctionLike = { { 4928 /* enterFunctionLike */, 2, 232, 2, 234, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig enterNamespace = { { 4960 /* enterNamespace */, 1, 235, 1, 236, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig enterClosureBind = { { 5012 /* enterClosureBind */, 3, 237, 3, 240, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig restoreOriginalScopeAfterClosureBind = { { 5043 /* restoreOriginalScopeAfterClosureBind */, 1, 241, 1, 242, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig restoreThis = { { 5097 /* restoreThis */, 1, 243, 1, 244, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig enterClosureCall = { { 5109 /* enterClosureCall */, 2, 245, 2, 247, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig isInClosureBind = { { 5126 /* isInClosureBind */, 0, 248, 0, 248, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig withClosureBindScopeClasses = { { 5142 /* withClosureBindScopeClasses */, 1, 249, 1, 250, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig enterAnonymousFunction = { { 5250 /* enterAnonymousFunction */, 2, 251, 3, 254, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig enterAnonymousFunctionWithoutReflection = { { 5273 /* enterAnonymousFunctionWithoutReflection */, 3, 255, 3, 258, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig expressionTypeIsUnchangeable = { { 5362 /* expressionTypeIsUnchangeable */, 1, 259, 1, 260, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig invalidateStaticExpressions = { { 5391 /* invalidateStaticExpressions */, 1, 261, 1, 262, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig enterArrowFunction = { { 5467 /* enterArrowFunction */, 2, 263, 3, 266, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig enterArrowFunctionWithoutReflection = { { 5486 /* enterArrowFunctionWithoutReflection */, 3, 267, 3, 270, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig isParameterValueNullable = { { 5543 /* isParameterValueNullable */, 1, 271, 1, 272, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getFunctionType = { { 5590 /* getFunctionType */, 3, 273, 3, 276, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getCallableParameterType = { { 5612 /* getCallableParameterType */, 3, 277, 3, 280, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig buildVariadicArrayTypeFromCallableParameters = { { 5648 /* buildVariadicArrayTypeFromCallableParameters */, 2, 281, 2, 283, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig intersectButNotNever = { { 5717 /* intersectButNotNever */, 2, 284, 2, 286, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig enterMatch = { { 5789 /* enterMatch */, 3, 287, 3, 290, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig enterForeach = { { 5870 /* enterForeach */, 7, 291, 7, 298, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig enterForeachKey = { { 5883 /* enterForeachKey */, 5, 299, 5, 304, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig enterCatchType = { { 5909 /* enterCatchType */, 2, 305, 2, 307, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig enterExpressionAssign = { { 5937 /* enterExpressionAssign */, 1, 308, 2, 310, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig exitExpressionAssign = { { 5959 /* exitExpressionAssign */, 1, 311, 1, 312, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig isInExpressionAssign = { { 5980 /* isInExpressionAssign */, 1, 313, 1, 314, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig isInWriteExpressionAssign = { { 6001 /* isInWriteExpressionAssign */, 1, 315, 1, 316, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig setAllowedUndefinedExpression = { { 6027 /* setAllowedUndefinedExpression */, 1, 317, 1, 318, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig unsetAllowedUndefinedExpression = { { 6057 /* unsetAllowedUndefinedExpression */, 1, 319, 1, 320, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig isUndefinedExpressionAllowed = { { 6089 /* isUndefinedExpressionAllowed */, 1, 321, 1, 322, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig assignVariable = { { 6154 /* assignVariable */, 4, 323, 5, 328, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig overwriteExpression = { { 6169 /* overwriteExpression */, 3, 329, 3, 332, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig resolveIntertwinedAssignedType = { { 6241 /* resolveIntertwinedAssignedType */, 5, 333, 5, 338, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig isDimFetchPathReachable = { { 6315 /* isDimFetchPathReachable */, 2, 339, 2, 341, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig unsetExpression = { { 6339 /* unsetExpression */, 1, 342, 1, 343, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig getStateType = { { 6355 /* getStateType */, 1, 344, 1, 345, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getScopeStateType = { { 6368 /* getScopeStateType */, 1, 346, 1, 347, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig getScopeStateNativeType = { { 6386 /* getScopeStateNativeType */, 1, 348, 1, 349, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig resolveScopeStateType = { { 6410 /* resolveScopeStateType */, 2, 350, 2, 352, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig specifyExpressionType = { { 6432 /* specifyExpressionType */, 4, 353, 4, 357, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig openSpecificationScope = { { 6454 /* openSpecificationScope */, 0, 358, 0, 358, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig isSpecifyExpressionTypeNoop = { { 6477 /* isSpecifyExpressionTypeNoop */, 2, 359, 2, 361, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig specifyExpressionTypeInPlace = { { 6505 /* specifyExpressionTypeInPlace */, 4, 362, 4, 366, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig assignExpression = { { 6534 /* assignExpression */, 3, 367, 3, 370, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig assignInitializedProperty = { { 6565 /* assignInitializedProperty */, 2, 371, 2, 373, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig invalidateExpression = { { 6674 /* invalidateExpression */, 1, 374, 4, 378, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig isPrivatePropertyOfDifferentClass = { { 6695 /* isPrivatePropertyOfDifferentClass */, 2, 379, 2, 381, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig invalidateMethodsOnExpression = { { 6729 /* invalidateMethodsOnExpression */, 1, 382, 1, 383, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig setExpressionCertaintyKeepingType = { { 6759 /* setExpressionCertaintyKeepingType */, 2, 384, 2, 386, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig isComplexUnionType = { { 6793 /* isComplexUnionType */, 1, 387, 1, 388, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig addTypeToExpression = { { 6812 /* addTypeToExpression */, 2, 389, 2, 391, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig removeTypeFromExpression = { { 6845 /* removeTypeFromExpression */, 2, 392, 2, 394, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig filterByTruthyValue = { { 6870 /* filterByTruthyValue */, 1, 395, 1, 396, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig filterByFalseyValue = { { 6890 /* filterByFalseyValue */, 1, 397, 1, 398, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig applySpecifiedTypes = { { 6925 /* applySpecifiedTypes */, 1, 399, 1, 400, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig processConditionalExpressionsAfterSpecifying = { { 6966 /* processConditionalExpressionsAfterSpecifying */, 1, 401, 1, 402, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig getConditionalExpressions = { { 7011 /* getConditionalExpressions */, 0, 403, 0, 403, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig addConditionalExpressions = { { 7066 /* addConditionalExpressions */, 2, 404, 2, 406, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig exitFirstLevelStatements = { { 7092 /* exitFirstLevelStatements */, 0, 407, 0, 407, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig isInFirstLevelStatement = { { 7117 /* isInFirstLevelStatement */, 0, 408, 0, 408, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig mergeWith = { { 7180 /* mergeWith */, 1, 409, 2, 411, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig collectAbsorbedClosures = { { 7202 /* collectAbsorbedClosures */, 2, 412, 2, 414, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig mergeWithVariableState = { { 7226 /* mergeWithVariableState */, 1, 415, 2, 417, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig withoutPreciseClassConstantFetches = { { 7313 /* withoutPreciseClassConstantFetches */, 3, 418, 3, 421, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig preserveVacuousConditionalExpressions = { { 7428 /* preserveVacuousConditionalExpressions */, 3, 422, 3, 425, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig mergeSameGuardConditionalExpressions = { { 7520 /* mergeSameGuardConditionalExpressions */, 3, 426, 3, 429, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig mergeConditionalExpressions = { { 7614 /* mergeConditionalExpressions */, 2, 430, 2, 432, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig mergeInitializedProperties = { { 7660 /* mergeInitializedProperties */, 1, 433, 1, 434, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig processFinallyScope = { { 7721 /* processFinallyScope */, 2, 435, 2, 437, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig processFinallyScopeVariableTypeHolders = { { 7819 /* processFinallyScopeVariableTypeHolders */, 3, 438, 3, 441, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig processClosureScope = { { 7891 /* processClosureScope */, 3, 442, 3, 445, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig processAlwaysIterableForeachScopeWithoutPollute = { { 7922 /* processAlwaysIterableForeachScopeWithoutPollute */, 1, 446, 1, 447, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig generalizeWith = { { 7992 /* generalizeWith */, 1, 448, 2, 450, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig generalizeWithVariableState = { { 8007 /* generalizeWithVariableState */, 2, 451, 2, 453, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig generalizeVariableTypeHolders = { { 8080 /* generalizeVariableTypeHolders */, 3, 454, 3, 457, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig flattenUnionForGeneralization = { { 8110 /* flattenUnionForGeneralization */, 1, 458, 1, 459, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig generalizeType = { { 8150 /* generalizeType */, 3, 460, 3, 463, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig getArrayDepth = { { 8165 /* getArrayDepth */, 1, 464, 1, 465, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig equals = { { 8179 /* equals */, 1, 466, 1, 467, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig compareConditionalExpressions = { { 8214 /* compareConditionalExpressions */, 2, 468, 2, 470, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig compareVariableTypeHolders = { { 8244 /* compareVariableTypeHolders */, 2, 471, 2, 473, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig canAccessProperty = { { 8328 /* canAccessProperty */, 1, 474, 1, 475, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig canReadProperty = { { 8392 /* canReadProperty */, 1, 476, 1, 477, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig canWriteProperty = { { 8408 /* canWriteProperty */, 1, 478, 1, 479, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig canCallMethod = { { 8478 /* canCallMethod */, 1, 480, 1, 481, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig canAccessConstant = { { 8554 /* canAccessConstant */, 1, 482, 1, 483, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig canAccessClassMember = { { 8635 /* canAccessClassMember */, 1, 484, 1, 485, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig debug = { { 8656 /* debug */, 0, 486, 0, 486, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig filterTypeWithMethod = { { 8688 /* filterTypeWithMethod */, 2, 487, 2, 489, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getMethodReflection = { { 8753 /* getMethodReflection */, 2, 490, 2, 492, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getNakedMethod = { { 8773 /* getNakedMethod */, 2, 493, 2, 495, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getPropertyReflection = { { 8805 /* getPropertyReflection */, 2, 496, 2, 498, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getInstancePropertyReflection = { { 8827 /* getInstancePropertyReflection */, 2, 499, 2, 501, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getStaticPropertyReflection = { { 8857 /* getStaticPropertyReflection */, 2, 502, 2, 504, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getConstantReflection = { { 8915 /* getConstantReflection */, 2, 505, 2, 507, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getConstantExplicitTypeFromConfig = { { 8950 /* getConstantExplicitTypeFromConfig */, 2, 508, 2, 510, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getConstantTypes = { { 8984 /* getConstantTypes */, 0, 511, 0, 511, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig getGlobalConstantType = { { 9001 /* getGlobalConstantType */, 1, 512, 1, 513, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig createGlobalConstantFetches = { { 9034 /* createGlobalConstantFetches */, 3, 514, 3, 517, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig getNativeConstantTypes = { { 9062 /* getNativeConstantTypes */, 0, 518, 0, 518, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig getIterableKeyType = { { 9085 /* getIterableKeyType */, 1, 519, 1, 520, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getIterableValueType = { { 9104 /* getIterableValueType */, 1, 521, 1, 522, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getPhpVersion = { { 9149 /* getPhpVersion */, 0, 523, 0, 523, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig isOverallPhpVersionRange = { { 9163 /* isOverallPhpVersionRange */, 1, 524, 1, 525, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig invokeNodeCallback = { { 9203 /* invokeNodeCallback */, 1, 526, 1, 527, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig emitCollectedData = { { 9241 /* emitCollectedData */, 2, 528, 2, 530, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig trackValueDependency = { { 9274 /* trackValueDependency */, 2, 531, 2, 533, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig trackFileDependency = { { 9300 /* trackFileDependency */, 1, 534, 1, 535, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig trackDirectoryDependency = { { 9342 /* trackDirectoryDependency */, 1, 536, 2, 538, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig trackClassDependency = { { 9367 /* trackClassDependency */, 1, 539, 1, 540, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig enterClassMethod = { { 4418 /* enterClassMethod */, 9, 174, 21, 195, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig enterPropertyHook = { { 4596 /* enterPropertyHook */, 10, 196, 11, 207, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig transformStaticType = { { 4619 /* transformStaticType */, 1, 208, 1, 209, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig getRealParameterTypes = { { 4680 /* getRealParameterTypes */, 1, 210, 1, 211, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig getRealParameterDefaultValues = { { 4702 /* getRealParameterDefaultValues */, 1, 212, 1, 213, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig getParameterAttributes = { { 4822 /* getParameterAttributes */, 1, 214, 1, 215, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig enterFunction = { { 4948 /* enterFunction */, 8, 216, 17, 233, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig enterFunctionLike = { { 5006 /* enterFunctionLike */, 2, 234, 2, 236, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig enterNamespace = { { 5038 /* enterNamespace */, 1, 237, 1, 238, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig enterClosureBind = { { 5090 /* enterClosureBind */, 3, 239, 3, 242, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig restoreOriginalScopeAfterClosureBind = { { 5121 /* restoreOriginalScopeAfterClosureBind */, 1, 243, 1, 244, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig restoreThis = { { 5175 /* restoreThis */, 1, 245, 1, 246, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig enterClosureCall = { { 5187 /* enterClosureCall */, 2, 247, 2, 249, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig isInClosureBind = { { 5204 /* isInClosureBind */, 0, 250, 0, 250, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig withClosureBindScopeClasses = { { 5220 /* withClosureBindScopeClasses */, 1, 251, 1, 252, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig enterAnonymousFunction = { { 5328 /* enterAnonymousFunction */, 2, 253, 3, 256, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig enterAnonymousFunctionWithoutReflection = { { 5351 /* enterAnonymousFunctionWithoutReflection */, 3, 257, 3, 260, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig expressionTypeIsUnchangeable = { { 5440 /* expressionTypeIsUnchangeable */, 1, 261, 1, 262, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig invalidateStaticExpressions = { { 5469 /* invalidateStaticExpressions */, 1, 263, 1, 264, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig enterArrowFunction = { { 5545 /* enterArrowFunction */, 2, 265, 3, 268, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig enterArrowFunctionWithoutReflection = { { 5564 /* enterArrowFunctionWithoutReflection */, 3, 269, 3, 272, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig isParameterValueNullable = { { 5621 /* isParameterValueNullable */, 1, 273, 1, 274, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getFunctionType = { { 5668 /* getFunctionType */, 3, 275, 3, 278, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getCallableParameterType = { { 5690 /* getCallableParameterType */, 3, 279, 3, 282, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig buildVariadicArrayTypeFromCallableParameters = { { 5726 /* buildVariadicArrayTypeFromCallableParameters */, 2, 283, 2, 285, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig intersectButNotNever = { { 5795 /* intersectButNotNever */, 2, 286, 2, 288, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig enterMatch = { { 5867 /* enterMatch */, 3, 289, 3, 292, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig enterForeach = { { 5948 /* enterForeach */, 7, 293, 7, 300, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig enterForeachKey = { { 5961 /* enterForeachKey */, 5, 301, 5, 306, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig enterCatchType = { { 5987 /* enterCatchType */, 2, 307, 2, 309, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig enterExpressionAssign = { { 6015 /* enterExpressionAssign */, 1, 310, 2, 312, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig exitExpressionAssign = { { 6037 /* exitExpressionAssign */, 1, 313, 1, 314, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig isInExpressionAssign = { { 6058 /* isInExpressionAssign */, 1, 315, 1, 316, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig isInWriteExpressionAssign = { { 6079 /* isInWriteExpressionAssign */, 1, 317, 1, 318, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig setAllowedUndefinedExpression = { { 6105 /* setAllowedUndefinedExpression */, 1, 319, 1, 320, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig unsetAllowedUndefinedExpression = { { 6135 /* unsetAllowedUndefinedExpression */, 1, 321, 1, 322, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig isUndefinedExpressionAllowed = { { 6167 /* isUndefinedExpressionAllowed */, 1, 323, 1, 324, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig assignVariable = { { 6232 /* assignVariable */, 4, 325, 5, 330, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig overwriteExpression = { { 6247 /* overwriteExpression */, 3, 331, 3, 334, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig resolveIntertwinedAssignedType = { { 6319 /* resolveIntertwinedAssignedType */, 5, 335, 5, 340, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig isDimFetchPathReachable = { { 6393 /* isDimFetchPathReachable */, 2, 341, 2, 343, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig unsetExpression = { { 6417 /* unsetExpression */, 1, 344, 1, 345, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig getStateType = { { 6433 /* getStateType */, 1, 346, 1, 347, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getScopeStateType = { { 6446 /* getScopeStateType */, 1, 348, 1, 349, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig getScopeStateNativeType = { { 6464 /* getScopeStateNativeType */, 1, 350, 1, 351, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig resolveScopeStateType = { { 6488 /* resolveScopeStateType */, 2, 352, 2, 354, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig specifyExpressionType = { { 6510 /* specifyExpressionType */, 4, 355, 4, 359, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig openSpecificationScope = { { 6532 /* openSpecificationScope */, 0, 360, 0, 360, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig isSpecifyExpressionTypeNoop = { { 6555 /* isSpecifyExpressionTypeNoop */, 2, 361, 2, 363, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig specifyExpressionTypeInPlace = { { 6583 /* specifyExpressionTypeInPlace */, 4, 364, 4, 368, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig assignExpression = { { 6612 /* assignExpression */, 3, 369, 3, 372, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig assignInitializedProperty = { { 6643 /* assignInitializedProperty */, 2, 373, 2, 375, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig invalidateExpression = { { 6752 /* invalidateExpression */, 1, 376, 4, 380, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig isPrivatePropertyOfDifferentClass = { { 6773 /* isPrivatePropertyOfDifferentClass */, 2, 381, 2, 383, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig invalidateMethodsOnExpression = { { 6807 /* invalidateMethodsOnExpression */, 1, 384, 1, 385, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig setExpressionCertaintyKeepingType = { { 6837 /* setExpressionCertaintyKeepingType */, 2, 386, 2, 388, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig isComplexUnionType = { { 6871 /* isComplexUnionType */, 1, 389, 1, 390, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig addTypeToExpression = { { 6890 /* addTypeToExpression */, 2, 391, 2, 393, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig removeTypeFromExpression = { { 6923 /* removeTypeFromExpression */, 2, 394, 2, 396, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig filterByTruthyValue = { { 6948 /* filterByTruthyValue */, 1, 397, 1, 398, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig filterByFalseyValue = { { 6968 /* filterByFalseyValue */, 1, 399, 1, 400, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig applySpecifiedTypes = { { 7003 /* applySpecifiedTypes */, 1, 401, 1, 402, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig processConditionalExpressionsAfterSpecifying = { { 7044 /* processConditionalExpressionsAfterSpecifying */, 1, 403, 1, 404, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig getConditionalExpressions = { { 7089 /* getConditionalExpressions */, 0, 405, 0, 405, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig addConditionalExpressions = { { 7144 /* addConditionalExpressions */, 2, 406, 2, 408, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig exitFirstLevelStatements = { { 7170 /* exitFirstLevelStatements */, 0, 409, 0, 409, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig isInFirstLevelStatement = { { 7195 /* isInFirstLevelStatement */, 0, 410, 0, 410, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig mergeWith = { { 7258 /* mergeWith */, 1, 411, 2, 413, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig collectAbsorbedClosures = { { 7280 /* collectAbsorbedClosures */, 2, 414, 2, 416, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig mergeWithVariableState = { { 7304 /* mergeWithVariableState */, 1, 417, 2, 419, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig withoutPreciseClassConstantFetches = { { 7391 /* withoutPreciseClassConstantFetches */, 3, 420, 3, 423, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig preserveVacuousConditionalExpressions = { { 7506 /* preserveVacuousConditionalExpressions */, 3, 424, 3, 427, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig mergeSameGuardConditionalExpressions = { { 7598 /* mergeSameGuardConditionalExpressions */, 3, 428, 3, 431, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig mergeConditionalExpressions = { { 7692 /* mergeConditionalExpressions */, 2, 432, 2, 434, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig mergeInitializedProperties = { { 7738 /* mergeInitializedProperties */, 1, 435, 1, 436, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig processFinallyScope = { { 7799 /* processFinallyScope */, 2, 437, 2, 439, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig processFinallyScopeVariableTypeHolders = { { 7897 /* processFinallyScopeVariableTypeHolders */, 3, 440, 3, 443, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig processClosureScope = { { 7969 /* processClosureScope */, 3, 444, 3, 447, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig processAlwaysIterableForeachScopeWithoutPollute = { { 8000 /* processAlwaysIterableForeachScopeWithoutPollute */, 1, 448, 1, 449, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig generalizeWith = { { 8070 /* generalizeWith */, 1, 450, 2, 452, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig generalizeWithVariableState = { { 8085 /* generalizeWithVariableState */, 2, 453, 2, 455, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig generalizeVariableTypeHolders = { { 8158 /* generalizeVariableTypeHolders */, 3, 456, 3, 459, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig flattenUnionForGeneralization = { { 8188 /* flattenUnionForGeneralization */, 1, 460, 1, 461, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig generalizeType = { { 8228 /* generalizeType */, 3, 462, 3, 465, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig getArrayDepth = { { 8243 /* getArrayDepth */, 1, 466, 1, 467, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig equals = { { 8257 /* equals */, 1, 468, 1, 469, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig compareConditionalExpressions = { { 8292 /* compareConditionalExpressions */, 2, 470, 2, 472, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig compareVariableTypeHolders = { { 8322 /* compareVariableTypeHolders */, 2, 473, 2, 475, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig canAccessProperty = { { 8406 /* canAccessProperty */, 1, 476, 1, 477, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig canReadProperty = { { 8470 /* canReadProperty */, 1, 478, 1, 479, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig canWriteProperty = { { 8486 /* canWriteProperty */, 1, 480, 1, 481, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig canCallMethod = { { 8556 /* canCallMethod */, 1, 482, 1, 483, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig canAccessConstant = { { 8632 /* canAccessConstant */, 1, 484, 1, 485, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig canAccessClassMember = { { 8713 /* canAccessClassMember */, 1, 486, 1, 487, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig debug = { { 8734 /* debug */, 0, 488, 0, 488, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig filterTypeWithMethod = { { 8766 /* filterTypeWithMethod */, 2, 489, 2, 491, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getMethodReflection = { { 8831 /* getMethodReflection */, 2, 492, 2, 494, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getNakedMethod = { { 8851 /* getNakedMethod */, 2, 495, 2, 497, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getPropertyReflection = { { 8883 /* getPropertyReflection */, 2, 498, 2, 500, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getInstancePropertyReflection = { { 8905 /* getInstancePropertyReflection */, 2, 501, 2, 503, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getStaticPropertyReflection = { { 8935 /* getStaticPropertyReflection */, 2, 504, 2, 506, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getConstantReflection = { { 8993 /* getConstantReflection */, 2, 507, 2, 509, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getConstantExplicitTypeFromConfig = { { 9028 /* getConstantExplicitTypeFromConfig */, 2, 510, 2, 512, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getConstantTypes = { { 9062 /* getConstantTypes */, 0, 513, 0, 513, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig getGlobalConstantType = { { 9079 /* getGlobalConstantType */, 1, 514, 1, 515, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig createGlobalConstantFetches = { { 9112 /* createGlobalConstantFetches */, 3, 516, 3, 519, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig getNativeConstantTypes = { { 9140 /* getNativeConstantTypes */, 0, 520, 0, 520, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig getIterableKeyType = { { 9163 /* getIterableKeyType */, 1, 521, 1, 522, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getIterableValueType = { { 9182 /* getIterableValueType */, 1, 523, 1, 524, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getPhpVersion = { { 9227 /* getPhpVersion */, 0, 525, 0, 525, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig isOverallPhpVersionRange = { { 9241 /* isOverallPhpVersionRange */, 1, 526, 1, 527, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig invokeNodeCallback = { { 9281 /* invokeNodeCallback */, 1, 528, 1, 529, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig emitCollectedData = { { 9319 /* emitCollectedData */, 2, 530, 2, 532, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig trackValueDependency = { { 9352 /* trackValueDependency */, 2, 533, 2, 535, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig trackFileDependency = { { 9378 /* trackFileDependency */, 1, 536, 1, 537, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig trackDirectoryDependency = { { 9420 /* trackDirectoryDependency */, 1, 538, 2, 540, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig trackClassDependency = { { 9445 /* trackClassDependency */, 1, 541, 1, 542, ZEND_ACC_PUBLIC } }; } // namespace sig } // namespace ptdecl::MutatingScope diff --git a/turbo-ext/src/generated/SimpleImpurePoint.h b/turbo-ext/src/generated/SimpleImpurePoint.h index 244afb7accb..bed9ba3e7fd 100644 --- a/turbo-ext/src/generated/SimpleImpurePoint.h +++ b/turbo-ext/src/generated/SimpleImpurePoint.h @@ -49,9 +49,10 @@ inline constexpr char strings[] = "createFromVariant\0" /* 270 */ "PHPStan\\TrinaryLogic\0" /* 288 */ "resolvePureUnlessCallableIsImpureVerdict\0" /* 309 */ - "getIdentifier\0" /* 350 */ - "getDescription\0" /* 364 */ - "isCertain"; /* 379 */ + "resolvePureUnlessParameterPassedVerdict\0" /* 350 */ + "getIdentifier\0" /* 390 */ + "getDescription\0" /* 404 */ + "isCertain"; /* 419 */ inline constexpr reg::PackedArg args[] = { reg::packed(0, MAY_BE_STRING), /* __construct $identifier */ reg::packed(11, MAY_BE_STRING), /* __construct $description */ @@ -65,6 +66,9 @@ inline constexpr reg::PackedArg args[] = { reg::packed(180, 0, 186), /* resolvePureUnlessCallableIsImpureVerdict $scope */ reg::packed(214, MAY_BE_ARRAY), /* resolvePureUnlessCallableIsImpureVerdict $args */ reg::packed(222, MAY_BE_NULL, 288), /* resolvePureUnlessCallableIsImpureVerdict return */ + reg::packed(134, 0, 142), /* resolvePureUnlessParameterPassedVerdict $variant */ + reg::packed(214, MAY_BE_ARRAY), /* resolvePureUnlessParameterPassedVerdict $args */ + reg::packed(222, MAY_BE_NULL, 288), /* resolvePureUnlessParameterPassedVerdict return */ reg::packed(222, MAY_BE_STRING), /* getIdentifier return */ reg::packed(222, MAY_BE_STRING), /* getDescription return */ reg::packed(222, MAY_BE_BOOL), /* isCertain return */ @@ -77,9 +81,10 @@ namespace sig { inline constexpr sigtab::Sig __construct = { { 31 /* __construct */, 3, 0, 3, reg::NoArg, ZEND_ACC_PUBLIC } }; inline constexpr sigtab::Sig createFromVariant = { { 270 /* createFromVariant */, 2, 3, 4, 7, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; inline constexpr sigtab::Sig resolvePureUnlessCallableIsImpureVerdict = { { 309 /* resolvePureUnlessCallableIsImpureVerdict */, 3, 8, 3, 11, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig getIdentifier = { { 350 /* getIdentifier */, 0, 12, 0, 12, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getDescription = { { 364 /* getDescription */, 0, 13, 0, 13, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig isCertain = { { 379 /* isCertain */, 0, 14, 0, 14, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig resolvePureUnlessParameterPassedVerdict = { { 350 /* resolvePureUnlessParameterPassedVerdict */, 2, 12, 2, 14, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig getIdentifier = { { 390 /* getIdentifier */, 0, 15, 0, 15, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getDescription = { { 404 /* getDescription */, 0, 16, 0, 16, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig isCertain = { { 419 /* isCertain */, 0, 17, 0, 17, ZEND_ACC_PUBLIC } }; } // namespace sig } // namespace ptdecl::SimpleImpurePoint diff --git a/turbo-ext/src/support.h b/turbo-ext/src/support.h index ecb5ffecd60..6939db6feec 100644 --- a/turbo-ext/src/support.h +++ b/turbo-ext/src/support.h @@ -3393,7 +3393,7 @@ extern zend_class_entry *pt_ce_deprecated_attribute_resolver; extern zend_class_entry *pt_ce_property_hooks_processor; extern zend_class_entry *pt_ce_called_method_processor; -/* $phpDocsResolver->getPhpDocs($scope, $node) without the array: the 21 +/* $phpDocsResolver->getPhpDocs($scope, $node) without the array: the 22 * values of the twin's list, owned. For the shadowing class every item is * set; any other resolver's method runs and its array is unpacked like the * caller's list() — an item it lacks stays UNDEF, unless its bit (1 << index) @@ -3401,7 +3401,7 @@ extern zend_class_entry *pt_ce_called_method_processor; * false = pending exception */ enum : uint32_t { - PT_PHP_DOCS_COUNT = 21, + PT_PHP_DOCS_COUNT = 22, PT_PHP_DOCS_TEMPLATE_TYPE_MAP = 0, PT_PHP_DOCS_PARAMETER_TYPES, PT_PHP_DOCS_IMMEDIATELY_INVOKED_CALLABLE_PARAMETERS, @@ -3423,6 +3423,7 @@ enum : uint32_t PT_PHP_DOCS_IS_ALLOWED_PRIVATE_MUTATION, PT_PHP_DOCS_RESOLVED_PHP_DOC, PT_PHP_DOCS_PURE_UNLESS_CALLABLE_IS_IMPURE_PARAMETERS, + PT_PHP_DOCS_PURE_UNLESS_PARAMETER_PASSED_PARAMETERS, }; struct pt_php_docs { diff --git a/turbo-ext/tests/scope-family.php b/turbo-ext/tests/scope-family.php index d27fd2a7f12..a0fb591f0d5 100644 --- a/turbo-ext/tests/scope-family.php +++ b/turbo-ext/tests/scope-family.php @@ -1590,6 +1590,8 @@ public function exprRef(Node\Expr $expr): mixed }, 'enterClassMethod(static)' => static fn () => $scope->enterClassMethod($sfProbeStaticClassMethod, $sfEmptyTemplateTypeMap, [], null, null, null, false, true, true, true, false, null, new \PHPStan\Type\ObjectWithoutClassType(), 'doc', ['a' => new \PHPStan\Type\IntegerType()], ['a' => true], [], true), 'enterFunction' => static fn () => $scope->enterFunction($sfProbeFunction, $sfEmptyTemplateTypeMap, ['a' => new \PHPStan\Type\IntegerType()], new \PHPStan\Type\StringType(), null, null, false, false), + 'enterClassMethod(pure unless passed)' => static fn () => $scope->enterClassMethod($sfProbeClassMethod, $sfEmptyTemplateTypeMap, [], null, null, null, false, false, false, phpDocPureUnlessCallableIsImpureParameters: ['a' => true], phpDocPureUnlessParameterPassedParameters: ['b' => true, 'd' => false]), + 'enterFunction(pure unless passed)' => static fn () => $scope->enterFunction($sfProbeFunction, $sfEmptyTemplateTypeMap, [], null, null, null, false, false, pureUnlessParameterPassedParameters: ['a' => true]), 'enterPropertyHook(get)' => static fn () => $scope->enterPropertyHook($sfProbeGetHook, 'name', new \PhpParser\Node\Identifier('string'), new \PHPStan\Type\StringType(), [], null, null, false, null, null), 'enterPropertyHook(set)' => static fn () => $scope->enterPropertyHook($sfProbeSetHook, 'name', new \PhpParser\Node\Identifier('string'), new \PHPStan\Type\StringType(), [], null, 'gone', true, false, 'doc'), 'enterPropertyHook(set, param)' => static fn () => $scope->enterPropertyHook($sfProbeSetHookWithParam, 'name', new \PhpParser\Node\Identifier('string'), new \PHPStan\Type\StringType(), [], null, null, false, true, null), diff --git a/turbo-ext/tests/type-family.php b/turbo-ext/tests/type-family.php index 142258f5c84..a5c6a463588 100644 --- a/turbo-ext/tests/type-family.php +++ b/turbo-ext/tests/type-family.php @@ -6888,7 +6888,9 @@ public function getResolvedPhpDoc(): ?\PHPStan\PhpDoc\ResolvedPhpDocBlock { retu // print_r() and var_export() (positional and named, truthy, maybe and falsy // arguments), the pure-unless-callable-is-impure parameters of array_filter() // and array_reduce() fed pure, impure, maybe-pure, null, non-callable and -// omitted callbacks (positional and named), the transformed and plain +// omitted callbacks (positional and named), the pure-unless-parameter-passed +// out parameters of str_replace() and preg_match() passed and omitted +// (positional, named and unpacked), the transformed and plain // fixture methods, a missing scope or variant, and unconstructed instances $observations['native ' . \PHPStan\Reflection\Callables\SimpleImpurePoint::class] = (new ReflectionMethod(\PHPStan\Reflection\Callables\SimpleImpurePoint::class, 'createFromVariant'))->isInternal(); { @@ -6922,9 +6924,15 @@ public function getResolvedPhpDoc(): ?\PHPStan\PhpDoc\ResolvedPhpDocBlock { retu 'named callback pure' => [$arg('array', 'array'), $arg('pure', 'callback')], 'named two maybe' => [$arg('array', 'one'), $arg('maybe', 'two')], 'keyed' => [1 => $arg('impure'), 0 => $arg('array')], + 'three' => [$arg('string'), $arg('string'), $arg('string')], + 'four' => [$arg('string'), $arg('string'), $arg('string'), $arg('true')], + 'named count' => [$arg('string'), $arg('string'), $arg('string'), $arg('true', 'count')], + 'named matches' => [$arg('string', 'pattern'), $arg('string', 'subject'), $arg('true', 'matches')], + 'named subject only' => [$arg('string', 'pattern'), $arg('string', 'subject')], + 'unpacked' => [new \PhpParser\Node\Arg(new \PhpParser\Node\Expr\Variable('array'), unpack: true)], ]; $functions = []; - foreach (['strlen', 'usleep', 'print_r', 'var_export', 'highlight_string', 'array_filter', 'array_reduce', 'array_map', 'rand'] as $functionName) { + foreach (['strlen', 'usleep', 'print_r', 'var_export', 'highlight_string', 'array_filter', 'array_reduce', 'array_map', 'rand', 'str_replace', 'preg_match'] as $functionName) { $functions[$functionName] = $stringReflectionProvider->getFunction(new \PhpParser\Node\Name($functionName), null); } foreach ($functions as $functionName => $function) { @@ -6934,6 +6942,7 @@ public function getResolvedPhpDoc(): ?\PHPStan\PhpDoc\ResolvedPhpDocBlock { retu foreach ($argLists as $argListName => $args) { $r["function $functionName $argListName"] = $catching(static fn () => $viewImpurePoint(\PHPStan\Reflection\Callables\SimpleImpurePoint::createFromVariant($function, $variant, $sipScope, $args))); $r["function $functionName $argListName verdict"] = $catching(static fn () => $view(\PHPStan\Reflection\Callables\SimpleImpurePoint::resolvePureUnlessCallableIsImpureVerdict($variant, $sipScope, $args))); + $r["function $functionName $argListName passed verdict"] = $catching(static fn () => $view(\PHPStan\Reflection\Callables\SimpleImpurePoint::resolvePureUnlessParameterPassedVerdict($variant, $args))); } } foreach (['returnsStatic', 'withValue', 'each', 'fails'] as $methodName) { From 5328327b78de792815bd5194737a3646611cfd1a Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Wed, 8 Jul 2026 02:13:57 +0900 Subject: [PATCH 04/29] Mark by-ref out-parameter builtins as @pure-unless-parameter-passed str_replace, str_ireplace, preg_replace, preg_match, preg_match_all and similar_text are pure unless their optional by-ref out parameter (count / matches / percent) receives an argument. Add the pureUnlessParameterPassedParameters metadata shape to the generator, the metadata schema test, and the metadata files. --- bin/functionMetadata_original.php | 12 +++++++- bin/generate-function-metadata.php | 30 +++++++++++++++++-- resources/functionMetadata.php | 11 ++++++- .../SignatureMap/FunctionMetadataTest.php | 1 + 4 files changed, 50 insertions(+), 4 deletions(-) diff --git a/bin/functionMetadata_original.php b/bin/functionMetadata_original.php index 82c316f0558..09cea7a5a9f 100644 --- a/bin/functionMetadata_original.php +++ b/bin/functionMetadata_original.php @@ -13,9 +13,13 @@ * the call is pure unless one of the listed callable parameters * (keyed by parameter name) receives an impure callable, e.g. array_map() * whose only side effects come from its 'callback' argument. + * - ['pureUnlessParameterPassedParameters' => array] + * the call is pure unless one of the listed (by-ref out) parameters + * (keyed by parameter name) receives an argument, e.g. str_replace() + * whose only side effect is writing to its optional 'count' argument. */ -/** @var array}> */ +/** @var array}|array{pureUnlessParameterPassedParameters: array}> */ return [ 'abs' => ['hasSideEffects' => false], 'acos' => ['hasSideEffects' => false], @@ -264,7 +268,11 @@ 'output_reset_rewrite_vars' => ['hasSideEffects' => true], 'pclose' => ['hasSideEffects' => true], 'popen' => ['hasSideEffects' => true], + 'preg_match' => ['pureUnlessParameterPassedParameters' => ['matches' => true]], + 'preg_match_all' => ['pureUnlessParameterPassedParameters' => ['matches' => true]], + 'preg_replace' => ['pureUnlessParameterPassedParameters' => ['count' => true]], 'preg_replace_callback' => ['pureUnlessCallableIsImpureParameters' => ['callback' => true]], + 'similar_text' => ['pureUnlessParameterPassedParameters' => ['percent' => true]], 'readfile' => ['hasSideEffects' => true], 'rename' => ['hasSideEffects' => true], 'rewind' => ['hasSideEffects' => true], @@ -272,6 +280,8 @@ 'sprintf' => ['hasSideEffects' => false], 'str_decrement' => ['hasSideEffects' => false], 'str_increment' => ['hasSideEffects' => false], + 'str_ireplace' => ['pureUnlessParameterPassedParameters' => ['count' => true]], + 'str_replace' => ['pureUnlessParameterPassedParameters' => ['count' => true]], 'symlink' => ['hasSideEffects' => true], 'time' => ['hasSideEffects' => true], 'tempnam' => ['hasSideEffects' => true], diff --git a/bin/generate-function-metadata.php b/bin/generate-function-metadata.php index 0018406d1c6..398dd4f9ec2 100755 --- a/bin/generate-function-metadata.php +++ b/bin/generate-function-metadata.php @@ -124,7 +124,7 @@ public function enterNode(Node $node) ); } - /** @var array}> $metadata */ + /** @var array, pureUnlessParameterPassedParameters?: array}> $metadata */ $metadata = require __DIR__ . '/functionMetadata_original.php'; foreach ($visitor->functions as $functionName) { if (array_key_exists($functionName, $metadata)) { @@ -139,6 +139,14 @@ public function enterNode(Node $node) continue; } + + if (isset($metadata[$functionName]['pureUnlessParameterPassedParameters'])) { + $metadata[$functionName] = [ + 'pureUnlessParameterPassedParameters' => $metadata[$functionName]['pureUnlessParameterPassedParameters'], + ]; + + continue; + } } $metadata[$functionName] = ['hasSideEffects' => false]; } @@ -197,9 +205,12 @@ public function enterNode(Node $node) * - ['pureUnlessCallableIsImpureParameters' => array] - pure unless * one of the listed callable parameters (keyed by parameter name) receives an * impure callable, e.g. array_map()'s 'callback'. + * - ['pureUnlessParameterPassedParameters' => array] - pure unless + * one of the listed (by-ref out) parameters (keyed by parameter name) receives + * an argument, e.g. str_replace()'s 'replace_count'. */ -/** @var array}> */ +/** @var array}|array{pureUnlessParameterPassedParameters: array}> */ return [ %s ]; @@ -221,6 +232,20 @@ public function enterNode(Node $node) ), ), ]; + $encodePureUnlessParameterPassedParameters = static fn (array $meta) => [ + $escape('pureUnlessParameterPassedParameters'), + sprintf( + '[%s]', + implode( + ' ,', + array_map( + static fn ($key, $param) => sprintf('%s => %s', $escape($key), $escape($param)), + array_keys($meta['pureUnlessParameterPassedParameters']), + $meta['pureUnlessParameterPassedParameters'], + ), + ), + ), + ]; foreach ($metadata as $name => $meta) { $content .= sprintf( @@ -229,6 +254,7 @@ public function enterNode(Node $node) ...match (true) { isset($meta['hasSideEffects']) => $encodeHasSideEffects($meta), isset($meta['pureUnlessCallableIsImpureParameters']) => $encodePureUnlessCallableIsImpureParameters($meta), + isset($meta['pureUnlessParameterPassedParameters']) => $encodePureUnlessParameterPassedParameters($meta), default => throw new ShouldNotHappenException($escape($meta)), }, ); diff --git a/resources/functionMetadata.php b/resources/functionMetadata.php index 794d9ce6c3f..9690de4fd23 100644 --- a/resources/functionMetadata.php +++ b/resources/functionMetadata.php @@ -19,9 +19,12 @@ * - ['pureUnlessCallableIsImpureParameters' => array] - pure unless * one of the listed callable parameters (keyed by parameter name) receives an * impure callable, e.g. array_map()'s 'callback'. + * - ['pureUnlessParameterPassedParameters' => array] - pure unless + * one of the listed (by-ref out) parameters (keyed by parameter name) receives + * an argument, e.g. str_replace()'s 'count'. */ -/** @var array}> */ +/** @var array}|array{pureUnlessParameterPassedParameters: array}> */ return [ 'BackedEnum::from' => ['hasSideEffects' => false], 'BackedEnum::tryFrom' => ['hasSideEffects' => false], @@ -1633,7 +1636,10 @@ 'preg_grep' => ['hasSideEffects' => false], 'preg_last_error' => ['hasSideEffects' => true], 'preg_last_error_msg' => ['hasSideEffects' => true], + 'preg_match' => ['pureUnlessParameterPassedParameters' => ['matches' => true]], + 'preg_match_all' => ['pureUnlessParameterPassedParameters' => ['matches' => true]], 'preg_quote' => ['hasSideEffects' => false], + 'preg_replace' => ['pureUnlessParameterPassedParameters' => ['count' => true]], 'preg_replace_callback' => ['pureUnlessCallableIsImpureParameters' => ['callback' => true]], 'preg_split' => ['hasSideEffects' => false], 'property_exists' => ['hasSideEffects' => false], @@ -1665,6 +1671,7 @@ 'rtrim' => ['hasSideEffects' => false], 'sha1' => ['hasSideEffects' => false], 'sha1_file' => ['hasSideEffects' => true], + 'similar_text' => ['pureUnlessParameterPassedParameters' => ['percent' => true]], 'sin' => ['hasSideEffects' => false], 'sinh' => ['hasSideEffects' => false], 'sizeof' => ['hasSideEffects' => false], @@ -1679,8 +1686,10 @@ 'str_ends_with' => ['hasSideEffects' => false], 'str_getcsv' => ['hasSideEffects' => false], 'str_increment' => ['hasSideEffects' => false], + 'str_ireplace' => ['pureUnlessParameterPassedParameters' => ['count' => true]], 'str_pad' => ['hasSideEffects' => false], 'str_repeat' => ['hasSideEffects' => false], + 'str_replace' => ['pureUnlessParameterPassedParameters' => ['count' => true]], 'str_rot13' => ['hasSideEffects' => false], 'str_split' => ['hasSideEffects' => false], 'str_starts_with' => ['hasSideEffects' => false], diff --git a/tests/PHPStan/Reflection/SignatureMap/FunctionMetadataTest.php b/tests/PHPStan/Reflection/SignatureMap/FunctionMetadataTest.php index 365e3d49757..789c1a387cd 100644 --- a/tests/PHPStan/Reflection/SignatureMap/FunctionMetadataTest.php +++ b/tests/PHPStan/Reflection/SignatureMap/FunctionMetadataTest.php @@ -20,6 +20,7 @@ public function testSchema(): void Expect::structure([ 'hasSideEffects' => Expect::bool(), 'pureUnlessCallableIsImpureParameters' => Expect::arrayOf(Expect::bool(), Expect::string()), + 'pureUnlessParameterPassedParameters' => Expect::arrayOf(Expect::bool(), Expect::string()), ]) ->assert(static fn ($v) => count((array) $v) > 0, 'Metadata entries must not be empty.') ->required(), From dd88f4c065a3ad6b10a03a32cc53b30e010bbb83 Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Wed, 8 Jul 2026 02:13:57 +0900 Subject: [PATCH 05/29] Add tests for @pure-unless-parameter-passed --- .../Rules/Pure/PureFunctionRuleTest.php | 28 +++++++++ .../pure-unless-parameter-passed-builtin.php | 41 ++++++++++++ .../data/pure-unless-parameter-passed.php | 63 +++++++++++++++++++ 3 files changed, 132 insertions(+) create mode 100644 tests/PHPStan/Rules/Pure/data/pure-unless-parameter-passed-builtin.php create mode 100644 tests/PHPStan/Rules/Pure/data/pure-unless-parameter-passed.php diff --git a/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php b/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php index dac0917d590..788d7510d39 100644 --- a/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php +++ b/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php @@ -397,4 +397,32 @@ public function testPropertyHookImpurePoint(): void ]); } + public function testPureUnlessParameterPassed(): void + { + $this->analyse([__DIR__ . '/data/pure-unless-parameter-passed.php'], [ + [ + 'Possibly impure call to function PureUnlessParameterPassedFunction\myReplace() in pure function PureUnlessParameterPassedFunction\purePassingByRef().', + 44, + ], + [ + 'Possibly impure call to function PureUnlessParameterPassedFunction\myReplacePhpstanAlias() in pure function PureUnlessParameterPassedFunction\purePassingByRefAlias().', + 62, + ], + ]); + } + + public function testPureUnlessParameterPassedBuiltin(): void + { + $this->analyse([__DIR__ . '/data/pure-unless-parameter-passed-builtin.php'], [ + [ + 'Possibly impure call to function str_replace() in pure function PureUnlessParameterPassedBuiltin\pureStrReplaceWithCount().', + 22, + ], + [ + 'Possibly impure call to function preg_match() in pure function PureUnlessParameterPassedBuiltin\purePregMatchWithMatches().', + 40, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Pure/data/pure-unless-parameter-passed-builtin.php b/tests/PHPStan/Rules/Pure/data/pure-unless-parameter-passed-builtin.php new file mode 100644 index 00000000000..a8f607ef1d5 --- /dev/null +++ b/tests/PHPStan/Rules/Pure/data/pure-unless-parameter-passed-builtin.php @@ -0,0 +1,41 @@ + Date: Wed, 8 Jul 2026 02:46:03 +0900 Subject: [PATCH 06/29] Cover the pre-PHP 8 parameter names for the by-ref out-parameter builtins str_replace/str_ireplace's count and preg_match/preg_match_all's matches resolve to different parameter names (replace_count/subpatterns) when the target PHP version is below 8.0: NativeFunctionReflectionProvider falls back from php-8-stubs to the legacy functionMap.php names in that case. List both names in the metadata so the by-ref-omitted suppression works regardless of the analysed PHP version. Found via the old-PHPUnit (7.4) CI job, which runs the test suite against phpVersion 7.4 by default. --- bin/functionMetadata_original.php | 12 ++++++++---- resources/functionMetadata.php | 8 ++++---- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/bin/functionMetadata_original.php b/bin/functionMetadata_original.php index 09cea7a5a9f..1d6bfdfd42e 100644 --- a/bin/functionMetadata_original.php +++ b/bin/functionMetadata_original.php @@ -268,8 +268,10 @@ 'output_reset_rewrite_vars' => ['hasSideEffects' => true], 'pclose' => ['hasSideEffects' => true], 'popen' => ['hasSideEffects' => true], - 'preg_match' => ['pureUnlessParameterPassedParameters' => ['matches' => true]], - 'preg_match_all' => ['pureUnlessParameterPassedParameters' => ['matches' => true]], + // 'matches'/'subpatterns': PHP 8+ uses the php-8-stubs parameter name, PHP <8 falls + // back to the legacy functionMap.php name. + 'preg_match' => ['pureUnlessParameterPassedParameters' => ['matches' => true, 'subpatterns' => true]], + 'preg_match_all' => ['pureUnlessParameterPassedParameters' => ['matches' => true, 'subpatterns' => true]], 'preg_replace' => ['pureUnlessParameterPassedParameters' => ['count' => true]], 'preg_replace_callback' => ['pureUnlessCallableIsImpureParameters' => ['callback' => true]], 'similar_text' => ['pureUnlessParameterPassedParameters' => ['percent' => true]], @@ -280,8 +282,10 @@ 'sprintf' => ['hasSideEffects' => false], 'str_decrement' => ['hasSideEffects' => false], 'str_increment' => ['hasSideEffects' => false], - 'str_ireplace' => ['pureUnlessParameterPassedParameters' => ['count' => true]], - 'str_replace' => ['pureUnlessParameterPassedParameters' => ['count' => true]], + // 'count'/'replace_count': PHP 8+ uses the php-8-stubs parameter name, PHP <8 falls + // back to the legacy functionMap.php name. + 'str_ireplace' => ['pureUnlessParameterPassedParameters' => ['count' => true, 'replace_count' => true]], + 'str_replace' => ['pureUnlessParameterPassedParameters' => ['count' => true, 'replace_count' => true]], 'symlink' => ['hasSideEffects' => true], 'time' => ['hasSideEffects' => true], 'tempnam' => ['hasSideEffects' => true], diff --git a/resources/functionMetadata.php b/resources/functionMetadata.php index 9690de4fd23..b743d47a282 100644 --- a/resources/functionMetadata.php +++ b/resources/functionMetadata.php @@ -1636,8 +1636,8 @@ 'preg_grep' => ['hasSideEffects' => false], 'preg_last_error' => ['hasSideEffects' => true], 'preg_last_error_msg' => ['hasSideEffects' => true], - 'preg_match' => ['pureUnlessParameterPassedParameters' => ['matches' => true]], - 'preg_match_all' => ['pureUnlessParameterPassedParameters' => ['matches' => true]], + 'preg_match' => ['pureUnlessParameterPassedParameters' => ['matches' => true, 'subpatterns' => true]], + 'preg_match_all' => ['pureUnlessParameterPassedParameters' => ['matches' => true, 'subpatterns' => true]], 'preg_quote' => ['hasSideEffects' => false], 'preg_replace' => ['pureUnlessParameterPassedParameters' => ['count' => true]], 'preg_replace_callback' => ['pureUnlessCallableIsImpureParameters' => ['callback' => true]], @@ -1686,10 +1686,10 @@ 'str_ends_with' => ['hasSideEffects' => false], 'str_getcsv' => ['hasSideEffects' => false], 'str_increment' => ['hasSideEffects' => false], - 'str_ireplace' => ['pureUnlessParameterPassedParameters' => ['count' => true]], + 'str_ireplace' => ['pureUnlessParameterPassedParameters' => ['count' => true, 'replace_count' => true]], 'str_pad' => ['hasSideEffects' => false], 'str_repeat' => ['hasSideEffects' => false], - 'str_replace' => ['pureUnlessParameterPassedParameters' => ['count' => true]], + 'str_replace' => ['pureUnlessParameterPassedParameters' => ['count' => true, 'replace_count' => true]], 'str_rot13' => ['hasSideEffects' => false], 'str_split' => ['hasSideEffects' => false], 'str_starts_with' => ['hasSideEffects' => false], From d391af614bd34b7dddc6ecd5fac31d4e8f862d83 Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Wed, 8 Jul 2026 18:25:47 +0900 Subject: [PATCH 07/29] Honor @pure-unless-parameter-passed on constructor calls, unpacked and named arguments NewHandler now applies the parameter-passed verdict to 'new' the same way the sibling callable verdict is applied (constructors return void, so createFromVariant cannot be reused). The verdict also treats argument unpacking conservatively (an unpacked argument might cover the flagged by-ref parameter, so the call stays possibly impure) and respects the flag's own TrinaryLogic certainty from union-variant composition (an uncertain flag downgrades a passed argument to Maybe instead of No). --- src/Analyser/ExprHandler/NewHandler.php | 10 ++ .../Callables/SimpleImpurePoint.php | 22 +++- .../Rules/Pure/PureFunctionRuleTest.php | 16 +++ .../data/pure-unless-parameter-passed.php | 107 +++++++++++++++++- turbo-ext/src/NewHandler.cpp | 8 ++ turbo-ext/src/SimpleImpurePoint.cpp | 25 +++- turbo-ext/src/support.h | 3 + 7 files changed, 187 insertions(+), 4 deletions(-) diff --git a/src/Analyser/ExprHandler/NewHandler.php b/src/Analyser/ExprHandler/NewHandler.php index 6bdf611ff35..5776e1a0f33 100644 --- a/src/Analyser/ExprHandler/NewHandler.php +++ b/src/Analyser/ExprHandler/NewHandler.php @@ -389,6 +389,16 @@ private function getConstructorImpurePoints(?ExtendedMethodReflection $construct $certain = true; } + if (!$certain) { + $passedVerdict = SimpleImpurePoint::resolvePureUnlessParameterPassedVerdict($parametersAcceptor, $expr->getArgs()); + if ($passedVerdict !== null && $passedVerdict->yes()) { + return []; + } + if ($passedVerdict !== null && $passedVerdict->no()) { + $certain = true; + } + } + return [ new ImpurePoint( $scopeBeforeArgs, diff --git a/src/Reflection/Callables/SimpleImpurePoint.php b/src/Reflection/Callables/SimpleImpurePoint.php index f19ec1a6dd7..dc4cbf79437 100644 --- a/src/Reflection/Callables/SimpleImpurePoint.php +++ b/src/Reflection/Callables/SimpleImpurePoint.php @@ -239,8 +239,14 @@ public static function resolvePureUnlessParameterPassedVerdict(ParametersAccepto $verdict ??= TrinaryLogic::createYes(); $matchedArg = null; + $hasUnpackedArg = false; $hasNamedParameter = false; foreach ($args as $i => $arg) { + if ($arg->unpack) { + $hasUnpackedArg = true; + continue; + } + if ($arg->name !== null) { $hasNamedParameter = true; if ($arg->name->name === $parameter->getName()) { @@ -258,10 +264,24 @@ public static function resolvePureUnlessParameterPassedVerdict(ParametersAccepto } if ($matchedArg === null) { + if ($hasUnpackedArg) { + // An unpacked argument list (...$args) might supply the flagged + // by-ref parameter, so we cannot be sure the call stays pure. + $verdict = $verdict->and(TrinaryLogic::createMaybe()); + } + + continue; + } + + if ($parameter->isPureUnlessParameterPassedParameter()->yes()) { + $verdict = $verdict->and(TrinaryLogic::createNo()); continue; } - $verdict = $verdict->and(TrinaryLogic::createNo()); + // The flag itself is uncertain (e.g. only one variant of a union type + // declares @pure-unless-parameter-passed), so passing an argument here + // only makes the call possibly impure, not certainly impure. + $verdict = $verdict->and(TrinaryLogic::createMaybe()); } return $verdict; diff --git a/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php b/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php index 788d7510d39..4686f7a08a2 100644 --- a/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php +++ b/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php @@ -408,6 +408,22 @@ public function testPureUnlessParameterPassed(): void 'Possibly impure call to function PureUnlessParameterPassedFunction\myReplacePhpstanAlias() in pure function PureUnlessParameterPassedFunction\purePassingByRefAlias().', 62, ], + [ + 'Possibly impure call to function PureUnlessParameterPassedFunction\myReplace() in pure function PureUnlessParameterPassedFunction\pureUnpackingArgs().', + 72, + ], + [ + 'Possibly impure call to function PureUnlessParameterPassedFunction\myReplace() in pure function PureUnlessParameterPassedFunction\pureNamedArgForFlaggedParameter().', + 91, + ], + [ + 'Impure instantiation of class PureUnlessParameterPassedFunction\MyReplacerConstructor in pure function PureUnlessParameterPassedFunction\pureConstructorPassingByRef().', + 127, + ], + [ + 'Possibly impure call to method PureUnlessParameterPassedFunction\PureUnlessParameterPassedA::m() in pure function PureUnlessParameterPassedFunction\pureUnionMethodPassingCount().', + 167, + ], ]); } diff --git a/tests/PHPStan/Rules/Pure/data/pure-unless-parameter-passed.php b/tests/PHPStan/Rules/Pure/data/pure-unless-parameter-passed.php index 0bc6b618220..5ae18df8d29 100644 --- a/tests/PHPStan/Rules/Pure/data/pure-unless-parameter-passed.php +++ b/tests/PHPStan/Rules/Pure/data/pure-unless-parameter-passed.php @@ -1,4 +1,4 @@ -= 8.0 namespace PureUnlessParameterPassedFunction; @@ -61,3 +61,108 @@ function purePassingByRefAlias(string $s): string return myReplacePhpstanAlias($s, $count); } + +/** + * @phpstan-pure + */ +function pureUnpackingArgs(string $s): string +{ + $args = [$s]; + // An unpacked argument list might supply $count, so this stays possibly impure. + return myReplace(...$args); +} + +/** + * @phpstan-pure + */ +function pureNamedArgForOtherParameter(string $s): string +{ + // The named argument targets $subject, not the flagged $count, so this stays pure. + return myReplace(subject: $s); +} + +/** + * @phpstan-pure + */ +function pureNamedArgForFlaggedParameter(string $s): string +{ + $count = 0; + // The named argument explicitly targets the flagged $count parameter. + return myReplace(subject: $s, count: $count); +} + +class MyReplacerConstructor +{ + + public string $s; + + /** + * @param-out int $count + * @pure-unless-parameter-passed $count + */ + public function __construct(string $s, int &$count = 0) + { + $this->s = $s; + $count = 1; + } + +} + +/** + * @phpstan-pure + */ +function pureConstructorNotPassingByRef(string $s): MyReplacerConstructor +{ + // $count is omitted, so instantiation stays pure. + return new MyReplacerConstructor($s); +} + +/** + * @phpstan-pure + */ +function pureConstructorPassingByRef(string $s): MyReplacerConstructor +{ + $count = 0; + // $count is passed, so instantiation is impure (the flag is certain). + return new MyReplacerConstructor($s, $count); +} + +interface PureUnlessParameterPassedA +{ + + /** + * @param-out int $count + * @pure-unless-parameter-passed $count + */ + public function m(string $s, int &$count = 0): string; + +} + +interface PureUnlessParameterPassedB +{ + + public function m(string $s, int &$count = 0): string; + +} + +/** + * @param PureUnlessParameterPassedA|PureUnlessParameterPassedB $obj + * @phpstan-pure + */ +function pureUnionMethodOmittingCount($obj, string $s): string +{ + // The flag is Yes in A and absent (No) in B, so combineAcceptors() merges it to Maybe. + // $count is omitted here, so the call stays pure regardless of the flag's certainty. + return $obj->m($s); +} + +/** + * @param PureUnlessParameterPassedA|PureUnlessParameterPassedB $obj + * @phpstan-pure + */ +function pureUnionMethodPassingCount($obj, string $s): string +{ + $count = 0; + // $count is passed against the Maybe-flagged parameter, so this is possibly impure. + return $obj->m($s, $count); +} diff --git a/turbo-ext/src/NewHandler.cpp b/turbo-ext/src/NewHandler.cpp index 2569cc0111e..48d023422a3 100644 --- a/turbo-ext/src/NewHandler.cpp +++ b/turbo-ext/src/NewHandler.cpp @@ -815,6 +815,14 @@ class NewHandler if (hasVerdict && verdict == PT_TRI_YES) return zv::Val::null(); if (hasVerdict && verdict == PT_TRI_NO) certain = true; + if (!certain) { + bool hasPassedVerdict; + zend_long passedVerdict; + if (UNEXPECTED(!pt_simple_impure_point_resolve_passed_verdict(parametersAcceptor, args, hasPassedVerdict, passedVerdict))) return zv::Val(); + if (hasPassedVerdict && passedVerdict == PT_TRI_YES) return zv::Val::null(); + if (hasPassedVerdict && passedVerdict == PT_TRI_NO) certain = true; + } + return constructorInstantiationImpurePoint(scopeBeforeArgs, expr, constructorReflection, certain); } diff --git a/turbo-ext/src/SimpleImpurePoint.cpp b/turbo-ext/src/SimpleImpurePoint.cpp index e73a67154b0..fea042789c3 100644 --- a/turbo-ext/src/SimpleImpurePoint.cpp +++ b/turbo-ext/src/SimpleImpurePoint.cpp @@ -36,6 +36,7 @@ namespace { pt_property_site pt_sip_arg_name_site; pt_property_site pt_sip_arg_value_site; +pt_property_site pt_sip_arg_unpack_site; pt_property_site pt_sip_identifier_name_site; /* $variant->getReturnType() */ @@ -420,10 +421,17 @@ class SimpleImpurePoint } zval *matchedArg = NULL; + bool hasUnpackedArg = false; bool hasNamedParameter = false; if (EXPECTED(Z_TYPE_P(args) == IS_ARRAY)) { for (auto argEntry : zv::TableRef(Z_ARRVAL_P(args))) { zval *arg = argEntry.value().deref().raw(); + zval *unpack = nodeProperty(pt_sip_arg_unpack_site, arg, PT_LC("unpack")); + if (UNEXPECTED(EG(exception))) return false; + if (zend_is_true(unpack)) { + hasUnpackedArg = true; + continue; + } zval *argName = nodeProperty(pt_sip_arg_name_site, arg, PT_LC("name")); if (UNEXPECTED(EG(exception))) return false; if (Z_TYPE_P(argName) != IS_NULL) { @@ -448,9 +456,17 @@ class SimpleImpurePoint if (UNEXPECTED(EG(exception))) return false; } - if (matchedArg == NULL) continue; + if (matchedArg == NULL) { + if (hasUnpackedArg) { + verdict = pt_trinary_and(verdict, PT_TRI_MAYBE); + } + continue; + } - verdict = pt_trinary_and(verdict, PT_TRI_NO); + /* $parameter->isPureUnlessParameterPassedParameter()->yes(), asked again */ + zend_long certainFlag = trinaryOf(parameterIsPureUnlessParameterPassed(parameter)); + if (UNEXPECTED(certainFlag < 0)) return false; + verdict = pt_trinary_and(verdict, certainFlag == PT_TRI_YES ? PT_TRI_NO : PT_TRI_MAYBE); } return true; @@ -542,6 +558,11 @@ bool pt_simple_impure_point_resolve_verdict(zval *variant, zval *scope, zval *ar return SimpleImpurePoint::resolveVerdict(variant, scope, args, hasVerdict, verdict); } +bool pt_simple_impure_point_resolve_passed_verdict(zval *variant, zval *args, bool &hasVerdict, zend_long &verdict) +{ + return SimpleImpurePoint::resolvePassedVerdict(variant, args, hasVerdict, verdict); +} + /* }}} */ /* {{{ engine ABI glue: parameter parsing + registration */ diff --git a/turbo-ext/src/support.h b/turbo-ext/src/support.h index 6939db6feec..6d8c866c39c 100644 --- a/turbo-ext/src/support.h +++ b/turbo-ext/src/support.h @@ -2745,6 +2745,9 @@ zv::Val pt_template_type_map_map(zval *map, zval *cb); * $scope, $args): hasVerdict false for the twin's null, verdict the PT_TRI_* * value otherwise; false = pending exception */ [[nodiscard]] bool pt_simple_impure_point_resolve_verdict(zval *variant, zval *scope, zval *args, bool &hasVerdict, zend_long &verdict); +/* SimpleImpurePoint::resolvePureUnlessParameterPassedVerdict($variant, + * $args): the same answers */ +[[nodiscard]] bool pt_simple_impure_point_resolve_passed_verdict(zval *variant, zval *args, bool &hasVerdict, zend_long &verdict); /* }}} */ From 64671b7756ee660e7f9425f44d3475bdeb7fb31a Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Wed, 8 Jul 2026 18:29:55 +0900 Subject: [PATCH 08/29] Parse @pure-unless-parameter-passed via phpdoc-parser 2.3.3 phpstan/phpdoc-parser#259 is merged and released, so replace the generic-tag stopgap with PhpDocNode::getPureUnlessParameterIsPassedTagValues() and bump the dependency. --- src/PhpDoc/PhpDocNodeResolver.php | 31 ++----------------------------- 1 file changed, 2 insertions(+), 29 deletions(-) diff --git a/src/PhpDoc/PhpDocNodeResolver.php b/src/PhpDoc/PhpDocNodeResolver.php index 9dff372d533..048031ae7fb 100644 --- a/src/PhpDoc/PhpDocNodeResolver.php +++ b/src/PhpDoc/PhpDocNodeResolver.php @@ -28,7 +28,6 @@ use PHPStan\PhpDoc\Tag\UsesTag; use PHPStan\PhpDoc\Tag\VarTag; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprNullNode; -use PHPStan\PhpDocParser\Ast\PhpDoc\GenericTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\MixinTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode; use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode; @@ -48,10 +47,8 @@ use function array_reverse; use function count; use function in_array; -use function ltrim; use function method_exists; use function str_starts_with; -use function strpos; use function substr; #[AutowiredService] @@ -412,33 +409,9 @@ public function resolveParamPureUnlessCallableIsImpure(PhpDocNode $phpDocNode): public function resolveParamPureUnlessParameterPassed(PhpDocNode $phpDocNode): array { $parameters = []; - // TODO: replace this generic-tag parsing with - // $phpDocNode->getPureUnlessParameterIsPassedTagValues() once - // phpstan/phpdoc-parser#259 is merged and the parser understands the tag. foreach (['@pure-unless-parameter-passed', '@phpstan-pure-unless-parameter-passed'] as $tagName) { - foreach ($phpDocNode->getTags() as $tag) { - if ($tag->name !== $tagName) { - continue; - } - if (!$tag->value instanceof GenericTagValueNode) { - continue; - } - - $value = ltrim($tag->value->value); - if ($value === '' || $value[0] !== '$') { - continue; - } - - $parameterName = substr($value, 1); - $spacePosition = strpos($parameterName, ' '); - if ($spacePosition !== false) { - $parameterName = substr($parameterName, 0, $spacePosition); - } - - if ($parameterName === '') { - continue; - } - + foreach ($phpDocNode->getPureUnlessParameterIsPassedTagValues($tagName) as $tag) { + $parameterName = substr($tag->parameterName, 1); $parameters[$parameterName] = true; } } From ebbb85817d75be4289c54d70c1929733e1fc8b08 Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Wed, 8 Jul 2026 19:20:39 +0900 Subject: [PATCH 09/29] Report certain impurity when a @pure-unless-parameter-passed argument is passed createFromVariant suppressed the impure point when the flagged parameter was omitted, but never promoted the verdict to certain when it was passed, unlike its @pure-unless-callable-is-impure sibling right above it and unlike NewHandler's own constructor handling. Passing the by-ref out-parameter is a definite side effect, not a possible one. Also covers intersection types, method inheritance (incl. a renamed parameter), and first-class callables. --- .../Callables/SimpleImpurePoint.php | 13 +- .../Rules/Pure/PureFunctionRuleTest.php | 35 +++- .../pure-unless-parameter-passed-builtin.php | 4 +- .../data/pure-unless-parameter-passed.php | 180 +++++++++++++++++- turbo-ext/src/SimpleImpurePoint.cpp | 5 +- 5 files changed, 223 insertions(+), 14 deletions(-) diff --git a/src/Reflection/Callables/SimpleImpurePoint.php b/src/Reflection/Callables/SimpleImpurePoint.php index dc4cbf79437..745753e907c 100644 --- a/src/Reflection/Callables/SimpleImpurePoint.php +++ b/src/Reflection/Callables/SimpleImpurePoint.php @@ -78,10 +78,15 @@ public static function createFromVariant(FunctionReflection|ExtendedMethodReflec if (!$certain && $scope !== null && $variant !== null) { $passedVerdict = self::resolvePureUnlessParameterPassedVerdict($variant, $args); - if ($passedVerdict !== null && $passedVerdict->yes()) { - // None of the @pure-unless-parameter-passed by-ref parameters - // received an argument, so the call is pure. - return null; + if ($passedVerdict !== null) { + if ($passedVerdict->yes()) { + // None of the @pure-unless-parameter-passed by-ref parameters + // received an argument, so the call is pure. + return null; + } + if ($passedVerdict->no()) { + $certain = true; + } } } diff --git a/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php b/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php index 4686f7a08a2..4be31d713e6 100644 --- a/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php +++ b/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php @@ -397,15 +397,16 @@ public function testPropertyHookImpurePoint(): void ]); } + #[RequiresPhp('>= 8.1.0')] public function testPureUnlessParameterPassed(): void { $this->analyse([__DIR__ . '/data/pure-unless-parameter-passed.php'], [ [ - 'Possibly impure call to function PureUnlessParameterPassedFunction\myReplace() in pure function PureUnlessParameterPassedFunction\purePassingByRef().', + 'Impure call to function PureUnlessParameterPassedFunction\myReplace() in pure function PureUnlessParameterPassedFunction\purePassingByRef().', 44, ], [ - 'Possibly impure call to function PureUnlessParameterPassedFunction\myReplacePhpstanAlias() in pure function PureUnlessParameterPassedFunction\purePassingByRefAlias().', + 'Impure call to function PureUnlessParameterPassedFunction\myReplacePhpstanAlias() in pure function PureUnlessParameterPassedFunction\purePassingByRefAlias().', 62, ], [ @@ -413,7 +414,7 @@ public function testPureUnlessParameterPassed(): void 72, ], [ - 'Possibly impure call to function PureUnlessParameterPassedFunction\myReplace() in pure function PureUnlessParameterPassedFunction\pureNamedArgForFlaggedParameter().', + 'Impure call to function PureUnlessParameterPassedFunction\myReplace() in pure function PureUnlessParameterPassedFunction\pureNamedArgForFlaggedParameter().', 91, ], [ @@ -424,6 +425,30 @@ public function testPureUnlessParameterPassed(): void 'Possibly impure call to method PureUnlessParameterPassedFunction\PureUnlessParameterPassedA::m() in pure function PureUnlessParameterPassedFunction\pureUnionMethodPassingCount().', 167, ], + [ + 'Impure call to method PureUnlessParameterPassedFunction\PureUnlessParameterPassedIntersectionA::m() in pure function PureUnlessParameterPassedFunction\pureIntersectionMethodPassingCount().', + 206, + ], + [ + 'Impure call to method PureUnlessParameterPassedFunction\Replacer::replace() in pure function PureUnlessParameterPassedFunction\pureCallingMethodPassingByRef().', + 241, + ], + [ + 'Impure call to method PureUnlessParameterPassedFunction\InheritedReplacerChild::replace() in pure function PureUnlessParameterPassedFunction\pureCallingInheritedMethodPassingByRef().', + 297, + ], + [ + 'Impure call to method PureUnlessParameterPassedFunction\InheritedReplacerRenamedChild::replace() in pure function PureUnlessParameterPassedFunction\pureCallingRenamedInheritedMethodPassingByRef().', + 318, + ], + [ + 'Possibly impure call to function PureUnlessParameterPassedFunction\myReplace() in pure function PureUnlessParameterPassedFunction\pureCallingFirstClassCallableOmittingCount().', + 331, + ], + [ + 'Possibly impure call to function PureUnlessParameterPassedFunction\myReplace() in pure function PureUnlessParameterPassedFunction\pureCallingFirstClassCallablePassingCount().', + 343, + ], ]); } @@ -431,11 +456,11 @@ public function testPureUnlessParameterPassedBuiltin(): void { $this->analyse([__DIR__ . '/data/pure-unless-parameter-passed-builtin.php'], [ [ - 'Possibly impure call to function str_replace() in pure function PureUnlessParameterPassedBuiltin\pureStrReplaceWithCount().', + 'Impure call to function str_replace() in pure function PureUnlessParameterPassedBuiltin\pureStrReplaceWithCount().', 22, ], [ - 'Possibly impure call to function preg_match() in pure function PureUnlessParameterPassedBuiltin\purePregMatchWithMatches().', + 'Impure call to function preg_match() in pure function PureUnlessParameterPassedBuiltin\purePregMatchWithMatches().', 40, ], ]); diff --git a/tests/PHPStan/Rules/Pure/data/pure-unless-parameter-passed-builtin.php b/tests/PHPStan/Rules/Pure/data/pure-unless-parameter-passed-builtin.php index a8f607ef1d5..e53b989e951 100644 --- a/tests/PHPStan/Rules/Pure/data/pure-unless-parameter-passed-builtin.php +++ b/tests/PHPStan/Rules/Pure/data/pure-unless-parameter-passed-builtin.php @@ -16,7 +16,7 @@ function pureStrReplaceWithoutCount(string $s): string */ function pureStrReplaceWithCount(string $s): string { - // The by-ref $count is passed, so str_replace() is possibly impure. + // The by-ref $count is passed, so str_replace() is impure (the flag is certain). $count = 0; return str_replace('a', 'b', $s, $count); @@ -36,6 +36,6 @@ function purePregMatchWithoutMatches(string $s): int */ function purePregMatchWithMatches(string $s): int { - // The by-ref $matches is passed, so preg_match() is possibly impure. + // The by-ref $matches is passed, so preg_match() is impure (the flag is certain). return (int) preg_match('/a/', $s, $matches); } diff --git a/tests/PHPStan/Rules/Pure/data/pure-unless-parameter-passed.php b/tests/PHPStan/Rules/Pure/data/pure-unless-parameter-passed.php index 5ae18df8d29..2844305adb6 100644 --- a/tests/PHPStan/Rules/Pure/data/pure-unless-parameter-passed.php +++ b/tests/PHPStan/Rules/Pure/data/pure-unless-parameter-passed.php @@ -1,4 +1,4 @@ -= 8.0 += 8.1 namespace PureUnlessParameterPassedFunction; @@ -38,7 +38,7 @@ function pureNotPassingByRef(string $s): string */ function purePassingByRef(string $s): string { - // $count is passed, so myReplace() is possibly impure. + // $count is passed, so myReplace() is impure (the flag is certain). $count = 0; return myReplace($s, $count); @@ -166,3 +166,179 @@ function pureUnionMethodPassingCount($obj, string $s): string // $count is passed against the Maybe-flagged parameter, so this is possibly impure. return $obj->m($s, $count); } + +interface PureUnlessParameterPassedIntersectionA +{ + + /** + * @param-out int $count + * @pure-unless-parameter-passed $count + */ + public function m(string $s, int &$count = 0): string; + +} + +interface PureUnlessParameterPassedIntersectionB +{ + + public function n(): string; + +} + +/** + * @param PureUnlessParameterPassedIntersectionA&PureUnlessParameterPassedIntersectionB $obj + * @phpstan-pure + */ +function pureIntersectionMethodOmittingCount($obj, string $s): string +{ + // $count is omitted here, so the call stays pure. + return $obj->m($s); +} + +/** + * @param PureUnlessParameterPassedIntersectionA&PureUnlessParameterPassedIntersectionB $obj + * @phpstan-pure + */ +function pureIntersectionMethodPassingCount($obj, string $s): string +{ + $count = 0; + // $count is passed, so this is impure (the flag is certain). + return $obj->m($s, $count); +} + +class Replacer +{ + + /** + * @param-out int $count + * @pure-unless-parameter-passed $count + */ + public function replace(string $subject, int &$count = 0): string + { + $count = 1; + + return $subject; + } + +} + +/** + * @phpstan-pure + */ +function pureCallingMethodNotPassingByRef(Replacer $replacer, string $s): string +{ + // $count is omitted, so the call stays pure. + return $replacer->replace($s); +} + +/** + * @phpstan-pure + */ +function pureCallingMethodPassingByRef(Replacer $replacer, string $s): string +{ + $count = 0; + // $count is passed, so the call is impure (the flag is certain). + return $replacer->replace($s, $count); +} + +abstract class InheritedReplacerParent +{ + + /** + * @param-out int $count + * @pure-unless-parameter-passed $count + */ + abstract public function replace(string $subject, int &$count = 0): string; + +} + +class InheritedReplacerChild extends InheritedReplacerParent +{ + + public function replace(string $subject, int &$count = 0): string + { + $count = 1; + + return $subject; + } + +} + +class InheritedReplacerRenamedChild extends InheritedReplacerParent +{ + + public function replace(string $subject, int &$cnt = 0): string + { + $cnt = 1; + + return $subject; + } + +} + +/** + * @phpstan-pure + */ +function pureCallingInheritedMethodNotPassingByRef(InheritedReplacerChild $replacer, string $s): string +{ + // The child does not re-declare @pure-unless-parameter-passed; it is inherited + // from the parent. $count is omitted here, so the call stays pure. + return $replacer->replace($s); +} + +/** + * @phpstan-pure + */ +function pureCallingInheritedMethodPassingByRef(InheritedReplacerChild $replacer, string $s): string +{ + $count = 0; + // The inherited @pure-unless-parameter-passed applies to the child, so passing + // $count makes the call impure (the flag is certain). + return $replacer->replace($s, $count); +} + +/** + * @phpstan-pure + */ +function pureCallingRenamedInheritedMethodNotPassingByRef(InheritedReplacerRenamedChild $replacer, string $s): string +{ + // The parent flags $count; the child renames it to $cnt. The inherited flag + // still applies to the renamed parameter. It is omitted here, so this stays pure. + return $replacer->replace($s); +} + +/** + * @phpstan-pure + */ +function pureCallingRenamedInheritedMethodPassingByRef(InheritedReplacerRenamedChild $replacer, string $s): string +{ + $cnt = 0; + // The inherited flag applies to the renamed parameter, so passing it makes + // the call impure (the flag is certain). + return $replacer->replace($s, $cnt); +} + +/** + * @phpstan-pure + */ +function pureCallingFirstClassCallableOmittingCount(string $s): string +{ + $f = myReplace(...); + // A first-class callable's purity is evaluated from its ParametersAcceptor + // alone, without scope/args, so @pure-unless-parameter-passed cannot gate on + // whether $count is actually passed at this call site; it stays possibly + // impure even though $count is omitted here. + return $f($s); +} + +/** + * @phpstan-pure + */ +function pureCallingFirstClassCallablePassingCount(string $s): string +{ + $count = 0; + $f = myReplace(...); + // The first-class callable is called with the flagged $count passed, so this + // stays possibly impure. + return $f($s, $count); +} diff --git a/turbo-ext/src/SimpleImpurePoint.cpp b/turbo-ext/src/SimpleImpurePoint.cpp index fea042789c3..254890b8e33 100644 --- a/turbo-ext/src/SimpleImpurePoint.cpp +++ b/turbo-ext/src/SimpleImpurePoint.cpp @@ -237,7 +237,10 @@ class SimpleImpurePoint zend_long passedVerdict = PT_TRI_YES; bool hasPassedVerdict = false; if (UNEXPECTED(!resolvePassedVerdict(variant, args, hasPassedVerdict, passedVerdict))) return false; - if (hasPassedVerdict && passedVerdict == PT_TRI_YES) return true; + if (hasPassedVerdict) { + if (passedVerdict == PT_TRI_YES) return true; + if (passedVerdict == PT_TRI_NO) certain = true; + } } zend_class_entry *functionReflectionCe = pt_class(PT_CLASS_FUNCTION_REFLECTION); From cb58bf7c605c95394ec709392e4244fea2a3b688 Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Thu, 9 Jul 2026 15:31:17 +0900 Subject: [PATCH 10/29] Recognize @phpstan-pure-unless-callable-is-impure as a known tag InvalidPHPStanDocTagRule matches @phpstan-* tags by name against a hardcoded list, so native phpdoc-parser recognition does not exempt them. @phpstan-pure-unless-callable-is-impure was never added to that list and is currently reported as an unknown tag; add it alongside @phpstan-pure-unless-parameter-passed and drop the stale TODO. --- src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php | 3 +-- tests/PHPStan/Rules/PhpDoc/data/invalid-phpstan-doc.php | 9 +++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php b/src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php index 5ce4a370fb8..e7f58e41bbc 100644 --- a/src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php +++ b/src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php @@ -64,8 +64,7 @@ final class InvalidPHPStanDocTagRule implements MultipleNodeTypesRule '@phpstan-param-immediately-invoked-callable', '@phpstan-param-later-invoked-callable', '@phpstan-param-closure-this', - // TODO: drop this once phpstan/phpdoc-parser#259 is merged and the parser - // recognizes the tag natively (like @phpstan-pure-unless-callable-is-impure). + '@phpstan-pure-unless-callable-is-impure', '@phpstan-pure-unless-parameter-passed', '@phpstan-all-methods-pure', '@phpstan-all-methods-impure', diff --git a/tests/PHPStan/Rules/PhpDoc/data/invalid-phpstan-doc.php b/tests/PHPStan/Rules/PhpDoc/data/invalid-phpstan-doc.php index 60a35c8178e..8dcb6d21473 100644 --- a/tests/PHPStan/Rules/PhpDoc/data/invalid-phpstan-doc.php +++ b/tests/PHPStan/Rules/PhpDoc/data/invalid-phpstan-doc.php @@ -58,3 +58,12 @@ public function doFoo(): string } } + +/** + * @param-out int $count + * @phpstan-pure-unless-callable-is-impure $callback + * @phpstan-pure-unless-parameter-passed $count + */ +function withPurityTags(callable $callback, int &$count = 0): void +{ +} From 99df90091f5ef31e849ea47a91eb6a4fd55c3d82 Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Thu, 9 Jul 2026 16:10:48 +0900 Subject: [PATCH 11/29] Report @pure-unless-parameter-passed on a non-optional parameter A non-optional parameter is always passed, so the tag can never keep the function or method pure. Report it as a misuse, mirroring the existing @pure-unless-callable-is-impure redundancy check. The declaration-side flag was dropped for functions: enterFunction() was never given the @pure-unless-parameter-passed parameters (the method path already passed them), so FunctionPurityCheck could not see it. Thread it through as well. --- src/Rules/Pure/FunctionPurityCheck.php | 17 +++++++++++ .../Rules/Pure/PureFunctionRuleTest.php | 4 +++ .../PHPStan/Rules/Pure/PureMethodRuleTest.php | 11 +++++++ .../pure-unless-parameter-passed-method.php | 30 +++++++++++++++++++ .../data/pure-unless-parameter-passed.php | 13 ++++++++ 5 files changed, 75 insertions(+) create mode 100644 tests/PHPStan/Rules/Pure/data/pure-unless-parameter-passed-method.php diff --git a/src/Rules/Pure/FunctionPurityCheck.php b/src/Rules/Pure/FunctionPurityCheck.php index be1ec07a5ae..c5877bea23c 100644 --- a/src/Rules/Pure/FunctionPurityCheck.php +++ b/src/Rules/Pure/FunctionPurityCheck.php @@ -91,6 +91,23 @@ public function check( ))->identifier(sprintf('pure%s.redundantUnlessCallable', $identifier))->build(); } + foreach ($parameters as $parameter) { + if (!$parameter->isPureUnlessParameterPassedParameter()->yes()) { + continue; + } + if ($parameter->isOptional()) { + continue; + } + + $errors[] = RuleErrorBuilder::message(sprintf( + '%s is marked @pure-unless-parameter-passed for parameter $%s, but $%s is not optional, so %s is never pure.', + $functionDescription, + $parameter->getName(), + $parameter->getName(), + lcfirst($functionDescription), + ))->identifier(sprintf('pure%s.nonOptionalParameterPassed', $identifier))->build(); + } + if ($isPure->yes()) { foreach ($parameters as $parameter) { if (!$parameter->passedByReference()->createsNewVariable()) { diff --git a/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php b/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php index 4be31d713e6..23156082a04 100644 --- a/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php +++ b/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php @@ -449,6 +449,10 @@ public function testPureUnlessParameterPassed(): void 'Possibly impure call to function PureUnlessParameterPassedFunction\myReplace() in pure function PureUnlessParameterPassedFunction\pureCallingFirstClassCallablePassingCount().', 343, ], + [ + 'Function PureUnlessParameterPassedFunction\nonOptionalUnlessParameterPassed() is marked @pure-unless-parameter-passed for parameter $count, but $count is not optional, so function PureUnlessParameterPassedFunction\nonOptionalUnlessParameterPassed() is never pure.', + 350, + ], ]); } diff --git a/tests/PHPStan/Rules/Pure/PureMethodRuleTest.php b/tests/PHPStan/Rules/Pure/PureMethodRuleTest.php index 61e32c9d606..f6aa7b42d14 100644 --- a/tests/PHPStan/Rules/Pure/PureMethodRuleTest.php +++ b/tests/PHPStan/Rules/Pure/PureMethodRuleTest.php @@ -419,4 +419,15 @@ public function testBug14557(): void ]); } + public function testPureUnlessParameterPassed(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/pure-unless-parameter-passed-method.php'], [ + [ + 'Method PureUnlessParameterPassedMethod\Replacer::replace() is marked @pure-unless-parameter-passed for parameter $count, but $count is not optional, so method PureUnlessParameterPassedMethod\Replacer::replace() is never pure.', + 12, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Pure/data/pure-unless-parameter-passed-method.php b/tests/PHPStan/Rules/Pure/data/pure-unless-parameter-passed-method.php new file mode 100644 index 00000000000..8d13645a602 --- /dev/null +++ b/tests/PHPStan/Rules/Pure/data/pure-unless-parameter-passed-method.php @@ -0,0 +1,30 @@ + Date: Thu, 9 Jul 2026 17:15:09 +0900 Subject: [PATCH 12/29] Expose @pure-unless-parameter-passed parameters as a function-level map FunctionPurityCheck read the per-parameter TrinaryLogic flag via ->yes() to find the flagged parameters, which is an equivalent mutation at a declaration site (the flag is only ever Yes or No there, never Maybe). Mirror the @pure-unless-callable-is-impure sibling instead: expose the flagged parameters as a function/method-level array getter and look them up with array_key_exists. --- .../AnnotationMethodReflection.php | 5 ++ .../Dummy/ChangedTypeMethodReflection.php | 5 ++ .../Dummy/DummyConstructorReflection.php | 5 ++ .../Dummy/DummyMethodReflection.php | 5 ++ src/Reflection/ExtendedMethodReflection.php | 5 ++ src/Reflection/FunctionReflection.php | 5 ++ .../Native/NativeFunctionReflection.php | 5 ++ .../Native/NativeMethodReflection.php | 5 ++ .../Php/ClosureCallMethodReflection.php | 5 ++ .../Php/EnumCasesMethodReflection.php | 5 ++ src/Reflection/Php/ExitFunctionReflection.php | 5 ++ .../PhpFunctionFromParserNodeReflection.php | 8 +++ src/Reflection/Php/PhpFunctionReflection.php | 8 +++ src/Reflection/Php/PhpMethodReflection.php | 8 +++ src/Reflection/ResolvedMethodReflection.php | 5 ++ .../Type/IntersectionTypeMethodReflection.php | 5 ++ ...gedPureUnlessParameterPassedParameters.php | 56 +++++++++++++++++++ .../Type/UnionTypeMethodReflection.php | 5 ++ .../WrappedExtendedMethodReflection.php | 5 ++ src/Rules/Pure/FunctionPurityCheck.php | 3 +- ...ewrittenDeclaringClassMethodReflection.php | 5 ++ turbo-ext/src/ChangedTypeMethodReflection.cpp | 3 + turbo-ext/src/PhpMethodReflectionAccess.cpp | 16 ++++-- turbo-ext/src/ResolvedMethodReflection.cpp | 4 ++ .../generated/ChangedTypeMethodReflection.h | 19 ++++--- .../src/generated/ResolvedMethodReflection.h | 43 +++++++------- turbo-ext/src/support.h | 1 + .../tests/php-class-reflection-family.php | 1 + turbo-ext/tests/type-family.php | 3 +- 29 files changed, 217 insertions(+), 36 deletions(-) create mode 100644 src/Reflection/Type/MergedPureUnlessParameterPassedParameters.php diff --git a/src/Reflection/Annotations/AnnotationMethodReflection.php b/src/Reflection/Annotations/AnnotationMethodReflection.php index dbd3f650f40..0da8fbc78b7 100644 --- a/src/Reflection/Annotations/AnnotationMethodReflection.php +++ b/src/Reflection/Annotations/AnnotationMethodReflection.php @@ -184,6 +184,11 @@ public function getPureUnlessCallableIsImpureParameters(): array return []; } + public function getPureUnlessParameterPassedParameters(): array + { + return []; + } + public function getAttributes(): array { return []; diff --git a/src/Reflection/Dummy/ChangedTypeMethodReflection.php b/src/Reflection/Dummy/ChangedTypeMethodReflection.php index 1a9266d046d..5192d95a353 100644 --- a/src/Reflection/Dummy/ChangedTypeMethodReflection.php +++ b/src/Reflection/Dummy/ChangedTypeMethodReflection.php @@ -175,6 +175,11 @@ public function getPureUnlessCallableIsImpureParameters(): array return $this->reflection->getPureUnlessCallableIsImpureParameters(); } + public function getPureUnlessParameterPassedParameters(): array + { + return $this->reflection->getPureUnlessParameterPassedParameters(); + } + public function getAttributes(): array { return $this->reflection->getAttributes(); diff --git a/src/Reflection/Dummy/DummyConstructorReflection.php b/src/Reflection/Dummy/DummyConstructorReflection.php index 8737d82ec02..f4b47652f21 100644 --- a/src/Reflection/Dummy/DummyConstructorReflection.php +++ b/src/Reflection/Dummy/DummyConstructorReflection.php @@ -160,6 +160,11 @@ public function getPureUnlessCallableIsImpureParameters(): array return []; } + public function getPureUnlessParameterPassedParameters(): array + { + return []; + } + public function getAttributes(): array { return []; diff --git a/src/Reflection/Dummy/DummyMethodReflection.php b/src/Reflection/Dummy/DummyMethodReflection.php index 106f9b51c63..448a18c37d2 100644 --- a/src/Reflection/Dummy/DummyMethodReflection.php +++ b/src/Reflection/Dummy/DummyMethodReflection.php @@ -152,6 +152,11 @@ public function getPureUnlessCallableIsImpureParameters(): array return []; } + public function getPureUnlessParameterPassedParameters(): array + { + return []; + } + public function getAttributes(): array { return []; diff --git a/src/Reflection/ExtendedMethodReflection.php b/src/Reflection/ExtendedMethodReflection.php index 9e9898e258e..c5042f0668f 100644 --- a/src/Reflection/ExtendedMethodReflection.php +++ b/src/Reflection/ExtendedMethodReflection.php @@ -76,6 +76,11 @@ public function isPure(): TrinaryLogic; */ public function getPureUnlessCallableIsImpureParameters(): array; + /** + * @return array + */ + public function getPureUnlessParameterPassedParameters(): array; + /** @return list */ public function getAttributes(): array; diff --git a/src/Reflection/FunctionReflection.php b/src/Reflection/FunctionReflection.php index 51f8ad4146e..a35b6ed8ee7 100644 --- a/src/Reflection/FunctionReflection.php +++ b/src/Reflection/FunctionReflection.php @@ -75,6 +75,11 @@ public function isPure(): TrinaryLogic; */ public function getPureUnlessCallableIsImpureParameters(): array; + /** + * @return array + */ + public function getPureUnlessParameterPassedParameters(): array; + /** @return list */ public function getAttributes(): array; diff --git a/src/Reflection/Native/NativeFunctionReflection.php b/src/Reflection/Native/NativeFunctionReflection.php index 9884b9855f8..b7394badc6a 100644 --- a/src/Reflection/Native/NativeFunctionReflection.php +++ b/src/Reflection/Native/NativeFunctionReflection.php @@ -117,6 +117,11 @@ public function getPureUnlessCallableIsImpureParameters(): array return []; } + public function getPureUnlessParameterPassedParameters(): array + { + return []; + } + private function isVoid(): bool { foreach ($this->variants as $variant) { diff --git a/src/Reflection/Native/NativeMethodReflection.php b/src/Reflection/Native/NativeMethodReflection.php index 298780447a7..da7a8fc14f9 100644 --- a/src/Reflection/Native/NativeMethodReflection.php +++ b/src/Reflection/Native/NativeMethodReflection.php @@ -194,6 +194,11 @@ public function getPureUnlessCallableIsImpureParameters(): array return []; } + public function getPureUnlessParameterPassedParameters(): array + { + return []; + } + private function isVoid(): bool { foreach ($this->variants as $variant) { diff --git a/src/Reflection/Php/ClosureCallMethodReflection.php b/src/Reflection/Php/ClosureCallMethodReflection.php index e031576c097..a65a321a4c4 100644 --- a/src/Reflection/Php/ClosureCallMethodReflection.php +++ b/src/Reflection/Php/ClosureCallMethodReflection.php @@ -208,6 +208,11 @@ public function getPureUnlessCallableIsImpureParameters(): array return $this->nativeMethodReflection->getPureUnlessCallableIsImpureParameters(); } + public function getPureUnlessParameterPassedParameters(): array + { + return $this->nativeMethodReflection->getPureUnlessParameterPassedParameters(); + } + public function getAttributes(): array { return $this->nativeMethodReflection->getAttributes(); diff --git a/src/Reflection/Php/EnumCasesMethodReflection.php b/src/Reflection/Php/EnumCasesMethodReflection.php index 40c170ba720..b0391071290 100644 --- a/src/Reflection/Php/EnumCasesMethodReflection.php +++ b/src/Reflection/Php/EnumCasesMethodReflection.php @@ -164,6 +164,11 @@ public function getPureUnlessCallableIsImpureParameters(): array return []; } + public function getPureUnlessParameterPassedParameters(): array + { + return []; + } + public function getAttributes(): array { return []; diff --git a/src/Reflection/Php/ExitFunctionReflection.php b/src/Reflection/Php/ExitFunctionReflection.php index a3609465e4a..5e92cc951f0 100644 --- a/src/Reflection/Php/ExitFunctionReflection.php +++ b/src/Reflection/Php/ExitFunctionReflection.php @@ -146,6 +146,11 @@ public function getPureUnlessCallableIsImpureParameters(): array return []; } + public function getPureUnlessParameterPassedParameters(): array + { + return []; + } + public function getAttributes(): array { return []; diff --git a/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php b/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php index cbbf67e1c7e..12ec0f36278 100644 --- a/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php +++ b/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php @@ -334,6 +334,14 @@ public function getPureUnlessCallableIsImpureParameters(): array return array_map(static fn (bool $value): TrinaryLogic => TrinaryLogic::createFromBoolean($value), $this->pureUnlessCallableIsImpureParameters); } + /** + * @return array + */ + public function getPureUnlessParameterPassedParameters(): array + { + return array_map(static fn (bool $value): TrinaryLogic => TrinaryLogic::createFromBoolean($value), $this->pureUnlessParameterPassedParameters); + } + public function getAttributes(): array { return $this->attributes; diff --git a/src/Reflection/Php/PhpFunctionReflection.php b/src/Reflection/Php/PhpFunctionReflection.php index f1fb979dd45..1dbee429546 100644 --- a/src/Reflection/Php/PhpFunctionReflection.php +++ b/src/Reflection/Php/PhpFunctionReflection.php @@ -227,6 +227,14 @@ public function getPureUnlessCallableIsImpureParameters(): array return array_map(static fn (bool $value): TrinaryLogic => TrinaryLogic::createFromBoolean($value), $this->phpDocParameterPureUnlessCallableIsImpure); } + /** + * @return array + */ + public function getPureUnlessParameterPassedParameters(): array + { + return array_map(static fn (bool $value): TrinaryLogic => TrinaryLogic::createFromBoolean($value), $this->phpDocParameterPureUnlessParameterPassed); + } + public function isBuiltin(): bool { return $this->reflection->isInternal(); diff --git a/src/Reflection/Php/PhpMethodReflection.php b/src/Reflection/Php/PhpMethodReflection.php index 1b394f5fb4b..db2f8a8d69c 100644 --- a/src/Reflection/Php/PhpMethodReflection.php +++ b/src/Reflection/Php/PhpMethodReflection.php @@ -427,6 +427,14 @@ public function getPureUnlessCallableIsImpureParameters(): array return array_map(static fn (bool $value): TrinaryLogic => TrinaryLogic::createFromBoolean($value), $this->pureUnlessCallableIsImpureParameters); } + /** + * @return array + */ + public function getPureUnlessParameterPassedParameters(): array + { + return array_map(static fn (bool $value): TrinaryLogic => TrinaryLogic::createFromBoolean($value), $this->pureUnlessParameterPassedParameters); + } + public function changePropertyGetHookPhpDocType(Type $phpDocType): self { return new self( diff --git a/src/Reflection/ResolvedMethodReflection.php b/src/Reflection/ResolvedMethodReflection.php index 4d9475bccfd..002776a7525 100644 --- a/src/Reflection/ResolvedMethodReflection.php +++ b/src/Reflection/ResolvedMethodReflection.php @@ -175,6 +175,11 @@ public function getPureUnlessCallableIsImpureParameters(): array return $this->reflection->getPureUnlessCallableIsImpureParameters(); } + public function getPureUnlessParameterPassedParameters(): array + { + return $this->reflection->getPureUnlessParameterPassedParameters(); + } + public function getAsserts(): Assertions { return $this->asserts ??= $this->reflection->getAsserts()->mapTypes(fn (Type $type) => TemplateTypeHelper::resolveTemplateTypes( diff --git a/src/Reflection/Type/IntersectionTypeMethodReflection.php b/src/Reflection/Type/IntersectionTypeMethodReflection.php index d62cc9ce6c7..e7e9929c9c2 100644 --- a/src/Reflection/Type/IntersectionTypeMethodReflection.php +++ b/src/Reflection/Type/IntersectionTypeMethodReflection.php @@ -219,6 +219,11 @@ public function getPureUnlessCallableIsImpureParameters(): array return MergedPureUnlessCallableIsImpureParameters::merge($this->methods); } + public function getPureUnlessParameterPassedParameters(): array + { + return MergedPureUnlessParameterPassedParameters::merge($this->methods); + } + public function getDocComment(): ?string { return null; diff --git a/src/Reflection/Type/MergedPureUnlessParameterPassedParameters.php b/src/Reflection/Type/MergedPureUnlessParameterPassedParameters.php new file mode 100644 index 00000000000..79dce93398a --- /dev/null +++ b/src/Reflection/Type/MergedPureUnlessParameterPassedParameters.php @@ -0,0 +1,56 @@ + + */ + public static function merge(array $methods): array + { + $parameterNames = []; + $maps = []; + foreach ($methods as $method) { + $map = $method->getPureUnlessParameterPassedParameters(); + $maps[] = $map; + foreach (array_keys($map) as $name) { + $parameterNames[$name] = true; + } + } + + $merged = []; + foreach (array_keys($parameterNames) as $name) { + $value = null; + foreach ($maps as $map) { + $current = $map[$name] ?? TrinaryLogic::createNo(); + if ($value === null) { + $value = $current; + continue; + } + $value = $value->equals($current) ? $value : TrinaryLogic::createMaybe(); + } + + if ($value === null) { + continue; + } + + $merged[$name] = $value; + } + + return $merged; + } + +} diff --git a/src/Reflection/Type/UnionTypeMethodReflection.php b/src/Reflection/Type/UnionTypeMethodReflection.php index 6953dafaad1..e1f4e4a28cf 100644 --- a/src/Reflection/Type/UnionTypeMethodReflection.php +++ b/src/Reflection/Type/UnionTypeMethodReflection.php @@ -176,6 +176,11 @@ public function getPureUnlessCallableIsImpureParameters(): array return MergedPureUnlessCallableIsImpureParameters::merge($this->methods); } + public function getPureUnlessParameterPassedParameters(): array + { + return MergedPureUnlessParameterPassedParameters::merge($this->methods); + } + public function getDocComment(): ?string { return null; diff --git a/src/Reflection/WrappedExtendedMethodReflection.php b/src/Reflection/WrappedExtendedMethodReflection.php index 1c613c116aa..38a4ab5e288 100644 --- a/src/Reflection/WrappedExtendedMethodReflection.php +++ b/src/Reflection/WrappedExtendedMethodReflection.php @@ -154,6 +154,11 @@ public function getPureUnlessCallableIsImpureParameters(): array return []; } + public function getPureUnlessParameterPassedParameters(): array + { + return []; + } + public function getAsserts(): Assertions { return Assertions::createEmpty(); diff --git a/src/Rules/Pure/FunctionPurityCheck.php b/src/Rules/Pure/FunctionPurityCheck.php index c5877bea23c..468079b793a 100644 --- a/src/Rules/Pure/FunctionPurityCheck.php +++ b/src/Rules/Pure/FunctionPurityCheck.php @@ -91,8 +91,9 @@ public function check( ))->identifier(sprintf('pure%s.redundantUnlessCallable', $identifier))->build(); } + $pureUnlessParameterPassedParameters = $functionReflection->getPureUnlessParameterPassedParameters(); foreach ($parameters as $parameter) { - if (!$parameter->isPureUnlessParameterPassedParameter()->yes()) { + if (!array_key_exists($parameter->getName(), $pureUnlessParameterPassedParameters)) { continue; } if ($parameter->isOptional()) { diff --git a/src/Rules/RestrictedUsage/RewrittenDeclaringClassMethodReflection.php b/src/Rules/RestrictedUsage/RewrittenDeclaringClassMethodReflection.php index f49c197a554..eb287f8b56f 100644 --- a/src/Rules/RestrictedUsage/RewrittenDeclaringClassMethodReflection.php +++ b/src/Rules/RestrictedUsage/RewrittenDeclaringClassMethodReflection.php @@ -106,6 +106,11 @@ public function getPureUnlessCallableIsImpureParameters(): array return $this->methodReflection->getPureUnlessCallableIsImpureParameters(); } + public function getPureUnlessParameterPassedParameters(): array + { + return $this->methodReflection->getPureUnlessParameterPassedParameters(); + } + public function getAttributes(): array { return $this->methodReflection->getAttributes(); diff --git a/turbo-ext/src/ChangedTypeMethodReflection.cpp b/turbo-ext/src/ChangedTypeMethodReflection.cpp index 474d70d7193..ccb2008e967 100644 --- a/turbo-ext/src/ChangedTypeMethodReflection.cpp +++ b/turbo-ext/src/ChangedTypeMethodReflection.cpp @@ -122,6 +122,7 @@ class ChangedTypeMethodReflection zv::Val isPure() const { return delegate(PT_MR_IS_PURE); } zv::Val getPureUnlessCallableIsImpureParameters() const { return delegate(PT_MR_GET_PURE_UNLESS_CALLABLE_IS_IMPURE_PARAMETERS); } + zv::Val getPureUnlessParameterPassedParameters() const { return delegate(PT_MR_GET_PURE_UNLESS_PARAMETER_PASSED_PARAMETERS); } zv::Val getAttributes() const { return delegate(PT_MR_GET_ATTRIBUTES); } zv::Val mustUseReturnValue() const { return delegate(PT_MR_MUST_USE_RETURN_VALUE); } zv::Val getResolvedPhpDoc() const { return delegate(PT_MR_GET_RESOLVED_PHP_DOC); } @@ -187,6 +188,7 @@ zv::Val pt_changed_type_method_reflection_call(zend_object *method, pt_method_re case PT_MR_HAS_SIDE_EFFECTS: return reflection.hasSideEffects(); case PT_MR_IS_PURE: return reflection.isPure(); case PT_MR_GET_PURE_UNLESS_CALLABLE_IS_IMPURE_PARAMETERS: return reflection.getPureUnlessCallableIsImpureParameters(); + case PT_MR_GET_PURE_UNLESS_PARAMETER_PASSED_PARAMETERS: return reflection.getPureUnlessParameterPassedParameters(); case PT_MR_GET_ASSERTS: return reflection.getAsserts(); case PT_MR_ACCEPTS_NAMED_ARGUMENTS: return reflection.acceptsNamedArguments(); case PT_MR_GET_SELF_OUT_TYPE: return reflection.getSelfOutType(); @@ -252,6 +254,7 @@ PT_MINIT_REGISTRATION(pt_register_changed_type_method_reflection) cls.method<&ChangedTypeMethodReflection::isAbstract>(sigs::isAbstract); cls.method<&ChangedTypeMethodReflection::isPure>(sigs::isPure); cls.method<&ChangedTypeMethodReflection::getPureUnlessCallableIsImpureParameters>(sigs::getPureUnlessCallableIsImpureParameters); + cls.method<&ChangedTypeMethodReflection::getPureUnlessParameterPassedParameters>(sigs::getPureUnlessParameterPassedParameters); cls.method<&ChangedTypeMethodReflection::getAttributes>(sigs::getAttributes); cls.method<&ChangedTypeMethodReflection::mustUseReturnValue>(sigs::mustUseReturnValue); cls.method<&ChangedTypeMethodReflection::getResolvedPhpDoc>(sigs::getResolvedPhpDoc); diff --git a/turbo-ext/src/PhpMethodReflectionAccess.cpp b/turbo-ext/src/PhpMethodReflectionAccess.cpp index 5fee6db2b35..d9cfae0c683 100644 --- a/turbo-ext/src/PhpMethodReflectionAccess.cpp +++ b/turbo-ext/src/PhpMethodReflectionAccess.cpp @@ -51,13 +51,14 @@ enum : uint32_t PT_PMR_PHP_DOC_COMMENT, PT_PMR_ATTRIBUTES, PT_PMR_PURE_UNLESS_CALLABLE_IS_IMPURE_PARAMETERS, + PT_PMR_PURE_UNLESS_PARAMETER_PASSED_PARAMETERS, PT_PMR_SLOT_COUNT, }; const char *const pt_pmr_names[PT_PMR_SLOT_COUNT] = { "name", "variants", "returnType", "declaringClass", "reflection", "phpDocThrowType", "resolvedPhpDocBlock", "deprecatedDescription", "isDeprecated", "isInternal", "isFinal", "isPure", "asserts", "acceptsNamedArguments", - "selfOutType", "phpDocComment", "attributes", "pureUnlessCallableIsImpureParameters", + "selfOutType", "phpDocComment", "attributes", "pureUnlessCallableIsImpureParameters", "pureUnlessParameterPassedParameters", }; struct Layout @@ -211,11 +212,12 @@ Answer hasSideEffects(zend_object *method, zv::Val &out) return PT_PMR_ANSWERED; } -/* getPureUnlessCallableIsImpureParameters(): array_map(static fn (bool $value) => - * TrinaryLogic::createFromBoolean($value), $this->pureUnlessCallableIsImpureParameters) */ -Answer pureUnlessCallableIsImpureParameters(zend_object *method, zv::Val &out) +/* getPureUnlessCallableIsImpureParameters() / getPureUnlessParameterPassedParameters(): + * array_map(static fn (bool $value) => TrinaryLogic::createFromBoolean($value), + * $this->pureUnlessCallableIsImpureParameters / $this->pureUnlessParameterPassedParameters) */ +Answer trinaryMapSlot(zend_object *method, uint32_t slot, zv::Val &out) { - zval *parameters = slotOf(method, PT_PMR_PURE_UNLESS_CALLABLE_IS_IMPURE_PARAMETERS); + zval *parameters = slotOf(method, slot); if (UNEXPECTED(parameters == NULL || Z_TYPE_P(parameters) != IS_ARRAY)) return PT_PMR_UNANSWERED; for (zv::ArrayEntry entry : zv::ArrRef(parameters)) { zval *value = entry.value().deref().raw(); @@ -336,7 +338,9 @@ int pt_php_method_reflection_answer(zend_object *method, pt_method_reflection_me return PT_PMR_ANSWERED; } case PT_MR_GET_PURE_UNLESS_CALLABLE_IS_IMPURE_PARAMETERS: - return pureUnlessCallableIsImpureParameters(method, out); + return trinaryMapSlot(method, PT_PMR_PURE_UNLESS_CALLABLE_IS_IMPURE_PARAMETERS, out); + case PT_MR_GET_PURE_UNLESS_PARAMETER_PASSED_PARAMETERS: + return trinaryMapSlot(method, PT_PMR_PURE_UNLESS_PARAMETER_PASSED_PARAMETERS, out); case PT_MR_GET_ASSERTS: return slotAnswer(method, PT_PMR_ASSERTS, out); case PT_MR_ACCEPTS_NAMED_ARGUMENTS: { diff --git a/turbo-ext/src/ResolvedMethodReflection.cpp b/turbo-ext/src/ResolvedMethodReflection.cpp index 552fabb46a0..b33dd831d01 100644 --- a/turbo-ext/src/ResolvedMethodReflection.cpp +++ b/turbo-ext/src/ResolvedMethodReflection.cpp @@ -67,6 +67,7 @@ const MemberName memberNames[PT_MR_MEMBER_COUNT] = { /* PT_MR_HAS_SIDE_EFFECTS */ {PT_LC("hassideeffects")}, /* PT_MR_IS_PURE */ {PT_LC("ispure")}, /* PT_MR_GET_PURE_UNLESS_CALLABLE_IS_IMPURE_PARAMETERS */ {PT_LC("getpureunlesscallableisimpureparameters")}, + /* PT_MR_GET_PURE_UNLESS_PARAMETER_PASSED_PARAMETERS */ {PT_LC("getpureunlessparameterpassedparameters")}, /* PT_MR_GET_ASSERTS */ {PT_LC("getasserts")}, /* PT_MR_ACCEPTS_NAMED_ARGUMENTS */ {PT_LC("acceptsnamedarguments")}, /* PT_MR_GET_SELF_OUT_TYPE */ {PT_LC("getselfouttype")}, @@ -217,6 +218,7 @@ class ResolvedMethodReflection zv::Val isPure() const { return delegate(PT_MR_IS_PURE); } zv::Val getPureUnlessCallableIsImpureParameters() const { return delegate(PT_MR_GET_PURE_UNLESS_CALLABLE_IS_IMPURE_PARAMETERS); } + zv::Val getPureUnlessParameterPassedParameters() const { return delegate(PT_MR_GET_PURE_UNLESS_PARAMETER_PASSED_PARAMETERS); } /* Mirrors getAsserts(): $this->asserts ??= the wrapped reflection's * asserts with their types resolved against the template maps */ @@ -354,6 +356,7 @@ zv::Val pt_resolved_method_reflection_call(zend_object *method, pt_method_reflec case PT_MR_HAS_SIDE_EFFECTS: return reflection.hasSideEffects(); case PT_MR_IS_PURE: return reflection.isPure(); case PT_MR_GET_PURE_UNLESS_CALLABLE_IS_IMPURE_PARAMETERS: return reflection.getPureUnlessCallableIsImpureParameters(); + case PT_MR_GET_PURE_UNLESS_PARAMETER_PASSED_PARAMETERS: return reflection.getPureUnlessParameterPassedParameters(); case PT_MR_GET_ASSERTS: return reflection.getAsserts(); case PT_MR_ACCEPTS_NAMED_ARGUMENTS: return reflection.acceptsNamedArguments(); case PT_MR_GET_SELF_OUT_TYPE: return reflection.getSelfOutType(); @@ -429,6 +432,7 @@ PT_MINIT_REGISTRATION(pt_register_resolved_method_reflection) cls.method<&ResolvedMethodReflection::hasSideEffects>(sigs::hasSideEffects); cls.method<&ResolvedMethodReflection::isPure>(sigs::isPure); cls.method<&ResolvedMethodReflection::getPureUnlessCallableIsImpureParameters>(sigs::getPureUnlessCallableIsImpureParameters); + cls.method<&ResolvedMethodReflection::getPureUnlessParameterPassedParameters>(sigs::getPureUnlessParameterPassedParameters); cls.method<&ResolvedMethodReflection::getAsserts>(sigs::getAsserts); cls.method<&ResolvedMethodReflection::acceptsNamedArguments>(sigs::acceptsNamedArguments); cls.method<&ResolvedMethodReflection::getSelfOutType>(sigs::getSelfOutType); diff --git a/turbo-ext/src/generated/ChangedTypeMethodReflection.h b/turbo-ext/src/generated/ChangedTypeMethodReflection.h index e0255c49761..9a53fbb9380 100644 --- a/turbo-ext/src/generated/ChangedTypeMethodReflection.h +++ b/turbo-ext/src/generated/ChangedTypeMethodReflection.h @@ -81,10 +81,11 @@ inline constexpr char strings[] = "isAbstract\0" /* 652 */ "isPure\0" /* 663 */ "getPureUnlessCallableIsImpureParameters\0" /* 670 */ - "getAttributes\0" /* 710 */ - "mustUseReturnValue\0" /* 724 */ - "PHPStan\\PhpDoc\\ResolvedPhpDocBlock\0" /* 743 */ - "getResolvedPhpDoc"; /* 778 */ + "getPureUnlessParameterPassedParameters\0" /* 710 */ + "getAttributes\0" /* 749 */ + "mustUseReturnValue\0" /* 763 */ + "PHPStan\\PhpDoc\\ResolvedPhpDocBlock\0" /* 782 */ + "getResolvedPhpDoc"; /* 817 */ inline constexpr reg::PackedArg args[] = { reg::packed(0, 0, 15), /* __construct $declaringClass */ reg::packed(50, 0, 61), /* __construct $reflection */ @@ -118,9 +119,10 @@ inline constexpr reg::PackedArg args[] = { reg::packed(230, 0, 452), /* isAbstract return */ reg::packed(230, 0, 452), /* isPure return */ reg::packed(230, MAY_BE_ARRAY), /* getPureUnlessCallableIsImpureParameters return */ + reg::packed(230, MAY_BE_ARRAY), /* getPureUnlessParameterPassedParameters return */ reg::packed(230, MAY_BE_ARRAY), /* getAttributes return */ reg::packed(230, 0, 452), /* mustUseReturnValue return */ - reg::packed(230, MAY_BE_NULL, 743), /* getResolvedPhpDoc return */ + reg::packed(230, MAY_BE_NULL, 782), /* getResolvedPhpDoc return */ }; using Sig = reg::Sig; } // namespace sigtab @@ -153,9 +155,10 @@ inline constexpr sigtab::Sig returnsByReference = { { 633 /* returnsByReference inline constexpr sigtab::Sig isAbstract = { { 652 /* isAbstract */, 0, 29, 0, 29, ZEND_ACC_PUBLIC } }; inline constexpr sigtab::Sig isPure = { { 663 /* isPure */, 0, 30, 0, 30, ZEND_ACC_PUBLIC } }; inline constexpr sigtab::Sig getPureUnlessCallableIsImpureParameters = { { 670 /* getPureUnlessCallableIsImpureParameters */, 0, 31, 0, 31, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getAttributes = { { 710 /* getAttributes */, 0, 32, 0, 32, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig mustUseReturnValue = { { 724 /* mustUseReturnValue */, 0, 33, 0, 33, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getResolvedPhpDoc = { { 778 /* getResolvedPhpDoc */, 0, 34, 0, 34, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getPureUnlessParameterPassedParameters = { { 710 /* getPureUnlessParameterPassedParameters */, 0, 32, 0, 32, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getAttributes = { { 749 /* getAttributes */, 0, 33, 0, 33, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig mustUseReturnValue = { { 763 /* mustUseReturnValue */, 0, 34, 0, 34, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getResolvedPhpDoc = { { 817 /* getResolvedPhpDoc */, 0, 35, 0, 35, ZEND_ACC_PUBLIC } }; } // namespace sig } // namespace ptdecl::ChangedTypeMethodReflection diff --git a/turbo-ext/src/generated/ResolvedMethodReflection.h b/turbo-ext/src/generated/ResolvedMethodReflection.h index b2b7931b61a..725aaae35a7 100644 --- a/turbo-ext/src/generated/ResolvedMethodReflection.h +++ b/turbo-ext/src/generated/ResolvedMethodReflection.h @@ -77,16 +77,17 @@ inline constexpr char strings[] = "hasSideEffects\0" /* 611 */ "isPure\0" /* 626 */ "getPureUnlessCallableIsImpureParameters\0" /* 633 */ - "PHPStan\\Reflection\\Assertions\0" /* 673 */ - "getAsserts\0" /* 703 */ - "acceptsNamedArguments\0" /* 714 */ - "getSelfOutType\0" /* 736 */ - "returnsByReference\0" /* 751 */ - "isAbstract\0" /* 770 */ - "getAttributes\0" /* 781 */ - "mustUseReturnValue\0" /* 795 */ - "PHPStan\\PhpDoc\\ResolvedPhpDocBlock\0" /* 814 */ - "getResolvedPhpDoc"; /* 849 */ + "getPureUnlessParameterPassedParameters\0" /* 673 */ + "PHPStan\\Reflection\\Assertions\0" /* 712 */ + "getAsserts\0" /* 742 */ + "acceptsNamedArguments\0" /* 753 */ + "getSelfOutType\0" /* 775 */ + "returnsByReference\0" /* 790 */ + "isAbstract\0" /* 809 */ + "getAttributes\0" /* 820 */ + "mustUseReturnValue\0" /* 834 */ + "PHPStan\\PhpDoc\\ResolvedPhpDocBlock\0" /* 853 */ + "getResolvedPhpDoc"; /* 888 */ inline constexpr reg::PackedArg args[] = { reg::packed(0, 0, 11), /* __construct $reflection */ reg::packed(55, 0, 79), /* __construct $resolvedTemplateTypeMap */ @@ -113,14 +114,15 @@ inline constexpr reg::PackedArg args[] = { reg::packed(193, 0, 475), /* hasSideEffects return */ reg::packed(193, 0, 475), /* isPure return */ reg::packed(193, MAY_BE_ARRAY), /* getPureUnlessCallableIsImpureParameters return */ - reg::packed(193, 0, 673), /* getAsserts return */ + reg::packed(193, MAY_BE_ARRAY), /* getPureUnlessParameterPassedParameters return */ + reg::packed(193, 0, 712), /* getAsserts return */ reg::packed(193, 0, 475), /* acceptsNamedArguments return */ reg::packed(193, MAY_BE_NULL, 580), /* getSelfOutType return */ reg::packed(193, 0, 475), /* returnsByReference return */ reg::packed(193, 0, 475), /* isAbstract return */ reg::packed(193, MAY_BE_ARRAY), /* getAttributes return */ reg::packed(193, 0, 475), /* mustUseReturnValue return */ - reg::packed(193, MAY_BE_NULL, 814), /* getResolvedPhpDoc return */ + reg::packed(193, MAY_BE_NULL, 853), /* getResolvedPhpDoc return */ }; using Sig = reg::Sig; } // namespace sigtab @@ -149,14 +151,15 @@ inline constexpr sigtab::Sig getThrowType = { { 598 /* getThrowType */, 0, 21, 0 inline constexpr sigtab::Sig hasSideEffects = { { 611 /* hasSideEffects */, 0, 22, 0, 22, ZEND_ACC_PUBLIC } }; inline constexpr sigtab::Sig isPure = { { 626 /* isPure */, 0, 23, 0, 23, ZEND_ACC_PUBLIC } }; inline constexpr sigtab::Sig getPureUnlessCallableIsImpureParameters = { { 633 /* getPureUnlessCallableIsImpureParameters */, 0, 24, 0, 24, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getAsserts = { { 703 /* getAsserts */, 0, 25, 0, 25, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig acceptsNamedArguments = { { 714 /* acceptsNamedArguments */, 0, 26, 0, 26, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getSelfOutType = { { 736 /* getSelfOutType */, 0, 27, 0, 27, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig returnsByReference = { { 751 /* returnsByReference */, 0, 28, 0, 28, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig isAbstract = { { 770 /* isAbstract */, 0, 29, 0, 29, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getAttributes = { { 781 /* getAttributes */, 0, 30, 0, 30, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig mustUseReturnValue = { { 795 /* mustUseReturnValue */, 0, 31, 0, 31, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getResolvedPhpDoc = { { 849 /* getResolvedPhpDoc */, 0, 32, 0, 32, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getPureUnlessParameterPassedParameters = { { 673 /* getPureUnlessParameterPassedParameters */, 0, 25, 0, 25, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getAsserts = { { 742 /* getAsserts */, 0, 26, 0, 26, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig acceptsNamedArguments = { { 753 /* acceptsNamedArguments */, 0, 27, 0, 27, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getSelfOutType = { { 775 /* getSelfOutType */, 0, 28, 0, 28, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig returnsByReference = { { 790 /* returnsByReference */, 0, 29, 0, 29, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig isAbstract = { { 809 /* isAbstract */, 0, 30, 0, 30, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getAttributes = { { 820 /* getAttributes */, 0, 31, 0, 31, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig mustUseReturnValue = { { 834 /* mustUseReturnValue */, 0, 32, 0, 32, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getResolvedPhpDoc = { { 888 /* getResolvedPhpDoc */, 0, 33, 0, 33, ZEND_ACC_PUBLIC } }; } // namespace sig } // namespace ptdecl::ResolvedMethodReflection diff --git a/turbo-ext/src/support.h b/turbo-ext/src/support.h index 6d8c866c39c..fdc299c19c8 100644 --- a/turbo-ext/src/support.h +++ b/turbo-ext/src/support.h @@ -2396,6 +2396,7 @@ enum pt_method_reflection_member PT_MR_HAS_SIDE_EFFECTS, PT_MR_IS_PURE, PT_MR_GET_PURE_UNLESS_CALLABLE_IS_IMPURE_PARAMETERS, + PT_MR_GET_PURE_UNLESS_PARAMETER_PASSED_PARAMETERS, PT_MR_GET_ASSERTS, PT_MR_ACCEPTS_NAMED_ARGUMENTS, PT_MR_GET_SELF_OUT_TYPE, diff --git a/turbo-ext/tests/php-class-reflection-family.php b/turbo-ext/tests/php-class-reflection-family.php index a52a06a04c9..fdf3f9be371 100644 --- a/turbo-ext/tests/php-class-reflection-family.php +++ b/turbo-ext/tests/php-class-reflection-family.php @@ -208,6 +208,7 @@ public function describeMethod(\PHPStan\Reflection\ExtendedMethodReflection $met 'asserts' => $this->describe($method->getAsserts(), $depth + 1), 'attributes' => $this->describe($method->getAttributes(), $depth + 1), 'pureUnlessCallableIsImpureParameters' => $method->getPureUnlessCallableIsImpureParameters(), + 'pureUnlessParameterPassedParameters' => $method->getPureUnlessParameterPassedParameters(), 'mustUseReturnValue' => $method->mustUseReturnValue()->describe(), 'resolvedPhpDoc' => $this->describe($method->getResolvedPhpDoc(), $depth + 1), 'variants' => array_map(fn ($variant) => $this->describeVariant($variant, $depth + 1), $method->getVariants()), diff --git a/turbo-ext/tests/type-family.php b/turbo-ext/tests/type-family.php index a5c6a463588..55570d52186 100644 --- a/turbo-ext/tests/type-family.php +++ b/turbo-ext/tests/type-family.php @@ -6668,7 +6668,7 @@ protected function getResult(): \PHPStan\Type\Type 'onlyVariant' => $catching(static fn () => $viewVariant($m->getOnlyVariant())), 'prototype' => $catching(static fn () => [get_class($m->getPrototype()), $m->getPrototype()->getDeclaringClass()->getName()]), 'flags' => $catching(static fn () => [$m->isStatic(), $m->isPrivate(), $m->isPublic(), $m->getDocComment(), $view($m->isDeprecated()), $m->getDeprecatedDescription(), $view($m->isFinal()), $view($m->isFinalByKeyword()), $view($m->isInternal()), $view($m->isBuiltin())]), - 'purity' => $catching(static fn () => [$view($m->hasSideEffects()), $view($m->isPure()), $m->getPureUnlessCallableIsImpureParameters(), $view($m->acceptsNamedArguments()), $view($m->returnsByReference()), $view($m->isAbstract()), $view($m->mustUseReturnValue())]), + 'purity' => $catching(static fn () => [$view($m->hasSideEffects()), $view($m->isPure()), $m->getPureUnlessCallableIsImpureParameters(), $m->getPureUnlessParameterPassedParameters(), $view($m->acceptsNamedArguments()), $view($m->returnsByReference()), $view($m->isAbstract()), $view($m->mustUseReturnValue())]), 'attributes' => $catching(static fn () => $viewAttributes($m->getAttributes())), 'phpDoc' => $catching(static fn () => $view($m->getResolvedPhpDoc())), 'memo' => $catching(static fn () => [$m->getVariants() === $m->getVariants(), $m->getNamedArgumentsVariants() === $m->getNamedArgumentsVariants(), $m->getAsserts() === $m->getAsserts(), $m->getSelfOutType() === $m->getSelfOutType(), $m->hasSideEffects() === $m->hasSideEffects(), $m->getDeclaringClass() === $m->getDeclaringClass()]), @@ -6725,6 +6725,7 @@ public function getThrowType(): ?\PHPStan\Type\Type { return $this->answer ? new public function hasSideEffects(): \PHPStan\TrinaryLogic { return \PHPStan\TrinaryLogic::createFromBoolean(!$this->answer); } public function isPure(): \PHPStan\TrinaryLogic { return \PHPStan\TrinaryLogic::createMaybe(); } public function getPureUnlessCallableIsImpureParameters(): array { return $this->answer ? ['callback' => true] : []; } + public function getPureUnlessParameterPassedParameters(): array { return $this->answer ? ['count' => \PHPStan\TrinaryLogic::createYes()] : []; } public function getAsserts(): \PHPStan\Reflection\Assertions { return $this->inner->getAsserts(); } public function acceptsNamedArguments(): \PHPStan\TrinaryLogic { return \PHPStan\TrinaryLogic::createFromBoolean($this->answer); } public function getSelfOutType(): ?\PHPStan\Type\Type { return $this->answer ? new \PHPStan\Type\Generic\GenericObjectType(\PHPStanTurboTests\PrototypeFixture::class, [(new \PHPStan\Type\Generic\TemplateTypeReference(\PHPStan\Type\Generic\TemplateTypeFactory::create(\PHPStan\Type\Generic\TemplateTypeScope::createWithClass(\PHPStanTurboTests\PrototypeFixture::class), 'T', null, \PHPStan\Type\Generic\TemplateTypeVariance::createInvariant()), \PHPStan\Type\Generic\TemplateTypeVariance::createInvariant()))->getType()]) : null; } From 2c8b6cb21f347cbd90dc73bcdcca106cac3f2348 Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Thu, 9 Jul 2026 22:42:23 +0900 Subject: [PATCH 13/29] Test @pure-unless-parameter-passed with a variadic parameter Cover a function whose flagged by-ref parameter is followed by a trailing variadic: omitting the parameter stays pure, passing it is impure, and the extra variadic arguments do not affect the flagged parameter. --- .../Rules/Pure/PureFunctionRuleTest.php | 8 ++++ .../data/pure-unless-parameter-passed.php | 42 +++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php b/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php index 23156082a04..49559a55273 100644 --- a/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php +++ b/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php @@ -453,6 +453,14 @@ public function testPureUnlessParameterPassed(): void 'Function PureUnlessParameterPassedFunction\nonOptionalUnlessParameterPassed() is marked @pure-unless-parameter-passed for parameter $count, but $count is not optional, so function PureUnlessParameterPassedFunction\nonOptionalUnlessParameterPassed() is never pure.', 350, ], + [ + 'Impure call to function PureUnlessParameterPassedFunction\myReplaceVariadic() in pure function PureUnlessParameterPassedFunction\pureVariadicPassingCount().', + 387, + ], + [ + 'Impure call to function PureUnlessParameterPassedFunction\myReplaceVariadic() in pure function PureUnlessParameterPassedFunction\pureVariadicPassingCountWithExtra().', + 398, + ], ]); } diff --git a/tests/PHPStan/Rules/Pure/data/pure-unless-parameter-passed.php b/tests/PHPStan/Rules/Pure/data/pure-unless-parameter-passed.php index d2fc40e6a05..b273da96595 100644 --- a/tests/PHPStan/Rules/Pure/data/pure-unless-parameter-passed.php +++ b/tests/PHPStan/Rules/Pure/data/pure-unless-parameter-passed.php @@ -355,3 +355,45 @@ function nonOptionalUnlessParameterPassed(string $subject, int &$count): string return $subject; } + +/** + * @param-out int $count + * @pure-unless-parameter-passed $count + */ +function myReplaceVariadic(string $subject, int &$count = 0, string ...$extra): string +{ + $count = 1; + + return $subject; +} + +/** + * @phpstan-pure + */ +function pureVariadicOmittingCount(string $s): string +{ + // The signature has a trailing variadic, but the flagged $count is omitted, so + // the call stays pure. + return myReplaceVariadic($s); +} + +/** + * @phpstan-pure + */ +function pureVariadicPassingCount(string $s): string +{ + $count = 0; + // $count is passed, so the call is impure regardless of the trailing variadic. + return myReplaceVariadic($s, $count); +} + +/** + * @phpstan-pure + */ +function pureVariadicPassingCountWithExtra(string $s): string +{ + $count = 0; + // $count is passed and the trailing variadic is also filled; the extra variadic + // arguments do not affect the flagged parameter, so the call is impure. + return myReplaceVariadic($s, $count, 'a', 'b'); +} From 4ff79985845e5c1c70137735ed294c38a98dce87 Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Thu, 9 Jul 2026 23:07:10 +0900 Subject: [PATCH 14/29] Mark preg_filter() as @pure-unless-parameter-passed preg_filter() has the same signature as the already-covered preg_replace() (a pure regex substitution with an optional by-ref $count out-parameter), so it stays pure unless $count is passed. --- bin/functionMetadata_original.php | 1 + resources/functionMetadata.php | 1 + .../Rules/Pure/PureFunctionRuleTest.php | 4 ++++ .../pure-unless-parameter-passed-builtin.php | 20 +++++++++++++++++++ 4 files changed, 26 insertions(+) diff --git a/bin/functionMetadata_original.php b/bin/functionMetadata_original.php index 1d6bfdfd42e..f2ec9da706c 100644 --- a/bin/functionMetadata_original.php +++ b/bin/functionMetadata_original.php @@ -268,6 +268,7 @@ 'output_reset_rewrite_vars' => ['hasSideEffects' => true], 'pclose' => ['hasSideEffects' => true], 'popen' => ['hasSideEffects' => true], + 'preg_filter' => ['pureUnlessParameterPassedParameters' => ['count' => true]], // 'matches'/'subpatterns': PHP 8+ uses the php-8-stubs parameter name, PHP <8 falls // back to the legacy functionMap.php name. 'preg_match' => ['pureUnlessParameterPassedParameters' => ['matches' => true, 'subpatterns' => true]], diff --git a/resources/functionMetadata.php b/resources/functionMetadata.php index b743d47a282..cd24fd94f9d 100644 --- a/resources/functionMetadata.php +++ b/resources/functionMetadata.php @@ -1633,6 +1633,7 @@ 'posix_ttyname' => ['hasSideEffects' => false], 'posix_uname' => ['hasSideEffects' => false], 'pow' => ['hasSideEffects' => false], + 'preg_filter' => ['pureUnlessParameterPassedParameters' => ['count' => true]], 'preg_grep' => ['hasSideEffects' => false], 'preg_last_error' => ['hasSideEffects' => true], 'preg_last_error_msg' => ['hasSideEffects' => true], diff --git a/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php b/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php index 49559a55273..535d844246d 100644 --- a/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php +++ b/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php @@ -475,6 +475,10 @@ public function testPureUnlessParameterPassedBuiltin(): void 'Impure call to function preg_match() in pure function PureUnlessParameterPassedBuiltin\purePregMatchWithMatches().', 40, ], + [ + 'Impure call to function preg_filter() in pure function PureUnlessParameterPassedBuiltin\purePregFilterWithCount().', + 60, + ], ]); } diff --git a/tests/PHPStan/Rules/Pure/data/pure-unless-parameter-passed-builtin.php b/tests/PHPStan/Rules/Pure/data/pure-unless-parameter-passed-builtin.php index e53b989e951..b3e7c47192d 100644 --- a/tests/PHPStan/Rules/Pure/data/pure-unless-parameter-passed-builtin.php +++ b/tests/PHPStan/Rules/Pure/data/pure-unless-parameter-passed-builtin.php @@ -39,3 +39,23 @@ function purePregMatchWithMatches(string $s): int // The by-ref $matches is passed, so preg_match() is impure (the flag is certain). return (int) preg_match('/a/', $s, $matches); } + +/** + * @phpstan-pure + */ +function purePregFilterWithoutCount(string $s): ?string +{ + // The by-ref $count is omitted, so preg_filter() is pure. + return preg_filter('/a/', 'b', $s); +} + +/** + * @phpstan-pure + */ +function purePregFilterWithCount(string $s): ?string +{ + // The by-ref $count is passed, so preg_filter() is impure (the flag is certain). + $count = 0; + + return preg_filter('/a/', 'b', $s, -1, $count); +} From 431c09fc69ce3b2d7842e691977123d7992669eb Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Fri, 10 Jul 2026 00:35:31 +0900 Subject: [PATCH 15/29] Combine the callable and parameter-passed purity verdicts A function can carry both @pure-unless-callable-is-impure and @pure-unless-parameter-passed at once - preg_replace_callback() is pure unless its callback is impure or its $count is passed. The two verdicts were checked in sequence, so a pure callback short-circuited to "pure" and never looked at $count. Combine them with TrinaryLogic::and() in both SimpleImpurePoint::createFromVariant() and NewHandler, and flag preg_replace_callback()'s $count. --- bin/functionMetadata_original.php | 9 +++-- resources/functionMetadata.php | 4 +-- src/Analyser/ExprHandler/NewHandler.php | 18 +++++----- .../Callables/SimpleImpurePoint.php | 23 +++++-------- .../Rules/Pure/PureFunctionRuleTest.php | 12 +++++++ .../pure-unless-parameter-passed-builtin.php | 33 +++++++++++++++++++ turbo-ext/src/NewHandler.cpp | 16 ++++----- turbo-ext/src/SimpleImpurePoint.cpp | 16 ++++----- 8 files changed, 85 insertions(+), 46 deletions(-) diff --git a/bin/functionMetadata_original.php b/bin/functionMetadata_original.php index f2ec9da706c..5ce171a7973 100644 --- a/bin/functionMetadata_original.php +++ b/bin/functionMetadata_original.php @@ -5,7 +5,7 @@ * keyed by lowercase function name or "Class::method". resources/functionMetadata.php * is generated from this file by bin/generate-function-metadata.php. * - * Each entry is exactly one of these shapes: + * Each entry has one of these shapes: * * - ['hasSideEffects' => bool] * false: the call is pure. true: the call has side effects. @@ -17,9 +17,12 @@ * the call is pure unless one of the listed (by-ref out) parameters * (keyed by parameter name) receives an argument, e.g. str_replace() * whose only side effect is writing to its optional 'count' argument. + * + * The last two can be combined for a call that is pure unless either happens, + * e.g. preg_replace_callback() (impure callback or a passed 'count'). */ -/** @var array}|array{pureUnlessParameterPassedParameters: array}> */ +/** @var array}|array{pureUnlessParameterPassedParameters: array}|array{pureUnlessCallableIsImpureParameters: array, pureUnlessParameterPassedParameters: array}> */ return [ 'abs' => ['hasSideEffects' => false], 'acos' => ['hasSideEffects' => false], @@ -274,7 +277,7 @@ 'preg_match' => ['pureUnlessParameterPassedParameters' => ['matches' => true, 'subpatterns' => true]], 'preg_match_all' => ['pureUnlessParameterPassedParameters' => ['matches' => true, 'subpatterns' => true]], 'preg_replace' => ['pureUnlessParameterPassedParameters' => ['count' => true]], - 'preg_replace_callback' => ['pureUnlessCallableIsImpureParameters' => ['callback' => true]], + 'preg_replace_callback' => ['pureUnlessCallableIsImpureParameters' => ['callback' => true], 'pureUnlessParameterPassedParameters' => ['count' => true]], 'similar_text' => ['pureUnlessParameterPassedParameters' => ['percent' => true]], 'readfile' => ['hasSideEffects' => true], 'rename' => ['hasSideEffects' => true], diff --git a/resources/functionMetadata.php b/resources/functionMetadata.php index cd24fd94f9d..1bdeb8ba0b5 100644 --- a/resources/functionMetadata.php +++ b/resources/functionMetadata.php @@ -24,7 +24,7 @@ * an argument, e.g. str_replace()'s 'count'. */ -/** @var array}|array{pureUnlessParameterPassedParameters: array}> */ +/** @var array}|array{pureUnlessParameterPassedParameters: array}|array{pureUnlessCallableIsImpureParameters: array, pureUnlessParameterPassedParameters: array}> */ return [ 'BackedEnum::from' => ['hasSideEffects' => false], 'BackedEnum::tryFrom' => ['hasSideEffects' => false], @@ -1641,7 +1641,7 @@ 'preg_match_all' => ['pureUnlessParameterPassedParameters' => ['matches' => true, 'subpatterns' => true]], 'preg_quote' => ['hasSideEffects' => false], 'preg_replace' => ['pureUnlessParameterPassedParameters' => ['count' => true]], - 'preg_replace_callback' => ['pureUnlessCallableIsImpureParameters' => ['callback' => true]], + 'preg_replace_callback' => ['pureUnlessCallableIsImpureParameters' => ['callback' => true], 'pureUnlessParameterPassedParameters' => ['count' => true]], 'preg_split' => ['hasSideEffects' => false], 'property_exists' => ['hasSideEffects' => false], 'quoted_printable_decode' => ['hasSideEffects' => false], diff --git a/src/Analyser/ExprHandler/NewHandler.php b/src/Analyser/ExprHandler/NewHandler.php index 5776e1a0f33..1ffcad58620 100644 --- a/src/Analyser/ExprHandler/NewHandler.php +++ b/src/Analyser/ExprHandler/NewHandler.php @@ -381,7 +381,15 @@ private function getConstructorImpurePoints(?ExtendedMethodReflection $construct } $certain = $constructorReflection->isPure()->no(); + // A constructor can carry both flags at once, so combine the verdicts + // the same way SimpleImpurePoint::createFromVariant() does for calls: + // Yes = pure, No = impure, Maybe = possibly impure. $verdict = SimpleImpurePoint::resolvePureUnlessCallableIsImpureVerdict($parametersAcceptor, $scope, $expr->getArgs()); + $passedVerdict = SimpleImpurePoint::resolvePureUnlessParameterPassedVerdict($parametersAcceptor, $expr->getArgs()); + if ($passedVerdict !== null) { + $verdict = $verdict === null ? $passedVerdict : $verdict->and($passedVerdict); + } + if ($verdict !== null && $verdict->yes()) { return []; } @@ -389,16 +397,6 @@ private function getConstructorImpurePoints(?ExtendedMethodReflection $construct $certain = true; } - if (!$certain) { - $passedVerdict = SimpleImpurePoint::resolvePureUnlessParameterPassedVerdict($parametersAcceptor, $expr->getArgs()); - if ($passedVerdict !== null && $passedVerdict->yes()) { - return []; - } - if ($passedVerdict !== null && $passedVerdict->no()) { - $certain = true; - } - } - return [ new ImpurePoint( $scopeBeforeArgs, diff --git a/src/Reflection/Callables/SimpleImpurePoint.php b/src/Reflection/Callables/SimpleImpurePoint.php index 745753e907c..0318829ccd3 100644 --- a/src/Reflection/Callables/SimpleImpurePoint.php +++ b/src/Reflection/Callables/SimpleImpurePoint.php @@ -65,7 +65,16 @@ public static function createFromVariant(FunctionReflection|ExtendedMethodReflec } if (!$certain && $scope !== null && $variant !== null) { + // A function can carry both flags at once (e.g. preg_replace_callback, + // which is pure unless its callback is impure or its $count is passed). + // It stays pure only when both verdicts agree it is pure, so combine + // them: Yes = pure, No = impure, Maybe = possibly impure. $verdict = self::resolvePureUnlessCallableIsImpureVerdict($variant, $scope, $args); + $passedVerdict = self::resolvePureUnlessParameterPassedVerdict($variant, $args); + if ($passedVerdict !== null) { + $verdict = $verdict === null ? $passedVerdict : $verdict->and($passedVerdict); + } + if ($verdict !== null) { if ($verdict->yes()) { return null; @@ -76,20 +85,6 @@ public static function createFromVariant(FunctionReflection|ExtendedMethodReflec } } - if (!$certain && $scope !== null && $variant !== null) { - $passedVerdict = self::resolvePureUnlessParameterPassedVerdict($variant, $args); - if ($passedVerdict !== null) { - if ($passedVerdict->yes()) { - // None of the @pure-unless-parameter-passed by-ref parameters - // received an argument, so the call is pure. - return null; - } - if ($passedVerdict->no()) { - $certain = true; - } - } - } - if ($function instanceof FunctionReflection) { if (isset(self::SIDE_EFFECT_FLIP_PARAMETERS[$function->getName()]) && $scope !== null) { [ diff --git a/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php b/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php index 535d844246d..9302af4b99e 100644 --- a/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php +++ b/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php @@ -479,6 +479,18 @@ public function testPureUnlessParameterPassedBuiltin(): void 'Impure call to function preg_filter() in pure function PureUnlessParameterPassedBuiltin\purePregFilterWithCount().', 60, ], + [ + 'Impure call to function preg_replace_callback() in pure function PureUnlessParameterPassedBuiltin\purePregReplaceCallbackWithCount().', + 80, + ], + [ + 'Impure call to function preg_replace_callback() in pure function PureUnlessParameterPassedBuiltin\purePregReplaceCallbackImpureCallback().', + 89, + ], + [ + 'Impure echo in pure function PureUnlessParameterPassedBuiltin\purePregReplaceCallbackImpureCallback().', + 90, + ], ]); } diff --git a/tests/PHPStan/Rules/Pure/data/pure-unless-parameter-passed-builtin.php b/tests/PHPStan/Rules/Pure/data/pure-unless-parameter-passed-builtin.php index b3e7c47192d..68087b3119d 100644 --- a/tests/PHPStan/Rules/Pure/data/pure-unless-parameter-passed-builtin.php +++ b/tests/PHPStan/Rules/Pure/data/pure-unless-parameter-passed-builtin.php @@ -59,3 +59,36 @@ function purePregFilterWithCount(string $s): ?string return preg_filter('/a/', 'b', $s, -1, $count); } + +/** + * @phpstan-pure + */ +function purePregReplaceCallbackPureCallback(string $s): ?string +{ + // preg_replace_callback() carries both flags: the callback is pure and the by-ref + // $count is omitted, so the call stays pure. + return preg_replace_callback('/a/', static fn (array $m): string => $m[0], $s); +} + +/** + * @phpstan-pure + */ +function purePregReplaceCallbackWithCount(string $s): ?string +{ + $count = 0; + // The callback is pure but the by-ref $count is passed, so the call is impure. + return preg_replace_callback('/a/', static fn (array $m): string => $m[0], $s, -1, $count); +} + +/** + * @phpstan-pure + */ +function purePregReplaceCallbackImpureCallback(string $s): ?string +{ + // The callback itself is impure, so the call is impure regardless of $count. + return preg_replace_callback('/a/', static function (array $m): string { + echo $m[0]; + + return $m[0]; + }, $s); +} diff --git a/turbo-ext/src/NewHandler.cpp b/turbo-ext/src/NewHandler.cpp index 48d023422a3..bddea63b684 100644 --- a/turbo-ext/src/NewHandler.cpp +++ b/turbo-ext/src/NewHandler.cpp @@ -809,20 +809,20 @@ class NewHandler bool certain = isPure == PT_TRI_NO; zval *args = exprArgs(expr); if (UNEXPECTED(args == NULL)) return zv::Val(); + /* both verdicts combined, as createFromVariant() does for calls */ bool hasVerdict; zend_long verdict; if (UNEXPECTED(!pt_simple_impure_point_resolve_verdict(parametersAcceptor, scope, args, hasVerdict, verdict))) return zv::Val(); + bool hasPassedVerdict; + zend_long passedVerdict; + if (UNEXPECTED(!pt_simple_impure_point_resolve_passed_verdict(parametersAcceptor, args, hasPassedVerdict, passedVerdict))) return zv::Val(); + if (hasPassedVerdict) { + verdict = hasVerdict ? pt_trinary_and(verdict, passedVerdict) : passedVerdict; + hasVerdict = true; + } if (hasVerdict && verdict == PT_TRI_YES) return zv::Val::null(); if (hasVerdict && verdict == PT_TRI_NO) certain = true; - if (!certain) { - bool hasPassedVerdict; - zend_long passedVerdict; - if (UNEXPECTED(!pt_simple_impure_point_resolve_passed_verdict(parametersAcceptor, args, hasPassedVerdict, passedVerdict))) return zv::Val(); - if (hasPassedVerdict && passedVerdict == PT_TRI_YES) return zv::Val::null(); - if (hasPassedVerdict && passedVerdict == PT_TRI_NO) certain = true; - } - return constructorInstantiationImpurePoint(scopeBeforeArgs, expr, constructorReflection, certain); } diff --git a/turbo-ext/src/SimpleImpurePoint.cpp b/turbo-ext/src/SimpleImpurePoint.cpp index 254890b8e33..1cb32f534f4 100644 --- a/turbo-ext/src/SimpleImpurePoint.cpp +++ b/turbo-ext/src/SimpleImpurePoint.cpp @@ -224,22 +224,20 @@ class SimpleImpurePoint } if (!certain && scope != NULL && variant != NULL) { + /* both verdicts combined: Yes = pure, No = impure, Maybe = possibly impure */ zend_long verdict = PT_TRI_YES; bool hasVerdict = false; if (UNEXPECTED(!resolveVerdict(variant, scope, args, hasVerdict, verdict))) return false; - if (hasVerdict) { - if (verdict == PT_TRI_YES) return true; - if (verdict == PT_TRI_NO) certain = true; - } - } - - if (!certain && scope != NULL && variant != NULL) { zend_long passedVerdict = PT_TRI_YES; bool hasPassedVerdict = false; if (UNEXPECTED(!resolvePassedVerdict(variant, args, hasPassedVerdict, passedVerdict))) return false; if (hasPassedVerdict) { - if (passedVerdict == PT_TRI_YES) return true; - if (passedVerdict == PT_TRI_NO) certain = true; + verdict = hasVerdict ? pt_trinary_and(verdict, passedVerdict) : passedVerdict; + hasVerdict = true; + } + if (hasVerdict) { + if (verdict == PT_TRI_YES) return true; + if (verdict == PT_TRI_NO) certain = true; } } From c3953e5bc5e0f9ab92ab323c38313c5356c425ef Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Tue, 14 Jul 2026 22:10:29 +0900 Subject: [PATCH 16/29] Update bin/functionMetadata_original.php Co-authored-by: Markus Staab --- bin/functionMetadata_original.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/functionMetadata_original.php b/bin/functionMetadata_original.php index 5ce171a7973..c9e33dbd861 100644 --- a/bin/functionMetadata_original.php +++ b/bin/functionMetadata_original.php @@ -14,7 +14,7 @@ * (keyed by parameter name) receives an impure callable, e.g. array_map() * whose only side effects come from its 'callback' argument. * - ['pureUnlessParameterPassedParameters' => array] - * the call is pure unless one of the listed (by-ref out) parameters + * the call is pure unless one of the listed (by-ref out) optional parameters * (keyed by parameter name) receives an argument, e.g. str_replace() * whose only side effect is writing to its optional 'count' argument. * From b7a61d6cf3b2faa12b3a6ae45c4e3b7aa2a1b3e2 Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Thu, 10 Sep 2026 15:13:28 +0900 Subject: [PATCH 17/29] Match arguments collected by a flagged variadic parameter A named argument whose name matches no declared parameter is collected by a trailing variadic parameter as a string-keyed element, so for a signature like myVariadicOut(string $subject, int &...$out) the call myVariadicOut($s, extra: $x) does reach the flagged $out. The exact-name check left $matchedArg null and returned a pure verdict. Extract the argument matching shared by both purity verdicts into matchArgForParameter(), and let it treat positional arguments from the variadic's position onwards, and named arguments not bound to any declared parameter, as reaching the variadic. The shared helper also gives the @pure-unless-callable-is-impure verdict the unpacked-argument handling that only the @pure-unless-parameter-passed one had. --- .../Callables/SimpleImpurePoint.php | 117 ++++++++---- .../Rules/Pure/PureFunctionRuleTest.php | 8 + .../data/pure-unless-parameter-passed.php | 53 ++++++ turbo-ext/src/SimpleImpurePoint.cpp | 175 +++++++++++------- turbo-ext/src/generated/SimpleImpurePoint.h | 28 ++- 5 files changed, 266 insertions(+), 115 deletions(-) diff --git a/src/Reflection/Callables/SimpleImpurePoint.php b/src/Reflection/Callables/SimpleImpurePoint.php index 0318829ccd3..52b3a2b1d97 100644 --- a/src/Reflection/Callables/SimpleImpurePoint.php +++ b/src/Reflection/Callables/SimpleImpurePoint.php @@ -8,10 +8,12 @@ use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Reflection\ExtendedParameterReflection; use PHPStan\Reflection\FunctionReflection; +use PHPStan\Reflection\ParameterReflection; use PHPStan\Reflection\ParametersAcceptor; use PHPStan\TrinaryLogic; use PHPStan\Turbo\ShadowedByTurboExtension; use PHPStan\Type\Type; +use function array_key_exists; use function count; use function sprintf; @@ -153,6 +155,7 @@ public static function createFromVariant(FunctionReflection|ExtendedMethodReflec public static function resolvePureUnlessCallableIsImpureVerdict(ParametersAcceptor $variant, Scope $scope, array $args): ?TrinaryLogic { $parameters = $variant->getParameters(); + $declaredParameterNames = self::collectParameterNames($parameters); $verdict = null; foreach ($parameters as $parameterIndex => $parameter) { @@ -165,26 +168,17 @@ public static function resolvePureUnlessCallableIsImpureVerdict(ParametersAccept $verdict ??= TrinaryLogic::createYes(); - $matchedArg = null; - $hasNamedParameter = false; - foreach ($args as $i => $arg) { - if ($arg->name !== null) { - $hasNamedParameter = true; - if ($arg->name->name === $parameter->getName()) { - $matchedArg = $arg; - break; - } + [$matchedArg, $hasUnpackedArg] = self::matchArgForParameter($args, $parameter, $parameterIndex, $declaredParameterNames); - continue; - } + if ($matchedArg === null) { + if ($hasUnpackedArg) { + // An unpacked argument list (...$args) might supply the flagged + // callable, so we cannot be sure the call stays pure. + $verdict = $verdict->and(TrinaryLogic::createMaybe()); - if (!$hasNamedParameter && $i === $parameterIndex) { - $matchedArg = $arg; - break; + continue; } - } - if ($matchedArg === null) { // Optional callback omitted (e.g. array_filter($arr)) - pure. continue; } @@ -226,6 +220,7 @@ public static function resolvePureUnlessCallableIsImpureVerdict(ParametersAccept public static function resolvePureUnlessParameterPassedVerdict(ParametersAcceptor $variant, array $args): ?TrinaryLogic { $parameters = $variant->getParameters(); + $declaredParameterNames = self::collectParameterNames($parameters); $verdict = null; foreach ($parameters as $parameterIndex => $parameter) { @@ -238,30 +233,7 @@ public static function resolvePureUnlessParameterPassedVerdict(ParametersAccepto $verdict ??= TrinaryLogic::createYes(); - $matchedArg = null; - $hasUnpackedArg = false; - $hasNamedParameter = false; - foreach ($args as $i => $arg) { - if ($arg->unpack) { - $hasUnpackedArg = true; - continue; - } - - if ($arg->name !== null) { - $hasNamedParameter = true; - if ($arg->name->name === $parameter->getName()) { - $matchedArg = $arg; - break; - } - - continue; - } - - if (!$hasNamedParameter && $i === $parameterIndex) { - $matchedArg = $arg; - break; - } - } + [$matchedArg, $hasUnpackedArg] = self::matchArgForParameter($args, $parameter, $parameterIndex, $declaredParameterNames); if ($matchedArg === null) { if ($hasUnpackedArg) { @@ -287,6 +259,71 @@ public static function resolvePureUnlessParameterPassedVerdict(ParametersAccepto return $verdict; } + /** + * @param ParameterReflection[] $parameters + * @return array + */ + private static function collectParameterNames(array $parameters): array + { + $names = []; + foreach ($parameters as $parameter) { + $names[$parameter->getName()] = true; + } + + return $names; + } + + /** + * Finds the argument a flagged parameter receives at a call site. + * + * A trailing variadic parameter collects every positional argument from its own + * position onwards, and - because a named argument that matches no declared + * parameter is collected by the variadic as a string-keyed element - those named + * arguments too. + * + * @param Arg[] $args + * @param array $declaredParameterNames + * @return array{?Arg, bool} the matched argument (null when the parameter received + * none) and whether an unpacked argument list might have + * supplied it + */ + private static function matchArgForParameter(array $args, ExtendedParameterReflection $parameter, int $parameterIndex, array $declaredParameterNames): array + { + $isVariadic = $parameter->isVariadic(); + $hasUnpackedArg = false; + $hasNamedArg = false; + + foreach ($args as $i => $arg) { + if ($arg->unpack) { + $hasUnpackedArg = true; + continue; + } + + if ($arg->name !== null) { + $hasNamedArg = true; + if ($arg->name->name === $parameter->getName()) { + return [$arg, $hasUnpackedArg]; + } + + if ($isVariadic && !array_key_exists($arg->name->name, $declaredParameterNames)) { + return [$arg, $hasUnpackedArg]; + } + + continue; + } + + if ($hasNamedArg) { + continue; + } + + if ($i === $parameterIndex || ($isVariadic && $i > $parameterIndex)) { + return [$arg, $hasUnpackedArg]; + } + } + + return [null, $hasUnpackedArg]; + } + /** @return ImpurePointIdentifier */ public function getIdentifier(): string { diff --git a/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php b/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php index 9302af4b99e..54ddfc22727 100644 --- a/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php +++ b/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php @@ -461,6 +461,14 @@ public function testPureUnlessParameterPassed(): void 'Impure call to function PureUnlessParameterPassedFunction\myReplaceVariadic() in pure function PureUnlessParameterPassedFunction\pureVariadicPassingCountWithExtra().', 398, ], + [ + 'Impure call to function PureUnlessParameterPassedFunction\myVariadicOut() in pure function PureUnlessParameterPassedFunction\pureVariadicOutPositional().', + 430, + ], + [ + 'Impure call to function PureUnlessParameterPassedFunction\myVariadicOut() in pure function PureUnlessParameterPassedFunction\pureVariadicOutNamed().', + 441, + ], ]); } diff --git a/tests/PHPStan/Rules/Pure/data/pure-unless-parameter-passed.php b/tests/PHPStan/Rules/Pure/data/pure-unless-parameter-passed.php index b273da96595..9af1dd0633f 100644 --- a/tests/PHPStan/Rules/Pure/data/pure-unless-parameter-passed.php +++ b/tests/PHPStan/Rules/Pure/data/pure-unless-parameter-passed.php @@ -397,3 +397,56 @@ function pureVariadicPassingCountWithExtra(string $s): string // arguments do not affect the flagged parameter, so the call is impure. return myReplaceVariadic($s, $count, 'a', 'b'); } + +/** + * @param-out int $out + * @pure-unless-parameter-passed $out + */ +function myVariadicOut(string $subject, int &...$out): string +{ + foreach ($out as $key => $value) { + $out[$key] = 1; + } + + return $subject; +} + +/** + * @phpstan-pure + */ +function pureVariadicOutOmitted(string $s): string +{ + // The flagged variadic collects nothing, so the call stays pure. + return myVariadicOut($s); +} + +/** + * @phpstan-pure + */ +function pureVariadicOutPositional(string $s): string +{ + $out = 0; + // The positional argument lands in the flagged variadic, so the call is impure. + return myVariadicOut($s, $out); +} + +/** + * @phpstan-pure + */ +function pureVariadicOutNamed(string $s): string +{ + $out = 0; + // A named argument matching no declared parameter is collected by the flagged + // variadic as a string-keyed element, so the call is impure. + return myVariadicOut($s, extra: $out); +} + +/** + * @phpstan-pure + */ +function pureVariadicOutNamedForOtherParameter(string $s): string +{ + // The named argument targets the declared $subject, not the flagged variadic, + // so the call stays pure. + return myVariadicOut(subject: $s); +} diff --git a/turbo-ext/src/SimpleImpurePoint.cpp b/turbo-ext/src/SimpleImpurePoint.cpp index 1cb32f534f4..ba220cdc71d 100644 --- a/turbo-ext/src/SimpleImpurePoint.cpp +++ b/turbo-ext/src/SimpleImpurePoint.cpp @@ -103,15 +103,6 @@ zend_long trinaryOf(zv::Val value) return pt_type_trinary_value(value.raw()); } -/* $i === $parameterIndex of two array keys */ -bool identicalKeys(const zv::ArrayEntry &a, const zv::ArrayEntry &b) -{ - zend_string *aKey = a.stringKeyOrNull(); - zend_string *bKey = b.stringKeyOrNull(); - if (aKey == NULL) return bKey == NULL && a.indexKey() == b.indexKey(); - return bKey != NULL && zend_string_equals(aKey, bKey); -} - /* private const SIDE_EFFECT_FLIP_PARAMETERS = [functionName => [name, pos, testName]] */ struct FlipParameter { @@ -304,6 +295,8 @@ class SimpleImpurePoint } zend_class_entry *extendedParameterCe = pt_class(PT_CLASS_EXTENDED_PARAMETER_REFLECTION); if (UNEXPECTED(extendedParameterCe == NULL)) return false; + zv::Arr declaredParameterNames = zv::Arr::create(0); + if (UNEXPECTED(!collectParameterNames(parameters.raw(), declaredParameterNames))) return false; for (auto parameterEntry : zv::TableRef(Z_ARRVAL_P(parameters.raw()))) { zval *parameter = parameterEntry.value().deref().raw(); @@ -318,36 +311,16 @@ class SimpleImpurePoint } zval *matchedArg = NULL; - bool hasNamedParameter = false; - if (EXPECTED(Z_TYPE_P(args) == IS_ARRAY)) { - for (auto argEntry : zv::TableRef(Z_ARRVAL_P(args))) { - zval *arg = argEntry.value().deref().raw(); - zval *argName = nodeProperty(pt_sip_arg_name_site, arg, PT_LC("name")); - if (UNEXPECTED(EG(exception))) return false; - if (Z_TYPE_P(argName) != IS_NULL) { - hasNamedParameter = true; - zval *identifierName = nodeProperty(pt_sip_identifier_name_site, argName, PT_LC("name")); - if (UNEXPECTED(EG(exception))) return false; - zv::Val parameterNameValue = parameterName(parameter); - if (UNEXPECTED(parameterNameValue.isUndef())) return false; - if (zend_is_identical(identifierName, parameterNameValue.raw())) { - matchedArg = arg; - break; - } - continue; - } - if (!hasNamedParameter && identicalKeys(argEntry, parameterEntry)) { - matchedArg = arg; - break; - } + bool hasUnpackedArg = false; + if (UNEXPECTED(!matchArg(args, parameter, parameterEntry, declaredParameterNames.table(), matchedArg, hasUnpackedArg))) return false; + + if (matchedArg == NULL) { + if (hasUnpackedArg) { + verdict = pt_trinary_and(verdict, PT_TRI_MAYBE); } - } else { - zend_error(E_WARNING, "foreach() argument must be of type array|object, %s given", zend_zval_value_name(args)); - if (UNEXPECTED(EG(exception))) return false; + continue; } - if (matchedArg == NULL) continue; - zval *argValue = nodeProperty(pt_sip_arg_value_site, matchedArg, PT_LC("value")); if (UNEXPECTED(EG(exception))) return false; if (UNEXPECTED(Z_TYPE_P(scope) != IS_OBJECT)) { @@ -408,6 +381,8 @@ class SimpleImpurePoint } zend_class_entry *extendedParameterCe = pt_class(PT_CLASS_EXTENDED_PARAMETER_REFLECTION); if (UNEXPECTED(extendedParameterCe == NULL)) return false; + zv::Arr declaredParameterNames = zv::Arr::create(0); + if (UNEXPECTED(!collectParameterNames(parameters.raw(), declaredParameterNames))) return false; for (auto parameterEntry : zv::TableRef(Z_ARRVAL_P(parameters.raw()))) { zval *parameter = parameterEntry.value().deref().raw(); @@ -423,39 +398,7 @@ class SimpleImpurePoint zval *matchedArg = NULL; bool hasUnpackedArg = false; - bool hasNamedParameter = false; - if (EXPECTED(Z_TYPE_P(args) == IS_ARRAY)) { - for (auto argEntry : zv::TableRef(Z_ARRVAL_P(args))) { - zval *arg = argEntry.value().deref().raw(); - zval *unpack = nodeProperty(pt_sip_arg_unpack_site, arg, PT_LC("unpack")); - if (UNEXPECTED(EG(exception))) return false; - if (zend_is_true(unpack)) { - hasUnpackedArg = true; - continue; - } - zval *argName = nodeProperty(pt_sip_arg_name_site, arg, PT_LC("name")); - if (UNEXPECTED(EG(exception))) return false; - if (Z_TYPE_P(argName) != IS_NULL) { - hasNamedParameter = true; - zval *identifierName = nodeProperty(pt_sip_identifier_name_site, argName, PT_LC("name")); - if (UNEXPECTED(EG(exception))) return false; - zv::Val parameterNameValue = parameterName(parameter); - if (UNEXPECTED(parameterNameValue.isUndef())) return false; - if (zend_is_identical(identifierName, parameterNameValue.raw())) { - matchedArg = arg; - break; - } - continue; - } - if (!hasNamedParameter && identicalKeys(argEntry, parameterEntry)) { - matchedArg = arg; - break; - } - } - } else { - zend_error(E_WARNING, "foreach() argument must be of type array|object, %s given", zend_zval_value_name(args)); - if (UNEXPECTED(EG(exception))) return false; - } + if (UNEXPECTED(!matchArg(args, parameter, parameterEntry, declaredParameterNames.table(), matchedArg, hasUnpackedArg))) return false; if (matchedArg == NULL) { if (hasUnpackedArg) { @@ -474,6 +417,100 @@ class SimpleImpurePoint } private: + /* Mirrors collectParameterNames(): $names[$parameter->getName()] = true + * over every parameter; false = pending exception */ + [[nodiscard]] static bool collectParameterNames(zval *parameters, zv::Arr &names) + { + for (auto entry : zv::TableRef(Z_ARRVAL_P(parameters))) { + zv::Val name = parameterName(entry.value().deref().raw()); + if (UNEXPECTED(name.isUndef())) return false; + zval trueValue; + ZVAL_TRUE(&trueValue); + /* getName(): string, so the key is the name under array-key rules */ + if (UNEXPECTED(array_set_zval_key(names.table(), name.raw(), &trueValue) == FAILURE)) return false; + } + return true; + } + + /* Mirrors matchArgForParameter(): the argument the parameter receives + * (NULL for none) and whether an unpacked argument list might have + * supplied it. A trailing variadic collects the positional arguments + * from its own position onwards and the named arguments bound to no + * declared parameter. false = pending exception. */ + [[nodiscard]] static bool matchArg(zval *args, zval *parameter, const zv::ArrayEntry ¶meterEntry, HashTable *declaredParameterNames, zval *&matched, bool &hasUnpackedArg) + { + matched = NULL; + hasUnpackedArg = false; + if (UNEXPECTED(parameterEntry.stringKeyOrNull() != NULL)) { + zend_type_error("PHPStan\\Reflection\\Callables\\SimpleImpurePoint::matchArgForParameter(): Argument #3 ($parameterIndex) must be of type int, string given"); + return false; + } + zend_long parameterIndex = (zend_long) parameterEntry.indexKey(); + bool isVariadic = false; + if (UNEXPECTED(!pt_parameter_reflection_bool(parameter, PT_PR_IS_VARIADIC, isVariadic))) return false; + if (UNEXPECTED(Z_TYPE_P(args) != IS_ARRAY)) { + zend_error(E_WARNING, "foreach() argument must be of type array|object, %s given", zend_zval_value_name(args)); + return !EG(exception); + } + bool hasNamedArg = false; + for (auto argEntry : zv::TableRef(Z_ARRVAL_P(args))) { + zval *arg = argEntry.value().deref().raw(); + zval *unpack = nodeProperty(pt_sip_arg_unpack_site, arg, PT_LC("unpack")); + if (UNEXPECTED(EG(exception))) return false; + if (zend_is_true(unpack)) { + hasUnpackedArg = true; + continue; + } + zval *argName = nodeProperty(pt_sip_arg_name_site, arg, PT_LC("name")); + if (UNEXPECTED(EG(exception))) return false; + if (Z_TYPE_P(argName) != IS_NULL) { + hasNamedArg = true; + zval *identifierName = nodeProperty(pt_sip_identifier_name_site, argName, PT_LC("name")); + if (UNEXPECTED(EG(exception))) return false; + zv::Val parameterNameValue = parameterName(parameter); + if (UNEXPECTED(parameterNameValue.isUndef())) return false; + if (zend_is_identical(identifierName, parameterNameValue.raw())) { + matched = arg; + return true; + } + if (isVariadic) { + /* !array_key_exists($arg->name->name, $declaredParameterNames) */ + if (UNEXPECTED(Z_TYPE_P(identifierName) != IS_STRING)) { + /* Identifier::$name is a string; anything else is the builtin's TypeError */ + zend_type_error("array_key_exists(): Argument #1 ($key) must be a valid array offset type"); + return false; + } + if (!zend_symtable_exists(declaredParameterNames, Z_STR_P(identifierName))) { + matched = arg; + return true; + } + } + continue; + } + if (hasNamedArg) continue; + /* $i === $parameterIndex || ($isVariadic && $i > $parameterIndex) */ + zend_string *argKey = argEntry.stringKeyOrNull(); + if (argKey == NULL && (zend_long) argEntry.indexKey() == parameterIndex) { + matched = arg; + return true; + } + if (isVariadic) { + zval iZv, parameterIndexZv; + if (argKey != NULL) { + ZVAL_STR(&iZv, argKey); + } else { + ZVAL_LONG(&iZv, (zend_long) argEntry.indexKey()); + } + ZVAL_LONG(¶meterIndexZv, parameterIndex); + if (zend_compare(&iZv, ¶meterIndexZv) > 0) { + matched = arg; + return true; + } + } + } + return true; + } + zend_object *self; /* a copy of the typed slot; UNDEF with the uninitialized-read Error pending */ diff --git a/turbo-ext/src/generated/SimpleImpurePoint.h b/turbo-ext/src/generated/SimpleImpurePoint.h index bed9ba3e7fd..3491a6c9501 100644 --- a/turbo-ext/src/generated/SimpleImpurePoint.h +++ b/turbo-ext/src/generated/SimpleImpurePoint.h @@ -50,9 +50,16 @@ inline constexpr char strings[] = "PHPStan\\TrinaryLogic\0" /* 288 */ "resolvePureUnlessCallableIsImpureVerdict\0" /* 309 */ "resolvePureUnlessParameterPassedVerdict\0" /* 350 */ - "getIdentifier\0" /* 390 */ - "getDescription\0" /* 404 */ - "isCertain"; /* 419 */ + "parameters\0" /* 390 */ + "collectParameterNames\0" /* 401 */ + "parameter\0" /* 423 */ + "PHPStan\\Reflection\\ExtendedParameterReflection\0" /* 433 */ + "parameterIndex\0" /* 480 */ + "declaredParameterNames\0" /* 495 */ + "matchArgForParameter\0" /* 518 */ + "getIdentifier\0" /* 539 */ + "getDescription\0" /* 553 */ + "isCertain"; /* 568 */ inline constexpr reg::PackedArg args[] = { reg::packed(0, MAY_BE_STRING), /* __construct $identifier */ reg::packed(11, MAY_BE_STRING), /* __construct $description */ @@ -69,6 +76,13 @@ inline constexpr reg::PackedArg args[] = { reg::packed(134, 0, 142), /* resolvePureUnlessParameterPassedVerdict $variant */ reg::packed(214, MAY_BE_ARRAY), /* resolvePureUnlessParameterPassedVerdict $args */ reg::packed(222, MAY_BE_NULL, 288), /* resolvePureUnlessParameterPassedVerdict return */ + reg::packed(390, MAY_BE_ARRAY), /* collectParameterNames $parameters */ + reg::packed(222, MAY_BE_ARRAY), /* collectParameterNames return */ + reg::packed(214, MAY_BE_ARRAY), /* matchArgForParameter $args */ + reg::packed(423, 0, 433), /* matchArgForParameter $parameter */ + reg::packed(480, MAY_BE_LONG), /* matchArgForParameter $parameterIndex */ + reg::packed(495, MAY_BE_ARRAY), /* matchArgForParameter $declaredParameterNames */ + reg::packed(222, MAY_BE_ARRAY), /* matchArgForParameter return */ reg::packed(222, MAY_BE_STRING), /* getIdentifier return */ reg::packed(222, MAY_BE_STRING), /* getDescription return */ reg::packed(222, MAY_BE_BOOL), /* isCertain return */ @@ -82,9 +96,11 @@ inline constexpr sigtab::Sig __construct = { { 31 /* __construct */, 3, 0, 3, re inline constexpr sigtab::Sig createFromVariant = { { 270 /* createFromVariant */, 2, 3, 4, 7, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; inline constexpr sigtab::Sig resolvePureUnlessCallableIsImpureVerdict = { { 309 /* resolvePureUnlessCallableIsImpureVerdict */, 3, 8, 3, 11, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; inline constexpr sigtab::Sig resolvePureUnlessParameterPassedVerdict = { { 350 /* resolvePureUnlessParameterPassedVerdict */, 2, 12, 2, 14, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig getIdentifier = { { 390 /* getIdentifier */, 0, 15, 0, 15, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getDescription = { { 404 /* getDescription */, 0, 16, 0, 16, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig isCertain = { { 419 /* isCertain */, 0, 17, 0, 17, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig collectParameterNames = { { 401 /* collectParameterNames */, 1, 15, 1, 16, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig matchArgForParameter = { { 518 /* matchArgForParameter */, 4, 17, 4, 21, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig getIdentifier = { { 539 /* getIdentifier */, 0, 22, 0, 22, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getDescription = { { 553 /* getDescription */, 0, 23, 0, 23, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig isCertain = { { 568 /* isCertain */, 0, 24, 0, 24, ZEND_ACC_PUBLIC } }; } // namespace sig } // namespace ptdecl::SimpleImpurePoint From e6981087e5f997480e8dc2f18d86c3547e5b8bb1 Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Thu, 10 Sep 2026 15:15:57 +0900 Subject: [PATCH 18/29] Check the body of a @pure-unless-parameter-passed function The tag alone left isPure() at Maybe and reportImpurePoints() only ran when @pure-unless-callable-is-impure parameters were present, so a function carrying only @pure-unless-parameter-passed could echo, write to a global or call an impure function and every call omitting the flagged argument was still classified as pure. Writing through the flagged by-ref parameter is not an impure point, so the rest of the body can be checked as-is: run the same validation the sibling tag gets. Also reject the tag on a parameter that is not passed by reference - passing a by-value parameter cannot introduce a side effect on its own, so the impurity such a declaration claims would not be conditional on the argument at all. --- src/Rules/Pure/FunctionPurityCheck.php | 33 +++++++++++++++---- .../Rules/Pure/PureFunctionRuleTest.php | 8 +++++ .../PHPStan/Rules/Pure/PureMethodRuleTest.php | 8 +++++ .../pure-unless-parameter-passed-method.php | 20 +++++++++++ .../data/pure-unless-parameter-passed.php | 24 ++++++++++++++ 5 files changed, 87 insertions(+), 6 deletions(-) diff --git a/src/Rules/Pure/FunctionPurityCheck.php b/src/Rules/Pure/FunctionPurityCheck.php index 468079b793a..94d265f6732 100644 --- a/src/Rules/Pure/FunctionPurityCheck.php +++ b/src/Rules/Pure/FunctionPurityCheck.php @@ -92,21 +92,39 @@ public function check( } $pureUnlessParameterPassedParameters = $functionReflection->getPureUnlessParameterPassedParameters(); + $pureUnlessParameterPassedParamNames = []; foreach ($parameters as $parameter) { if (!array_key_exists($parameter->getName(), $pureUnlessParameterPassedParameters)) { continue; } - if ($parameter->isOptional()) { + + $pureUnlessParameterPassedParamNames[$parameter->getName()] = true; + + if (!$parameter->isOptional()) { + $errors[] = RuleErrorBuilder::message(sprintf( + '%s is marked @pure-unless-parameter-passed for parameter $%s, but $%s is not optional, so %s is never pure.', + $functionDescription, + $parameter->getName(), + $parameter->getName(), + lcfirst($functionDescription), + ))->identifier(sprintf('pure%s.nonOptionalParameterPassed', $identifier))->build(); + } + + // Passing a by-value parameter cannot introduce a side effect on its own, + // so the body would have to be impure for the tag to mean anything - and + // that impurity is not conditional on the argument being passed. Only a + // by-ref out parameter is written through without producing an impure + // point, which is what makes the conditional verdict checkable. + if (!$parameter->passedByReference()->no()) { continue; } $errors[] = RuleErrorBuilder::message(sprintf( - '%s is marked @pure-unless-parameter-passed for parameter $%s, but $%s is not optional, so %s is never pure.', + '%s is marked @pure-unless-parameter-passed for parameter $%s, but $%s is not passed by reference.', $functionDescription, $parameter->getName(), $parameter->getName(), - lcfirst($functionDescription), - ))->identifier(sprintf('pure%s.nonOptionalParameterPassed', $identifier))->build(); + ))->identifier(sprintf('pure%s.parameterPassedNotByRef', $identifier))->build(); } if ($isPure->yes()) { @@ -136,10 +154,13 @@ public function check( } $errors = array_merge($errors, $this->reportImpurePoints($impurePoints, $pureUnlessCallableParamNames, $functionDescription)); - } elseif ($pureUnlessCallableParamNames !== []) { + } elseif ($pureUnlessCallableParamNames !== [] || $pureUnlessParameterPassedParamNames !== []) { // A function declared @pure-unless-callable-is-impure is pure except // for the flagged callables, so its body is checked for purity while - // the flagged callables' own invocations are exempt. + // the flagged callables' own invocations are exempt. The same holds for + // @pure-unless-parameter-passed: writing through the flagged by-ref + // parameter is not an impure point, so the rest of the body still has + // to be pure for the conditional verdict to hold. $errors = array_merge($errors, $this->reportImpurePoints($impurePoints, $pureUnlessCallableParamNames, $functionDescription)); } elseif ($isPure->no()) { if ( diff --git a/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php b/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php index 54ddfc22727..b4263dbfaf1 100644 --- a/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php +++ b/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php @@ -469,6 +469,14 @@ public function testPureUnlessParameterPassed(): void 'Impure call to function PureUnlessParameterPassedFunction\myVariadicOut() in pure function PureUnlessParameterPassedFunction\pureVariadicOutNamed().', 441, ], + [ + 'Function PureUnlessParameterPassedFunction\byValueUnlessParameterPassed() is marked @pure-unless-parameter-passed for parameter $flag, but $flag is not passed by reference.', + 457, + ], + [ + 'Impure echo in pure function PureUnlessParameterPassedFunction\sideEffectUnlessParameterPassed().', + 472, + ], ]); } diff --git a/tests/PHPStan/Rules/Pure/PureMethodRuleTest.php b/tests/PHPStan/Rules/Pure/PureMethodRuleTest.php index f6aa7b42d14..f67e9172b60 100644 --- a/tests/PHPStan/Rules/Pure/PureMethodRuleTest.php +++ b/tests/PHPStan/Rules/Pure/PureMethodRuleTest.php @@ -427,6 +427,14 @@ public function testPureUnlessParameterPassed(): void 'Method PureUnlessParameterPassedMethod\Replacer::replace() is marked @pure-unless-parameter-passed for parameter $count, but $count is not optional, so method PureUnlessParameterPassedMethod\Replacer::replace() is never pure.', 12, ], + [ + 'Method PureUnlessParameterPassedMethod\Replacer::replaceByValue() is marked @pure-unless-parameter-passed for parameter $flag, but $flag is not passed by reference.', + 33, + ], + [ + 'Impure echo in pure method PureUnlessParameterPassedMethod\Replacer::replaceWithSideEffect().', + 44, + ], ]); } diff --git a/tests/PHPStan/Rules/Pure/data/pure-unless-parameter-passed-method.php b/tests/PHPStan/Rules/Pure/data/pure-unless-parameter-passed-method.php index 8d13645a602..271ec7aa448 100644 --- a/tests/PHPStan/Rules/Pure/data/pure-unless-parameter-passed-method.php +++ b/tests/PHPStan/Rules/Pure/data/pure-unless-parameter-passed-method.php @@ -27,4 +27,24 @@ public function replaceOptional(string $subject, int &$count = 0): string return $subject; } + /** + * @pure-unless-parameter-passed $flag + */ + public function replaceByValue(string $subject, bool $flag = false): string + { + return $subject; + } + + /** + * @param-out int $count + * @pure-unless-parameter-passed $count + */ + public function replaceWithSideEffect(string $subject, int &$count = 0): string + { + echo $subject; + $count = 1; + + return $subject; + } + } diff --git a/tests/PHPStan/Rules/Pure/data/pure-unless-parameter-passed.php b/tests/PHPStan/Rules/Pure/data/pure-unless-parameter-passed.php index 9af1dd0633f..90ac0ae08ff 100644 --- a/tests/PHPStan/Rules/Pure/data/pure-unless-parameter-passed.php +++ b/tests/PHPStan/Rules/Pure/data/pure-unless-parameter-passed.php @@ -450,3 +450,27 @@ function pureVariadicOutNamedForOtherParameter(string $s): string // so the call stays pure. return myVariadicOut(subject: $s); } + +/** + * @pure-unless-parameter-passed $flag + */ +function byValueUnlessParameterPassed(string $subject, bool $flag = false): string +{ + // $flag is not passed by reference, so passing it cannot make the call impure + // on its own and the tag cannot be checked. + return $subject; +} + +/** + * @param-out int $count + * @pure-unless-parameter-passed $count + */ +function sideEffectUnlessParameterPassed(string $subject, int &$count = 0): string +{ + // The tag only exempts writing through the flagged by-ref parameter; the rest + // of the body still has to be pure. + echo $subject; + $count = 1; + + return $subject; +} From a9400d4a5a2e0aafc4ac453a1ed5793110dbf62d Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Thu, 10 Sep 2026 15:18:03 +0900 Subject: [PATCH 19/29] Emit every purity condition when generating the function metadata An entry can declare both pureUnlessCallableIsImpureParameters and pureUnlessParameterPassedParameters - preg_replace_callback() is pure unless its callback is impure or its 'count' is passed - but the merge loop rebuilt such an entry with whichever key it saw first, and the encoder's match() emitted a single key, so regenerating dropped the 'count' condition at both points. Keep every condition the hand-maintained entry declares and encode all the keys an entry has. Also emit the parameter map separator as ', ' so the generated file is valid style and round-trips; the two entries that already had two parameters are reformatted accordingly. --- bin/generate-function-metadata.php | 81 ++++++++++++++---------------- resources/functionMetadata.php | 4 +- 2 files changed, 40 insertions(+), 45 deletions(-) diff --git a/bin/generate-function-metadata.php b/bin/generate-function-metadata.php index 398dd4f9ec2..0c22cbfcc18 100755 --- a/bin/generate-function-metadata.php +++ b/bin/generate-function-metadata.php @@ -132,18 +132,20 @@ public function enterNode(Node $node) throw new ShouldNotHappenException($functionName); } - if (isset($metadata[$functionName]['pureUnlessCallableIsImpureParameters'])) { - $metadata[$functionName] = [ - 'pureUnlessCallableIsImpureParameters' => $metadata[$functionName]['pureUnlessCallableIsImpureParameters'], - ]; + // An entry can carry both conditions at once (e.g. preg_replace_callback, + // which is pure unless its callback is impure or its 'count' is passed), + // so keep every condition the hand-maintained entry declares. + $conditions = []; + foreach (['pureUnlessCallableIsImpureParameters', 'pureUnlessParameterPassedParameters'] as $conditionKey) { + if (!isset($metadata[$functionName][$conditionKey])) { + continue; + } - continue; + $conditions[$conditionKey] = $metadata[$functionName][$conditionKey]; } - if (isset($metadata[$functionName]['pureUnlessParameterPassedParameters'])) { - $metadata[$functionName] = [ - 'pureUnlessParameterPassedParameters' => $metadata[$functionName]['pureUnlessParameterPassedParameters'], - ]; + if ($conditions !== []) { + $metadata[$functionName] = $conditions; continue; } @@ -217,46 +219,39 @@ public function enterNode(Node $node) php; $content = ''; $escape = static fn (mixed $value): string => var_export($value, true); - $encodeHasSideEffects = static fn (array $meta) => [$escape('hasSideEffects'), $escape($meta['hasSideEffects'])]; - $encodePureUnlessCallableIsImpureParameters = static fn (array $meta) => [ - $escape('pureUnlessCallableIsImpureParameters'), - sprintf( - '[%s]', - implode( - ' ,', - array_map( - static fn ($key, $param) => sprintf('%s => %s', $escape($key), $escape($param)), - array_keys($meta['pureUnlessCallableIsImpureParameters']), - $meta['pureUnlessCallableIsImpureParameters'], - ), + $encodeParameterMap = static fn (array $parameters) => sprintf( + '[%s]', + implode( + ', ', + array_map( + static fn ($key, $param) => sprintf('%s => %s', $escape($key), $escape($param)), + array_keys($parameters), + $parameters, ), ), - ]; - $encodePureUnlessParameterPassedParameters = static fn (array $meta) => [ - $escape('pureUnlessParameterPassedParameters'), - sprintf( - '[%s]', - implode( - ' ,', - array_map( - static fn ($key, $param) => sprintf('%s => %s', $escape($key), $escape($param)), - array_keys($meta['pureUnlessParameterPassedParameters']), - $meta['pureUnlessParameterPassedParameters'], - ), - ), - ), - ]; + ); foreach ($metadata as $name => $meta) { + // An entry is either unconditional or carries one or both of the conditional + // purity keys, so encode every key it has instead of just the first one. + $entries = []; + if (isset($meta['hasSideEffects'])) { + $entries[] = sprintf('%s => %s', $escape('hasSideEffects'), $escape($meta['hasSideEffects'])); + } + if (isset($meta['pureUnlessCallableIsImpureParameters'])) { + $entries[] = sprintf('%s => %s', $escape('pureUnlessCallableIsImpureParameters'), $encodeParameterMap($meta['pureUnlessCallableIsImpureParameters'])); + } + if (isset($meta['pureUnlessParameterPassedParameters'])) { + $entries[] = sprintf('%s => %s', $escape('pureUnlessParameterPassedParameters'), $encodeParameterMap($meta['pureUnlessParameterPassedParameters'])); + } + if ($entries === []) { + throw new ShouldNotHappenException($escape($meta)); + } + $content .= sprintf( - "\t%s => [%s => %s],\n", + "\t%s => [%s],\n", var_export($name, true), - ...match (true) { - isset($meta['hasSideEffects']) => $encodeHasSideEffects($meta), - isset($meta['pureUnlessCallableIsImpureParameters']) => $encodePureUnlessCallableIsImpureParameters($meta), - isset($meta['pureUnlessParameterPassedParameters']) => $encodePureUnlessParameterPassedParameters($meta), - default => throw new ShouldNotHappenException($escape($meta)), - }, + implode(', ', $entries), ); } diff --git a/resources/functionMetadata.php b/resources/functionMetadata.php index 1bdeb8ba0b5..73ef03c0640 100644 --- a/resources/functionMetadata.php +++ b/resources/functionMetadata.php @@ -780,10 +780,10 @@ 'array_sum' => ['hasSideEffects' => false], 'array_udiff' => ['pureUnlessCallableIsImpureParameters' => ['data_comp_func' => true]], 'array_udiff_assoc' => ['pureUnlessCallableIsImpureParameters' => ['key_comp_func' => true]], - 'array_udiff_uassoc' => ['pureUnlessCallableIsImpureParameters' => ['data_comp_func' => true ,'key_comp_func' => true]], + 'array_udiff_uassoc' => ['pureUnlessCallableIsImpureParameters' => ['data_comp_func' => true, 'key_comp_func' => true]], 'array_uintersect' => ['pureUnlessCallableIsImpureParameters' => ['data_compare_func' => true]], 'array_uintersect_assoc' => ['pureUnlessCallableIsImpureParameters' => ['data_compare_func' => true]], - 'array_uintersect_uassoc' => ['pureUnlessCallableIsImpureParameters' => ['data_compare_func' => true ,'key_compare_func' => true]], + 'array_uintersect_uassoc' => ['pureUnlessCallableIsImpureParameters' => ['data_compare_func' => true, 'key_compare_func' => true]], 'array_unique' => ['hasSideEffects' => false], 'array_unshift' => ['hasSideEffects' => true], 'array_values' => ['hasSideEffects' => false], From 74af49835bed7f80c3c62210d979202b84f6ed04 Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Thu, 10 Sep 2026 15:18:52 +0900 Subject: [PATCH 20/29] Cover every @pure-unless-parameter-passed builtin mapping The behavioral suite only exercised str_replace(), preg_match(), preg_filter() and preg_replace_callback(); str_ireplace(), preg_replace(), preg_match_all() and similar_text() were mapped but never called. Since the mapping is keyed by the exact parameter name, a typo there would silently leave the builtin possibly impure - verified by breaking each name in turn and watching these cases fail. --- .../Rules/Pure/PureFunctionRuleTest.php | 16 ++++ .../pure-unless-parameter-passed-builtin.php | 76 +++++++++++++++++++ 2 files changed, 92 insertions(+) diff --git a/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php b/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php index b4263dbfaf1..b8a0a3672b8 100644 --- a/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php +++ b/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php @@ -507,6 +507,22 @@ public function testPureUnlessParameterPassedBuiltin(): void 'Impure echo in pure function PureUnlessParameterPassedBuiltin\purePregReplaceCallbackImpureCallback().', 90, ], + [ + 'Impure call to function str_ireplace() in pure function PureUnlessParameterPassedBuiltin\pureStrIreplaceWithCount().', + 113, + ], + [ + 'Impure call to function preg_replace() in pure function PureUnlessParameterPassedBuiltin\purePregReplaceWithCount().', + 133, + ], + [ + 'Impure call to function preg_match_all() in pure function PureUnlessParameterPassedBuiltin\purePregMatchAllWithMatches().', + 151, + ], + [ + 'Impure call to function similar_text() in pure function PureUnlessParameterPassedBuiltin\pureSimilarTextWithPercent().', + 169, + ], ]); } diff --git a/tests/PHPStan/Rules/Pure/data/pure-unless-parameter-passed-builtin.php b/tests/PHPStan/Rules/Pure/data/pure-unless-parameter-passed-builtin.php index 68087b3119d..fe0f2e1de47 100644 --- a/tests/PHPStan/Rules/Pure/data/pure-unless-parameter-passed-builtin.php +++ b/tests/PHPStan/Rules/Pure/data/pure-unless-parameter-passed-builtin.php @@ -92,3 +92,79 @@ function purePregReplaceCallbackImpureCallback(string $s): ?string return $m[0]; }, $s); } + +/** + * @phpstan-pure + */ +function pureStrIreplaceWithoutCount(string $s): string +{ + // The by-ref $count is omitted, so str_ireplace() is pure. + return str_ireplace('a', 'b', $s); +} + +/** + * @phpstan-pure + */ +function pureStrIreplaceWithCount(string $s): string +{ + // The by-ref $count is passed, so str_ireplace() is impure (the flag is certain). + $count = 0; + + return str_ireplace('a', 'b', $s, $count); +} + +/** + * @phpstan-pure + */ +function purePregReplaceWithoutCount(string $s): ?string +{ + // The by-ref $count is omitted, so preg_replace() is pure. + return preg_replace('/a/', 'b', $s); +} + +/** + * @phpstan-pure + */ +function purePregReplaceWithCount(string $s): ?string +{ + // The by-ref $count is passed, so preg_replace() is impure (the flag is certain). + $count = 0; + + return preg_replace('/a/', 'b', $s, -1, $count); +} + +/** + * @phpstan-pure + */ +function purePregMatchAllWithoutMatches(string $s): int +{ + // The by-ref $matches is omitted, so preg_match_all() is pure. + return (int) preg_match_all('/a/', $s); +} + +/** + * @phpstan-pure + */ +function purePregMatchAllWithMatches(string $s): int +{ + // The by-ref $matches is passed, so preg_match_all() is impure (the flag is certain). + return (int) preg_match_all('/a/', $s, $matches); +} + +/** + * @phpstan-pure + */ +function pureSimilarTextWithoutPercent(string $a, string $b): int +{ + // The by-ref $percent is omitted, so similar_text() is pure. + return similar_text($a, $b); +} + +/** + * @phpstan-pure + */ +function pureSimilarTextWithPercent(string $a, string $b): int +{ + // The by-ref $percent is passed, so similar_text() is impure (the flag is certain). + return similar_text($a, $b, $percent); +} From 9303d78a55fd71d0f4e0ffd32548c33cd8545a70 Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Thu, 10 Sep 2026 15:23:32 +0900 Subject: [PATCH 21/29] Settle conditional purity where a callable value is invoked A callable value's impure points are resolved from its ParametersAcceptor alone, with no scope and no arguments, so a conditionally pure function reached through a first-class callable arrived at the call site as an unconditional "possibly impure" point: $f = myReplace(...); $f($s) disagreed with the equivalent myReplace($s), and so did $f = array_map(...); $f('strtoupper', $arr). The per-parameter flags do survive into the Closure type, and the arguments are known where the callable is invoked, so apply the verdict there: narrowByConditionalPurity() drops the impure points when the call is pure and makes them certain when it is not. --- src/Analyser/ExprHandler/FuncCallHandler.php | 8 +- .../Callables/SimpleImpurePoint.php | 63 ++++++++++--- .../Rules/Pure/PureFunctionRuleTest.php | 37 +++++--- ...allable-is-impure-first-class-callable.php | 46 ++++++++++ .../data/pure-unless-parameter-passed.php | 10 +-- turbo-ext/src/FuncCallHandler.cpp | 20 ++++- turbo-ext/src/SimpleImpurePoint.cpp | 89 +++++++++++++++++-- turbo-ext/src/generated/SimpleImpurePoint.h | 60 ++++++++----- turbo-ext/src/support.h | 3 + turbo-ext/tests/type-family.php | 5 +- 10 files changed, 274 insertions(+), 67 deletions(-) create mode 100644 tests/PHPStan/Rules/Pure/data/pure-unless-callable-is-impure-first-class-callable.php diff --git a/src/Analyser/ExprHandler/FuncCallHandler.php b/src/Analyser/ExprHandler/FuncCallHandler.php index 16bdb0f4c4f..4ff33f80889 100644 --- a/src/Analyser/ExprHandler/FuncCallHandler.php +++ b/src/Analyser/ExprHandler/FuncCallHandler.php @@ -199,7 +199,13 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $callableThrowPoints = array_values(array_filter($callableThrowPoints, static fn (InternalThrowPoint $throwPoint) => $throwPoint->isExplicit())); } $throwPoints = array_merge($throwPoints, $callableThrowPoints); - $impurePoints = array_merge($impurePoints, array_map(static fn (SimpleImpurePoint $impurePoint) => new ImpurePoint($scope, $expr, $impurePoint->getIdentifier(), $impurePoint->getDescription(), $impurePoint->isCertain()), $parametersAcceptor->getImpurePoints())); + // A callable value's impure points are resolved from its + // ParametersAcceptor alone, so a conditionally pure function reached + // through a first-class callable arrives here as an unconditional + // "possibly impure" point. The arguments are known at this call site, + // so settle the verdict here. + $callableImpurePoints = SimpleImpurePoint::narrowByConditionalPurity($parametersAcceptor->getImpurePoints(), $parametersAcceptor, $scope, $expr->getArgs()); + $impurePoints = array_merge($impurePoints, array_map(static fn (SimpleImpurePoint $impurePoint) => new ImpurePoint($scope, $expr, $impurePoint->getIdentifier(), $impurePoint->getDescription(), $impurePoint->isCertain()), $callableImpurePoints)); $scope = $this->closureProcessor->processImmediatelyCalledCallable($scope, $parametersAcceptor->getInvalidateExpressions(), $parametersAcceptor->getUsedVariables()); } diff --git a/src/Reflection/Callables/SimpleImpurePoint.php b/src/Reflection/Callables/SimpleImpurePoint.php index 52b3a2b1d97..6397c876ac6 100644 --- a/src/Reflection/Callables/SimpleImpurePoint.php +++ b/src/Reflection/Callables/SimpleImpurePoint.php @@ -14,6 +14,7 @@ use PHPStan\Turbo\ShadowedByTurboExtension; use PHPStan\Type\Type; use function array_key_exists; +use function array_map; use function count; use function sprintf; @@ -67,16 +68,7 @@ public static function createFromVariant(FunctionReflection|ExtendedMethodReflec } if (!$certain && $scope !== null && $variant !== null) { - // A function can carry both flags at once (e.g. preg_replace_callback, - // which is pure unless its callback is impure or its $count is passed). - // It stays pure only when both verdicts agree it is pure, so combine - // them: Yes = pure, No = impure, Maybe = possibly impure. - $verdict = self::resolvePureUnlessCallableIsImpureVerdict($variant, $scope, $args); - $passedVerdict = self::resolvePureUnlessParameterPassedVerdict($variant, $args); - if ($passedVerdict !== null) { - $verdict = $verdict === null ? $passedVerdict : $verdict->and($passedVerdict); - } - + $verdict = self::resolveConditionalPurityVerdict($variant, $scope, $args); if ($verdict !== null) { if ($verdict->yes()) { return null; @@ -144,6 +136,57 @@ public static function createFromVariant(FunctionReflection|ExtendedMethodReflec return null; } + /** + * Purity verdict of a call to a variant that declares conditional purity. + * + * A function can carry both flags at once (e.g. preg_replace_callback, which is + * pure unless its callback is impure or its $count is passed). It stays pure only + * when both verdicts agree it is pure, so they are combined: Yes = pure, + * No = impure, Maybe = possibly impure. Returns null when the variant declares + * neither flag, so the caller keeps its current behavior. + * + * @param Arg[] $args + */ + public static function resolveConditionalPurityVerdict(ParametersAcceptor $variant, Scope $scope, array $args): ?TrinaryLogic + { + $verdict = self::resolvePureUnlessCallableIsImpureVerdict($variant, $scope, $args); + $passedVerdict = self::resolvePureUnlessParameterPassedVerdict($variant, $args); + if ($passedVerdict === null) { + return $verdict; + } + + return $verdict === null ? $passedVerdict : $verdict->and($passedVerdict); + } + + /** + * Applies the conditional purity verdict of this call site to impure points that + * were resolved without one - a callable value's impure points are computed from + * its ParametersAcceptor alone, so a first-class callable of a conditionally pure + * function arrives here as an unconditional "possibly impure" point. + * + * @param SimpleImpurePoint[] $impurePoints + * @param Arg[] $args + * @return SimpleImpurePoint[] + */ + public static function narrowByConditionalPurity(array $impurePoints, ParametersAcceptor $variant, Scope $scope, array $args): array + { + $verdict = self::resolveConditionalPurityVerdict($variant, $scope, $args); + if ($verdict === null || $verdict->maybe()) { + return $impurePoints; + } + + if ($verdict->yes()) { + return []; + } + + return array_map( + static fn (self $impurePoint) => $impurePoint->isCertain() + ? $impurePoint + : new self($impurePoint->getIdentifier(), $impurePoint->getDescription(), true), + $impurePoints, + ); + } + /** * Combined purity verdict of all arguments passed to parameters flagged * with @pure-unless-callable-is-impure. Returns null when the variant has diff --git a/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php b/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php index b8a0a3672b8..2161715148b 100644 --- a/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php +++ b/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php @@ -296,6 +296,21 @@ public function testPureUnlessCallableIsImpure(): void ]); } + #[RequiresPhp('>= 8.1.0')] + public function testPureUnlessCallableIsImpureFirstClassCallable(): void + { + $this->analyse([__DIR__ . '/data/pure-unless-callable-is-impure-first-class-callable.php'], [ + [ + 'Impure call to function array_map() in pure function PureUnlessCallableIsImpureFirstClassCallable\pureCallingFirstClassCallableWithImpureCallback().', + 27, + ], + [ + 'Possibly impure call to function array_map() in pure function PureUnlessCallableIsImpureFirstClassCallable\pureCallingFirstClassCallableWithOpaqueCallback().', + 45, + ], + ]); + } + #[RequiresPhp('>= 8.0.0')] public function testPureUnlessCallableIsImpureNamedArgs(): void { @@ -442,40 +457,36 @@ public function testPureUnlessParameterPassed(): void 318, ], [ - 'Possibly impure call to function PureUnlessParameterPassedFunction\myReplace() in pure function PureUnlessParameterPassedFunction\pureCallingFirstClassCallableOmittingCount().', - 331, - ], - [ - 'Possibly impure call to function PureUnlessParameterPassedFunction\myReplace() in pure function PureUnlessParameterPassedFunction\pureCallingFirstClassCallablePassingCount().', - 343, + 'Impure call to function PureUnlessParameterPassedFunction\myReplace() in pure function PureUnlessParameterPassedFunction\pureCallingFirstClassCallablePassingCount().', + 341, ], [ 'Function PureUnlessParameterPassedFunction\nonOptionalUnlessParameterPassed() is marked @pure-unless-parameter-passed for parameter $count, but $count is not optional, so function PureUnlessParameterPassedFunction\nonOptionalUnlessParameterPassed() is never pure.', - 350, + 348, ], [ 'Impure call to function PureUnlessParameterPassedFunction\myReplaceVariadic() in pure function PureUnlessParameterPassedFunction\pureVariadicPassingCount().', - 387, + 385, ], [ 'Impure call to function PureUnlessParameterPassedFunction\myReplaceVariadic() in pure function PureUnlessParameterPassedFunction\pureVariadicPassingCountWithExtra().', - 398, + 396, ], [ 'Impure call to function PureUnlessParameterPassedFunction\myVariadicOut() in pure function PureUnlessParameterPassedFunction\pureVariadicOutPositional().', - 430, + 428, ], [ 'Impure call to function PureUnlessParameterPassedFunction\myVariadicOut() in pure function PureUnlessParameterPassedFunction\pureVariadicOutNamed().', - 441, + 439, ], [ 'Function PureUnlessParameterPassedFunction\byValueUnlessParameterPassed() is marked @pure-unless-parameter-passed for parameter $flag, but $flag is not passed by reference.', - 457, + 455, ], [ 'Impure echo in pure function PureUnlessParameterPassedFunction\sideEffectUnlessParameterPassed().', - 472, + 470, ], ]); } diff --git a/tests/PHPStan/Rules/Pure/data/pure-unless-callable-is-impure-first-class-callable.php b/tests/PHPStan/Rules/Pure/data/pure-unless-callable-is-impure-first-class-callable.php new file mode 100644 index 00000000000..99a24a9caa4 --- /dev/null +++ b/tests/PHPStan/Rules/Pure/data/pure-unless-callable-is-impure-first-class-callable.php @@ -0,0 +1,46 @@ += 8.1 + +namespace PureUnlessCallableIsImpureFirstClassCallable; + +/** + * @param array $arr + * @return array + * @phpstan-pure + */ +function pureCallingFirstClassCallableWithPureCallback(array $arr): array +{ + $f = array_map(...); + // The flag travels with the callable's parameters, so a pure callback keeps + // the indirect call as pure as the direct one. + return $f(static fn (int $x): int => $x * 2, $arr); +} + +/** + * @param array $arr + * @return array + * @phpstan-pure + */ +function pureCallingFirstClassCallableWithImpureCallback(array $arr): array +{ + $f = array_map(...); + + return $f(static function (int $x): int { + echo $x; + + return $x * 2; + }, $arr); +} + +/** + * @param array $arr + * @param callable(int): int $cb + * @return array + * @phpstan-pure + */ +function pureCallingFirstClassCallableWithOpaqueCallback(array $arr, callable $cb): array +{ + $f = array_map(...); + // An opaque callable makes the indirect call possibly impure, same as the + // direct one. + return $f($cb, $arr); +} diff --git a/tests/PHPStan/Rules/Pure/data/pure-unless-parameter-passed.php b/tests/PHPStan/Rules/Pure/data/pure-unless-parameter-passed.php index 90ac0ae08ff..adc54457081 100644 --- a/tests/PHPStan/Rules/Pure/data/pure-unless-parameter-passed.php +++ b/tests/PHPStan/Rules/Pure/data/pure-unless-parameter-passed.php @@ -324,10 +324,8 @@ function pureCallingRenamedInheritedMethodPassingByRef(InheritedReplacerRenamedC function pureCallingFirstClassCallableOmittingCount(string $s): string { $f = myReplace(...); - // A first-class callable's purity is evaluated from its ParametersAcceptor - // alone, without scope/args, so @pure-unless-parameter-passed cannot gate on - // whether $count is actually passed at this call site; it stays possibly - // impure even though $count is omitted here. + // The flag travels with the callable's parameters, so omitting $count keeps + // the indirect call as pure as the direct one. return $f($s); } @@ -338,8 +336,8 @@ function pureCallingFirstClassCallablePassingCount(string $s): string { $count = 0; $f = myReplace(...); - // The first-class callable is called with the flagged $count passed, so this - // stays possibly impure. + // The first-class callable is called with the flagged $count passed, so the + // call is impure (the flag is certain), same as calling myReplace() directly. return $f($s, $count); } diff --git a/turbo-ext/src/FuncCallHandler.cpp b/turbo-ext/src/FuncCallHandler.cpp index 94f361786d3..e47625e64ad 100644 --- a/turbo-ext/src/FuncCallHandler.cpp +++ b/turbo-ext/src/FuncCallHandler.cpp @@ -1508,12 +1508,24 @@ class FuncCallHandler } if (UNEXPECTED(!arrayMerge(throwPoints, callableThrowPointsHold.raw()))) return false; - zv::Val simpleImpurePoints = pt_parameters_acceptor_call(parametersAcceptor, PT_PA_GET_IMPURE_POINTS); - if (UNEXPECTED(simpleImpurePoints.isUndef())) return false; - if (UNEXPECTED(Z_TYPE_P(simpleImpurePoints.raw()) != IS_ARRAY)) { - zend_type_error("array_map(): Argument #2 ($array) must be of type array, %s given", zend_zval_value_name(simpleImpurePoints.raw())); + zv::Val acceptorImpurePoints = pt_parameters_acceptor_call(parametersAcceptor, PT_PA_GET_IMPURE_POINTS); + if (UNEXPECTED(acceptorImpurePoints.isUndef())) return false; + if (UNEXPECTED(Z_TYPE_P(acceptorImpurePoints.raw()) != IS_ARRAY)) { + zend_type_error("PHPStan\\Reflection\\Callables\\SimpleImpurePoint::narrowByConditionalPurity(): Argument #1 ($impurePoints) must be of type array, %s given", zend_zval_value_name(acceptorImpurePoints.raw())); + return false; + } + /* SimpleImpurePoint::narrowByConditionalPurity($parametersAcceptor->getImpurePoints(), + * $parametersAcceptor, $scope, $expr->getArgs()): the conditional purity + * of a callable value is settled here, where its arguments are known */ + zv::Val argsHold; + zval *callArgs = pt_call_like_args(Z_OBJ_P(expr), argsHold); + if (UNEXPECTED(callArgs == NULL)) return false; + if (UNEXPECTED(Z_TYPE_P(callArgs) != IS_ARRAY)) { + zend_type_error("PHPStan\\Reflection\\Callables\\SimpleImpurePoint::narrowByConditionalPurity(): Argument #4 ($args) must be of type array, %s given", zend_zval_value_name(callArgs)); return false; } + zv::Val simpleImpurePoints = pt_simple_impure_point_narrow_by_conditional_purity(acceptorImpurePoints.raw(), parametersAcceptor, scope.raw(), callArgs); + if (UNEXPECTED(simpleImpurePoints.isUndef())) return false; zv::Arr callableImpurePoints = zv::Arr::create(zend_hash_num_elements(Z_ARRVAL_P(simpleImpurePoints.raw()))); for (zv::ArrayEntry entry : zv::ArrRef(simpleImpurePoints.raw())) { zv::Val impurePoint = impurePointOf(scope.raw(), expr, entry.value().deref().raw()); diff --git a/turbo-ext/src/SimpleImpurePoint.cpp b/turbo-ext/src/SimpleImpurePoint.cpp index ba220cdc71d..1c546f4aaac 100644 --- a/turbo-ext/src/SimpleImpurePoint.cpp +++ b/turbo-ext/src/SimpleImpurePoint.cpp @@ -215,17 +215,9 @@ class SimpleImpurePoint } if (!certain && scope != NULL && variant != NULL) { - /* both verdicts combined: Yes = pure, No = impure, Maybe = possibly impure */ zend_long verdict = PT_TRI_YES; bool hasVerdict = false; - if (UNEXPECTED(!resolveVerdict(variant, scope, args, hasVerdict, verdict))) return false; - zend_long passedVerdict = PT_TRI_YES; - bool hasPassedVerdict = false; - if (UNEXPECTED(!resolvePassedVerdict(variant, args, hasPassedVerdict, passedVerdict))) return false; - if (hasPassedVerdict) { - verdict = hasVerdict ? pt_trinary_and(verdict, passedVerdict) : passedVerdict; - hasVerdict = true; - } + if (UNEXPECTED(!resolveConditionalVerdict(variant, scope, args, hasVerdict, verdict))) return false; if (hasVerdict) { if (verdict == PT_TRI_YES) return true; if (verdict == PT_TRI_NO) certain = true; @@ -280,6 +272,64 @@ class SimpleImpurePoint return true; } + /* Mirrors resolveConditionalPurityVerdict(): both verdicts combined + * (Yes = pure, No = impure, Maybe = possibly impure); hasVerdict false + * for the twin's null; false = pending exception */ + [[nodiscard]] static bool resolveConditionalVerdict(zval *variant, zval *scope, zval *args, bool &hasVerdict, zend_long &verdict) + { + if (UNEXPECTED(!resolveVerdict(variant, scope, args, hasVerdict, verdict))) return false; + zend_long passedVerdict = PT_TRI_YES; + bool hasPassedVerdict = false; + if (UNEXPECTED(!resolvePassedVerdict(variant, args, hasPassedVerdict, passedVerdict))) return false; + if (!hasPassedVerdict) return true; + verdict = hasVerdict ? pt_trinary_and(verdict, passedVerdict) : passedVerdict; + hasVerdict = true; + return true; + } + + /* Mirrors narrowByConditionalPurity(); UNDEF = pending exception */ + static zv::Val narrowByConditionalPurity(zval *impurePoints, zval *variant, zval *scope, zval *args) + { + bool hasVerdict = false; + zend_long verdict = PT_TRI_YES; + if (UNEXPECTED(!resolveConditionalVerdict(variant, scope, args, hasVerdict, verdict))) return zv::Val(); + if (!hasVerdict || verdict == PT_TRI_MAYBE) return zv::Val::copyOf(zv::Ref(impurePoints)); + if (verdict == PT_TRI_YES) return zv::Val(zv::Arr::empty()); + + /* array_map() keeps the keys of its one array */ + zv::Arr narrowed = zv::Arr::create(zend_hash_num_elements(Z_ARRVAL_P(impurePoints))); + for (zv::ArrayEntry entry : zv::ArrRef(impurePoints)) { + zval *impurePoint = entry.value().deref().raw(); + if (UNEXPECTED(Z_TYPE_P(impurePoint) != IS_OBJECT || !instanceof_function(Z_OBJCE_P(impurePoint), pt_ce_simple_impure_point))) { + zend_type_error("{closure}(): Argument #1 ($impurePoint) must be of type PHPStan\\Reflection\\Callables\\SimpleImpurePoint, %s given", zend_zval_value_name(impurePoint)); + return zv::Val(); + } + SimpleImpurePoint point(Z_OBJ_P(impurePoint)); + zv::Val certain = point.isCertain(); + if (UNEXPECTED(certain.isUndef())) return zv::Val(); + zv::Val mapped; + if (zend_is_true(certain.raw())) { + mapped = zv::Val::copyOf(zv::Ref(impurePoint)); + } else { + zv::Val identifier = point.getIdentifier(); + if (UNEXPECTED(identifier.isUndef())) return zv::Val(); + zv::Val description = point.getDescription(); + if (UNEXPECTED(description.isUndef())) return zv::Val(); + mapped = create(Z_STR_P(identifier.raw()), Z_STR_P(description.raw()), true); + if (UNEXPECTED(mapped.isUndef())) return zv::Val(); + } + zend_string *key = entry.stringKeyOrNull(); + if (key != NULL) { + narrowed.set(key, std::move(mapped)); + } else { + narrowed.separate(); + zval value = mapped.take(); + zend_hash_index_update(narrowed.table(), entry.indexKey(), &value); + } + } + return zv::Val(std::move(narrowed)); + } + /* Mirrors resolvePureUnlessCallableIsImpureVerdict(): hasVerdict false * for the twin's null, verdict the PT_TRI_* value otherwise; false = * pending exception */ @@ -601,6 +651,11 @@ bool pt_simple_impure_point_resolve_passed_verdict(zval *variant, zval *args, bo return SimpleImpurePoint::resolvePassedVerdict(variant, args, hasVerdict, verdict); } +zv::Val pt_simple_impure_point_narrow_by_conditional_purity(zval *impurePoints, zval *variant, zval *scope, zval *args) +{ + return SimpleImpurePoint::narrowByConditionalPurity(impurePoints, variant, scope, args); +} + /* }}} */ /* {{{ engine ABI glue: parameter parsing + registration */ @@ -656,6 +711,22 @@ PT_MINIT_REGISTRATION(pt_register_simple_impure_point) RETURN_COPY(pt_trinary_singleton(verdict)); }); + cls.method(sigs::resolveConditionalPurityVerdict, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *variant, *scope, *args; + if (!zp::parse(execute_data, variant, scope, args)) RETURN_THROWS(); + bool hasVerdict = false; + zend_long verdict = PT_TRI_YES; + if (UNEXPECTED(!SimpleImpurePoint::resolveConditionalVerdict(variant, scope, args, hasVerdict, verdict))) RETURN_THROWS(); + if (!hasVerdict) RETURN_NULL(); + RETURN_COPY(pt_trinary_singleton(verdict)); + }); + + cls.method(sigs::narrowByConditionalPurity, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *impurePoints, *variant, *scope, *args; + if (!zp::parse(execute_data, impurePoints, variant, scope, args)) RETURN_THROWS(); + PT_RETURN_VAL(SimpleImpurePoint::narrowByConditionalPurity(impurePoints, variant, scope, args)); + }); + cls.method(sigs::resolvePureUnlessParameterPassedVerdict, [](INTERNAL_FUNCTION_PARAMETERS) { zval *variant, *args; if (!zp::parse(execute_data, variant, args)) RETURN_THROWS(); diff --git a/turbo-ext/src/generated/SimpleImpurePoint.h b/turbo-ext/src/generated/SimpleImpurePoint.h index 3491a6c9501..31b37e409d5 100644 --- a/turbo-ext/src/generated/SimpleImpurePoint.h +++ b/turbo-ext/src/generated/SimpleImpurePoint.h @@ -48,18 +48,21 @@ inline constexpr char strings[] = "PHPStan\\Reflection\\Callables\\SimpleImpurePoint\0" /* 223 */ "createFromVariant\0" /* 270 */ "PHPStan\\TrinaryLogic\0" /* 288 */ - "resolvePureUnlessCallableIsImpureVerdict\0" /* 309 */ - "resolvePureUnlessParameterPassedVerdict\0" /* 350 */ - "parameters\0" /* 390 */ - "collectParameterNames\0" /* 401 */ - "parameter\0" /* 423 */ - "PHPStan\\Reflection\\ExtendedParameterReflection\0" /* 433 */ - "parameterIndex\0" /* 480 */ - "declaredParameterNames\0" /* 495 */ - "matchArgForParameter\0" /* 518 */ - "getIdentifier\0" /* 539 */ - "getDescription\0" /* 553 */ - "isCertain"; /* 568 */ + "resolveConditionalPurityVerdict\0" /* 309 */ + "impurePoints\0" /* 341 */ + "narrowByConditionalPurity\0" /* 354 */ + "resolvePureUnlessCallableIsImpureVerdict\0" /* 380 */ + "resolvePureUnlessParameterPassedVerdict\0" /* 421 */ + "parameters\0" /* 461 */ + "collectParameterNames\0" /* 472 */ + "parameter\0" /* 494 */ + "PHPStan\\Reflection\\ExtendedParameterReflection\0" /* 504 */ + "parameterIndex\0" /* 551 */ + "declaredParameterNames\0" /* 566 */ + "matchArgForParameter\0" /* 589 */ + "getIdentifier\0" /* 610 */ + "getDescription\0" /* 624 */ + "isCertain"; /* 639 */ inline constexpr reg::PackedArg args[] = { reg::packed(0, MAY_BE_STRING), /* __construct $identifier */ reg::packed(11, MAY_BE_STRING), /* __construct $description */ @@ -69,6 +72,15 @@ inline constexpr reg::PackedArg args[] = { reg::packed(180, MAY_BE_NULL, 186, false, false, 209), /* createFromVariant $scope */ reg::packed(214, MAY_BE_ARRAY, reg::NoString, false, false, 219), /* createFromVariant $args */ reg::packed(222, MAY_BE_NULL, 223), /* createFromVariant return */ + reg::packed(134, 0, 142), /* resolveConditionalPurityVerdict $variant */ + reg::packed(180, 0, 186), /* resolveConditionalPurityVerdict $scope */ + reg::packed(214, MAY_BE_ARRAY), /* resolveConditionalPurityVerdict $args */ + reg::packed(222, MAY_BE_NULL, 288), /* resolveConditionalPurityVerdict return */ + reg::packed(341, MAY_BE_ARRAY), /* narrowByConditionalPurity $impurePoints */ + reg::packed(134, 0, 142), /* narrowByConditionalPurity $variant */ + reg::packed(180, 0, 186), /* narrowByConditionalPurity $scope */ + reg::packed(214, MAY_BE_ARRAY), /* narrowByConditionalPurity $args */ + reg::packed(222, MAY_BE_ARRAY), /* narrowByConditionalPurity return */ reg::packed(134, 0, 142), /* resolvePureUnlessCallableIsImpureVerdict $variant */ reg::packed(180, 0, 186), /* resolvePureUnlessCallableIsImpureVerdict $scope */ reg::packed(214, MAY_BE_ARRAY), /* resolvePureUnlessCallableIsImpureVerdict $args */ @@ -76,12 +88,12 @@ inline constexpr reg::PackedArg args[] = { reg::packed(134, 0, 142), /* resolvePureUnlessParameterPassedVerdict $variant */ reg::packed(214, MAY_BE_ARRAY), /* resolvePureUnlessParameterPassedVerdict $args */ reg::packed(222, MAY_BE_NULL, 288), /* resolvePureUnlessParameterPassedVerdict return */ - reg::packed(390, MAY_BE_ARRAY), /* collectParameterNames $parameters */ + reg::packed(461, MAY_BE_ARRAY), /* collectParameterNames $parameters */ reg::packed(222, MAY_BE_ARRAY), /* collectParameterNames return */ reg::packed(214, MAY_BE_ARRAY), /* matchArgForParameter $args */ - reg::packed(423, 0, 433), /* matchArgForParameter $parameter */ - reg::packed(480, MAY_BE_LONG), /* matchArgForParameter $parameterIndex */ - reg::packed(495, MAY_BE_ARRAY), /* matchArgForParameter $declaredParameterNames */ + reg::packed(494, 0, 504), /* matchArgForParameter $parameter */ + reg::packed(551, MAY_BE_LONG), /* matchArgForParameter $parameterIndex */ + reg::packed(566, MAY_BE_ARRAY), /* matchArgForParameter $declaredParameterNames */ reg::packed(222, MAY_BE_ARRAY), /* matchArgForParameter return */ reg::packed(222, MAY_BE_STRING), /* getIdentifier return */ reg::packed(222, MAY_BE_STRING), /* getDescription return */ @@ -94,13 +106,15 @@ using Sig = reg::Sig; namespace sig { inline constexpr sigtab::Sig __construct = { { 31 /* __construct */, 3, 0, 3, reg::NoArg, ZEND_ACC_PUBLIC } }; inline constexpr sigtab::Sig createFromVariant = { { 270 /* createFromVariant */, 2, 3, 4, 7, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig resolvePureUnlessCallableIsImpureVerdict = { { 309 /* resolvePureUnlessCallableIsImpureVerdict */, 3, 8, 3, 11, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig resolvePureUnlessParameterPassedVerdict = { { 350 /* resolvePureUnlessParameterPassedVerdict */, 2, 12, 2, 14, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig collectParameterNames = { { 401 /* collectParameterNames */, 1, 15, 1, 16, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig matchArgForParameter = { { 518 /* matchArgForParameter */, 4, 17, 4, 21, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig getIdentifier = { { 539 /* getIdentifier */, 0, 22, 0, 22, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getDescription = { { 553 /* getDescription */, 0, 23, 0, 23, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig isCertain = { { 568 /* isCertain */, 0, 24, 0, 24, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig resolveConditionalPurityVerdict = { { 309 /* resolveConditionalPurityVerdict */, 3, 8, 3, 11, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig narrowByConditionalPurity = { { 354 /* narrowByConditionalPurity */, 4, 12, 4, 16, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig resolvePureUnlessCallableIsImpureVerdict = { { 380 /* resolvePureUnlessCallableIsImpureVerdict */, 3, 17, 3, 20, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig resolvePureUnlessParameterPassedVerdict = { { 421 /* resolvePureUnlessParameterPassedVerdict */, 2, 21, 2, 23, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig collectParameterNames = { { 472 /* collectParameterNames */, 1, 24, 1, 25, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig matchArgForParameter = { { 589 /* matchArgForParameter */, 4, 26, 4, 30, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig getIdentifier = { { 610 /* getIdentifier */, 0, 31, 0, 31, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getDescription = { { 624 /* getDescription */, 0, 32, 0, 32, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig isCertain = { { 639 /* isCertain */, 0, 33, 0, 33, ZEND_ACC_PUBLIC } }; } // namespace sig } // namespace ptdecl::SimpleImpurePoint diff --git a/turbo-ext/src/support.h b/turbo-ext/src/support.h index fdc299c19c8..424a83ace35 100644 --- a/turbo-ext/src/support.h +++ b/turbo-ext/src/support.h @@ -2749,6 +2749,9 @@ zv::Val pt_template_type_map_map(zval *map, zval *cb); /* SimpleImpurePoint::resolvePureUnlessParameterPassedVerdict($variant, * $args): the same answers */ [[nodiscard]] bool pt_simple_impure_point_resolve_passed_verdict(zval *variant, zval *args, bool &hasVerdict, zend_long &verdict); +/* SimpleImpurePoint::narrowByConditionalPurity($impurePoints, $variant, + * $scope, $args) (everything borrowed); UNDEF = pending exception */ +zv::Val pt_simple_impure_point_narrow_by_conditional_purity(zval *impurePoints, zval *variant, zval *scope, zval *args); /* }}} */ diff --git a/turbo-ext/tests/type-family.php b/turbo-ext/tests/type-family.php index 55570d52186..5b7124deb50 100644 --- a/turbo-ext/tests/type-family.php +++ b/turbo-ext/tests/type-family.php @@ -6891,7 +6891,8 @@ public function getResolvedPhpDoc(): ?\PHPStan\PhpDoc\ResolvedPhpDocBlock { retu // and array_reduce() fed pure, impure, maybe-pure, null, non-callable and // omitted callbacks (positional and named), the pure-unless-parameter-passed // out parameters of str_replace() and preg_match() passed and omitted -// (positional, named and unpacked), the transformed and plain +// (positional, named and unpacked), both verdicts combined and applied to a +// keyed list of certain and uncertain points, the transformed and plain // fixture methods, a missing scope or variant, and unconstructed instances $observations['native ' . \PHPStan\Reflection\Callables\SimpleImpurePoint::class] = (new ReflectionMethod(\PHPStan\Reflection\Callables\SimpleImpurePoint::class, 'createFromVariant'))->isInternal(); { @@ -6944,6 +6945,8 @@ public function getResolvedPhpDoc(): ?\PHPStan\PhpDoc\ResolvedPhpDocBlock { retu $r["function $functionName $argListName"] = $catching(static fn () => $viewImpurePoint(\PHPStan\Reflection\Callables\SimpleImpurePoint::createFromVariant($function, $variant, $sipScope, $args))); $r["function $functionName $argListName verdict"] = $catching(static fn () => $view(\PHPStan\Reflection\Callables\SimpleImpurePoint::resolvePureUnlessCallableIsImpureVerdict($variant, $sipScope, $args))); $r["function $functionName $argListName passed verdict"] = $catching(static fn () => $view(\PHPStan\Reflection\Callables\SimpleImpurePoint::resolvePureUnlessParameterPassedVerdict($variant, $args))); + $r["function $functionName $argListName conditional verdict"] = $catching(static fn () => $view(\PHPStan\Reflection\Callables\SimpleImpurePoint::resolveConditionalPurityVerdict($variant, $sipScope, $args))); + $r["function $functionName $argListName narrowed"] = $catching(static fn () => array_map($viewImpurePoint, \PHPStan\Reflection\Callables\SimpleImpurePoint::narrowByConditionalPurity(['certain' => new \PHPStan\Reflection\Callables\SimpleImpurePoint('functionCall', 'certain call', true), 3 => new \PHPStan\Reflection\Callables\SimpleImpurePoint('methodCall', 'possible call', false)], $variant, $sipScope, $args))); } } foreach (['returnsStatic', 'withValue', 'each', 'fails'] as $methodName) { From f60045ed7a5c92f9aaa2a819799feddeb17a5d71 Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Sat, 12 Sep 2026 21:51:58 +0900 Subject: [PATCH 22/29] Describe the function metadata entries with one optional-key shape The @var spelled the entry type as a union of four array shapes, one per combination of the two conditional purity keys, which does not scale and already repeated itself. A single shape with optional keys says the same thing, and matches how bin/generate-function-metadata.php already annotated the array it loads. --- bin/functionMetadata_original.php | 2 +- bin/generate-function-metadata.php | 2 +- resources/functionMetadata.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bin/functionMetadata_original.php b/bin/functionMetadata_original.php index c9e33dbd861..bcacda03d42 100644 --- a/bin/functionMetadata_original.php +++ b/bin/functionMetadata_original.php @@ -22,7 +22,7 @@ * e.g. preg_replace_callback() (impure callback or a passed 'count'). */ -/** @var array}|array{pureUnlessParameterPassedParameters: array}|array{pureUnlessCallableIsImpureParameters: array, pureUnlessParameterPassedParameters: array}> */ +/** @var array, pureUnlessParameterPassedParameters?: array}> */ return [ 'abs' => ['hasSideEffects' => false], 'acos' => ['hasSideEffects' => false], diff --git a/bin/generate-function-metadata.php b/bin/generate-function-metadata.php index 0c22cbfcc18..9e30e062bfb 100755 --- a/bin/generate-function-metadata.php +++ b/bin/generate-function-metadata.php @@ -212,7 +212,7 @@ public function enterNode(Node $node) * an argument, e.g. str_replace()'s 'replace_count'. */ -/** @var array}|array{pureUnlessParameterPassedParameters: array}> */ +/** @var array, pureUnlessParameterPassedParameters?: array}> */ return [ %s ]; diff --git a/resources/functionMetadata.php b/resources/functionMetadata.php index 73ef03c0640..ac09a1109b3 100644 --- a/resources/functionMetadata.php +++ b/resources/functionMetadata.php @@ -24,7 +24,7 @@ * an argument, e.g. str_replace()'s 'count'. */ -/** @var array}|array{pureUnlessParameterPassedParameters: array}|array{pureUnlessCallableIsImpureParameters: array, pureUnlessParameterPassedParameters: array}> */ +/** @var array, pureUnlessParameterPassedParameters?: array}> */ return [ 'BackedEnum::from' => ['hasSideEffects' => false], 'BackedEnum::tryFrom' => ['hasSideEffects' => false], From aeb041b57efb863463361e3184f8f0d17fec55b8 Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Sat, 12 Sep 2026 21:52:06 +0900 Subject: [PATCH 23/29] Drop comments that restate the code The two comments in the generator described what the loops below them plainly do. In SimpleImpurePoint, resolvePureUnlessParameterPassedVerdict() and narrowByConditionalPurity() read off their names, and the Yes/No/Maybe gloss on resolveConditionalPurityVerdict() explained TrinaryLogic rather than this method. Kept only the parts that are not obvious: that a function can carry both flags at once, and when each verdict returns null. --- bin/generate-function-metadata.php | 5 ----- .../Callables/SimpleImpurePoint.php | 19 +++---------------- 2 files changed, 3 insertions(+), 21 deletions(-) diff --git a/bin/generate-function-metadata.php b/bin/generate-function-metadata.php index 9e30e062bfb..335cc64fafb 100755 --- a/bin/generate-function-metadata.php +++ b/bin/generate-function-metadata.php @@ -132,9 +132,6 @@ public function enterNode(Node $node) throw new ShouldNotHappenException($functionName); } - // An entry can carry both conditions at once (e.g. preg_replace_callback, - // which is pure unless its callback is impure or its 'count' is passed), - // so keep every condition the hand-maintained entry declares. $conditions = []; foreach (['pureUnlessCallableIsImpureParameters', 'pureUnlessParameterPassedParameters'] as $conditionKey) { if (!isset($metadata[$functionName][$conditionKey])) { @@ -232,8 +229,6 @@ public function enterNode(Node $node) ); foreach ($metadata as $name => $meta) { - // An entry is either unconditional or carries one or both of the conditional - // purity keys, so encode every key it has instead of just the first one. $entries = []; if (isset($meta['hasSideEffects'])) { $entries[] = sprintf('%s => %s', $escape('hasSideEffects'), $escape($meta['hasSideEffects'])); diff --git a/src/Reflection/Callables/SimpleImpurePoint.php b/src/Reflection/Callables/SimpleImpurePoint.php index 6397c876ac6..b159270adcc 100644 --- a/src/Reflection/Callables/SimpleImpurePoint.php +++ b/src/Reflection/Callables/SimpleImpurePoint.php @@ -137,13 +137,9 @@ public static function createFromVariant(FunctionReflection|ExtendedMethodReflec } /** - * Purity verdict of a call to a variant that declares conditional purity. - * * A function can carry both flags at once (e.g. preg_replace_callback, which is - * pure unless its callback is impure or its $count is passed). It stays pure only - * when both verdicts agree it is pure, so they are combined: Yes = pure, - * No = impure, Maybe = possibly impure. Returns null when the variant declares - * neither flag, so the caller keeps its current behavior. + * pure unless its callback is impure or its $count is passed), so the two + * verdicts are combined. Returns null when the variant declares neither flag. * * @param Arg[] $args */ @@ -159,11 +155,6 @@ public static function resolveConditionalPurityVerdict(ParametersAcceptor $varia } /** - * Applies the conditional purity verdict of this call site to impure points that - * were resolved without one - a callable value's impure points are computed from - * its ParametersAcceptor alone, so a first-class callable of a conditionally pure - * function arrives here as an unconditional "possibly impure" point. - * * @param SimpleImpurePoint[] $impurePoints * @param Arg[] $args * @return SimpleImpurePoint[] @@ -252,11 +243,7 @@ public static function resolvePureUnlessCallableIsImpureVerdict(ParametersAccept } /** - * Purity verdict for parameters flagged with @pure-unless-parameter-passed: - * the call stays pure as long as none of those (by-ref out) parameters - * received an argument. Returns Yes when no flagged parameter was passed, - * No when at least one was, and null when the variant has no such parameters - * (so the caller keeps its current behavior). + * Returns null when the variant has no flagged parameters. * * @param Arg[] $args */ From 29888c7b5472b1cce78f9e0a8c34e24d787c979e Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Sat, 12 Sep 2026 21:52:20 +0900 Subject: [PATCH 24/29] Merge the two conditional purity parameter mergers into one class MergedPureUnlessCallableIsImpureParameters and MergedPureUnlessParameterPassedParameters were identical apart from which getter they read off each method reflection. MergedConditionalPurityParameters keeps that merge once and exposes one entry point per tag. Co-authored-by: Claude Opus 5 --- .../Type/IntersectionTypeMethodReflection.php | 4 +- .../MergedConditionalPurityParameters.php | 77 +++++++++++++++++++ ...edPureUnlessCallableIsImpureParameters.php | 56 -------------- ...gedPureUnlessParameterPassedParameters.php | 56 -------------- .../Type/UnionTypeMethodReflection.php | 4 +- 5 files changed, 81 insertions(+), 116 deletions(-) create mode 100644 src/Reflection/Type/MergedConditionalPurityParameters.php delete mode 100644 src/Reflection/Type/MergedPureUnlessCallableIsImpureParameters.php delete mode 100644 src/Reflection/Type/MergedPureUnlessParameterPassedParameters.php diff --git a/src/Reflection/Type/IntersectionTypeMethodReflection.php b/src/Reflection/Type/IntersectionTypeMethodReflection.php index e7e9929c9c2..108047fb638 100644 --- a/src/Reflection/Type/IntersectionTypeMethodReflection.php +++ b/src/Reflection/Type/IntersectionTypeMethodReflection.php @@ -216,12 +216,12 @@ public function isPure(): TrinaryLogic public function getPureUnlessCallableIsImpureParameters(): array { - return MergedPureUnlessCallableIsImpureParameters::merge($this->methods); + return MergedConditionalPurityParameters::mergePureUnlessCallableIsImpure($this->methods); } public function getPureUnlessParameterPassedParameters(): array { - return MergedPureUnlessParameterPassedParameters::merge($this->methods); + return MergedConditionalPurityParameters::mergePureUnlessParameterPassed($this->methods); } public function getDocComment(): ?string diff --git a/src/Reflection/Type/MergedConditionalPurityParameters.php b/src/Reflection/Type/MergedConditionalPurityParameters.php new file mode 100644 index 00000000000..a29a94dac02 --- /dev/null +++ b/src/Reflection/Type/MergedConditionalPurityParameters.php @@ -0,0 +1,77 @@ + + */ + public static function mergePureUnlessCallableIsImpure(array $methods): array + { + return self::merge(array_map( + static fn (ExtendedMethodReflection $method) => $method->getPureUnlessCallableIsImpureParameters(), + $methods, + )); + } + + /** + * @param ExtendedMethodReflection[] $methods + * @return array + */ + public static function mergePureUnlessParameterPassed(array $methods): array + { + return self::merge(array_map( + static fn (ExtendedMethodReflection $method) => $method->getPureUnlessParameterPassedParameters(), + $methods, + )); + } + + /** + * @param array> $maps + * @return array + */ + private static function merge(array $maps): array + { + $parameterNames = []; + foreach ($maps as $map) { + foreach (array_keys($map) as $name) { + $parameterNames[$name] = true; + } + } + + $merged = []; + foreach (array_keys($parameterNames) as $name) { + $value = null; + foreach ($maps as $map) { + $current = $map[$name] ?? TrinaryLogic::createNo(); + if ($value === null) { + $value = $current; + continue; + } + $value = $value->equals($current) ? $value : TrinaryLogic::createMaybe(); + } + + if ($value === null) { + continue; + } + + $merged[$name] = $value; + } + + return $merged; + } + +} diff --git a/src/Reflection/Type/MergedPureUnlessCallableIsImpureParameters.php b/src/Reflection/Type/MergedPureUnlessCallableIsImpureParameters.php deleted file mode 100644 index b5ecfd9ba1b..00000000000 --- a/src/Reflection/Type/MergedPureUnlessCallableIsImpureParameters.php +++ /dev/null @@ -1,56 +0,0 @@ - - */ - public static function merge(array $methods): array - { - $parameterNames = []; - $maps = []; - foreach ($methods as $method) { - $map = $method->getPureUnlessCallableIsImpureParameters(); - $maps[] = $map; - foreach (array_keys($map) as $name) { - $parameterNames[$name] = true; - } - } - - $merged = []; - foreach (array_keys($parameterNames) as $name) { - $value = null; - foreach ($maps as $map) { - $current = $map[$name] ?? TrinaryLogic::createNo(); - if ($value === null) { - $value = $current; - continue; - } - $value = $value->equals($current) ? $value : TrinaryLogic::createMaybe(); - } - - if ($value === null) { - continue; - } - - $merged[$name] = $value; - } - - return $merged; - } - -} diff --git a/src/Reflection/Type/MergedPureUnlessParameterPassedParameters.php b/src/Reflection/Type/MergedPureUnlessParameterPassedParameters.php deleted file mode 100644 index 79dce93398a..00000000000 --- a/src/Reflection/Type/MergedPureUnlessParameterPassedParameters.php +++ /dev/null @@ -1,56 +0,0 @@ - - */ - public static function merge(array $methods): array - { - $parameterNames = []; - $maps = []; - foreach ($methods as $method) { - $map = $method->getPureUnlessParameterPassedParameters(); - $maps[] = $map; - foreach (array_keys($map) as $name) { - $parameterNames[$name] = true; - } - } - - $merged = []; - foreach (array_keys($parameterNames) as $name) { - $value = null; - foreach ($maps as $map) { - $current = $map[$name] ?? TrinaryLogic::createNo(); - if ($value === null) { - $value = $current; - continue; - } - $value = $value->equals($current) ? $value : TrinaryLogic::createMaybe(); - } - - if ($value === null) { - continue; - } - - $merged[$name] = $value; - } - - return $merged; - } - -} diff --git a/src/Reflection/Type/UnionTypeMethodReflection.php b/src/Reflection/Type/UnionTypeMethodReflection.php index e1f4e4a28cf..8daaf43cb0d 100644 --- a/src/Reflection/Type/UnionTypeMethodReflection.php +++ b/src/Reflection/Type/UnionTypeMethodReflection.php @@ -173,12 +173,12 @@ public function isPure(): TrinaryLogic public function getPureUnlessCallableIsImpureParameters(): array { - return MergedPureUnlessCallableIsImpureParameters::merge($this->methods); + return MergedConditionalPurityParameters::mergePureUnlessCallableIsImpure($this->methods); } public function getPureUnlessParameterPassedParameters(): array { - return MergedPureUnlessParameterPassedParameters::merge($this->methods); + return MergedConditionalPurityParameters::mergePureUnlessParameterPassed($this->methods); } public function getDocComment(): ?string From 9f785b3ac1314c77fb249e7cdcc2e9f76082dcf1 Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Sat, 3 Oct 2026 08:33:14 +0900 Subject: [PATCH 25/29] Bump expected turbo version --- src/Turbo/TurboExtensionEnabler.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Turbo/TurboExtensionEnabler.php b/src/Turbo/TurboExtensionEnabler.php index ed9e593a96a..4cb7087927e 100644 --- a/src/Turbo/TurboExtensionEnabler.php +++ b/src/Turbo/TurboExtensionEnabler.php @@ -33,7 +33,7 @@ final class TurboExtensionEnabler { - public const EXPECTED_EXTENSION_VERSION = 'c116397'; + public const EXPECTED_EXTENSION_VERSION = 'bee08d3'; private static bool $active = false; From 1b763eec802e966331b5fcfa1443bc4cb30de1e6 Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Sat, 3 Oct 2026 09:43:52 +0900 Subject: [PATCH 26/29] Match the generator's header example to the generated metadata --- bin/generate-function-metadata.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/generate-function-metadata.php b/bin/generate-function-metadata.php index 335cc64fafb..c2fb348e681 100755 --- a/bin/generate-function-metadata.php +++ b/bin/generate-function-metadata.php @@ -206,7 +206,7 @@ public function enterNode(Node $node) * impure callable, e.g. array_map()'s 'callback'. * - ['pureUnlessParameterPassedParameters' => array] - pure unless * one of the listed (by-ref out) parameters (keyed by parameter name) receives - * an argument, e.g. str_replace()'s 'replace_count'. + * an argument, e.g. str_replace()'s 'count'. */ /** @var array, pureUnlessParameterPassedParameters?: array}> */ From 84e2b0bf881b58ae589566004c5ab4edf53ff817 Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Sat, 3 Oct 2026 09:51:14 +0900 Subject: [PATCH 27/29] Leave the PCRE functions out of @pure-unless-parameter-passed preg_match(), preg_match_all(), preg_replace() and preg_filter() raise a warning on an invalid pattern and set preg_last_error(), so they are not pure even when their out parameter is omitted. Code relies on that: a bare preg_match($pattern, '') under set_error_handler() validates a pattern. preg_replace_callback() goes back to its callback-only entry. No built-in carries both conditions any more, so a userland function tagged with both covers the combined verdict. --- bin/functionMetadata_original.php | 11 +- resources/functionMetadata.php | 6 +- .../Callables/SimpleImpurePoint.php | 6 +- .../Rules/Pure/PureFunctionRuleTest.php | 54 ++++----- .../pure-unless-parameter-passed-builtin.php | 103 +----------------- .../data/pure-unless-parameter-passed.php | 66 +++++++++++ turbo-ext/tests/type-family.php | 5 +- 7 files changed, 106 insertions(+), 145 deletions(-) diff --git a/bin/functionMetadata_original.php b/bin/functionMetadata_original.php index bcacda03d42..f4ac32c6b9e 100644 --- a/bin/functionMetadata_original.php +++ b/bin/functionMetadata_original.php @@ -18,8 +18,7 @@ * (keyed by parameter name) receives an argument, e.g. str_replace() * whose only side effect is writing to its optional 'count' argument. * - * The last two can be combined for a call that is pure unless either happens, - * e.g. preg_replace_callback() (impure callback or a passed 'count'). + * The last two can be combined for a call that is pure unless either happens. */ /** @var array, pureUnlessParameterPassedParameters?: array}> */ @@ -271,13 +270,7 @@ 'output_reset_rewrite_vars' => ['hasSideEffects' => true], 'pclose' => ['hasSideEffects' => true], 'popen' => ['hasSideEffects' => true], - 'preg_filter' => ['pureUnlessParameterPassedParameters' => ['count' => true]], - // 'matches'/'subpatterns': PHP 8+ uses the php-8-stubs parameter name, PHP <8 falls - // back to the legacy functionMap.php name. - 'preg_match' => ['pureUnlessParameterPassedParameters' => ['matches' => true, 'subpatterns' => true]], - 'preg_match_all' => ['pureUnlessParameterPassedParameters' => ['matches' => true, 'subpatterns' => true]], - 'preg_replace' => ['pureUnlessParameterPassedParameters' => ['count' => true]], - 'preg_replace_callback' => ['pureUnlessCallableIsImpureParameters' => ['callback' => true], 'pureUnlessParameterPassedParameters' => ['count' => true]], + 'preg_replace_callback' => ['pureUnlessCallableIsImpureParameters' => ['callback' => true]], 'similar_text' => ['pureUnlessParameterPassedParameters' => ['percent' => true]], 'readfile' => ['hasSideEffects' => true], 'rename' => ['hasSideEffects' => true], diff --git a/resources/functionMetadata.php b/resources/functionMetadata.php index ac09a1109b3..adde45e0f7d 100644 --- a/resources/functionMetadata.php +++ b/resources/functionMetadata.php @@ -1633,15 +1633,11 @@ 'posix_ttyname' => ['hasSideEffects' => false], 'posix_uname' => ['hasSideEffects' => false], 'pow' => ['hasSideEffects' => false], - 'preg_filter' => ['pureUnlessParameterPassedParameters' => ['count' => true]], 'preg_grep' => ['hasSideEffects' => false], 'preg_last_error' => ['hasSideEffects' => true], 'preg_last_error_msg' => ['hasSideEffects' => true], - 'preg_match' => ['pureUnlessParameterPassedParameters' => ['matches' => true, 'subpatterns' => true]], - 'preg_match_all' => ['pureUnlessParameterPassedParameters' => ['matches' => true, 'subpatterns' => true]], 'preg_quote' => ['hasSideEffects' => false], - 'preg_replace' => ['pureUnlessParameterPassedParameters' => ['count' => true]], - 'preg_replace_callback' => ['pureUnlessCallableIsImpureParameters' => ['callback' => true], 'pureUnlessParameterPassedParameters' => ['count' => true]], + 'preg_replace_callback' => ['pureUnlessCallableIsImpureParameters' => ['callback' => true]], 'preg_split' => ['hasSideEffects' => false], 'property_exists' => ['hasSideEffects' => false], 'quoted_printable_decode' => ['hasSideEffects' => false], diff --git a/src/Reflection/Callables/SimpleImpurePoint.php b/src/Reflection/Callables/SimpleImpurePoint.php index b159270adcc..98a229de6c6 100644 --- a/src/Reflection/Callables/SimpleImpurePoint.php +++ b/src/Reflection/Callables/SimpleImpurePoint.php @@ -137,9 +137,9 @@ public static function createFromVariant(FunctionReflection|ExtendedMethodReflec } /** - * A function can carry both flags at once (e.g. preg_replace_callback, which is - * pure unless its callback is impure or its $count is passed), so the two - * verdicts are combined. Returns null when the variant declares neither flag. + * A function can carry both flags at once (pure unless a callback is impure + * or an out parameter is passed), so the two verdicts are combined. Returns + * null when the variant declares neither flag. * * @param Arg[] $args */ diff --git a/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php b/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php index 2161715148b..d6cf230e4c0 100644 --- a/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php +++ b/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php @@ -488,51 +488,55 @@ public function testPureUnlessParameterPassed(): void 'Impure echo in pure function PureUnlessParameterPassedFunction\sideEffectUnlessParameterPassed().', 470, ], - ]); - } - - public function testPureUnlessParameterPassedBuiltin(): void - { - $this->analyse([__DIR__ . '/data/pure-unless-parameter-passed-builtin.php'], [ [ - 'Impure call to function str_replace() in pure function PureUnlessParameterPassedBuiltin\pureStrReplaceWithCount().', - 22, + 'Impure call to function PureUnlessParameterPassedFunction\myReplaceCallback() in pure function PureUnlessParameterPassedFunction\pureBothPureCallbackPassingCount().', + 505, ], [ - 'Impure call to function preg_match() in pure function PureUnlessParameterPassedBuiltin\purePregMatchWithMatches().', - 40, + 'Impure call to function PureUnlessParameterPassedFunction\myReplaceCallback() in pure function PureUnlessParameterPassedFunction\pureBothImpureCallbackOmittingCount().', + 514, ], [ - 'Impure call to function preg_filter() in pure function PureUnlessParameterPassedBuiltin\purePregFilterWithCount().', - 60, + 'Impure echo in pure function PureUnlessParameterPassedFunction\pureBothImpureCallbackOmittingCount().', + 515, ], [ - 'Impure call to function preg_replace_callback() in pure function PureUnlessParameterPassedBuiltin\purePregReplaceCallbackWithCount().', - 80, + 'Possibly impure call to a callable in pure function PureUnlessParameterPassedFunction\pureBothOpaqueCallbackOmittingCount().', + 528, ], [ - 'Impure call to function preg_replace_callback() in pure function PureUnlessParameterPassedBuiltin\purePregReplaceCallbackImpureCallback().', - 89, + 'Possibly impure call to function PureUnlessParameterPassedFunction\myReplaceCallback() in pure function PureUnlessParameterPassedFunction\pureBothOpaqueCallbackOmittingCount().', + 528, ], [ - 'Impure echo in pure function PureUnlessParameterPassedBuiltin\purePregReplaceCallbackImpureCallback().', - 90, + 'Impure call to function PureUnlessParameterPassedFunction\myReplaceCallback() in pure function PureUnlessParameterPassedFunction\pureBothOpaqueCallbackPassingCount().', + 539, ], [ - 'Impure call to function str_ireplace() in pure function PureUnlessParameterPassedBuiltin\pureStrIreplaceWithCount().', - 113, + 'Possibly impure call to a callable in pure function PureUnlessParameterPassedFunction\pureBothOpaqueCallbackPassingCount().', + 539, ], + ]); + } + + public function testPureUnlessParameterPassedBuiltin(): void + { + $this->analyse([__DIR__ . '/data/pure-unless-parameter-passed-builtin.php'], [ [ - 'Impure call to function preg_replace() in pure function PureUnlessParameterPassedBuiltin\purePregReplaceWithCount().', - 133, + 'Impure call to function str_replace() in pure function PureUnlessParameterPassedBuiltin\pureStrReplaceWithCount().', + 22, ], [ - 'Impure call to function preg_match_all() in pure function PureUnlessParameterPassedBuiltin\purePregMatchAllWithMatches().', - 151, + 'Possibly impure call to function preg_match() in pure function PureUnlessParameterPassedBuiltin\purePregMatchWithoutMatches().', + 32, + ], + [ + 'Impure call to function str_ireplace() in pure function PureUnlessParameterPassedBuiltin\pureStrIreplaceWithCount().', + 52, ], [ 'Impure call to function similar_text() in pure function PureUnlessParameterPassedBuiltin\pureSimilarTextWithPercent().', - 169, + 70, ], ]); } diff --git a/tests/PHPStan/Rules/Pure/data/pure-unless-parameter-passed-builtin.php b/tests/PHPStan/Rules/Pure/data/pure-unless-parameter-passed-builtin.php index fe0f2e1de47..a821194e136 100644 --- a/tests/PHPStan/Rules/Pure/data/pure-unless-parameter-passed-builtin.php +++ b/tests/PHPStan/Rules/Pure/data/pure-unless-parameter-passed-builtin.php @@ -27,72 +27,11 @@ function pureStrReplaceWithCount(string $s): string */ function purePregMatchWithoutMatches(string $s): int { - // The by-ref $matches is omitted, so preg_match() is pure. + // preg_match() is left out of the metadata even with $matches omitted: an invalid + // pattern raises a warning and sets preg_last_error(), so the call stays possibly impure. return (int) preg_match('/a/', $s); } -/** - * @phpstan-pure - */ -function purePregMatchWithMatches(string $s): int -{ - // The by-ref $matches is passed, so preg_match() is impure (the flag is certain). - return (int) preg_match('/a/', $s, $matches); -} - -/** - * @phpstan-pure - */ -function purePregFilterWithoutCount(string $s): ?string -{ - // The by-ref $count is omitted, so preg_filter() is pure. - return preg_filter('/a/', 'b', $s); -} - -/** - * @phpstan-pure - */ -function purePregFilterWithCount(string $s): ?string -{ - // The by-ref $count is passed, so preg_filter() is impure (the flag is certain). - $count = 0; - - return preg_filter('/a/', 'b', $s, -1, $count); -} - -/** - * @phpstan-pure - */ -function purePregReplaceCallbackPureCallback(string $s): ?string -{ - // preg_replace_callback() carries both flags: the callback is pure and the by-ref - // $count is omitted, so the call stays pure. - return preg_replace_callback('/a/', static fn (array $m): string => $m[0], $s); -} - -/** - * @phpstan-pure - */ -function purePregReplaceCallbackWithCount(string $s): ?string -{ - $count = 0; - // The callback is pure but the by-ref $count is passed, so the call is impure. - return preg_replace_callback('/a/', static fn (array $m): string => $m[0], $s, -1, $count); -} - -/** - * @phpstan-pure - */ -function purePregReplaceCallbackImpureCallback(string $s): ?string -{ - // The callback itself is impure, so the call is impure regardless of $count. - return preg_replace_callback('/a/', static function (array $m): string { - echo $m[0]; - - return $m[0]; - }, $s); -} - /** * @phpstan-pure */ @@ -113,44 +52,6 @@ function pureStrIreplaceWithCount(string $s): string return str_ireplace('a', 'b', $s, $count); } -/** - * @phpstan-pure - */ -function purePregReplaceWithoutCount(string $s): ?string -{ - // The by-ref $count is omitted, so preg_replace() is pure. - return preg_replace('/a/', 'b', $s); -} - -/** - * @phpstan-pure - */ -function purePregReplaceWithCount(string $s): ?string -{ - // The by-ref $count is passed, so preg_replace() is impure (the flag is certain). - $count = 0; - - return preg_replace('/a/', 'b', $s, -1, $count); -} - -/** - * @phpstan-pure - */ -function purePregMatchAllWithoutMatches(string $s): int -{ - // The by-ref $matches is omitted, so preg_match_all() is pure. - return (int) preg_match_all('/a/', $s); -} - -/** - * @phpstan-pure - */ -function purePregMatchAllWithMatches(string $s): int -{ - // The by-ref $matches is passed, so preg_match_all() is impure (the flag is certain). - return (int) preg_match_all('/a/', $s, $matches); -} - /** * @phpstan-pure */ diff --git a/tests/PHPStan/Rules/Pure/data/pure-unless-parameter-passed.php b/tests/PHPStan/Rules/Pure/data/pure-unless-parameter-passed.php index adc54457081..d30d07ba674 100644 --- a/tests/PHPStan/Rules/Pure/data/pure-unless-parameter-passed.php +++ b/tests/PHPStan/Rules/Pure/data/pure-unless-parameter-passed.php @@ -472,3 +472,69 @@ function sideEffectUnlessParameterPassed(string $subject, int &$count = 0): stri return $subject; } + +/** + * @param callable(string): string $f + * @param-out int $count + * @pure-unless-callable-is-impure $f + * @pure-unless-parameter-passed $count + */ +function myReplaceCallback(callable $f, string $subject, int &$count = 0): string +{ + $count = 1; + + return $f($subject); +} + +/** + * @phpstan-pure + */ +function pureBothConditionsMet(string $s): string +{ + // The callback is pure and $count is omitted, so the call stays pure. + return myReplaceCallback(static fn (string $x): string => $x, $s); +} + +/** + * @phpstan-pure + */ +function pureBothPureCallbackPassingCount(string $s): string +{ + $count = 0; + // The callback is pure but $count is passed, so the call is impure. + return myReplaceCallback(static fn (string $x): string => $x, $s, $count); +} + +/** + * @phpstan-pure + */ +function pureBothImpureCallbackOmittingCount(string $s): string +{ + // $count is omitted but the callback is impure, so the call is impure. + return myReplaceCallback(static function (string $x): string { + echo $x; + + return $x; + }, $s); +} + +/** + * @param callable(string): string $f + * @phpstan-pure + */ +function pureBothOpaqueCallbackOmittingCount(callable $f, string $s): string +{ + // $count is omitted but the callback's purity is unknown, so the call stays possibly impure. + return myReplaceCallback($f, $s); +} + +/** + * @param callable(string): string $f + * @phpstan-pure + */ +function pureBothOpaqueCallbackPassingCount(callable $f, string $s): string +{ + $count = 0; + // $count is passed, so the call is impure whatever the callback does. + return myReplaceCallback($f, $s, $count); +} diff --git a/turbo-ext/tests/type-family.php b/turbo-ext/tests/type-family.php index 5b7124deb50..b3c2d16ab39 100644 --- a/turbo-ext/tests/type-family.php +++ b/turbo-ext/tests/type-family.php @@ -6890,7 +6890,7 @@ public function getResolvedPhpDoc(): ?\PHPStan\PhpDoc\ResolvedPhpDocBlock { retu // arguments), the pure-unless-callable-is-impure parameters of array_filter() // and array_reduce() fed pure, impure, maybe-pure, null, non-callable and // omitted callbacks (positional and named), the pure-unless-parameter-passed -// out parameters of str_replace() and preg_match() passed and omitted +// out parameters of str_replace() and similar_text() passed and omitted // (positional, named and unpacked), both verdicts combined and applied to a // keyed list of certain and uncertain points, the transformed and plain // fixture methods, a missing scope or variant, and unconstructed instances @@ -6931,10 +6931,11 @@ public function getResolvedPhpDoc(): ?\PHPStan\PhpDoc\ResolvedPhpDocBlock { retu 'named count' => [$arg('string'), $arg('string'), $arg('string'), $arg('true', 'count')], 'named matches' => [$arg('string', 'pattern'), $arg('string', 'subject'), $arg('true', 'matches')], 'named subject only' => [$arg('string', 'pattern'), $arg('string', 'subject')], + 'named percent' => [$arg('string', 'string1'), $arg('string', 'string2'), $arg('true', 'percent')], 'unpacked' => [new \PhpParser\Node\Arg(new \PhpParser\Node\Expr\Variable('array'), unpack: true)], ]; $functions = []; - foreach (['strlen', 'usleep', 'print_r', 'var_export', 'highlight_string', 'array_filter', 'array_reduce', 'array_map', 'rand', 'str_replace', 'preg_match'] as $functionName) { + foreach (['strlen', 'usleep', 'print_r', 'var_export', 'highlight_string', 'array_filter', 'array_reduce', 'array_map', 'rand', 'str_replace', 'similar_text'] as $functionName) { $functions[$functionName] = $stringReflectionProvider->getFunction(new \PhpParser\Node\Name($functionName), null); } foreach ($functions as $functionName => $function) { From 492041ba76a064a8ca5ce5c68853cadfd7edfe03 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 3 Oct 2026 13:32:52 +0200 Subject: [PATCH 28/29] undo change --- src/Turbo/TurboExtensionEnabler.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Turbo/TurboExtensionEnabler.php b/src/Turbo/TurboExtensionEnabler.php index 4cb7087927e..cf6c410fad9 100644 --- a/src/Turbo/TurboExtensionEnabler.php +++ b/src/Turbo/TurboExtensionEnabler.php @@ -33,7 +33,7 @@ final class TurboExtensionEnabler { - public const EXPECTED_EXTENSION_VERSION = 'bee08d3'; + public const EXPECTED_EXTENSION_VERSION = '12424ee'; private static bool $active = false; From c8b500fc521dd1338e1988d0eb09938517cedec8 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 3 Oct 2026 13:36:11 +0200 Subject: [PATCH 29/29] Bump expected turbo version --- src/Turbo/TurboExtensionEnabler.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Turbo/TurboExtensionEnabler.php b/src/Turbo/TurboExtensionEnabler.php index cf6c410fad9..5784aba9831 100644 --- a/src/Turbo/TurboExtensionEnabler.php +++ b/src/Turbo/TurboExtensionEnabler.php @@ -33,7 +33,7 @@ final class TurboExtensionEnabler { - public const EXPECTED_EXTENSION_VERSION = '12424ee'; + public const EXPECTED_EXTENSION_VERSION = '9303d78'; private static bool $active = false;