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