diff --git a/bin/functionMetadata_original.php b/bin/functionMetadata_original.php index d429d11b59..6849445d94 100644 --- a/bin/functionMetadata_original.php +++ b/bin/functionMetadata_original.php @@ -14,20 +14,23 @@ 'array_diff' => ['hasSideEffects' => false], 'array_diff_assoc' => ['hasSideEffects' => false], 'array_diff_key' => ['hasSideEffects' => false], - 'array_diff_uassoc' => ['hasSideEffects' => false], - 'array_diff_ukey' => ['hasSideEffects' => false], + 'array_diff_uassoc' => ['pureUnlessCallableIsImpureParameters' => ['key_compare_func' => true]], + 'array_diff_ukey' => ['pureUnlessCallableIsImpureParameters' => ['key_comp_func' => true]], 'array_fill' => ['hasSideEffects' => false], 'array_fill_keys' => ['hasSideEffects' => false], + 'array_filter' => ['pureUnlessCallableIsImpureParameters' => ['callback' => true]], + 'array_find' => ['pureUnlessCallableIsImpureParameters' => ['callback' => true]], 'array_flip' => ['hasSideEffects' => false], 'array_intersect' => ['hasSideEffects' => false], 'array_intersect_assoc' => ['hasSideEffects' => false], 'array_intersect_key' => ['hasSideEffects' => false], - 'array_intersect_uassoc' => ['hasSideEffects' => false], - 'array_intersect_ukey' => ['hasSideEffects' => false], + 'array_intersect_uassoc' => ['pureUnlessCallableIsImpureParameters' => ['key_compare_func' => true]], + 'array_intersect_ukey' => ['pureUnlessCallableIsImpureParameters' => ['key_compare_func' => true]], 'array_key_first' => ['hasSideEffects' => false], 'array_key_last' => ['hasSideEffects' => false], 'array_key_exists' => ['hasSideEffects' => false], 'array_keys' => ['hasSideEffects' => false], + 'array_map' => ['pureUnlessCallableIsImpureParameters' => ['callback' => true]], 'array_merge' => ['hasSideEffects' => false], 'array_merge_recursive' => ['hasSideEffects' => false], 'array_pad' => ['hasSideEffects' => false], @@ -35,18 +38,19 @@ 'array_product' => ['hasSideEffects' => false], 'array_push' => ['hasSideEffects' => true], 'array_rand' => ['hasSideEffects' => false], + 'array_reduce' => ['pureUnlessCallableIsImpureParameters' => ['callback' => true]], 'array_replace' => ['hasSideEffects' => false], 'array_replace_recursive' => ['hasSideEffects' => false], 'array_reverse' => ['hasSideEffects' => false], 'array_shift' => ['hasSideEffects' => true], 'array_slice' => ['hasSideEffects' => false], 'array_sum' => ['hasSideEffects' => false], - 'array_udiff' => ['hasSideEffects' => false], - 'array_udiff_assoc' => ['hasSideEffects' => false], - 'array_udiff_uassoc' => ['hasSideEffects' => false], - 'array_uintersect' => ['hasSideEffects' => false], - 'array_uintersect_assoc' => ['hasSideEffects' => false], - 'array_uintersect_uassoc' => ['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_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_unique' => ['hasSideEffects' => false], 'array_unshift' => ['hasSideEffects' => true], 'array_values' => ['hasSideEffects' => false], @@ -64,6 +68,8 @@ 'bcdiv' => ['hasSideEffects' => false], 'bcmod' => ['hasSideEffects' => false], 'bcmul' => ['hasSideEffects' => false], + 'call_user_func' => ['pureUnlessCallableIsImpureParameters' => ['function' => true]], + 'call_user_func_array' => ['pureUnlessCallableIsImpureParameters' => ['function' => true]], // continue functionMap.php, line 424 'chgrp' => ['hasSideEffects' => true], 'chmod' => ['hasSideEffects' => true], @@ -80,6 +86,8 @@ 'file_put_contents' => ['hasSideEffects' => true], 'flock' => ['hasSideEffects' => true], 'fopen' => ['hasSideEffects' => true], + 'forward_static_call' => ['pureUnlessCallableIsImpureParameters' => ['function' => true]], + 'forward_static_call_array' => ['pureUnlessCallableIsImpureParameters' => ['function' => true]], 'fpassthru' => ['hasSideEffects' => true], 'fputcsv' => ['hasSideEffects' => true], 'fputs' => ['hasSideEffects' => true], @@ -109,6 +117,7 @@ 'output_reset_rewrite_vars' => ['hasSideEffects' => true], 'pclose' => ['hasSideEffects' => true], 'popen' => ['hasSideEffects' => true], + 'preg_replace_callback' => ['pureUnlessCallableIsImpureParameters' => ['callback' => true]], 'readfile' => ['hasSideEffects' => true], 'rename' => ['hasSideEffects' => true], 'rewind' => ['hasSideEffects' => true], diff --git a/bin/generate-function-metadata.php b/bin/generate-function-metadata.php index d161d374e4..776e5987ab 100755 --- a/bin/generate-function-metadata.php +++ b/bin/generate-function-metadata.php @@ -123,9 +123,17 @@ public function enterNode(Node $node) $metadata = require __DIR__ . '/functionMetadata_original.php'; foreach ($visitor->functions as $functionName) { if (array_key_exists($functionName, $metadata)) { - if ($metadata[$functionName]['hasSideEffects']) { + if (isset($metadata[$functionName]['hasSideEffects']) && $metadata[$functionName]['hasSideEffects']) { throw new ShouldNotHappenException($functionName); } + + if (isset($metadata[$functionName]['pureUnlessCallableIsImpureParameters'])) { + $metadata[$functionName] = [ + 'pureUnlessCallableIsImpureParameters' => $metadata[$functionName]['pureUnlessCallableIsImpureParameters'], + ]; + + continue; + } } $metadata[$functionName] = ['hasSideEffects' => false]; } @@ -184,12 +192,32 @@ 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'], + ), + ), + ), + ]; + foreach ($metadata as $name => $meta) { $content .= sprintf( "\t%s => [%s => %s],\n", var_export($name, true), - var_export('hasSideEffects', true), - var_export($meta['hasSideEffects'], true), + ...match (true) { + isset($meta['hasSideEffects']) => $encodeHasSideEffects($meta), + isset($meta['pureUnlessCallableIsImpureParameters']) => $encodePureUnlessCallableIsImpureParameters($meta), + default => throw new ShouldNotHappenException($escape($meta)), + }, ); } diff --git a/resources/functionMetadata.php b/resources/functionMetadata.php index 97f858cc49..052d9bab6e 100644 --- a/resources/functionMetadata.php +++ b/resources/functionMetadata.php @@ -720,21 +720,24 @@ 'array_diff' => ['hasSideEffects' => false], 'array_diff_assoc' => ['hasSideEffects' => false], 'array_diff_key' => ['hasSideEffects' => false], - 'array_diff_uassoc' => ['hasSideEffects' => false], - 'array_diff_ukey' => ['hasSideEffects' => false], + 'array_diff_uassoc' => ['pureUnlessCallableIsImpureParameters' => ['key_compare_func' => true]], + 'array_diff_ukey' => ['pureUnlessCallableIsImpureParameters' => ['key_comp_func' => true]], 'array_fill' => ['hasSideEffects' => false], 'array_fill_keys' => ['hasSideEffects' => false], + 'array_filter' => ['pureUnlessCallableIsImpureParameters' => ['callback' => true]], + 'array_find' => ['pureUnlessCallableIsImpureParameters' => ['callback' => true]], 'array_flip' => ['hasSideEffects' => false], 'array_intersect' => ['hasSideEffects' => false], 'array_intersect_assoc' => ['hasSideEffects' => false], 'array_intersect_key' => ['hasSideEffects' => false], - 'array_intersect_uassoc' => ['hasSideEffects' => false], - 'array_intersect_ukey' => ['hasSideEffects' => false], + 'array_intersect_uassoc' => ['pureUnlessCallableIsImpureParameters' => ['key_compare_func' => true]], + 'array_intersect_ukey' => ['pureUnlessCallableIsImpureParameters' => ['key_compare_func' => true]], 'array_is_list' => ['hasSideEffects' => false], 'array_key_exists' => ['hasSideEffects' => false], 'array_key_first' => ['hasSideEffects' => false], 'array_key_last' => ['hasSideEffects' => false], 'array_keys' => ['hasSideEffects' => false], + 'array_map' => ['pureUnlessCallableIsImpureParameters' => ['callback' => true]], 'array_merge' => ['hasSideEffects' => false], 'array_merge_recursive' => ['hasSideEffects' => false], 'array_pad' => ['hasSideEffects' => false], @@ -742,6 +745,7 @@ 'array_product' => ['hasSideEffects' => false], 'array_push' => ['hasSideEffects' => true], 'array_rand' => ['hasSideEffects' => false], + 'array_reduce' => ['pureUnlessCallableIsImpureParameters' => ['callback' => true]], 'array_replace' => ['hasSideEffects' => false], 'array_replace_recursive' => ['hasSideEffects' => false], 'array_reverse' => ['hasSideEffects' => false], @@ -749,12 +753,12 @@ 'array_shift' => ['hasSideEffects' => true], 'array_slice' => ['hasSideEffects' => false], 'array_sum' => ['hasSideEffects' => false], - 'array_udiff' => ['hasSideEffects' => false], - 'array_udiff_assoc' => ['hasSideEffects' => false], - 'array_udiff_uassoc' => ['hasSideEffects' => false], - 'array_uintersect' => ['hasSideEffects' => false], - 'array_uintersect_assoc' => ['hasSideEffects' => false], - 'array_uintersect_uassoc' => ['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_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_unique' => ['hasSideEffects' => false], 'array_unshift' => ['hasSideEffects' => true], 'array_values' => ['hasSideEffects' => false], @@ -785,6 +789,8 @@ 'bzerror' => ['hasSideEffects' => false], 'bzerrstr' => ['hasSideEffects' => false], 'bzopen' => ['hasSideEffects' => false], + 'call_user_func' => ['pureUnlessCallableIsImpureParameters' => ['function' => true]], + 'call_user_func_array' => ['pureUnlessCallableIsImpureParameters' => ['function' => true]], 'ceil' => ['hasSideEffects' => false], 'checkdate' => ['hasSideEffects' => false], 'checkdnsrr' => ['hasSideEffects' => false], @@ -936,6 +942,8 @@ 'fmod' => ['hasSideEffects' => false], 'fnmatch' => ['hasSideEffects' => true], 'fopen' => ['hasSideEffects' => true], + 'forward_static_call' => ['pureUnlessCallableIsImpureParameters' => ['function' => true]], + 'forward_static_call_array' => ['pureUnlessCallableIsImpureParameters' => ['function' => true]], 'fpassthru' => ['hasSideEffects' => true], 'fputcsv' => ['hasSideEffects' => true], 'fputs' => ['hasSideEffects' => true], @@ -1482,6 +1490,7 @@ 'preg_last_error' => ['hasSideEffects' => true], 'preg_last_error_msg' => ['hasSideEffects' => true], 'preg_quote' => ['hasSideEffects' => false], + 'preg_replace_callback' => ['pureUnlessCallableIsImpureParameters' => ['callback' => true]], 'preg_split' => ['hasSideEffects' => false], 'property_exists' => ['hasSideEffects' => false], 'quoted_printable_decode' => ['hasSideEffects' => false], @@ -1622,4 +1631,4 @@ 'zlib_encode' => ['hasSideEffects' => false], 'zlib_get_coding_type' => ['hasSideEffects' => false], -]; \ No newline at end of file +]; diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 62ed96b7f0..e6ab99e23c 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -3088,6 +3088,7 @@ public function enterTrait(ClassReflection $traitReflection): self * @param Type[] $parameterOutTypes * @param array $immediatelyInvokedCallableParameters * @param array $phpDocClosureThisTypeParameters + * @param array $phpDocPureUnlessCallableIsImpureParameters */ public function enterClassMethod( Node\Stmt\ClassMethod $classMethod, @@ -3108,6 +3109,7 @@ public function enterClassMethod( array $immediatelyInvokedCallableParameters = [], array $phpDocClosureThisTypeParameters = [], bool $isConstructor = false, + array $phpDocPureUnlessCallableIsImpureParameters = [], ): self { if (!$this->isInClass()) { @@ -3142,6 +3144,7 @@ public function enterClassMethod( array_map(fn (Type $type): Type => $this->transformStaticType(TemplateTypeHelper::toArgument($type)), $phpDocClosureThisTypeParameters), $isConstructor, $this->attributeReflectionFactory->fromAttrGroups($classMethod->attrGroups, InitializerExprContext::fromStubParameter($this->getClassReflection()->getName(), $this->getFile(), $classMethod)), + $phpDocPureUnlessCallableIsImpureParameters, ), !$classMethod->isStatic(), ); @@ -3229,6 +3232,7 @@ public function enterPropertyHook( [], false, $this->attributeReflectionFactory->fromAttrGroups($hook->attrGroups, InitializerExprContext::fromStubParameter($this->getClassReflection()->getName(), $this->getFile(), $hook)), + [], ), true, ); @@ -3319,6 +3323,7 @@ private function getParameterAttributes(ClassMethod|Function_|PropertyHook $func * @param Type[] $parameterOutTypes * @param array $immediatelyInvokedCallableParameters * @param array $phpDocClosureThisTypeParameters + * @param array $pureUnlessCallableIsImpureParameters */ public function enterFunction( Node\Stmt\Function_ $function, @@ -3336,6 +3341,7 @@ public function enterFunction( array $parameterOutTypes = [], array $immediatelyInvokedCallableParameters = [], array $phpDocClosureThisTypeParameters = [], + array $pureUnlessCallableIsImpureParameters = [], ): self { return $this->enterFunctionLike( @@ -3361,6 +3367,7 @@ public function enterFunction( $immediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, $this->attributeReflectionFactory->fromAttrGroups($function->attrGroups, InitializerExprContext::fromStubParameter(null, $this->getFile(), $function)), + $pureUnlessCallableIsImpureParameters, ), false, ); diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 667445d34d..d29d22fbb5 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -651,7 +651,7 @@ private function processStmtNode( $throwPoints = []; $impurePoints = []; $this->processAttributeGroups($stmt, $stmt->attrGroups, $scope, $nodeCallback); - [$templateTypeMap, $phpDocParameterTypes, $phpDocImmediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, , $phpDocComment, $asserts, $selfOutType, $phpDocParameterOutTypes] = $this->getPhpDocs($scope, $stmt); + [$templateTypeMap, $phpDocParameterTypes, $phpDocImmediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, , $phpDocComment, $asserts, $selfOutType, $phpDocParameterOutTypes, $pureUnlessCallableIsImpureParameters] = $this->getPhpDocs($scope, $stmt); foreach ($stmt->params as $param) { $this->processParamNode($stmt, $param, $scope, $nodeCallback); @@ -5107,6 +5107,26 @@ private function processArgs( } $parameter = $lastParameter; } + + if ($parameter instanceof ExtendedParameterReflection + && $parameter->isPureUnlessCallableIsImpureParameter() + && $parameterType !== null + && $parameterType->isTrue()->yes() + ) { + if (count($parameterType->getCallableParametersAcceptors($scope)) > 0 && $calleeReflection !== null) { + $parameterCallable = $parameterType->getCallableParametersAcceptors($scope)[0]; + $certain = $parameterCallable->isPure()->yes(); + if ($certain) { + $impurePoints[] = new ImpurePoint( + $scope, + $callLike, + 'functionCall', + sprintf('call to function %s()', $calleeReflection->getName()), + $certain, + ); + } + } + } } $lookForUnset = false; @@ -6664,7 +6684,7 @@ private function processNodesForCalledMethod($node, string $fileName, MethodRefl } /** - * @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} + * @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, array} */ public function getPhpDocs(Scope $scope, Node\FunctionLike|Node\Stmt\Property $node): array { @@ -6694,6 +6714,7 @@ public function getPhpDocs(Scope $scope, Node\FunctionLike|Node\Stmt\Property $n $resolvedPhpDoc = null; $functionName = null; $phpDocParameterOutTypes = []; + $phpDocPureUnlessCallableIsImpureParameters = []; if ($node instanceof Node\Stmt\ClassMethod) { if (!$scope->isInClass()) { @@ -6821,9 +6842,10 @@ public function getPhpDocs(Scope $scope, Node\FunctionLike|Node\Stmt\Property $n $asserts = Assertions::createFromResolvedPhpDocBlock($resolvedPhpDoc); $selfOutType = $resolvedPhpDoc->getSelfOutTag() !== null ? $resolvedPhpDoc->getSelfOutTag()->getType() : null; $varTags = $resolvedPhpDoc->getVarTags(); + $phpDocPureUnlessCallableIsImpureParameters = $resolvedPhpDoc->getParamsPureUnlessCallableIsImpure(); } - return [$templateTypeMap, $phpDocParameterTypes, $phpDocImmediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, $isReadOnly, $docComment, $asserts, $selfOutType, $phpDocParameterOutTypes, $varTags, $isAllowedPrivateMutation]; + return [$templateTypeMap, $phpDocParameterTypes, $phpDocImmediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, $isReadOnly, $docComment, $asserts, $selfOutType, $phpDocParameterOutTypes, $varTags, $isAllowedPrivateMutation, $phpDocPureUnlessCallableIsImpureParameters]; } private function transformStaticType(ClassReflection $declaringClass, Type $type): Type diff --git a/src/PhpDoc/PhpDocNodeResolver.php b/src/PhpDoc/PhpDocNodeResolver.php index ff91a44225..134c9c5fbc 100644 --- a/src/PhpDoc/PhpDocNodeResolver.php +++ b/src/PhpDoc/PhpDocNodeResolver.php @@ -419,6 +419,19 @@ public function resolveParamImmediatelyInvokedCallable(PhpDocNode $phpDocNode): return $parameters; } + /** + * @return array + */ + public function resolveParamPureUnlessCallableIsImpure(PhpDocNode $phpDocNode): array + { + $parameters = []; + foreach ($phpDocNode->getPureUnlessCallableIsImpureTagValues() as $tag) { + $parameters[$tag->parameterName] = true; + } + + return $parameters; + } + /** * @return array */ diff --git a/src/PhpDoc/ResolvedPhpDocBlock.php b/src/PhpDoc/ResolvedPhpDocBlock.php index dc43e9af5e..a01401f32a 100644 --- a/src/PhpDoc/ResolvedPhpDocBlock.php +++ b/src/PhpDoc/ResolvedPhpDocBlock.php @@ -96,6 +96,9 @@ final class ResolvedPhpDocBlock /** @var array|false */ private array|false $paramsImmediatelyInvokedCallable = false; + /** @var array|false */ + private array|false $paramsPureUnlessCallableIsImpure = false; + /** @var array|false */ private array|false $paramClosureThisTags = false; @@ -216,6 +219,7 @@ public static function createEmpty(): self $self->paramTags = []; $self->paramOutTags = []; $self->paramsImmediatelyInvokedCallable = []; + $self->paramsPureUnlessCallableIsImpure = []; $self->paramClosureThisTags = []; $self->returnTag = null; $self->throwsTag = null; @@ -281,6 +285,7 @@ public function merge(array $parents, array $parentPhpDocBlocks): self $result->paramTags = self::mergeParamTags($this->getParamTags(), $parents, $parentPhpDocBlocks); $result->paramOutTags = self::mergeParamOutTags($this->getParamOutTags(), $parents, $parentPhpDocBlocks); $result->paramsImmediatelyInvokedCallable = self::mergeParamsImmediatelyInvokedCallable($this->getParamsImmediatelyInvokedCallable(), $parents, $parentPhpDocBlocks); + $result->paramsPureUnlessCallableIsImpure = self::mergeParamsPureUnlessCallableIsImpure($this->getParamsPureUnlessCallableIsImpure(), $parents, $parentPhpDocBlocks); $result->paramClosureThisTags = self::mergeParamClosureThisTags($this->getParamClosureThisTags(), $parents, $parentPhpDocBlocks); $result->returnTag = self::mergeReturnTags($this->getReturnTag(), $classReflection, $parents, $parentPhpDocBlocks); $result->throwsTag = self::mergeThrowsTags($this->getThrowsTag(), $parents); @@ -587,6 +592,18 @@ public function getParamsImmediatelyInvokedCallable(): array return $this->paramsImmediatelyInvokedCallable; } + /** + * @return array + */ + public function getParamsPureUnlessCallableIsImpure(): array + { + if ($this->paramsPureUnlessCallableIsImpure === false) { + $this->paramsPureUnlessCallableIsImpure = $this->phpDocNodeResolver->resolveParamPureUnlessCallableIsImpure($this->phpDocNode); + } + + return $this->paramsPureUnlessCallableIsImpure; + } + /** * @return array */ @@ -1154,6 +1171,40 @@ private static function mergeOneParentParamImmediatelyInvokedCallable(array $par return $paramsImmediatelyInvokedCallable; } + /** + * @param array $paramsPureUnlessCallableIsImpure + * @param array $parents + * @param array $parentPhpDocBlocks + * @return array + */ + private static function mergeParamsPureUnlessCallableIsImpure(array $paramsPureUnlessCallableIsImpure, array $parents, array $parentPhpDocBlocks): array + { + foreach ($parents as $i => $parent) { + $paramsPureUnlessCallableIsImpure = self::mergeOneParentParamPureUnlessCallableIsImpure($paramsPureUnlessCallableIsImpure, $parent, $parentPhpDocBlocks[$i]); + } + + return $paramsPureUnlessCallableIsImpure; + } + + /** + * @param array $paramsPureUnlessCallableIsImpure + * @return array + */ + private static function mergeOneParentParamPureUnlessCallableIsImpure(array $paramsPureUnlessCallableIsImpure, self $parent, PhpDocBlock $phpDocBlock): array + { + $parentPureUnlessCallableIsImpure = $phpDocBlock->transformArrayKeysWithParameterNameMapping($parent->getParamsPureUnlessCallableIsImpure()); + + foreach ($parentPureUnlessCallableIsImpure as $name => $parentIsPureUnlessCallableIsImpure) { + if (array_key_exists($name, $paramsPureUnlessCallableIsImpure)) { + continue; + } + + $paramsPureUnlessCallableIsImpure[$name] = $parentIsPureUnlessCallableIsImpure; + } + + return $paramsPureUnlessCallableIsImpure; + } + /** * @param array $paramsClosureThisTags * @param array $parents diff --git a/src/Reflection/Annotations/AnnotationMethodReflection.php b/src/Reflection/Annotations/AnnotationMethodReflection.php index 9b1886b544..f7ed70acce 100644 --- a/src/Reflection/Annotations/AnnotationMethodReflection.php +++ b/src/Reflection/Annotations/AnnotationMethodReflection.php @@ -178,6 +178,11 @@ public function isPure(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function getPureUnlessCallableIsImpureParameters(): array + { + return []; + } + public function getAttributes(): array { return []; diff --git a/src/Reflection/Annotations/AnnotationsMethodParameterReflection.php b/src/Reflection/Annotations/AnnotationsMethodParameterReflection.php index b01a6db6ff..e9ff4112e8 100644 --- a/src/Reflection/Annotations/AnnotationsMethodParameterReflection.php +++ b/src/Reflection/Annotations/AnnotationsMethodParameterReflection.php @@ -80,4 +80,9 @@ public function getAttributes(): array return []; } + public function isPureUnlessCallableIsImpureParameter(): bool + { + return false; + } + } diff --git a/src/Reflection/Dummy/ChangedTypeMethodReflection.php b/src/Reflection/Dummy/ChangedTypeMethodReflection.php index ade4e1c4aa..c0b3e8e123 100644 --- a/src/Reflection/Dummy/ChangedTypeMethodReflection.php +++ b/src/Reflection/Dummy/ChangedTypeMethodReflection.php @@ -165,6 +165,11 @@ public function isPure(): TrinaryLogic return $this->reflection->isPure(); } + public function getPureUnlessCallableIsImpureParameters(): array + { + return []; + } + public function getAttributes(): array { return $this->reflection->getAttributes(); diff --git a/src/Reflection/Dummy/DummyConstructorReflection.php b/src/Reflection/Dummy/DummyConstructorReflection.php index c48d6904ce..580c1824fb 100644 --- a/src/Reflection/Dummy/DummyConstructorReflection.php +++ b/src/Reflection/Dummy/DummyConstructorReflection.php @@ -152,6 +152,11 @@ public function isPure(): TrinaryLogic return TrinaryLogic::createYes(); } + public function getPureUnlessCallableIsImpureParameters(): array + { + return []; + } + public function getAttributes(): array { return []; diff --git a/src/Reflection/Dummy/DummyMethodReflection.php b/src/Reflection/Dummy/DummyMethodReflection.php index dced9b6206..23074a2718 100644 --- a/src/Reflection/Dummy/DummyMethodReflection.php +++ b/src/Reflection/Dummy/DummyMethodReflection.php @@ -144,6 +144,11 @@ public function isPure(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function getPureUnlessCallableIsImpureParameters(): array + { + return []; + } + public function getAttributes(): array { return []; diff --git a/src/Reflection/ExtendedMethodReflection.php b/src/Reflection/ExtendedMethodReflection.php index 5cea392754..97a2f5e57b 100644 --- a/src/Reflection/ExtendedMethodReflection.php +++ b/src/Reflection/ExtendedMethodReflection.php @@ -60,6 +60,11 @@ public function isBuiltin(): TrinaryLogic|bool; */ public function isPure(): TrinaryLogic; + /** + * @return array + */ + public function getPureUnlessCallableIsImpureParameters(): array; + /** * @return list */ diff --git a/src/Reflection/ExtendedParameterReflection.php b/src/Reflection/ExtendedParameterReflection.php index ab50b76bd8..ac33a7e0f6 100644 --- a/src/Reflection/ExtendedParameterReflection.php +++ b/src/Reflection/ExtendedParameterReflection.php @@ -26,4 +26,6 @@ public function getClosureThisType(): ?Type; */ public function getAttributes(): array; + public function isPureUnlessCallableIsImpureParameter(): bool; + } diff --git a/src/Reflection/FunctionReflection.php b/src/Reflection/FunctionReflection.php index 297e4dd7d3..cea032e668 100644 --- a/src/Reflection/FunctionReflection.php +++ b/src/Reflection/FunctionReflection.php @@ -57,6 +57,11 @@ public function returnsByReference(): TrinaryLogic; */ public function isPure(): TrinaryLogic; + /** + * @return array + */ + public function getPureUnlessCallableIsImpureParameters(): array; + /** * @return list */ diff --git a/src/Reflection/Native/ExtendedNativeParameterReflection.php b/src/Reflection/Native/ExtendedNativeParameterReflection.php index 00e2ea1a99..8a5a7fe03c 100644 --- a/src/Reflection/Native/ExtendedNativeParameterReflection.php +++ b/src/Reflection/Native/ExtendedNativeParameterReflection.php @@ -28,6 +28,7 @@ public function __construct( private TrinaryLogic $immediatelyInvokedCallable, private ?Type $closureThisType, private array $attributes, + private bool $pureUnlessCallableIsImpureParameter = false, ) { } @@ -97,4 +98,9 @@ public function getAttributes(): array return $this->attributes; } + public function isPureUnlessCallableIsImpureParameter(): bool + { + return $this->pureUnlessCallableIsImpureParameter; + } + } diff --git a/src/Reflection/Native/NativeFunctionReflection.php b/src/Reflection/Native/NativeFunctionReflection.php index 7668d51f9e..5980a4d888 100644 --- a/src/Reflection/Native/NativeFunctionReflection.php +++ b/src/Reflection/Native/NativeFunctionReflection.php @@ -109,6 +109,11 @@ public function isPure(): TrinaryLogic return $this->hasSideEffects->negate(); } + public function getPureUnlessCallableIsImpureParameters(): 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 91fc46229d..7dd8f505dd 100644 --- a/src/Reflection/Native/NativeMethodReflection.php +++ b/src/Reflection/Native/NativeMethodReflection.php @@ -186,6 +186,11 @@ public function isPure(): TrinaryLogic return $this->hasSideEffects->negate(); } + public function getPureUnlessCallableIsImpureParameters(): 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 aafd7b658e..a94ac40b1a 100644 --- a/src/Reflection/Php/ClosureCallMethodReflection.php +++ b/src/Reflection/Php/ClosureCallMethodReflection.php @@ -197,6 +197,11 @@ public function isPure(): TrinaryLogic return $this->nativeMethodReflection->isPure(); } + public function getPureUnlessCallableIsImpureParameters(): array + { + return []; + } + public function getAttributes(): array { return $this->nativeMethodReflection->getAttributes(); diff --git a/src/Reflection/Php/EnumCasesMethodReflection.php b/src/Reflection/Php/EnumCasesMethodReflection.php index ecf72e435d..153a991a7d 100644 --- a/src/Reflection/Php/EnumCasesMethodReflection.php +++ b/src/Reflection/Php/EnumCasesMethodReflection.php @@ -156,6 +156,11 @@ public function isPure(): TrinaryLogic return TrinaryLogic::createYes(); } + public function getPureUnlessCallableIsImpureParameters(): array + { + return []; + } + public function getAttributes(): array { return []; diff --git a/src/Reflection/Php/ExitFunctionReflection.php b/src/Reflection/Php/ExitFunctionReflection.php index 4020bbdc09..ecba44699a 100644 --- a/src/Reflection/Php/ExitFunctionReflection.php +++ b/src/Reflection/Php/ExitFunctionReflection.php @@ -138,6 +138,11 @@ public function isPure(): TrinaryLogic return TrinaryLogic::createNo(); } + public function getPureUnlessCallableIsImpureParameters(): array + { + return []; + } + public function getAttributes(): array { return []; diff --git a/src/Reflection/Php/ExtendedDummyParameter.php b/src/Reflection/Php/ExtendedDummyParameter.php index 19a917e0a1..806149e9ad 100644 --- a/src/Reflection/Php/ExtendedDummyParameter.php +++ b/src/Reflection/Php/ExtendedDummyParameter.php @@ -28,6 +28,7 @@ public function __construct( private TrinaryLogic $immediatelyInvokedCallable, private ?Type $closureThisType, private array $attributes, + private bool $pureUnlessCallableIsImpureParameter = false, ) { parent::__construct($name, $type, $optional, $passedByReference, $variadic, $defaultValue); @@ -68,4 +69,9 @@ public function getAttributes(): array return $this->attributes; } + public function isPureUnlessCallableIsImpureParameter(): bool + { + return $this->pureUnlessCallableIsImpureParameter; + } + } diff --git a/src/Reflection/Php/PhpClassReflectionExtension.php b/src/Reflection/Php/PhpClassReflectionExtension.php index 939e1550d3..35d64701d4 100644 --- a/src/Reflection/Php/PhpClassReflectionExtension.php +++ b/src/Reflection/Php/PhpClassReflectionExtension.php @@ -678,7 +678,7 @@ private function createMethod( } if ($this->signatureMapProvider->hasMethodMetadata($declaringClassName, $methodReflection->getName())) { - $hasSideEffects = TrinaryLogic::createFromBoolean($this->signatureMapProvider->getMethodMetadata($declaringClassName, $methodReflection->getName())['hasSideEffects']); + $hasSideEffects = TrinaryLogic::createFromBoolean($this->signatureMapProvider->getMethodMetadata($declaringClassName, $methodReflection->getName())['hasSideEffects'] ?? true); } else { $hasSideEffects = TrinaryLogic::createMaybe(); } @@ -840,10 +840,13 @@ public function createUserlandMethodReflection(ClassReflection $fileDeclaringCla $isInternal = $resolvedPhpDoc->isInternal(); $isFinal = $resolvedPhpDoc->isFinal(); $isPure = null; + $pureUnlessCallableIsImpureParameters = []; foreach ($actualDeclaringClass->getAncestors() as $className => $ancestor) { if ($this->signatureMapProvider->hasMethodMetadata($className, $methodReflection->getName())) { - $hasSideEffects = $this->signatureMapProvider->getMethodMetadata($className, $methodReflection->getName())['hasSideEffects']; + $methodMetadata = $this->signatureMapProvider->getMethodMetadata($className, $methodReflection->getName()); + $hasSideEffects = $methodMetadata['hasSideEffects'] ?? true; $isPure = !$hasSideEffects; + $pureUnlessCallableIsImpureParameters += $methodMetadata['pureUnlessCallableIsImpureParameters'] ?? []; break; } @@ -879,6 +882,7 @@ public function createUserlandMethodReflection(ClassReflection $fileDeclaringCla $closureThisParameters, $acceptsNamedArguments, $this->attributeReflectionFactory->fromNativeReflection($methodReflection->getAttributes(), InitializerExprContext::fromClassMethod($actualDeclaringClass->getName(), $declaringTraitName, $methodReflection->getName(), $actualDeclaringClass->getFileName())), + $pureUnlessCallableIsImpureParameters, ); } @@ -1071,7 +1075,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] = $this->nodeScopeResolver->getPhpDocs($classScope, $methodNode); + [$templateTypeMap, $phpDocParameterTypes, $phpDocImmediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, , $phpDocComment, $asserts, $selfOutType, $phpDocParameterOutTypes, $varTags, $isAllowedPrivateMutation, $phpDocPureUnlessCallableIsImpureParameters] = $this->nodeScopeResolver->getPhpDocs($classScope, $methodNode); $methodScope = $classScope->enterClassMethod( $methodNode, $templateTypeMap, @@ -1090,6 +1094,8 @@ private function inferAndCachePropertyTypes( $phpDocParameterOutTypes, $phpDocImmediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, + false, + $phpDocPureUnlessCallableIsImpureParameters, ); $propertyTypes = []; diff --git a/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php b/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php index d49cb57f4a..78083aef76 100644 --- a/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php +++ b/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php @@ -47,6 +47,7 @@ class PhpFunctionFromParserNodeReflection implements FunctionReflection, Extende * @param array $immediatelyInvokedCallableParameters * @param array $phpDocClosureThisTypeParameters * @param list $attributes + * @param array $pureUnlessCallableIsImpureParameters */ public function __construct( FunctionLike $functionLike, @@ -70,6 +71,7 @@ public function __construct( private array $immediatelyInvokedCallableParameters, private array $phpDocClosureThisTypeParameters, private array $attributes, + private array $pureUnlessCallableIsImpureParameters, ) { $this->functionLike = $functionLike; @@ -168,6 +170,12 @@ public function getParameters(): array $closureThisType = null; } + if (isset($this->pureUnlessCallableIsImpureParameters[$parameter->var->name])) { + $pureUnlessCallableIsImpureParameter = $this->pureUnlessCallableIsImpureParameters[$parameter->var->name]; + } else { + $pureUnlessCallableIsImpureParameter = false; + } + $parameters[] = new PhpParameterFromParserNodeReflection( $parameter->var->name, $isOptional, @@ -182,6 +190,7 @@ public function getParameters(): array $immediatelyInvokedCallable, $closureThisType, $this->parameterAttributes[$parameter->var->name] ?? [], + $pureUnlessCallableIsImpureParameter, ); } @@ -325,6 +334,11 @@ public function isPure(): TrinaryLogic return TrinaryLogic::createFromBoolean($this->isPure); } + public function getPureUnlessCallableIsImpureParameters(): array + { + return []; + } + public function getAttributes(): array { return $this->attributes; diff --git a/src/Reflection/Php/PhpFunctionReflection.php b/src/Reflection/Php/PhpFunctionReflection.php index 38604e8761..c0f4ad7eaa 100644 --- a/src/Reflection/Php/PhpFunctionReflection.php +++ b/src/Reflection/Php/PhpFunctionReflection.php @@ -245,6 +245,11 @@ public function isPure(): TrinaryLogic return TrinaryLogic::createFromBoolean($this->isPure); } + public function getPureUnlessCallableIsImpureParameters(): array + { + return []; + } + public function isBuiltin(): bool { return $this->reflection->isInternal(); diff --git a/src/Reflection/Php/PhpMethodFromParserNodeReflection.php b/src/Reflection/Php/PhpMethodFromParserNodeReflection.php index cd0e6fcbf6..56494b37e8 100644 --- a/src/Reflection/Php/PhpMethodFromParserNodeReflection.php +++ b/src/Reflection/Php/PhpMethodFromParserNodeReflection.php @@ -68,6 +68,7 @@ public function __construct( array $phpDocClosureThisTypeParameters, private bool $isConstructor, array $attributes, + array $pureUnlessCallableIsImpureParameters, ) { if ($this->classMethod instanceof Node\PropertyHook) { @@ -136,6 +137,7 @@ public function __construct( $immediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, $attributes, + $pureUnlessCallableIsImpureParameters, ); } diff --git a/src/Reflection/Php/PhpMethodReflection.php b/src/Reflection/Php/PhpMethodReflection.php index 22982ca1f3..3ceb3e25ea 100644 --- a/src/Reflection/Php/PhpMethodReflection.php +++ b/src/Reflection/Php/PhpMethodReflection.php @@ -97,6 +97,7 @@ public function __construct( private array $immediatelyInvokedCallableParameters, private array $phpDocClosureThisTypeParameters, private array $attributes, + private array $pureUnlessCallableIsImpureParameters, ) { } @@ -452,6 +453,11 @@ public function isPure(): TrinaryLogic return TrinaryLogic::createFromBoolean($this->isPure); } + public function getPureUnlessCallableIsImpureParameters(): array + { + return $this->pureUnlessCallableIsImpureParameters; + } + public function changePropertyGetHookPhpDocType(Type $phpDocType): self { return new self( @@ -479,6 +485,7 @@ public function changePropertyGetHookPhpDocType(Type $phpDocType): self $this->immediatelyInvokedCallableParameters, $this->phpDocClosureThisTypeParameters, $this->attributes, + $this->pureUnlessCallableIsImpureParameters, ); } @@ -512,6 +519,7 @@ public function changePropertySetHookPhpDocType(string $parameterName, Type $php $this->immediatelyInvokedCallableParameters, $this->phpDocClosureThisTypeParameters, $this->attributes, + $this->pureUnlessCallableIsImpureParameters, ); } diff --git a/src/Reflection/Php/PhpMethodReflectionFactory.php b/src/Reflection/Php/PhpMethodReflectionFactory.php index ec95a2de81..1203525647 100644 --- a/src/Reflection/Php/PhpMethodReflectionFactory.php +++ b/src/Reflection/Php/PhpMethodReflectionFactory.php @@ -19,6 +19,7 @@ interface PhpMethodReflectionFactory * @param array $immediatelyInvokedCallableParameters * @param array $phpDocClosureThisTypeParameters * @param list $attributes + * @param array $pureUnlessCallableIsImpureParameters */ public function create( ClassReflection $declaringClass, @@ -41,6 +42,7 @@ public function create( array $phpDocClosureThisTypeParameters, bool $acceptsNamedArguments, array $attributes, + array $pureUnlessCallableIsImpureParameters, ): PhpMethodReflection; } diff --git a/src/Reflection/Php/PhpParameterFromParserNodeReflection.php b/src/Reflection/Php/PhpParameterFromParserNodeReflection.php index f048ea7100..0dd8fccb1f 100644 --- a/src/Reflection/Php/PhpParameterFromParserNodeReflection.php +++ b/src/Reflection/Php/PhpParameterFromParserNodeReflection.php @@ -31,6 +31,7 @@ public function __construct( private TrinaryLogic $immediatelyInvokedCallable, private ?Type $closureThisType, private array $attributes, + private bool $pureUnlessCallableIsImpureParameter, ) { } @@ -113,4 +114,9 @@ public function getAttributes(): array return $this->attributes; } + public function isPureUnlessCallableIsImpureParameter(): bool + { + return $this->pureUnlessCallableIsImpureParameter; + } + } diff --git a/src/Reflection/Php/PhpParameterReflection.php b/src/Reflection/Php/PhpParameterReflection.php index 8469f7bef4..18ba178d21 100644 --- a/src/Reflection/Php/PhpParameterReflection.php +++ b/src/Reflection/Php/PhpParameterReflection.php @@ -34,6 +34,7 @@ public function __construct( private TrinaryLogic $immediatelyInvokedCallable, private ?Type $closureThisType, private array $attributes, + private bool $pureUnlessCallableIsImpureParameter = false, ) { } @@ -143,4 +144,9 @@ public function getAttributes(): array return $this->attributes; } + public function isPureUnlessCallableIsImpureParameter(): bool + { + return $this->pureUnlessCallableIsImpureParameter; + } + } diff --git a/src/Reflection/ResolvedMethodReflection.php b/src/Reflection/ResolvedMethodReflection.php index 134e566eca..4a512f5a71 100644 --- a/src/Reflection/ResolvedMethodReflection.php +++ b/src/Reflection/ResolvedMethodReflection.php @@ -175,6 +175,11 @@ public function isPure(): TrinaryLogic return $this->reflection->isPure(); } + public function getPureUnlessCallableIsImpureParameters(): array + { + return $this->reflection->getPureUnlessCallableIsImpureParameters(); + } + public function getAsserts(): Assertions { return $this->asserts ??= $this->reflection->getAsserts()->mapTypes(fn (Type $type) => TemplateTypeHelper::resolveTemplateTypes( diff --git a/src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php b/src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php index 202b74ddfc..a46dcc23b0 100644 --- a/src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php +++ b/src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php @@ -103,13 +103,24 @@ public function findFunctionReflection(string $functionName): ?NativeFunctionRef $acceptsNamedArguments = $phpDoc->acceptsNamedArguments(); } + $pureUnlessCallableIsImpureParameters = []; + if ($this->signatureMapProvider->hasFunctionMetadata($lowerCasedFunctionName)) { + $functionMetadata = $this->signatureMapProvider->getFunctionMetadata($lowerCasedFunctionName); + if (isset($functionMetadata['pureUnlessCallableIsImpureParameters'])) { + $pureUnlessCallableIsImpureParameters = $functionMetadata['pureUnlessCallableIsImpureParameters']; + } + } else { + $functionMetadata = null; + } + $variantsByType = ['positional' => []]; foreach ($functionSignaturesResult as $signatureType => $functionSignatures) { foreach ($functionSignatures ?? [] as $functionSignature) { $variantsByType[$signatureType][] = new ExtendedFunctionVariant( TemplateTypeMap::createEmpty(), null, - array_map(static function (ParameterSignature $parameterSignature) use ($phpDoc): ExtendedNativeParameterReflection { + array_map(static function (ParameterSignature $parameterSignature) use ($phpDoc, $pureUnlessCallableIsImpureParameters): ExtendedNativeParameterReflection { + $name = $parameterSignature->getName(); $type = $parameterSignature->getType(); $phpDocType = null; @@ -140,6 +151,7 @@ public function findFunctionReflection(string $functionName): ?NativeFunctionRef $immediatelyInvokedCallable, $closureThisType, [], + isset($pureUnlessCallableIsImpureParameters[$name]) && $pureUnlessCallableIsImpureParameters[$name], ); }, $functionSignature->getParameters()), $functionSignature->isVariadic(), @@ -150,8 +162,8 @@ public function findFunctionReflection(string $functionName): ?NativeFunctionRef } } - if ($this->signatureMapProvider->hasFunctionMetadata($lowerCasedFunctionName)) { - $hasSideEffects = TrinaryLogic::createFromBoolean($this->signatureMapProvider->getFunctionMetadata($lowerCasedFunctionName)['hasSideEffects']); + if (isset($functionMetadata['hasSideEffects'])) { + $hasSideEffects = TrinaryLogic::createFromBoolean($functionMetadata['hasSideEffects']); } else { $hasSideEffects = TrinaryLogic::createMaybe(); } diff --git a/src/Reflection/SignatureMap/SignatureMapProvider.php b/src/Reflection/SignatureMap/SignatureMapProvider.php index 3999d919b8..8afcf9b1b1 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} + * @return array{hasSideEffects?: bool, pureUnlessCallableIsImpureParameters?: array} */ public function getMethodMetadata(string $className, string $methodName): array; /** - * @return array{hasSideEffects: bool} + * @return array{hasSideEffects?: bool, pureUnlessCallableIsImpureParameters?: array} */ public function getFunctionMetadata(string $functionName): array; diff --git a/src/Reflection/Type/IntersectionTypeMethodReflection.php b/src/Reflection/Type/IntersectionTypeMethodReflection.php index eafd314157..7513273834 100644 --- a/src/Reflection/Type/IntersectionTypeMethodReflection.php +++ b/src/Reflection/Type/IntersectionTypeMethodReflection.php @@ -187,6 +187,11 @@ public function isPure(): TrinaryLogic return TrinaryLogic::lazyMaxMin($this->methods, static fn (ExtendedMethodReflection $method): TrinaryLogic => $method->isPure()); } + public function getPureUnlessCallableIsImpureParameters(): array + { + return []; + } + public function getDocComment(): ?string { return null; diff --git a/src/Reflection/Type/UnionTypeMethodReflection.php b/src/Reflection/Type/UnionTypeMethodReflection.php index b330b6fdad..1783d3843d 100644 --- a/src/Reflection/Type/UnionTypeMethodReflection.php +++ b/src/Reflection/Type/UnionTypeMethodReflection.php @@ -170,6 +170,11 @@ public function isPure(): TrinaryLogic return TrinaryLogic::lazyExtremeIdentity($this->methods, static fn (ExtendedMethodReflection $method): TrinaryLogic => $method->isPure()); } + public function getPureUnlessCallableIsImpureParameters(): array + { + return []; + } + public function getDocComment(): ?string { return null; diff --git a/src/Reflection/WrappedExtendedMethodReflection.php b/src/Reflection/WrappedExtendedMethodReflection.php index 5a9ea238cf..9994bf8346 100644 --- a/src/Reflection/WrappedExtendedMethodReflection.php +++ b/src/Reflection/WrappedExtendedMethodReflection.php @@ -143,6 +143,11 @@ public function isPure(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function getPureUnlessCallableIsImpureParameters(): array + { + return []; + } + public function getAsserts(): Assertions { return Assertions::createEmpty(); diff --git a/src/Rules/RestrictedUsage/RewrittenDeclaringClassMethodReflection.php b/src/Rules/RestrictedUsage/RewrittenDeclaringClassMethodReflection.php index a2575177a5..cfc0b331b8 100644 --- a/src/Rules/RestrictedUsage/RewrittenDeclaringClassMethodReflection.php +++ b/src/Rules/RestrictedUsage/RewrittenDeclaringClassMethodReflection.php @@ -100,6 +100,11 @@ public function isPure(): TrinaryLogic return $this->methodReflection->isPure(); } + public function getPureUnlessCallableIsImpureParameters(): array + { + return $this->methodReflection->getPureUnlessCallableIsImpureParameters(); + } + public function getAttributes(): array { return $this->methodReflection->getAttributes(); diff --git a/tests/PHPStan/Analyser/TestClosureTypeRuleTest.php b/tests/PHPStan/Analyser/TestClosureTypeRuleTest.php index aebfdc606f..a095b96ac4 100644 --- a/tests/PHPStan/Analyser/TestClosureTypeRuleTest.php +++ b/tests/PHPStan/Analyser/TestClosureTypeRuleTest.php @@ -21,11 +21,11 @@ public function testRule(): void $this->analyse([__DIR__ . '/nsrt/closure-passed-to-type.php'], [ [ 'Closure type: Closure(mixed): (1|2|3)', - 25, + 26, ], [ 'Closure type: Closure(mixed): (1|2|3)', - 35, + 36, ], ]); } diff --git a/tests/PHPStan/Analyser/nsrt/closure-passed-to-type.php b/tests/PHPStan/Analyser/nsrt/closure-passed-to-type.php index a34ded1591..f3fa9795d2 100644 --- a/tests/PHPStan/Analyser/nsrt/closure-passed-to-type.php +++ b/tests/PHPStan/Analyser/nsrt/closure-passed-to-type.php @@ -13,6 +13,7 @@ class Foo * @param array $items * @param callable(T): U $cb * @return array + * @pure-unless-callable-impure $cb */ public function doFoo(array $items, callable $cb) { diff --git a/tests/PHPStan/Analyser/nsrt/param-pure-unless-callable-is-impure.php b/tests/PHPStan/Analyser/nsrt/param-pure-unless-callable-is-impure.php new file mode 100644 index 0000000000..4ddd321282 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/param-pure-unless-callable-is-impure.php @@ -0,0 +1,28 @@ + $a + * @return array + * @pure-unless-callable-is-impure $f + */ +function map(Closure $f, iterable $a): array +{ + $result = []; + foreach ($a as $i => $v) { + $retult[$i] = $f($v); + } + + return $result; +} + +map('printf', []); +map('sprintf', []); + +assertType('array', map('printf', [])); diff --git a/tests/PHPStan/Reflection/SignatureMap/FunctionMetadataTest.php b/tests/PHPStan/Reflection/SignatureMap/FunctionMetadataTest.php index 24ef8431ee..365e3d4975 100644 --- a/tests/PHPStan/Reflection/SignatureMap/FunctionMetadataTest.php +++ b/tests/PHPStan/Reflection/SignatureMap/FunctionMetadataTest.php @@ -5,6 +5,7 @@ use Nette\Schema\Expect; use Nette\Schema\Processor; use PHPStan\Testing\PHPStanTestCase; +use function count; class FunctionMetadataTest extends PHPStanTestCase { @@ -17,8 +18,11 @@ public function testSchema(): void $processor = new Processor(); $processor->process(Expect::arrayOf( Expect::structure([ - 'hasSideEffects' => Expect::bool()->required(), - ])->required(), + 'hasSideEffects' => Expect::bool(), + 'pureUnlessCallableIsImpureParameters' => Expect::arrayOf(Expect::bool(), Expect::string()), + ]) + ->assert(static fn ($v) => count((array) $v) > 0, 'Metadata entries must not be empty.') + ->required(), )->required(), $data); }