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