From f262eaa213e1aa9f07e43b98d4d67c828c5db122 Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Thu, 19 Sep 2024 02:18:34 +0900 Subject: [PATCH 01/10] wip --- src/PhpDoc/PhpDocNodeResolver.php | 19 +++++++ src/PhpDoc/ResolvedPhpDocBlock.php | 51 +++++++++++++++++++ .../Analyser/TestClosureTypeRuleTest.php | 4 +- .../Analyser/nsrt/closure-passed-to-type.php | 1 + .../param-pure-unless-callable-is-impure.php | 28 ++++++++++ 5 files changed, 101 insertions(+), 2 deletions(-) create mode 100644 tests/PHPStan/Analyser/nsrt/param-pure-unless-callable-is-impure.php diff --git a/src/PhpDoc/PhpDocNodeResolver.php b/src/PhpDoc/PhpDocNodeResolver.php index ff91a44225..dc6927d486 100644 --- a/src/PhpDoc/PhpDocNodeResolver.php +++ b/src/PhpDoc/PhpDocNodeResolver.php @@ -48,6 +48,7 @@ use function count; use function in_array; use function method_exists; +use function preg_split; use function str_starts_with; use function substr; @@ -419,6 +420,24 @@ public function resolveParamImmediatelyInvokedCallable(PhpDocNode $phpDocNode): return $parameters; } + /** + * @return array + */ + public function resolveParamPureUnlessCallableIsImpure(PhpDocNode $phpDocNode): array + { + $parameters = []; + // TODO: implement phpstan/phpdoc-parser + foreach ($phpDocNode->getTagsByName('@pure-unless-callable-impure') as $tag) { + $value = preg_split('/\s/u', (string)$tag->value)[0] ?? null; + if ($value !== null && str_starts_with($value, '$')) { + $parameters[substr($value, 1)] = 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/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', [])); From a437d7d870c1c8dcd0b1c1422bf81bebfa2da606 Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Thu, 19 Sep 2024 21:43:22 +0900 Subject: [PATCH 02/10] Add pureUnlessCallableIsImpureParameters --- src/Analyser/MutatingScope.php | 6 ++++++ src/Analyser/NodeScopeResolver.php | 8 +++++--- src/PhpDoc/PhpDocNodeResolver.php | 9 +++++---- src/Reflection/Php/PhpClassReflectionExtension.php | 4 +++- .../Php/PhpFunctionFromParserNodeReflection.php | 2 ++ src/Reflection/Php/PhpMethodFromParserNodeReflection.php | 2 ++ .../SignatureMap/NativeFunctionReflectionProvider.php | 1 + 7 files changed, 24 insertions(+), 8 deletions(-) diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 62ed96b7f0..953ec0bdeb 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(), ); @@ -3319,6 +3322,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 +3340,7 @@ public function enterFunction( array $parameterOutTypes = [], array $immediatelyInvokedCallableParameters = [], array $phpDocClosureThisTypeParameters = [], + array $pureUnlessCallableIsImpureParameters = [], ): self { return $this->enterFunctionLike( @@ -3361,6 +3366,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..a7aba46368 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); @@ -6664,7 +6664,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 +6694,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 +6822,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 dc6927d486..6f90206753 100644 --- a/src/PhpDoc/PhpDocNodeResolver.php +++ b/src/PhpDoc/PhpDocNodeResolver.php @@ -428,16 +428,17 @@ public function resolveParamPureUnlessCallableIsImpure(PhpDocNode $phpDocNode): $parameters = []; // TODO: implement phpstan/phpdoc-parser foreach ($phpDocNode->getTagsByName('@pure-unless-callable-impure') as $tag) { - $value = preg_split('/\s/u', (string)$tag->value)[0] ?? null; - if ($value !== null && str_starts_with($value, '$')) { - $parameters[substr($value, 1)] = true; + $value = preg_split('/\s/u', (string) $tag->value)[0] ?? null; + if ($value === null || !str_starts_with($value, '$')) { + continue; } + + $parameters[substr($value, 1)] = true; } return $parameters; } - /** * @return array */ diff --git a/src/Reflection/Php/PhpClassReflectionExtension.php b/src/Reflection/Php/PhpClassReflectionExtension.php index 939e1550d3..ed774c3eff 100644 --- a/src/Reflection/Php/PhpClassReflectionExtension.php +++ b/src/Reflection/Php/PhpClassReflectionExtension.php @@ -1071,7 +1071,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 +1090,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..cadc653391 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; 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/SignatureMap/NativeFunctionReflectionProvider.php b/src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php index 202b74ddfc..30034f6760 100644 --- a/src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php +++ b/src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php @@ -140,6 +140,7 @@ public function findFunctionReflection(string $functionName): ?NativeFunctionRef $immediatelyInvokedCallable, $closureThisType, [], + isset($pureUnlessCallableIsImpureParameters[$name]) && $pureUnlessCallableIsImpureParameters[$name], ); }, $functionSignature->getParameters()), $functionSignature->isVariadic(), From 708023ac6757a5d3c2a10ecb65bcb6a1e776a08b Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Thu, 26 Sep 2024 02:35:16 +0900 Subject: [PATCH 03/10] Add isPureUnlessCallableIsImpureParameter method --- src/Analyser/NodeScopeResolver.php | 17 +++++++++++++++++ .../AnnotationsMethodParameterReflection.php | 5 +++++ src/Reflection/ExtendedParameterReflection.php | 2 ++ .../ExtendedNativeParameterReflection.php | 6 ++++++ src/Reflection/Php/ExtendedDummyParameter.php | 6 ++++++ .../Php/PhpFunctionFromParserNodeReflection.php | 7 +++++++ .../PhpParameterFromParserNodeReflection.php | 6 ++++++ src/Reflection/Php/PhpParameterReflection.php | 6 ++++++ 8 files changed, 55 insertions(+) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index a7aba46368..6c8d897bf9 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -5107,6 +5107,23 @@ private function processArgs( } $parameter = $lastParameter; } + + if ($parameter instanceof ExtendedParameterReflection + && $parameter->isPureUnlessCallableIsImpureParameter() + && $parameterType->isTrue()->yes() + ) { + if (count($parameterType->getCallableParametersAcceptors($scope)) > 0) { + $parameterCallable = $parameterType->getCallableParametersAcceptors($scope)[0]; + $certain = $parameterCallable->isPure()->yes(); + if ($certain) { + $impurePoints[] = new SimpleImpurePoint( + 'functionCall', + sprintf('call to function %s()', $calleeReflection->getName()), + $certain, + ); + } + } + } } $lookForUnset = false; 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/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/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/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/PhpFunctionFromParserNodeReflection.php b/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php index cadc653391..7b71fe7bdc 100644 --- a/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php +++ b/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php @@ -170,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, @@ -184,6 +190,7 @@ public function getParameters(): array $immediatelyInvokedCallable, $closureThisType, $this->parameterAttributes[$parameter->var->name] ?? [], + $pureUnlessCallableIsImpureParameter, ); } 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; + } + } From b8e6162b35a370090c413a1b7f902e911c2870d4 Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Thu, 26 Sep 2024 22:21:08 +0900 Subject: [PATCH 04/10] Replace with $phpDocNode->getPureUnlessCallableIsImpureTagValues() --- src/PhpDoc/PhpDocNodeResolver.php | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/PhpDoc/PhpDocNodeResolver.php b/src/PhpDoc/PhpDocNodeResolver.php index 6f90206753..134c9c5fbc 100644 --- a/src/PhpDoc/PhpDocNodeResolver.php +++ b/src/PhpDoc/PhpDocNodeResolver.php @@ -48,7 +48,6 @@ use function count; use function in_array; use function method_exists; -use function preg_split; use function str_starts_with; use function substr; @@ -426,14 +425,8 @@ public function resolveParamImmediatelyInvokedCallable(PhpDocNode $phpDocNode): public function resolveParamPureUnlessCallableIsImpure(PhpDocNode $phpDocNode): array { $parameters = []; - // TODO: implement phpstan/phpdoc-parser - foreach ($phpDocNode->getTagsByName('@pure-unless-callable-impure') as $tag) { - $value = preg_split('/\s/u', (string) $tag->value)[0] ?? null; - if ($value === null || !str_starts_with($value, '$')) { - continue; - } - - $parameters[substr($value, 1)] = true; + foreach ($phpDocNode->getPureUnlessCallableIsImpureTagValues() as $tag) { + $parameters[$tag->parameterName] = true; } return $parameters; From 29af2a142bf3e5aa70d6c66d3a1aa4f2fe2b1b47 Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Mon, 30 Sep 2024 21:26:27 +0900 Subject: [PATCH 05/10] Add pureUnlessCallableIsImpureParameters to functionMetadata --- bin/generate-function-metadata.php | 34 +++++++++++++++++-- resources/functionMetadata.php | 2 +- .../Php/PhpClassReflectionExtension.php | 2 +- .../NativeFunctionReflectionProvider.php | 17 ++++++++-- .../SignatureMap/SignatureMapProvider.php | 4 +-- .../SignatureMap/FunctionMetadataTest.php | 8 +++-- 6 files changed, 55 insertions(+), 12 deletions(-) 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..b5cc064b19 100644 --- a/resources/functionMetadata.php +++ b/resources/functionMetadata.php @@ -1622,4 +1622,4 @@ 'zlib_encode' => ['hasSideEffects' => false], 'zlib_get_coding_type' => ['hasSideEffects' => false], -]; \ No newline at end of file +]; diff --git a/src/Reflection/Php/PhpClassReflectionExtension.php b/src/Reflection/Php/PhpClassReflectionExtension.php index ed774c3eff..a4ad7c4278 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'] ?? false); } else { $hasSideEffects = TrinaryLogic::createMaybe(); } diff --git a/src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php b/src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php index 30034f6760..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; @@ -151,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/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); } From 55d3e87cf541c62ccc782eb751198dbb9095ffa7 Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Tue, 1 Oct 2024 00:13:10 +0900 Subject: [PATCH 06/10] Add pureUnlessCallableIsImpureParameters to functionMetadata_original.php --- bin/functionMetadata_original.php | 29 +++++++++++++++++++---------- resources/functionMetadata.php | 29 +++++++++++++++++++---------- 2 files changed, 38 insertions(+), 20 deletions(-) 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/resources/functionMetadata.php b/resources/functionMetadata.php index b5cc064b19..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], From 853e5bf5fe81af5a06bbf6027b67f1cfe42d1d96 Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Wed, 20 Nov 2024 03:52:32 +0900 Subject: [PATCH 07/10] Fix --- src/Analyser/NodeScopeResolver.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 6c8d897bf9..d29d22fbb5 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -5110,13 +5110,16 @@ private function processArgs( if ($parameter instanceof ExtendedParameterReflection && $parameter->isPureUnlessCallableIsImpureParameter() + && $parameterType !== null && $parameterType->isTrue()->yes() ) { - if (count($parameterType->getCallableParametersAcceptors($scope)) > 0) { + if (count($parameterType->getCallableParametersAcceptors($scope)) > 0 && $calleeReflection !== null) { $parameterCallable = $parameterType->getCallableParametersAcceptors($scope)[0]; $certain = $parameterCallable->isPure()->yes(); if ($certain) { - $impurePoints[] = new SimpleImpurePoint( + $impurePoints[] = new ImpurePoint( + $scope, + $callLike, 'functionCall', sprintf('call to function %s()', $calleeReflection->getName()), $certain, From c4999be6237e8b12cd82f4742dea40d14b23ee99 Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Fri, 1 Aug 2025 22:09:07 +0900 Subject: [PATCH 08/10] Fix --- src/Analyser/MutatingScope.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 953ec0bdeb..e6ab99e23c 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -3232,6 +3232,7 @@ public function enterPropertyHook( [], false, $this->attributeReflectionFactory->fromAttrGroups($hook->attrGroups, InitializerExprContext::fromStubParameter($this->getClassReflection()->getName(), $this->getFile(), $hook)), + [], ), true, ); From 1f669300c116f99e0288c7cd50e46d933bb77fe2 Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Fri, 1 Aug 2025 22:14:15 +0900 Subject: [PATCH 09/10] hasSideEffects default true --- src/Reflection/Php/PhpClassReflectionExtension.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Reflection/Php/PhpClassReflectionExtension.php b/src/Reflection/Php/PhpClassReflectionExtension.php index a4ad7c4278..ff8ff4fd8a 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'] ?? false); + $hasSideEffects = TrinaryLogic::createFromBoolean($this->signatureMapProvider->getMethodMetadata($declaringClassName, $methodReflection->getName())['hasSideEffects'] ?? true); } else { $hasSideEffects = TrinaryLogic::createMaybe(); } @@ -842,7 +842,7 @@ public function createUserlandMethodReflection(ClassReflection $fileDeclaringCla $isPure = null; foreach ($actualDeclaringClass->getAncestors() as $className => $ancestor) { if ($this->signatureMapProvider->hasMethodMetadata($className, $methodReflection->getName())) { - $hasSideEffects = $this->signatureMapProvider->getMethodMetadata($className, $methodReflection->getName())['hasSideEffects']; + $hasSideEffects = $this->signatureMapProvider->getMethodMetadata($className, $methodReflection->getName())['hasSideEffects'] ?? true; $isPure = !$hasSideEffects; break; From e4e54cb87979a99832261ce73b744e217cf1ff77 Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Fri, 1 Aug 2025 22:44:05 +0900 Subject: [PATCH 10/10] wip --- src/Reflection/Annotations/AnnotationMethodReflection.php | 5 +++++ src/Reflection/Dummy/ChangedTypeMethodReflection.php | 5 +++++ src/Reflection/Dummy/DummyConstructorReflection.php | 5 +++++ src/Reflection/Dummy/DummyMethodReflection.php | 5 +++++ src/Reflection/ExtendedMethodReflection.php | 5 +++++ src/Reflection/FunctionReflection.php | 5 +++++ src/Reflection/Native/NativeFunctionReflection.php | 5 +++++ src/Reflection/Native/NativeMethodReflection.php | 5 +++++ src/Reflection/Php/ClosureCallMethodReflection.php | 5 +++++ src/Reflection/Php/EnumCasesMethodReflection.php | 5 +++++ src/Reflection/Php/ExitFunctionReflection.php | 5 +++++ src/Reflection/Php/PhpClassReflectionExtension.php | 6 +++++- .../Php/PhpFunctionFromParserNodeReflection.php | 5 +++++ src/Reflection/Php/PhpFunctionReflection.php | 5 +++++ src/Reflection/Php/PhpMethodReflection.php | 8 ++++++++ src/Reflection/Php/PhpMethodReflectionFactory.php | 2 ++ src/Reflection/ResolvedMethodReflection.php | 5 +++++ src/Reflection/Type/IntersectionTypeMethodReflection.php | 5 +++++ src/Reflection/Type/UnionTypeMethodReflection.php | 5 +++++ src/Reflection/WrappedExtendedMethodReflection.php | 5 +++++ .../RewrittenDeclaringClassMethodReflection.php | 5 +++++ 21 files changed, 105 insertions(+), 1 deletion(-) 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/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/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/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/PhpClassReflectionExtension.php b/src/Reflection/Php/PhpClassReflectionExtension.php index ff8ff4fd8a..35d64701d4 100644 --- a/src/Reflection/Php/PhpClassReflectionExtension.php +++ b/src/Reflection/Php/PhpClassReflectionExtension.php @@ -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'] ?? true; + $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, ); } diff --git a/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php b/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php index 7b71fe7bdc..78083aef76 100644 --- a/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php +++ b/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php @@ -334,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/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/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/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();