1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Analyser;
4:
5: use PhpParser\Node as PhpParserNode;
6: use PhpParser\Node\Arg;
7: use PhpParser\Node\Expr\Array_;
8: use PhpParser\Node\Expr\FuncCall;
9: use PhpParser\Node\Expr\MethodCall;
10: use PhpParser\Node\Expr\New_;
11: use PhpParser\Node\Expr\StaticCall;
12: use PhpParser\Node\Identifier;
13: use PhpParser\Node\Scalar\Int_;
14: use PhpParser\Node\Scalar\String_;
15: use PHPStan\Node\Expr\TypeExpr;
16: use PHPStan\Node\Printer\ExprPrinter;
17: use PHPStan\Reflection\ParametersAcceptor;
18: use PHPStan\Reflection\ParametersAcceptorSelector;
19: use PHPStan\ShouldNotHappenException;
20: use PHPStan\TrinaryLogic;
21: use PHPStan\Type\Constant\ConstantArrayType;
22: use function array_is_list;
23: use function array_key_exists;
24: use function array_keys;
25: use function array_values;
26: use function count;
27: use function is_string;
28: use function key;
29: use function ksort;
30: use function max;
31: use function sprintf;
32:
33: /**
34: * @api
35: */
36: final class ArgumentsNormalizer
37: {
38:
39: public const ORIGINAL_ARG_ATTRIBUTE = 'originalArg';
40:
41: /**
42: * @return array{ParametersAcceptor, FuncCall, TrinaryLogic}|null
43: */
44: public static function reorderCallUserFuncArguments(
45: FuncCall $callUserFuncCall,
46: Scope $scope,
47: ): ?array
48: {
49: $args = $callUserFuncCall->getArgs();
50: if (count($args) < 1) {
51: return null;
52: }
53:
54: $passThruArgs = [];
55: $callbackArg = null;
56: foreach ($args as $i => $arg) {
57: if ($callbackArg === null) {
58: if ($arg->name === null && $i === 0) {
59: $callbackArg = $arg;
60: continue;
61: }
62: if ($arg->name !== null && $arg->name->toString() === 'callback') {
63: $callbackArg = $arg;
64: continue;
65: }
66: }
67:
68: $passThruArgs[] = $arg;
69: }
70:
71: if ($callbackArg === null) {
72: return null;
73: }
74:
75: $calledOnType = $scope->getType($callbackArg->value);
76: if (!$calledOnType->isCallable()->yes()) {
77: return null;
78: }
79:
80: $callableParametersAcceptors = $calledOnType->getCallableParametersAcceptors($scope);
81: $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs(
82: $scope,
83: $passThruArgs,
84: $callableParametersAcceptors,
85: null,
86: );
87:
88: $acceptsNamedArguments = TrinaryLogic::createYes();
89: foreach ($callableParametersAcceptors as $callableParametersAcceptor) {
90: $acceptsNamedArguments = $acceptsNamedArguments->and($callableParametersAcceptor->acceptsNamedArguments());
91: }
92:
93: return [$parametersAcceptor, new FuncCall(
94: $callbackArg->value,
95: $passThruArgs,
96: self::attributesWithoutPrintedForm($callUserFuncCall),
97: ), $acceptsNamedArguments];
98: }
99:
100: /**
101: * @return array{ParametersAcceptor, FuncCall, TrinaryLogic}|null
102: */
103: public static function reorderCallUserFuncArrayArguments(
104: FuncCall $callUserFuncArrayCall,
105: Scope $scope,
106: ): ?array
107: {
108: $args = $callUserFuncArrayCall->getArgs();
109: if (count($args) < 2) {
110: return null;
111: }
112:
113: $callbackArg = null;
114: $argsArrayArg = null;
115: foreach ($args as $i => $arg) {
116: if ($callbackArg === null) {
117: if ($arg->name === null && $i === 0) {
118: $callbackArg = $arg;
119: continue;
120: }
121: if ($arg->name !== null && $arg->name->toString() === 'callback') {
122: $callbackArg = $arg;
123: continue;
124: }
125: }
126:
127: if ($argsArrayArg !== null) {
128: continue;
129: }
130: if ($arg->name === null && $i === 1) {
131: $argsArrayArg = $arg;
132: continue;
133: }
134: if ($arg->name === null || $arg->name->toString() !== 'args') {
135: continue;
136: }
137: $argsArrayArg = $arg;
138: }
139:
140: if ($callbackArg === null || $argsArrayArg === null) {
141: return null;
142: }
143:
144: if (!$argsArrayArg->value instanceof Array_) {
145: return null;
146: }
147:
148: $passThruArgs = [];
149: foreach ($argsArrayArg->value->items as $item) {
150: $key = null;
151: if ($item->key instanceof String_) {
152: /** @var int|string $key */
153: $key = key([$item->key->value => null]);
154: if ($key === '') {
155: return null;
156: }
157: } elseif ($item->key !== null && !$item->key instanceof Int_) {
158: // Dynamic key, we cannot be sure.
159: return null;
160: }
161:
162: $passThruArgs[] = new Arg(
163: $item->value,
164: $item->byRef,
165: $item->unpack,
166: self::attributesWithoutPrintedForm($item),
167: is_string($key) ? new Identifier($key) : null,
168: );
169: }
170:
171: $calledOnType = $scope->getType($callbackArg->value);
172: if (!$calledOnType->isCallable()->yes()) {
173: return null;
174: }
175:
176: $callableParametersAcceptors = $calledOnType->getCallableParametersAcceptors($scope);
177: $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs(
178: $scope,
179: $passThruArgs,
180: $callableParametersAcceptors,
181: null,
182: );
183:
184: $acceptsNamedArguments = TrinaryLogic::createYes();
185: foreach ($callableParametersAcceptors as $callableParametersAcceptor) {
186: $acceptsNamedArguments = $acceptsNamedArguments->and($callableParametersAcceptor->acceptsNamedArguments());
187: }
188:
189: return [$parametersAcceptor, new FuncCall(
190: $callbackArg->value,
191: $passThruArgs,
192: self::attributesWithoutPrintedForm($callUserFuncArrayCall),
193: ), $acceptsNamedArguments];
194: }
195:
196: public static function reorderFuncArguments(
197: ParametersAcceptor $parametersAcceptor,
198: FuncCall $functionCall,
199: ): ?FuncCall
200: {
201: $args = $functionCall->getArgs();
202: $reorderedArgs = self::reorderArgs($parametersAcceptor, $args);
203:
204: if ($reorderedArgs === null) {
205: return null;
206: }
207:
208: // return identical object if not reordered, as TypeSpecifier relies on object identity
209: if ($reorderedArgs === $args) {
210: return $functionCall;
211: }
212:
213: return new FuncCall(
214: $functionCall->name,
215: $reorderedArgs,
216: self::attributesWithoutPrintedForm($functionCall),
217: );
218: }
219:
220: public static function reorderMethodArguments(
221: ParametersAcceptor $parametersAcceptor,
222: MethodCall $methodCall,
223: ): ?MethodCall
224: {
225: $args = $methodCall->getArgs();
226: $reorderedArgs = self::reorderArgs($parametersAcceptor, $args);
227:
228: if ($reorderedArgs === null) {
229: return null;
230: }
231:
232: // return identical object if not reordered, as TypeSpecifier relies on object identity
233: if ($reorderedArgs === $args) {
234: return $methodCall;
235: }
236:
237: return new MethodCall(
238: $methodCall->var,
239: $methodCall->name,
240: $reorderedArgs,
241: self::attributesWithoutPrintedForm($methodCall),
242: );
243: }
244:
245: public static function reorderStaticCallArguments(
246: ParametersAcceptor $parametersAcceptor,
247: StaticCall $staticCall,
248: ): ?StaticCall
249: {
250: $args = $staticCall->getArgs();
251: $reorderedArgs = self::reorderArgs($parametersAcceptor, $args);
252:
253: if ($reorderedArgs === null) {
254: return null;
255: }
256:
257: // return identical object if not reordered, as TypeSpecifier relies on object identity
258: if ($reorderedArgs === $args) {
259: return $staticCall;
260: }
261:
262: return new StaticCall(
263: $staticCall->class,
264: $staticCall->name,
265: $reorderedArgs,
266: self::attributesWithoutPrintedForm($staticCall),
267: );
268: }
269:
270: public static function reorderNewArguments(
271: ParametersAcceptor $parametersAcceptor,
272: New_ $new,
273: ): ?New_
274: {
275: $args = $new->getArgs();
276: $reorderedArgs = self::reorderArgs($parametersAcceptor, $args);
277:
278: if ($reorderedArgs === null) {
279: return null;
280: }
281:
282: // return identical object if not reordered, as TypeSpecifier relies on object identity
283: if ($reorderedArgs === $args) {
284: return $new;
285: }
286:
287: return new New_(
288: $new->class,
289: $reorderedArgs,
290: self::attributesWithoutPrintedForm($new),
291: );
292: }
293:
294: /**
295: * @param Arg[] $callArgs
296: * @return ?list<Arg>
297: */
298: public static function reorderArgs(ParametersAcceptor $parametersAcceptor, array $callArgs): ?array
299: {
300: if (count($callArgs) === 0) {
301: return [];
302: }
303:
304: $hasNamedArgs = false;
305: foreach ($callArgs as $arg) {
306: if ($arg->name !== null) {
307: $hasNamedArgs = true;
308: break;
309: }
310: }
311: if (!$hasNamedArgs) {
312: return array_values($callArgs);
313: }
314:
315: $hasVariadic = false;
316: $argumentPositions = [];
317: $signatureParameters = $parametersAcceptor->getParameters();
318: foreach ($signatureParameters as $i => $parameter) {
319: if ($hasVariadic) {
320: // variadic parameter must be last
321: return null;
322: }
323:
324: $hasVariadic = $parameter->isVariadic();
325: $argumentPositions[$parameter->getName()] = $i;
326: }
327:
328: $reorderedArgs = [];
329: $additionalNamedArgs = [];
330: $appendArgs = [];
331: foreach ($callArgs as $i => $arg) {
332: if ($arg->name === null) {
333: // add regular args as is
334:
335: $attributes = $arg->getAttributes();
336: $attributes[self::ORIGINAL_ARG_ATTRIBUTE] = $arg;
337: $reorderedArgs[$i] = new Arg(
338: $arg->value,
339: $arg->byRef,
340: $arg->unpack,
341: $attributes,
342: null,
343: );
344: } elseif (array_key_exists($arg->name->toString(), $argumentPositions)) {
345: $argName = $arg->name->toString();
346: // order named args into the position the signature expects them
347: $attributes = $arg->getAttributes();
348: $attributes[self::ORIGINAL_ARG_ATTRIBUTE] = $arg;
349: if (array_key_exists($argumentPositions[$argName], $reorderedArgs)) {
350: continue;
351: }
352: $reorderedArgs[$argumentPositions[$argName]] = new Arg(
353: $arg->value,
354: $arg->byRef,
355: $arg->unpack,
356: $attributes,
357: null,
358: );
359: } else {
360: if (!$hasVariadic) {
361: $attributes = $arg->getAttributes();
362: $attributes[self::ORIGINAL_ARG_ATTRIBUTE] = $arg;
363: $appendArgs[] = new Arg(
364: $arg->value,
365: $arg->byRef,
366: $arg->unpack,
367: $attributes,
368: null,
369: );
370: continue;
371: }
372:
373: $attributes = $arg->getAttributes();
374: $attributes[self::ORIGINAL_ARG_ATTRIBUTE] = $arg;
375: $additionalNamedArgs[] = new Arg(
376: $arg->value,
377: $arg->byRef,
378: $arg->unpack,
379: $attributes,
380: null,
381: );
382: }
383: }
384:
385: // replace variadic parameter with additional named args, except if it is already set
386: $additionalNamedArgsOffset = count($argumentPositions) - 1;
387: if (array_key_exists($additionalNamedArgsOffset, $reorderedArgs)) {
388: $additionalNamedArgsOffset++;
389: }
390:
391: foreach ($additionalNamedArgs as $i => $additionalNamedArg) {
392: $reorderedArgs[$additionalNamedArgsOffset + $i] = $additionalNamedArg;
393: }
394:
395: if (count($reorderedArgs) === 0) {
396: foreach ($appendArgs as $arg) {
397: $reorderedArgs[] = $arg;
398: }
399: return $reorderedArgs;
400: }
401:
402: // fill up all holes with default values until the last given argument
403: for ($j = 0; $j < max(array_keys($reorderedArgs)); $j++) {
404: if (array_key_exists($j, $reorderedArgs)) {
405: continue;
406: }
407: if (!array_key_exists($j, $signatureParameters)) {
408: return null;
409: }
410:
411: $parameter = $signatureParameters[$j];
412:
413: // we can only fill up optional parameters with default values
414: if (!$parameter->isOptional()) {
415: return null;
416: }
417:
418: $defaultValue = $parameter->getDefaultValue();
419: if ($defaultValue === null) {
420: if (!$parameter->isVariadic()) {
421: throw new ShouldNotHappenException(sprintf('An optional parameter $%s must have a default value', $parameter->getName()));
422: }
423: $defaultValue = new ConstantArrayType([], []);
424: }
425:
426: $reorderedArgs[$j] = new Arg(
427: new TypeExpr($defaultValue),
428: );
429: }
430:
431: ksort($reorderedArgs);
432:
433: foreach ($appendArgs as $arg) {
434: $reorderedArgs[] = $arg;
435: }
436:
437: if (!array_is_list($reorderedArgs)) {
438: $reorderedArgs = array_values($reorderedArgs);
439: }
440:
441: return $reorderedArgs;
442: }
443:
444: /**
445: * The printed form of an expression is derived from its own arguments, so
446: * it must not travel to a node whose arguments were just reordered — the
447: * normalized call would report the original's expression key, named
448: * arguments and all.
449: *
450: * @return array<string, mixed>
451: */
452: private static function attributesWithoutPrintedForm(PhpParserNode $node): array
453: {
454: $attributes = $node->getAttributes();
455: unset($attributes[ExprPrinter::ATTRIBUTE_CACHE_KEY]);
456:
457: return $attributes;
458: }
459:
460: }
461: