1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Analyser;
4:
5: use PhpParser\Node\Arg;
6: use PhpParser\Node\Expr\FuncCall;
7: use PhpParser\Node\Expr\MethodCall;
8: use PhpParser\Node\Expr\New_;
9: use PhpParser\Node\Expr\StaticCall;
10: use PHPStan\Node\Expr\TypeExpr;
11: use PHPStan\Reflection\ParametersAcceptor;
12: use PHPStan\Reflection\ParametersAcceptorSelector;
13: use PHPStan\ShouldNotHappenException;
14: use PHPStan\Type\Constant\ConstantArrayType;
15: use function array_key_exists;
16: use function array_keys;
17: use function count;
18: use function ksort;
19: use function max;
20:
21: /**
22: * @api
23: */
24: final class ArgumentsNormalizer
25: {
26:
27: public const ORIGINAL_ARG_ATTRIBUTE = 'originalArg';
28:
29: /**
30: * @return array{ParametersAcceptor, FuncCall, bool}|null
31: */
32: public static function reorderCallUserFuncArguments(
33: FuncCall $callUserFuncCall,
34: Scope $scope,
35: ): ?array
36: {
37: $args = $callUserFuncCall->getArgs();
38: if (count($args) < 1) {
39: return null;
40: }
41:
42: $passThruArgs = [];
43: $callbackArg = null;
44: foreach ($args as $i => $arg) {
45: if ($callbackArg === null) {
46: if ($arg->name === null && $i === 0) {
47: $callbackArg = $arg;
48: continue;
49: }
50: if ($arg->name !== null && $arg->name->toString() === 'callback') {
51: $callbackArg = $arg;
52: continue;
53: }
54: }
55:
56: $passThruArgs[] = $arg;
57: }
58:
59: if ($callbackArg === null) {
60: return null;
61: }
62:
63: $calledOnType = $scope->getType($callbackArg->value);
64: if (!$calledOnType->isCallable()->yes()) {
65: return null;
66: }
67:
68: $callableParametersAcceptors = $calledOnType->getCallableParametersAcceptors($scope);
69: $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs(
70: $scope,
71: $passThruArgs,
72: $callableParametersAcceptors,
73: null,
74: );
75:
76: $acceptsNamedArguments = true;
77: foreach ($callableParametersAcceptors as $callableParametersAcceptor) {
78: $acceptsNamedArguments = $acceptsNamedArguments && $callableParametersAcceptor->acceptsNamedArguments();
79: }
80:
81: return [$parametersAcceptor, new FuncCall(
82: $callbackArg->value,
83: $passThruArgs,
84: $callUserFuncCall->getAttributes(),
85: ), $acceptsNamedArguments];
86: }
87:
88: public static function reorderFuncArguments(
89: ParametersAcceptor $parametersAcceptor,
90: FuncCall $functionCall,
91: ): ?FuncCall
92: {
93: $reorderedArgs = self::reorderArgs($parametersAcceptor, $functionCall->getArgs());
94:
95: if ($reorderedArgs === null) {
96: return null;
97: }
98:
99: return new FuncCall(
100: $functionCall->name,
101: $reorderedArgs,
102: $functionCall->getAttributes(),
103: );
104: }
105:
106: public static function reorderMethodArguments(
107: ParametersAcceptor $parametersAcceptor,
108: MethodCall $methodCall,
109: ): ?MethodCall
110: {
111: $reorderedArgs = self::reorderArgs($parametersAcceptor, $methodCall->getArgs());
112:
113: if ($reorderedArgs === null) {
114: return null;
115: }
116:
117: return new MethodCall(
118: $methodCall->var,
119: $methodCall->name,
120: $reorderedArgs,
121: $methodCall->getAttributes(),
122: );
123: }
124:
125: public static function reorderStaticCallArguments(
126: ParametersAcceptor $parametersAcceptor,
127: StaticCall $staticCall,
128: ): ?StaticCall
129: {
130: $reorderedArgs = self::reorderArgs($parametersAcceptor, $staticCall->getArgs());
131:
132: if ($reorderedArgs === null) {
133: return null;
134: }
135:
136: return new StaticCall(
137: $staticCall->class,
138: $staticCall->name,
139: $reorderedArgs,
140: $staticCall->getAttributes(),
141: );
142: }
143:
144: public static function reorderNewArguments(
145: ParametersAcceptor $parametersAcceptor,
146: New_ $new,
147: ): ?New_
148: {
149: $reorderedArgs = self::reorderArgs($parametersAcceptor, $new->getArgs());
150:
151: if ($reorderedArgs === null) {
152: return null;
153: }
154:
155: return new New_(
156: $new->class,
157: $reorderedArgs,
158: $new->getAttributes(),
159: );
160: }
161:
162: /**
163: * @param Arg[] $callArgs
164: * @return ?array<int, Arg>
165: */
166: public static function reorderArgs(ParametersAcceptor $parametersAcceptor, array $callArgs): ?array
167: {
168: if (count($callArgs) === 0) {
169: return [];
170: }
171:
172: $signatureParameters = $parametersAcceptor->getParameters();
173:
174: $hasNamedArgs = false;
175: foreach ($callArgs as $arg) {
176: if ($arg->name !== null) {
177: $hasNamedArgs = true;
178: break;
179: }
180: }
181: if (!$hasNamedArgs) {
182: return $callArgs;
183: }
184:
185: $hasVariadic = false;
186: $argumentPositions = [];
187: foreach ($signatureParameters as $i => $parameter) {
188: if ($hasVariadic) {
189: // variadic parameter must be last
190: return null;
191: }
192:
193: $hasVariadic = $parameter->isVariadic();
194: $argumentPositions[$parameter->getName()] = $i;
195: }
196:
197: $reorderedArgs = [];
198: $additionalNamedArgs = [];
199: $appendArgs = [];
200: foreach ($callArgs as $i => $arg) {
201: if ($arg->name === null) {
202: // add regular args as is
203: $reorderedArgs[$i] = $arg;
204: } elseif (array_key_exists($arg->name->toString(), $argumentPositions)) {
205: $argName = $arg->name->toString();
206: // order named args into the position the signature expects them
207: $attributes = $arg->getAttributes();
208: $attributes[self::ORIGINAL_ARG_ATTRIBUTE] = $arg;
209: $reorderedArgs[$argumentPositions[$argName]] = new Arg(
210: $arg->value,
211: $arg->byRef,
212: $arg->unpack,
213: $attributes,
214: null,
215: );
216: } else {
217: if (!$hasVariadic) {
218: $attributes = $arg->getAttributes();
219: $attributes[self::ORIGINAL_ARG_ATTRIBUTE] = $arg;
220: $appendArgs[] = new Arg(
221: $arg->value,
222: $arg->byRef,
223: $arg->unpack,
224: $attributes,
225: null,
226: );
227: continue;
228: }
229:
230: $attributes = $arg->getAttributes();
231: $attributes[self::ORIGINAL_ARG_ATTRIBUTE] = $arg;
232: $additionalNamedArgs[] = new Arg(
233: $arg->value,
234: $arg->byRef,
235: $arg->unpack,
236: $attributes,
237: null,
238: );
239: }
240: }
241:
242: // replace variadic parameter with additional named args, except if it is already set
243: $additionalNamedArgsOffset = count($argumentPositions) - 1;
244: if (array_key_exists($additionalNamedArgsOffset, $reorderedArgs)) {
245: $additionalNamedArgsOffset++;
246: }
247:
248: foreach ($additionalNamedArgs as $i => $additionalNamedArg) {
249: $reorderedArgs[$additionalNamedArgsOffset + $i] = $additionalNamedArg;
250: }
251:
252: if (count($reorderedArgs) === 0) {
253: foreach ($appendArgs as $arg) {
254: $reorderedArgs[] = $arg;
255: }
256: return $reorderedArgs;
257: }
258:
259: // fill up all holes with default values until the last given argument
260: for ($j = 0; $j < max(array_keys($reorderedArgs)); $j++) {
261: if (array_key_exists($j, $reorderedArgs)) {
262: continue;
263: }
264: if (!array_key_exists($j, $signatureParameters)) {
265: throw new ShouldNotHappenException('Parameter signatures cannot have holes');
266: }
267:
268: $parameter = $signatureParameters[$j];
269:
270: // we can only fill up optional parameters with default values
271: if (!$parameter->isOptional()) {
272: return null;
273: }
274:
275: $defaultValue = $parameter->getDefaultValue();
276: if ($defaultValue === null) {
277: if (!$parameter->isVariadic()) {
278: throw new ShouldNotHappenException('An optional parameter must have a default value');
279: }
280: $defaultValue = new ConstantArrayType([], []);
281: }
282:
283: $reorderedArgs[$j] = new Arg(
284: new TypeExpr($defaultValue),
285: );
286: }
287:
288: ksort($reorderedArgs);
289:
290: foreach ($appendArgs as $arg) {
291: $reorderedArgs[] = $arg;
292: }
293:
294: return $reorderedArgs;
295: }
296:
297: }
298: