diff --git a/src/Analyser/ArgumentsNormalizer.php b/src/Analyser/ArgumentsNormalizer.php index 6c6db632ac..6baa6e0c6b 100644 --- a/src/Analyser/ArgumentsNormalizer.php +++ b/src/Analyser/ArgumentsNormalizer.php @@ -27,7 +27,7 @@ final class ArgumentsNormalizer public const ORIGINAL_ARG_ATTRIBUTE = 'originalArg'; /** - * @return array{ParametersAcceptor, FuncCall}|null + * @return array{ParametersAcceptor, FuncCall, bool}|null */ public static function reorderCallUserFuncArguments( FuncCall $callUserFuncCall, @@ -65,18 +65,24 @@ public static function reorderCallUserFuncArguments( return null; } + $callableParametersAcceptors = $calledOnType->getCallableParametersAcceptors($scope); $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( $scope, $passThruArgs, - $calledOnType->getCallableParametersAcceptors($scope), + $callableParametersAcceptors, null, ); + $acceptsNamedArguments = true; + foreach ($callableParametersAcceptors as $callableParametersAcceptor) { + $acceptsNamedArguments = $acceptsNamedArguments && $callableParametersAcceptor->acceptsNamedArguments(); + } + return [$parametersAcceptor, new FuncCall( $callbackArg->value, $passThruArgs, $callUserFuncCall->getAttributes(), - )]; + ), $acceptsNamedArguments]; } public static function reorderFuncArguments( diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index c3c3b2ee25..f10fc22508 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -1361,6 +1361,7 @@ static function (Node $node, Scope $scope) use ($arrowScope, &$arrowFunctionImpu $cachedClosureData['impurePoints'], $cachedClosureData['invalidateExpressions'], $cachedClosureData['usedVariables'], + true, ); } if (self::$resolveClosureTypeDepth >= 2) { @@ -1575,6 +1576,7 @@ static function (Node $node, Scope $scope) use ($arrowScope, &$arrowFunctionImpu $impurePointsForClosureType, $invalidateExpressions, $usedVariables, + true, ); } elseif ($node instanceof New_) { if ($node->class instanceof Name) { @@ -2523,9 +2525,11 @@ private function createFirstClassCallable( $throwPoints = []; $impurePoints = []; + $acceptsNamedArguments = true; if ($variant instanceof CallableParametersAcceptor) { $throwPoints = $variant->getThrowPoints(); $impurePoints = $variant->getImpurePoints(); + $acceptsNamedArguments = $variant->acceptsNamedArguments(); } elseif ($function !== null) { $returnTypeForThrow = $variant->getReturnType(); $throwType = $function->getThrowType(); @@ -2549,6 +2553,8 @@ private function createFirstClassCallable( if ($impurePoint !== null) { $impurePoints[] = $impurePoint; } + + $acceptsNamedArguments = $function->acceptsNamedArguments(); } $parameters = $variant->getParameters(); @@ -2564,6 +2570,7 @@ private function createFirstClassCallable( $impurePoints, [], [], + $acceptsNamedArguments, ); } diff --git a/src/PhpDoc/TypeNodeResolver.php b/src/PhpDoc/TypeNodeResolver.php index 18db804030..a52fde51f6 100644 --- a/src/PhpDoc/TypeNodeResolver.php +++ b/src/PhpDoc/TypeNodeResolver.php @@ -961,7 +961,7 @@ function (CallableTypeParameterNode $parameterNode) use ($nameScope, &$isVariadi ), ]); } elseif ($mainType instanceof ClosureType) { - $closure = new ClosureType($parameters, $returnType, $isVariadic, $templateTypeMap, null, null, $templateTags, [], $mainType->getImpurePoints()); + $closure = new ClosureType($parameters, $returnType, $isVariadic, $templateTypeMap, null, null, $templateTags, [], $mainType->getImpurePoints(), $mainType->getInvalidateExpressions(), $mainType->getUsedVariables(), $mainType->acceptsNamedArguments()); if ($closure->isPure()->yes() && $returnType->isVoid()->yes()) { return new ErrorType(); } diff --git a/src/Reflection/Annotations/AnnotationMethodReflection.php b/src/Reflection/Annotations/AnnotationMethodReflection.php index 79445fdbe1..0486d11048 100644 --- a/src/Reflection/Annotations/AnnotationMethodReflection.php +++ b/src/Reflection/Annotations/AnnotationMethodReflection.php @@ -141,6 +141,11 @@ public function getAsserts(): Assertions return Assertions::createEmpty(); } + public function acceptsNamedArguments(): bool + { + return $this->declaringClass->acceptsNamedArguments(); + } + public function getSelfOutType(): ?Type { return null; diff --git a/src/Reflection/BetterReflection/BetterReflectionProvider.php b/src/Reflection/BetterReflection/BetterReflectionProvider.php index 8ec65fd9d2..984d3615b3 100644 --- a/src/Reflection/BetterReflection/BetterReflectionProvider.php +++ b/src/Reflection/BetterReflection/BetterReflectionProvider.php @@ -280,6 +280,7 @@ private function getCustomFunction(string $functionName): PhpFunctionReflection $isFinal = false; $isPure = null; $asserts = Assertions::createEmpty(); + $acceptsNamedArguments = true; $phpDocComment = null; $phpDocParameterOutTags = []; $phpDocParameterImmediatelyInvokedCallable = []; @@ -305,6 +306,7 @@ private function getCustomFunction(string $functionName): PhpFunctionReflection if ($resolvedPhpDoc->hasPhpDocString()) { $phpDocComment = $resolvedPhpDoc->getPhpDocString(); } + $acceptsNamedArguments = $resolvedPhpDoc->acceptsNamedArguments(); $phpDocParameterOutTags = $resolvedPhpDoc->getParamOutTags(); $phpDocParameterImmediatelyInvokedCallable = $resolvedPhpDoc->getParamsImmediatelyInvokedCallable(); $phpDocParameterClosureThisTypeTags = $resolvedPhpDoc->getParamClosureThisTags(); @@ -323,6 +325,7 @@ private function getCustomFunction(string $functionName): PhpFunctionReflection $reflectionFunction->getFileName() !== false ? $reflectionFunction->getFileName() : null, $isPure, $asserts, + $acceptsNamedArguments, $phpDocComment, array_map(static fn (ParamOutTag $paramOutTag): Type => $paramOutTag->getType(), $phpDocParameterOutTags), $phpDocParameterImmediatelyInvokedCallable, diff --git a/src/Reflection/CallableFunctionVariantWithPhpDocs.php b/src/Reflection/CallableFunctionVariantWithPhpDocs.php index 8b8aa99567..9e6644c0f1 100644 --- a/src/Reflection/CallableFunctionVariantWithPhpDocs.php +++ b/src/Reflection/CallableFunctionVariantWithPhpDocs.php @@ -35,6 +35,7 @@ public function __construct( private array $impurePoints, private array $invalidateExpressions, private array $usedVariables, + private bool $acceptsNamedArguments, ) { parent::__construct( @@ -74,4 +75,9 @@ public function getUsedVariables(): array return $this->usedVariables; } + public function acceptsNamedArguments(): bool + { + return $this->acceptsNamedArguments; + } + } diff --git a/src/Reflection/Callables/CallableParametersAcceptor.php b/src/Reflection/Callables/CallableParametersAcceptor.php index 62371a0e85..259ede81fa 100644 --- a/src/Reflection/Callables/CallableParametersAcceptor.php +++ b/src/Reflection/Callables/CallableParametersAcceptor.php @@ -19,6 +19,8 @@ public function getThrowPoints(): array; public function isPure(): TrinaryLogic; + public function acceptsNamedArguments(): bool; + /** * @return SimpleImpurePoint[] */ diff --git a/src/Reflection/Callables/FunctionCallableVariant.php b/src/Reflection/Callables/FunctionCallableVariant.php index 992d53d13a..aef7210140 100644 --- a/src/Reflection/Callables/FunctionCallableVariant.php +++ b/src/Reflection/Callables/FunctionCallableVariant.php @@ -163,4 +163,9 @@ public function getUsedVariables(): array return []; } + public function acceptsNamedArguments(): bool + { + return $this->function->acceptsNamedArguments(); + } + } diff --git a/src/Reflection/Dummy/ChangedTypeMethodReflection.php b/src/Reflection/Dummy/ChangedTypeMethodReflection.php index 351d90b467..69d7a7a1a6 100644 --- a/src/Reflection/Dummy/ChangedTypeMethodReflection.php +++ b/src/Reflection/Dummy/ChangedTypeMethodReflection.php @@ -107,6 +107,11 @@ public function getAsserts(): Assertions return $this->reflection->getAsserts(); } + public function acceptsNamedArguments(): bool + { + return $this->reflection->acceptsNamedArguments(); + } + public function getSelfOutType(): ?Type { return $this->reflection->getSelfOutType(); diff --git a/src/Reflection/Dummy/DummyConstructorReflection.php b/src/Reflection/Dummy/DummyConstructorReflection.php index 4c98f8d596..7f81cd1363 100644 --- a/src/Reflection/Dummy/DummyConstructorReflection.php +++ b/src/Reflection/Dummy/DummyConstructorReflection.php @@ -111,6 +111,11 @@ public function getAsserts(): Assertions return Assertions::createEmpty(); } + public function acceptsNamedArguments(): bool + { + return $this->declaringClass->acceptsNamedArguments(); + } + public function getSelfOutType(): ?Type { return null; diff --git a/src/Reflection/Dummy/DummyMethodReflection.php b/src/Reflection/Dummy/DummyMethodReflection.php index d26ea0c4be..79e85fda5b 100644 --- a/src/Reflection/Dummy/DummyMethodReflection.php +++ b/src/Reflection/Dummy/DummyMethodReflection.php @@ -108,6 +108,11 @@ public function getAsserts(): Assertions return Assertions::createEmpty(); } + public function acceptsNamedArguments(): bool + { + return true; + } + public function getSelfOutType(): ?Type { return null; diff --git a/src/Reflection/ExtendedMethodReflection.php b/src/Reflection/ExtendedMethodReflection.php index 0ff0f8f2de..14f7061aa4 100644 --- a/src/Reflection/ExtendedMethodReflection.php +++ b/src/Reflection/ExtendedMethodReflection.php @@ -32,6 +32,8 @@ public function getVariants(): array; */ public function getNamedArgumentsVariants(): ?array; + public function acceptsNamedArguments(): bool; + public function getAsserts(): Assertions; public function getSelfOutType(): ?Type; diff --git a/src/Reflection/FunctionReflection.php b/src/Reflection/FunctionReflection.php index 91afcbaadb..bdd5ed8d63 100644 --- a/src/Reflection/FunctionReflection.php +++ b/src/Reflection/FunctionReflection.php @@ -23,6 +23,8 @@ public function getVariants(): array; */ public function getNamedArgumentsVariants(): ?array; + public function acceptsNamedArguments(): bool; + public function isDeprecated(): TrinaryLogic; public function getDeprecatedDescription(): ?string; diff --git a/src/Reflection/FunctionReflectionFactory.php b/src/Reflection/FunctionReflectionFactory.php index 67684abdab..4333954ff3 100644 --- a/src/Reflection/FunctionReflectionFactory.php +++ b/src/Reflection/FunctionReflectionFactory.php @@ -29,6 +29,7 @@ public function create( ?string $filename, ?bool $isPure, Assertions $asserts, + bool $acceptsNamedArguments, ?string $phpDocComment, array $phpDocParameterOutTypes, array $phpDocParameterImmediatelyInvokedCallable, diff --git a/src/Reflection/GenericParametersAcceptorResolver.php b/src/Reflection/GenericParametersAcceptorResolver.php index 09e9968a1a..5aa65587de 100644 --- a/src/Reflection/GenericParametersAcceptorResolver.php +++ b/src/Reflection/GenericParametersAcceptorResolver.php @@ -126,6 +126,7 @@ public static function resolve(array $argTypes, ParametersAcceptor $parametersAc $originalParametersAcceptor->getImpurePoints(), $originalParametersAcceptor->getInvalidateExpressions(), $originalParametersAcceptor->getUsedVariables(), + $originalParametersAcceptor->acceptsNamedArguments(), ); } diff --git a/src/Reflection/InaccessibleMethod.php b/src/Reflection/InaccessibleMethod.php index 58b63fe1c5..eaf63ef8a1 100644 --- a/src/Reflection/InaccessibleMethod.php +++ b/src/Reflection/InaccessibleMethod.php @@ -13,11 +13,11 @@ final class InaccessibleMethod implements CallableParametersAcceptor { - public function __construct(private MethodReflection $methodReflection) + public function __construct(private ExtendedMethodReflection $methodReflection) { } - public function getMethod(): MethodReflection + public function getMethod(): ExtendedMethodReflection { return $this->methodReflection; } @@ -86,4 +86,9 @@ public function getUsedVariables(): array return []; } + public function acceptsNamedArguments(): bool + { + return $this->methodReflection->acceptsNamedArguments(); + } + } diff --git a/src/Reflection/Native/NativeFunctionReflection.php b/src/Reflection/Native/NativeFunctionReflection.php index 870f0b7c66..2dd98f2951 100644 --- a/src/Reflection/Native/NativeFunctionReflection.php +++ b/src/Reflection/Native/NativeFunctionReflection.php @@ -29,6 +29,7 @@ public function __construct( ?Assertions $assertions = null, private ?string $phpDocComment = null, ?TrinaryLogic $returnsByReference = null, + private bool $acceptsNamedArguments = true, ) { $this->assertions = $assertions ?? Assertions::createEmpty(); @@ -132,4 +133,9 @@ public function returnsByReference(): TrinaryLogic return $this->returnsByReference; } + public function acceptsNamedArguments(): bool + { + return $this->acceptsNamedArguments; + } + } diff --git a/src/Reflection/Native/NativeMethodReflection.php b/src/Reflection/Native/NativeMethodReflection.php index 7c60edd6cc..425f75edd6 100644 --- a/src/Reflection/Native/NativeMethodReflection.php +++ b/src/Reflection/Native/NativeMethodReflection.php @@ -32,6 +32,7 @@ public function __construct( private TrinaryLogic $hasSideEffects, private ?Type $throwType, private Assertions $assertions, + private bool $acceptsNamedArguments, private ?Type $selfOutType, private ?string $phpDocComment, ) @@ -187,6 +188,11 @@ public function getAsserts(): Assertions return $this->assertions; } + public function acceptsNamedArguments(): bool + { + return $this->declaringClass->acceptsNamedArguments() && $this->acceptsNamedArguments; + } + public function getSelfOutType(): ?Type { return $this->selfOutType; diff --git a/src/Reflection/ParametersAcceptorSelector.php b/src/Reflection/ParametersAcceptorSelector.php index 5cbdbd2907..4d14c56ced 100644 --- a/src/Reflection/ParametersAcceptorSelector.php +++ b/src/Reflection/ParametersAcceptorSelector.php @@ -606,6 +606,7 @@ public static function combineAcceptors(array $acceptors): ParametersAcceptorWit $impurePoints = []; $invalidateExpressions = []; $usedVariables = []; + $acceptsNamedArguments = false; foreach ($acceptors as $acceptor) { $returnTypes[] = $acceptor->getReturnType(); @@ -621,6 +622,7 @@ public static function combineAcceptors(array $acceptors): ParametersAcceptorWit $impurePoints = array_merge($impurePoints, $acceptor->getImpurePoints()); $invalidateExpressions = array_merge($invalidateExpressions, $acceptor->getInvalidateExpressions()); $usedVariables = array_merge($usedVariables, $acceptor->getUsedVariables()); + $acceptsNamedArguments = $acceptsNamedArguments || $acceptor->acceptsNamedArguments(); } $isVariadic = $isVariadic || $acceptor->isVariadic(); @@ -722,6 +724,7 @@ public static function combineAcceptors(array $acceptors): ParametersAcceptorWit $impurePoints, $invalidateExpressions, $usedVariables, + $acceptsNamedArguments, ); } @@ -757,6 +760,7 @@ private static function wrapAcceptor(ParametersAcceptor $acceptor): ParametersAc $acceptor->getImpurePoints(), $acceptor->getInvalidateExpressions(), $acceptor->getUsedVariables(), + $acceptor->acceptsNamedArguments(), ); } diff --git a/src/Reflection/Php/ClosureCallMethodReflection.php b/src/Reflection/Php/ClosureCallMethodReflection.php index 7e2b402bf1..0f47e2c746 100644 --- a/src/Reflection/Php/ClosureCallMethodReflection.php +++ b/src/Reflection/Php/ClosureCallMethodReflection.php @@ -150,6 +150,11 @@ public function getAsserts(): Assertions return $this->nativeMethodReflection->getAsserts(); } + public function acceptsNamedArguments(): bool + { + return $this->nativeMethodReflection->acceptsNamedArguments(); + } + public function getSelfOutType(): ?Type { return $this->nativeMethodReflection->getSelfOutType(); diff --git a/src/Reflection/Php/EnumCasesMethodReflection.php b/src/Reflection/Php/EnumCasesMethodReflection.php index b66c18b805..ec9f1b3b9d 100644 --- a/src/Reflection/Php/EnumCasesMethodReflection.php +++ b/src/Reflection/Php/EnumCasesMethodReflection.php @@ -120,6 +120,11 @@ public function getAsserts(): Assertions return Assertions::createEmpty(); } + public function acceptsNamedArguments(): bool + { + return $this->declaringClass->acceptsNamedArguments(); + } + public function getSelfOutType(): ?Type { return null; diff --git a/src/Reflection/Php/PhpClassReflectionExtension.php b/src/Reflection/Php/PhpClassReflectionExtension.php index b6ba30dbec..550be0da4f 100644 --- a/src/Reflection/Php/PhpClassReflectionExtension.php +++ b/src/Reflection/Php/PhpClassReflectionExtension.php @@ -477,6 +477,7 @@ private function createMethod( $reflectionMethod = null; $throwType = null; $asserts = Assertions::createEmpty(); + $acceptsNamedArguments = true; $selfOutType = null; $phpDocComment = null; if ($classReflection->getNativeReflection()->hasMethod($methodReflection->getName())) { @@ -539,6 +540,7 @@ private function createMethod( } $asserts = Assertions::createFromResolvedPhpDocBlock($stubPhpDoc); + $acceptsNamedArguments = $stubPhpDoc->acceptsNamedArguments(); $selfOutTypeTag = $stubPhpDoc->getSelfOutTag(); if ($selfOutTypeTag !== null) { @@ -583,6 +585,7 @@ private function createMethod( $phpDocParameterTypes[$name] = $paramTag->getType(); } $asserts = Assertions::createFromResolvedPhpDocBlock($phpDocBlock); + $acceptsNamedArguments = $phpDocBlock->acceptsNamedArguments(); $selfOutTypeTag = $phpDocBlock->getSelfOutTag(); if ($selfOutTypeTag !== null) { @@ -625,6 +628,7 @@ private function createMethod( $hasSideEffects, $throwType, $asserts, + $acceptsNamedArguments, $selfOutType, $phpDocComment, ); @@ -773,6 +777,7 @@ public function createUserlandMethodReflection(ClassReflection $fileDeclaringCla $isFinal = $resolvedPhpDoc->isFinal(); $isPure = $resolvedPhpDoc->isPure(); $asserts = Assertions::createFromResolvedPhpDocBlock($resolvedPhpDoc); + $acceptsNamedArguments = $resolvedPhpDoc->acceptsNamedArguments(); $selfOutType = $resolvedPhpDoc->getSelfOutTag() !== null ? $resolvedPhpDoc->getSelfOutTag()->getType() : null; $phpDocComment = null; if ($resolvedPhpDoc->hasPhpDocString()) { @@ -793,6 +798,7 @@ public function createUserlandMethodReflection(ClassReflection $fileDeclaringCla $isFinal, $isPure, $asserts, + $acceptsNamedArguments, $selfOutType, $phpDocComment, $phpDocParameterOutTypes, diff --git a/src/Reflection/Php/PhpFunctionReflection.php b/src/Reflection/Php/PhpFunctionReflection.php index ed04458668..5303d38b0f 100644 --- a/src/Reflection/Php/PhpFunctionReflection.php +++ b/src/Reflection/Php/PhpFunctionReflection.php @@ -60,6 +60,7 @@ public function __construct( private ?string $filename, private ?bool $isPure, private Assertions $asserts, + private bool $acceptsNamedArguments, private ?string $phpDocComment, private array $phpDocParameterOutTypes, private array $phpDocParameterImmediatelyInvokedCallable, @@ -297,4 +298,9 @@ public function returnsByReference(): TrinaryLogic return TrinaryLogic::createFromBoolean($this->reflection->returnsReference()); } + public function acceptsNamedArguments(): bool + { + return $this->acceptsNamedArguments; + } + } diff --git a/src/Reflection/Php/PhpMethodReflection.php b/src/Reflection/Php/PhpMethodReflection.php index ce6306bada..5a75f85a8a 100644 --- a/src/Reflection/Php/PhpMethodReflection.php +++ b/src/Reflection/Php/PhpMethodReflection.php @@ -87,6 +87,7 @@ public function __construct( private bool $isFinal, private ?bool $isPure, private Assertions $asserts, + private bool $acceptsNamedArguments, private ?Type $selfOutType, private ?string $phpDocComment, private array $phpDocParameterOutTypes, @@ -456,6 +457,11 @@ public function getAsserts(): Assertions return $this->asserts; } + public function acceptsNamedArguments(): bool + { + return $this->declaringClass->acceptsNamedArguments() && $this->acceptsNamedArguments; + } + public function getSelfOutType(): ?Type { return $this->selfOutType; diff --git a/src/Reflection/Php/PhpMethodReflectionFactory.php b/src/Reflection/Php/PhpMethodReflectionFactory.php index 8da0dcb5c6..0745deee78 100644 --- a/src/Reflection/Php/PhpMethodReflectionFactory.php +++ b/src/Reflection/Php/PhpMethodReflectionFactory.php @@ -31,6 +31,7 @@ public function create( bool $isFinal, ?bool $isPure, Assertions $asserts, + bool $acceptsNamedArguments, ?Type $selfOutType, ?string $phpDocComment, array $phpDocParameterOutTypes, diff --git a/src/Reflection/ResolvedFunctionVariantWithCallable.php b/src/Reflection/ResolvedFunctionVariantWithCallable.php index c84670be53..ab121486a1 100644 --- a/src/Reflection/ResolvedFunctionVariantWithCallable.php +++ b/src/Reflection/ResolvedFunctionVariantWithCallable.php @@ -27,6 +27,7 @@ public function __construct( private array $impurePoints, private array $invalidateExpressions, private array $usedVariables, + private bool $acceptsNamedArguments, ) { } @@ -111,4 +112,9 @@ public function getUsedVariables(): array return $this->usedVariables; } + public function acceptsNamedArguments(): bool + { + return $this->acceptsNamedArguments; + } + } diff --git a/src/Reflection/ResolvedMethodReflection.php b/src/Reflection/ResolvedMethodReflection.php index 0f4cd81a03..69863e8666 100644 --- a/src/Reflection/ResolvedMethodReflection.php +++ b/src/Reflection/ResolvedMethodReflection.php @@ -170,6 +170,11 @@ public function getAsserts(): Assertions )); } + public function acceptsNamedArguments(): bool + { + return $this->reflection->acceptsNamedArguments(); + } + public function getSelfOutType(): ?Type { if ($this->selfOutType === false) { diff --git a/src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php b/src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php index 4b980e21d7..32a484c3c9 100644 --- a/src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php +++ b/src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php @@ -51,6 +51,7 @@ public function findFunctionReflection(string $functionName): ?NativeFunctionRef $asserts = Assertions::createEmpty(); $docComment = null; $returnsByReference = TrinaryLogic::createMaybe(); + $acceptsNamedArguments = true; try { $reflectionFunction = $this->reflector->reflectFunction($functionName); $reflectionFunctionAdapter = new ReflectionFunction($reflectionFunction); @@ -84,6 +85,7 @@ public function findFunctionReflection(string $functionName): ?NativeFunctionRef } $asserts = Assertions::createFromResolvedPhpDocBlock($phpDoc); $phpDocReturnType = $this->getReturnTypeFromPhpDoc($phpDoc); + $acceptsNamedArguments = $phpDoc->acceptsNamedArguments(); } $variantsByType = ['positional' => []]; @@ -148,6 +150,7 @@ public function findFunctionReflection(string $functionName): ?NativeFunctionRef $asserts, $docComment, $returnsByReference, + $acceptsNamedArguments, ); $this->functionMap[$lowerCasedFunctionName] = $functionReflection; diff --git a/src/Reflection/TrivialParametersAcceptor.php b/src/Reflection/TrivialParametersAcceptor.php index 51a89fc329..b6e638c979 100644 --- a/src/Reflection/TrivialParametersAcceptor.php +++ b/src/Reflection/TrivialParametersAcceptor.php @@ -94,4 +94,9 @@ public function getUsedVariables(): array return []; } + public function acceptsNamedArguments(): bool + { + return true; + } + } diff --git a/src/Reflection/Type/IntersectionTypeMethodReflection.php b/src/Reflection/Type/IntersectionTypeMethodReflection.php index 8698e6196c..c447875580 100644 --- a/src/Reflection/Type/IntersectionTypeMethodReflection.php +++ b/src/Reflection/Type/IntersectionTypeMethodReflection.php @@ -187,6 +187,16 @@ public function getAsserts(): Assertions return $assertions; } + public function acceptsNamedArguments(): bool + { + $accepts = true; + foreach ($this->methods as $method) { + $accepts = $accepts && $method->acceptsNamedArguments(); + } + + return $accepts; + } + public function getSelfOutType(): ?Type { return null; diff --git a/src/Reflection/Type/UnionTypeMethodReflection.php b/src/Reflection/Type/UnionTypeMethodReflection.php index 2982f71a62..3b2598c368 100644 --- a/src/Reflection/Type/UnionTypeMethodReflection.php +++ b/src/Reflection/Type/UnionTypeMethodReflection.php @@ -169,6 +169,16 @@ public function getAsserts(): Assertions return Assertions::createEmpty(); } + public function acceptsNamedArguments(): bool + { + $accepts = true; + foreach ($this->methods as $method) { + $accepts = $accepts && $method->acceptsNamedArguments(); + } + + return $accepts; + } + public function getSelfOutType(): ?Type { return null; diff --git a/src/Reflection/WrappedExtendedMethodReflection.php b/src/Reflection/WrappedExtendedMethodReflection.php index 2a953cf36b..78f31cdce6 100644 --- a/src/Reflection/WrappedExtendedMethodReflection.php +++ b/src/Reflection/WrappedExtendedMethodReflection.php @@ -137,6 +137,11 @@ public function getAsserts(): Assertions return Assertions::createEmpty(); } + public function acceptsNamedArguments(): bool + { + return $this->getDeclaringClass()->acceptsNamedArguments(); + } + public function getSelfOutType(): ?Type { return null; diff --git a/src/Rules/AttributesCheck.php b/src/Rules/AttributesCheck.php index cd549f67e3..e04381033e 100644 --- a/src/Rules/AttributesCheck.php +++ b/src/Rules/AttributesCheck.php @@ -151,8 +151,10 @@ public function check( 'Unknown parameter $%s in call to ' . $attributeClassName . ' constructor.', 'Return type of call to ' . $attributeClassName . ' constructor contains unresolvable type.', 'Parameter %s of attribute class ' . $attributeClassName . ' constructor contains unresolvable type.', + 'Attribute class ' . $attributeClassName . ' constructorinvoked with %s, but it\'s not allowed because of @no-named-arguments.', ], 'attribute', + $attributeConstructor->acceptsNamedArguments(), ); foreach ($parameterErrors as $error) { diff --git a/src/Rules/Classes/InstantiationRule.php b/src/Rules/Classes/InstantiationRule.php index e90aee6b80..8994a4754b 100644 --- a/src/Rules/Classes/InstantiationRule.php +++ b/src/Rules/Classes/InstantiationRule.php @@ -216,8 +216,10 @@ private function checkClassName(string $class, bool $isName, Node $node, Scope $ 'Unknown parameter $%s in call to ' . $classDisplayName . ' constructor.', 'Return type of call to ' . $classDisplayName . ' constructor contains unresolvable type.', 'Parameter %s of class ' . $classDisplayName . ' constructor contains unresolvable type.', + 'Class ' . $classDisplayName . ' constructor invoked with %s, but it\'s not allowed because of @no-named-arguments.', ], 'new', + $constructorReflection->acceptsNamedArguments(), )); } diff --git a/src/Rules/FunctionCallParametersCheck.php b/src/Rules/FunctionCallParametersCheck.php index 1925232132..4f0d2ae447 100644 --- a/src/Rules/FunctionCallParametersCheck.php +++ b/src/Rules/FunctionCallParametersCheck.php @@ -53,7 +53,7 @@ public function __construct( /** * @param Node\Expr\FuncCall|Node\Expr\MethodCall|Node\Expr\StaticCall|Node\Expr\New_ $funcCall - * @param array{0: string, 1: string, 2: string, 3: string, 4: string, 5: string, 6: string, 7: string, 8: string, 9: string, 10: string, 11: string, 12: string, 13?: string} $messages + * @param array{0: string, 1: string, 2: string, 3: string, 4: string, 5: string, 6: string, 7: string, 8: string, 9: string, 10: string, 11: string, 12: string, 13?: string, 14?: string} $messages * @param 'attribute'|'callable'|'method'|'staticMethod'|'function'|'new' $nodeType * @return list */ @@ -64,6 +64,7 @@ public function check( $funcCall, array $messages, string $nodeType = 'function', + bool $acceptsNamedArguments = true, ): array { $functionParametersMinCount = 0; @@ -289,6 +290,26 @@ public function check( } } + if (!$acceptsNamedArguments && $this->checkUnresolvableParameterTypes && isset($messages[14])) { + if ($argumentName !== null) { + $errors[] = RuleErrorBuilder::message(sprintf($messages[14], sprintf('named argument $%s', $argumentName))) + ->identifier('argument.named') + ->line($argumentLine) + ->nonIgnorable() + ->build(); + } elseif ($unpack) { + $unpackedArrayType = $scope->getType($argumentValue); + $hasStringKey = $unpackedArrayType->getIterableKeyType()->isString(); + if (!$hasStringKey->no()) { + $errors[] = RuleErrorBuilder::message(sprintf($messages[14], sprintf('unpacked array with %s', $hasStringKey->yes() ? 'string key' : 'possibly string key'))) + ->identifier('argument.named') + ->line($argumentLine) + ->nonIgnorable() + ->build(); + } + } + } + if ($this->checkArgumentTypes) { $parameterType = TypeUtils::resolveLateResolvableTypes($parameter->getType()); diff --git a/src/Rules/Functions/CallCallablesRule.php b/src/Rules/Functions/CallCallablesRule.php index c21790b738..d8fb352e52 100644 --- a/src/Rules/Functions/CallCallablesRule.php +++ b/src/Rules/Functions/CallCallablesRule.php @@ -79,6 +79,11 @@ public function processNode( $parametersAcceptors = $type->getCallableParametersAcceptors($scope); $messages = []; + $acceptsNamedArguments = true; + foreach ($parametersAcceptors as $parametersAcceptor) { + $acceptsNamedArguments = $acceptsNamedArguments && $parametersAcceptor->acceptsNamedArguments(); + } + if ( count($parametersAcceptors) === 1 && $parametersAcceptors[0] instanceof InaccessibleMethod @@ -127,8 +132,10 @@ public function processNode( 'Unknown parameter $%s in call to ' . $callableDescription . '.', 'Return type of call to ' . $callableDescription . ' contains unresolvable type.', 'Parameter %s of ' . $callableDescription . ' contains unresolvable type.', + ucfirst($callableDescription) . ' invoked with %s, but it\'s not allowed because of @no-named-arguments.', ], 'callable', + $acceptsNamedArguments, ), ); } diff --git a/src/Rules/Functions/CallToFunctionParametersRule.php b/src/Rules/Functions/CallToFunctionParametersRule.php index 24dc76e186..d1ca216791 100644 --- a/src/Rules/Functions/CallToFunctionParametersRule.php +++ b/src/Rules/Functions/CallToFunctionParametersRule.php @@ -64,8 +64,10 @@ public function processNode(Node $node, Scope $scope): array 'Unknown parameter $%s in call to function ' . $functionName . '.', 'Return type of call to function ' . $functionName . ' contains unresolvable type.', 'Parameter %s of function ' . $functionName . ' contains unresolvable type.', + 'Function ' . $functionName . ' invoked with %s, but it\'s not allowed because of @no-named-arguments.', ], 'function', + $function->acceptsNamedArguments(), ); } diff --git a/src/Rules/Functions/CallUserFuncRule.php b/src/Rules/Functions/CallUserFuncRule.php index 415e04b748..c4030961cf 100644 --- a/src/Rules/Functions/CallUserFuncRule.php +++ b/src/Rules/Functions/CallUserFuncRule.php @@ -56,7 +56,7 @@ public function processNode(Node $node, Scope $scope): array if ($result === null) { return []; } - [$parametersAcceptor, $funcCall] = $result; + [$parametersAcceptor, $funcCall, $acceptsNamedArguments] = $result; $callableDescription = 'callable passed to call_user_func()'; @@ -75,7 +75,8 @@ public function processNode(Node $node, Scope $scope): array 'Unknown parameter $%s in call to ' . $callableDescription . '.', 'Return type of call to ' . $callableDescription . ' contains unresolvable type.', 'Parameter %s of ' . $callableDescription . ' contains unresolvable type.', - ]); + ucfirst($callableDescription) . ' invoked with %s, but it\'s not allowed because of @no-named-arguments.', + ], 'function', $acceptsNamedArguments); } } diff --git a/src/Rules/Methods/CallMethodsRule.php b/src/Rules/Methods/CallMethodsRule.php index 67eee30157..4f45dbf9fd 100644 --- a/src/Rules/Methods/CallMethodsRule.php +++ b/src/Rules/Methods/CallMethodsRule.php @@ -70,8 +70,10 @@ public function processNode(Node $node, Scope $scope): array 'Unknown parameter $%s in call to method ' . $messagesMethodName . '.', 'Return type of call to method ' . $messagesMethodName . ' contains unresolvable type.', 'Parameter %s of method ' . $messagesMethodName . ' contains unresolvable type.', + 'Method ' . $messagesMethodName . ' invoked with %s, but it\'s not allowed because of @no-named-arguments.', ], 'method', + $methodReflection->acceptsNamedArguments(), )); } diff --git a/src/Rules/Methods/CallStaticMethodsRule.php b/src/Rules/Methods/CallStaticMethodsRule.php index 1706bdb76e..33612ff02c 100644 --- a/src/Rules/Methods/CallStaticMethodsRule.php +++ b/src/Rules/Methods/CallStaticMethodsRule.php @@ -78,8 +78,10 @@ public function processNode(Node $node, Scope $scope): array 'Unknown parameter $%s in call to ' . $lowercasedMethodName . '.', 'Return type of call to ' . $lowercasedMethodName . ' contains unresolvable type.', 'Parameter %s of ' . $lowercasedMethodName . ' contains unresolvable type.', + $displayMethodName . ' invoked with %s, but it\'s not allowed because of @no-named-arguments.', ], 'staticMethod', + $method->acceptsNamedArguments(), )); return $errors; diff --git a/src/Rules/RuleLevelHelper.php b/src/Rules/RuleLevelHelper.php index 80ef4bccdc..0f15683606 100644 --- a/src/Rules/RuleLevelHelper.php +++ b/src/Rules/RuleLevelHelper.php @@ -124,6 +124,9 @@ private function transformAcceptedType(Type $acceptingType, Type $acceptedType): $acceptedType->getTemplateTags(), $acceptedType->getThrowPoints(), $acceptedType->getImpurePoints(), + $acceptedType->getInvalidateExpressions(), + $acceptedType->getUsedVariables(), + $acceptedType->acceptsNamedArguments(), ); } diff --git a/src/Type/CallableType.php b/src/Type/CallableType.php index e51cf45631..ce30d8983c 100644 --- a/src/Type/CallableType.php +++ b/src/Type/CallableType.php @@ -294,6 +294,11 @@ public function getUsedVariables(): array return []; } + public function acceptsNamedArguments(): bool + { + return true; + } + public function toNumber(): Type { return new ErrorType(); diff --git a/src/Type/ClosureType.php b/src/Type/ClosureType.php index 93b9989102..6d987f6342 100644 --- a/src/Type/ClosureType.php +++ b/src/Type/ClosureType.php @@ -99,6 +99,7 @@ public function __construct( ?array $impurePoints = null, private array $invalidateExpressions = [], private array $usedVariables = [], + private bool $acceptsNamedArguments = true, ) { $this->parameters = $parameters ?? []; @@ -407,6 +408,11 @@ public function getUsedVariables(): array return $this->usedVariables; } + public function acceptsNamedArguments(): bool + { + return $this->acceptsNamedArguments; + } + public function isCloneable(): TrinaryLogic { return TrinaryLogic::createYes(); @@ -562,6 +568,7 @@ public function traverse(callable $cb): Type $this->impurePoints, $this->invalidateExpressions, $this->usedVariables, + $this->acceptsNamedArguments, ); } @@ -611,6 +618,7 @@ public function traverseSimultaneously(Type $right, callable $cb): Type $this->impurePoints, $this->invalidateExpressions, $this->usedVariables, + $this->acceptsNamedArguments, ); } @@ -780,6 +788,7 @@ public static function __set_state(array $properties): Type $properties['impurePoints'], $properties['invalidateExpressions'], $properties['usedVariables'], + $properties['acceptsNamedArguments'], ); } diff --git a/src/Type/Php/ClosureFromCallableDynamicReturnTypeExtension.php b/src/Type/Php/ClosureFromCallableDynamicReturnTypeExtension.php index 45ff96e2c9..d392b20500 100644 --- a/src/Type/Php/ClosureFromCallableDynamicReturnTypeExtension.php +++ b/src/Type/Php/ClosureFromCallableDynamicReturnTypeExtension.php @@ -48,6 +48,12 @@ public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, $variant->getTemplateTypeMap(), $variant->getResolvedTemplateTypeMap(), $variant instanceof ParametersAcceptorWithPhpDocs ? $variant->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), + [], + $variant->getThrowPoints(), + $variant->getImpurePoints(), + $variant->getInvalidateExpressions(), + $variant->getUsedVariables(), + $variant->acceptsNamedArguments(), ); } diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index 90859848d0..e1c0c8db09 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -1716,4 +1716,30 @@ public function testCountArrayShift(): void $this->analyse([__DIR__ . '/data/count-array-shift.php'], $errors); } + public function testNoNamedArguments(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/no-named-arguments.php'], [ + [ + 'Function NoNamedArgumentsFunction\\foo invoked with named argument $i, but it\'s not allowed because of @no-named-arguments.', + 14, + ], + [ + 'Function NoNamedArgumentsFunction\foo invoked with unpacked array with string key, but it\'s not allowed because of @no-named-arguments.', + 24, + ], + [ + 'Function NoNamedArgumentsFunction\foo invoked with unpacked array with possibly string key, but it\'s not allowed because of @no-named-arguments.', + 25, + ], + [ + 'Function NoNamedArgumentsFunction\\foo invoked with named argument $i, but it\'s not allowed because of @no-named-arguments.', + 29, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Functions/CallUserFuncRuleTest.php b/tests/PHPStan/Rules/Functions/CallUserFuncRuleTest.php index 9eed739399..d10eee8e48 100644 --- a/tests/PHPStan/Rules/Functions/CallUserFuncRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallUserFuncRuleTest.php @@ -83,4 +83,30 @@ public function testBug7057(): void $this->analyse([__DIR__ . '/data/bug-7057.php'], []); } + public function testNoNamedArguments(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/no-named-arguments-call-user-func.php'], [ + [ + 'Callable passed to call_user_func() invoked with named argument $i, but it\'s not allowed because of @no-named-arguments.', + 29, + ], + [ + 'Callable passed to call_user_func() invoked with named argument $i, but it\'s not allowed because of @no-named-arguments.', + 30, + ], + [ + 'Callable passed to call_user_func() invoked with named argument $i, but it\'s not allowed because of @no-named-arguments.', + 31, + ], + [ + 'Callable passed to call_user_func() invoked with named argument $i, but it\'s not allowed because of @no-named-arguments.', + 32, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Functions/data/no-named-arguments-call-user-func.php b/tests/PHPStan/Rules/Functions/data/no-named-arguments-call-user-func.php new file mode 100644 index 0000000000..d385b739c9 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/no-named-arguments-call-user-func.php @@ -0,0 +1,33 @@ += 8.1 + +namespace NoNamedArgumentsCallUserFunc; + +use function call_user_func; + +/** + * @no-named-arguments + */ +function foo(int $i): void +{ + +} + +class Foo +{ + + /** + * @no-named-arguments + */ + public function doFoo(int $i): void + { + + } + +} + +function (Foo $f): void { + call_user_func(foo(...), i: 1); + call_user_func('NoNamedArgumentsCallUserFunc\\foo', i: 1); + call_user_func([$f, 'doFoo'], i: 1); + call_user_func($f->doFoo(...), i: 1); +}; diff --git a/tests/PHPStan/Rules/Functions/data/no-named-arguments.php b/tests/PHPStan/Rules/Functions/data/no-named-arguments.php new file mode 100644 index 0000000000..843530132e --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/no-named-arguments.php @@ -0,0 +1,30 @@ += 8.0 + +namespace NoNamedArgumentsFunction; + +/** + * @no-named-arguments + */ +function foo(int $i): void +{ + +} + +function (): void { + foo(i: 5); +}; + +/** + * @param array $a + * @param array $b + * @param array $c + */ +function bar(array $a, array $b, array $c): void +{ + foo(...$a); + foo(...$b); + foo(...$c); + + foo(...[0 => 1]); + foo(...['i' => 1]); +} diff --git a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php index 8877e53e31..255d9f9c02 100644 --- a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php @@ -3322,4 +3322,27 @@ public function testClosureParameterGenerics(): void $this->analyse([__DIR__ . '/data/closure-parameter-generics.php'], []); } + public function testNoNamedArguments(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + + $this->analyse([__DIR__ . '/data/no-named-arguments.php'], [ + [ + 'Method NoNamedArgumentsMethod\Foo::doFoo() invoked with named argument $i, but it\'s not allowed because of @no-named-arguments.', + 32, + ], + [ + 'Method NoNamedArgumentsMethod\Bar::doFoo() invoked with named argument $i, but it\'s not allowed because of @no-named-arguments.', + 33, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Methods/data/no-named-arguments.php b/tests/PHPStan/Rules/Methods/data/no-named-arguments.php new file mode 100644 index 0000000000..e28e91d1d8 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/no-named-arguments.php @@ -0,0 +1,34 @@ += 8.0 + +namespace NoNamedArgumentsMethod; + +class Foo +{ + + /** + * @no-named-arguments + */ + public function doFoo(int $i): void + { + + } + +} + +/** + * @no-named-arguments + */ +class Bar +{ + + public function doFoo(int $i): void + { + + } + +} + +function (Foo $f, Bar $b): void { + $f->doFoo(i: 1); + $b->doFoo(i: 1); +};