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