From 7fc8bdf2027dc44512bad0c36e8693b801da3c2d Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Thu, 19 Sep 2024 02:18:34 +0900 Subject: [PATCH 1/7] 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 1c31ee55d0..a85cffa248 100644 --- a/src/PhpDoc/PhpDocNodeResolver.php +++ b/src/PhpDoc/PhpDocNodeResolver.php @@ -46,6 +46,7 @@ use function count; use function in_array; use function method_exists; +use function preg_split; use function str_starts_with; use function substr; @@ -416,6 +417,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 6cd991341b..5f5325deb1 100644 --- a/src/PhpDoc/ResolvedPhpDocBlock.php +++ b/src/PhpDoc/ResolvedPhpDocBlock.php @@ -95,6 +95,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; @@ -212,6 +215,7 @@ public static function createEmpty(): self $self->paramTags = []; $self->paramOutTags = []; $self->paramsImmediatelyInvokedCallable = []; + $self->paramsPureUnlessCallableIsImpure = []; $self->paramClosureThisTags = []; $self->returnTag = null; $self->throwsTag = null; @@ -276,6 +280,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); @@ -581,6 +586,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 */ @@ -1161,6 +1178,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 52fd1e8a63f72600b129d4995b002db21b8c9065 Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Thu, 19 Sep 2024 21:43:22 +0900 Subject: [PATCH 2/7] Add pureUnlessCallableIsImpureParameters --- src/Analyser/MutatingScope.php | 6 ++++++ src/Analyser/NodeScopeResolver.php | 8 +++++--- src/Reflection/Php/PhpClassReflectionExtension.php | 3 ++- .../Php/PhpFunctionFromParserNodeReflection.php | 2 ++ src/Reflection/Php/PhpMethodFromParserNodeReflection.php | 2 ++ 5 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index b8dcee4fd8..5af63ce9bc 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -2932,6 +2932,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, @@ -2951,6 +2952,7 @@ public function enterClassMethod( array $parameterOutTypes = [], array $immediatelyInvokedCallableParameters = [], array $phpDocClosureThisTypeParameters = [], + array $phpDocPureUnlessCallableIsImpureParameters = [], ): self { if (!$this->isInClass()) { @@ -2981,6 +2983,7 @@ public function enterClassMethod( array_map(fn (Type $type): Type => $this->transformStaticType(TemplateTypeHelper::toArgument($type)), $parameterOutTypes), $immediatelyInvokedCallableParameters, array_map(fn (Type $type): Type => $this->transformStaticType(TemplateTypeHelper::toArgument($type)), $phpDocClosureThisTypeParameters), + $phpDocPureUnlessCallableIsImpureParameters, ), !$classMethod->isStatic(), ); @@ -3050,6 +3053,7 @@ private function getRealParameterDefaultValues(Node\FunctionLike $functionLike): * @param Type[] $parameterOutTypes * @param array $immediatelyInvokedCallableParameters * @param array $phpDocClosureThisTypeParameters + * @param array $pureUnlessCallableIsImpureParameters */ public function enterFunction( Node\Stmt\Function_ $function, @@ -3067,6 +3071,7 @@ public function enterFunction( array $parameterOutTypes = [], array $immediatelyInvokedCallableParameters = [], array $phpDocClosureThisTypeParameters = [], + array $pureUnlessCallableIsImpureParameters = [], ): self { return $this->enterFunctionLike( @@ -3090,6 +3095,7 @@ public function enterFunction( array_map(static fn (Type $type): Type => TemplateTypeHelper::toArgument($type), $parameterOutTypes), $immediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, + $pureUnlessCallableIsImpureParameters, ), false, ); diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index f2ce6e9c2e..2744c37c3a 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -599,7 +599,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); @@ -6000,7 +6000,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 { @@ -6030,6 +6030,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()) { @@ -6152,9 +6153,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/Reflection/Php/PhpClassReflectionExtension.php b/src/Reflection/Php/PhpClassReflectionExtension.php index c42863737c..3c5a1d5ba3 100644 --- a/src/Reflection/Php/PhpClassReflectionExtension.php +++ b/src/Reflection/Php/PhpClassReflectionExtension.php @@ -982,7 +982,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, @@ -1001,6 +1001,7 @@ private function inferAndCachePropertyTypes( $phpDocParameterOutTypes, $phpDocImmediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, + $phpDocPureUnlessCallableIsImpureParameters, ); $propertyTypes = []; diff --git a/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php b/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php index 1d157476df..1f0d2eef8f 100644 --- a/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php +++ b/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php @@ -44,6 +44,7 @@ class PhpFunctionFromParserNodeReflection implements FunctionReflection, Extende * @param Type[] $parameterOutTypes * @param array $immediatelyInvokedCallableParameters * @param array $phpDocClosureThisTypeParameters + * @param array $pureUnlessCallableIsImpureParameters */ public function __construct( FunctionLike $functionLike, @@ -65,6 +66,7 @@ public function __construct( private array $parameterOutTypes, private array $immediatelyInvokedCallableParameters, private array $phpDocClosureThisTypeParameters, + private array $pureUnlessCallableIsImpureParameters, ) { $this->functionLike = $functionLike; diff --git a/src/Reflection/Php/PhpMethodFromParserNodeReflection.php b/src/Reflection/Php/PhpMethodFromParserNodeReflection.php index 8cd703c8b2..61f9612e6c 100644 --- a/src/Reflection/Php/PhpMethodFromParserNodeReflection.php +++ b/src/Reflection/Php/PhpMethodFromParserNodeReflection.php @@ -59,6 +59,7 @@ public function __construct( array $parameterOutTypes, array $immediatelyInvokedCallableParameters, array $phpDocClosureThisTypeParameters, + array $pureUnlessCallableIsImpureParameters, ) { $name = strtolower($classMethod->name->name); @@ -114,6 +115,7 @@ public function __construct( $parameterOutTypes, $immediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, + $pureUnlessCallableIsImpureParameters, ); } From a27956a051ebba0c753f479b41a5d577c783de7b Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Thu, 26 Sep 2024 02:35:16 +0900 Subject: [PATCH 3/7] 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 2744c37c3a..76f27d0458 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -4600,6 +4600,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 51bddcaabe..e29b42a641 100644 --- a/src/Reflection/Annotations/AnnotationsMethodParameterReflection.php +++ b/src/Reflection/Annotations/AnnotationsMethodParameterReflection.php @@ -70,4 +70,9 @@ public function getDefaultValue(): ?Type return $this->defaultValue; } + public function isPureUnlessCallableIsImpureParameter(): bool + { + return false; + } + } diff --git a/src/Reflection/ExtendedParameterReflection.php b/src/Reflection/ExtendedParameterReflection.php index db8df05ab8..e2e9971432 100644 --- a/src/Reflection/ExtendedParameterReflection.php +++ b/src/Reflection/ExtendedParameterReflection.php @@ -19,4 +19,6 @@ public function isImmediatelyInvokedCallable(): TrinaryLogic; public function getClosureThisType(): ?Type; + public function isPureUnlessCallableIsImpureParameter(): bool; + } diff --git a/src/Reflection/Native/ExtendedNativeParameterReflection.php b/src/Reflection/Native/ExtendedNativeParameterReflection.php index 7e1388bf5a..d7ff91f0dc 100644 --- a/src/Reflection/Native/ExtendedNativeParameterReflection.php +++ b/src/Reflection/Native/ExtendedNativeParameterReflection.php @@ -22,6 +22,7 @@ public function __construct( private ?Type $outType, private TrinaryLogic $immediatelyInvokedCallable, private ?Type $closureThisType, + private bool $pureUnlessCallableIsImpureParameter = false, ) { } @@ -81,4 +82,9 @@ public function getClosureThisType(): ?Type return $this->closureThisType; } + public function isPureUnlessCallableIsImpureParameter(): bool + { + return $this->pureUnlessCallableIsImpureParameter; + } + } diff --git a/src/Reflection/Php/ExtendedDummyParameter.php b/src/Reflection/Php/ExtendedDummyParameter.php index 91238c18b9..8bba795126 100644 --- a/src/Reflection/Php/ExtendedDummyParameter.php +++ b/src/Reflection/Php/ExtendedDummyParameter.php @@ -22,6 +22,7 @@ public function __construct( private ?Type $outType, private TrinaryLogic $immediatelyInvokedCallable, private ?Type $closureThisType, + private bool $pureUnlessCallableIsImpureParameter = false, ) { parent::__construct($name, $type, $optional, $passedByReference, $variadic, $defaultValue); @@ -52,4 +53,9 @@ public function getClosureThisType(): ?Type return $this->closureThisType; } + public function isPureUnlessCallableIsImpureParameter(): bool + { + return $this->pureUnlessCallableIsImpureParameter; + } + } diff --git a/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php b/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php index 1f0d2eef8f..f5a93cddb6 100644 --- a/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php +++ b/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php @@ -164,6 +164,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, @@ -177,6 +183,7 @@ public function getParameters(): array $this->parameterOutTypes[$parameter->var->name] ?? null, $immediatelyInvokedCallable, $closureThisType, + $pureUnlessCallableIsImpureParameter, ); } diff --git a/src/Reflection/Php/PhpParameterFromParserNodeReflection.php b/src/Reflection/Php/PhpParameterFromParserNodeReflection.php index 8ebb272bfd..c9d2caa147 100644 --- a/src/Reflection/Php/PhpParameterFromParserNodeReflection.php +++ b/src/Reflection/Php/PhpParameterFromParserNodeReflection.php @@ -26,6 +26,7 @@ public function __construct( private ?Type $outType, private TrinaryLogic $immediatelyInvokedCallable, private ?Type $closureThisType, + private bool $pureUnlessCallableIsImpureParameter, ) { } @@ -98,4 +99,9 @@ public function getClosureThisType(): ?Type return $this->closureThisType; } + public function isPureUnlessCallableIsImpureParameter(): bool + { + return $this->pureUnlessCallableIsImpureParameter; + } + } diff --git a/src/Reflection/Php/PhpParameterReflection.php b/src/Reflection/Php/PhpParameterReflection.php index 40b28e9ff6..4ab7b989a9 100644 --- a/src/Reflection/Php/PhpParameterReflection.php +++ b/src/Reflection/Php/PhpParameterReflection.php @@ -29,6 +29,7 @@ public function __construct( private ?Type $outType, private TrinaryLogic $immediatelyInvokedCallable, private ?Type $closureThisType, + private bool $pureUnlessCallableIsImpureParameter = false, ) { } @@ -133,4 +134,9 @@ public function getClosureThisType(): ?Type return $this->closureThisType; } + public function isPureUnlessCallableIsImpureParameter(): bool + { + return $this->pureUnlessCallableIsImpureParameter; + } + } From 081f883260c3099c25d974786beed06a825957a5 Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Thu, 26 Sep 2024 22:21:08 +0900 Subject: [PATCH 4/7] Replace with $phpDocNode->getPureUnlessCallableIsImpureTagValues() --- src/PhpDoc/PhpDocNodeResolver.php | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/PhpDoc/PhpDocNodeResolver.php b/src/PhpDoc/PhpDocNodeResolver.php index a85cffa248..d140166462 100644 --- a/src/PhpDoc/PhpDocNodeResolver.php +++ b/src/PhpDoc/PhpDocNodeResolver.php @@ -46,7 +46,6 @@ use function count; use function in_array; use function method_exists; -use function preg_split; use function str_starts_with; use function substr; @@ -423,18 +422,13 @@ 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, '$')) { - $parameters[substr($value, 1)] = true; - } + foreach ($phpDocNode->getPureUnlessCallableIsImpureTagValues() as $tag) { + $parameters[$tag->parameterName] = true; } return $parameters; } - /** * @return array */ From 0019dcfb0b249bd60ee27492a71c8096838a6f6c Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Mon, 30 Sep 2024 21:26:27 +0900 Subject: [PATCH 5/7] Add pureUnlessCallableIsImpureParameters to functionMetadata --- bin/generate-function-metadata.php | 34 +++++++++++++++++-- .../Php/PhpClassReflectionExtension.php | 2 +- .../NativeFunctionReflectionProvider.php | 18 ++++++++-- .../SignatureMap/SignatureMapProvider.php | 4 +-- .../SignatureMap/FunctionMetadataTest.php | 8 +++-- 5 files changed, 55 insertions(+), 11 deletions(-) diff --git a/bin/generate-function-metadata.php b/bin/generate-function-metadata.php index 97737499a5..6a55166482 100755 --- a/bin/generate-function-metadata.php +++ b/bin/generate-function-metadata.php @@ -77,7 +77,7 @@ 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']) { if (in_array($functionName, [ 'mt_rand', 'rand', @@ -91,6 +91,14 @@ public function enterNode(Node $node) } throw new ShouldNotHappenException($functionName); } + + if (isset($metadata[$functionName]['pureUnlessCallableIsImpureParameters'])) { + $metadata[$functionName] = [ + 'pureUnlessCallableIsImpureParameters' => $metadata[$functionName]['pureUnlessCallableIsImpureParameters'], + ]; + + continue; + } } $metadata[$functionName] = ['hasSideEffects' => false]; } @@ -128,12 +136,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( + 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/src/Reflection/Php/PhpClassReflectionExtension.php b/src/Reflection/Php/PhpClassReflectionExtension.php index 3c5a1d5ba3..2a0ecb0c8d 100644 --- a/src/Reflection/Php/PhpClassReflectionExtension.php +++ b/src/Reflection/Php/PhpClassReflectionExtension.php @@ -608,7 +608,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 2a94fc4da5..6389c548c0 100644 --- a/src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php +++ b/src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php @@ -88,13 +88,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; @@ -124,6 +135,7 @@ public function findFunctionReflection(string $functionName): ?NativeFunctionRef $phpDoc !== null ? NativeFunctionReflectionProvider::getParamOutTypeFromPhpDoc($parameterSignature->getName(), $phpDoc) : null, $immediatelyInvokedCallable, $closureThisType, + isset($pureUnlessCallableIsImpureParameters[$name]) && $pureUnlessCallableIsImpureParameters[$name], ); }, $functionSignature->getParameters()), $functionSignature->isVariadic(), @@ -134,8 +146,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 f7ec5ed5ce..e152d376ad 100644 --- a/src/Reflection/SignatureMap/SignatureMapProvider.php +++ b/src/Reflection/SignatureMap/SignatureMapProvider.php @@ -24,12 +24,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..1fbd63d47f 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 1cc1fad8c8467fd6566b53505ff415ba108a5279 Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Tue, 1 Oct 2024 00:13:10 +0900 Subject: [PATCH 6/7] 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 838d2ecd5c..4d84fb5f0e 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], @@ -83,6 +89,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], @@ -100,6 +108,7 @@ 'move_uploaded_file' => ['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 0c5c33759a..b7baf40184 100644 --- a/resources/functionMetadata.php +++ b/resources/functionMetadata.php @@ -700,21 +700,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], @@ -722,6 +725,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], @@ -729,12 +733,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], @@ -765,6 +769,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], @@ -915,6 +921,8 @@ 'fmod' => ['hasSideEffects' => false], 'fnmatch' => ['hasSideEffects' => false], '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], @@ -1447,6 +1455,7 @@ 'preg_last_error' => ['hasSideEffects' => false], 'preg_last_error_msg' => ['hasSideEffects' => false], 'preg_quote' => ['hasSideEffects' => false], + 'preg_replace_callback' => ['pureUnlessCallableIsImpureParameters' => ['callback' => true]], 'preg_split' => ['hasSideEffects' => false], 'property_exists' => ['hasSideEffects' => false], 'quoted_printable_decode' => ['hasSideEffects' => false], From adb02ca95ecb69b468ea9ae3189d5bd9bcf2b5fb Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Wed, 20 Nov 2024 03:52:32 +0900 Subject: [PATCH 7/7] 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 76f27d0458..97f7152dff 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -4603,13 +4603,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,