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: $reorderedArgs = self::reorderArgs($parametersAcceptor, $functionCall->getArgs());
98:
99: if ($reorderedArgs === null) {
100: return null;
101: }
102:
103: // return identical object if not reordered, as TypeSpecifier relies on object identity
104: if ($reorderedArgs === $functionCall->getArgs()) {
105: return $functionCall;
106: }
107:
108: return new FuncCall(
109: $functionCall->name,
110: $reorderedArgs,
111: $functionCall->getAttributes(),
112: );
113: }
114:
115: public static function reorderMethodArguments(
116: ParametersAcceptor $parametersAcceptor,
117: MethodCall $methodCall,
118: ): ?MethodCall
119: {
120: $reorderedArgs = self::reorderArgs($parametersAcceptor, $methodCall->getArgs());
121:
122: if ($reorderedArgs === null) {
123: return null;
124: }
125:
126: // return identical object if not reordered, as TypeSpecifier relies on object identity
127: if ($reorderedArgs === $methodCall->getArgs()) {
128: return $methodCall;
129: }
130:
131: return new MethodCall(
132: $methodCall->var,
133: $methodCall->name,
134: $reorderedArgs,
135: $methodCall->getAttributes(),
136: );
137: }
138:
139: public static function reorderStaticCallArguments(
140: ParametersAcceptor $parametersAcceptor,
141: StaticCall $staticCall,
142: ): ?StaticCall
143: {
144: $reorderedArgs = self::reorderArgs($parametersAcceptor, $staticCall->getArgs());
145:
146: if ($reorderedArgs === null) {
147: return null;
148: }
149:
150: // return identical object if not reordered, as TypeSpecifier relies on object identity
151: if ($reorderedArgs === $staticCall->getArgs()) {
152: return $staticCall;
153: }
154:
155: return new StaticCall(
156: $staticCall->class,
157: $staticCall->name,
158: $reorderedArgs,
159: $staticCall->getAttributes(),
160: );
161: }
162:
163: public static function reorderNewArguments(
164: ParametersAcceptor $parametersAcceptor,
165: New_ $new,
166: ): ?New_
167: {
168: $reorderedArgs = self::reorderArgs($parametersAcceptor, $new->getArgs());
169:
170: if ($reorderedArgs === null) {
171: return null;
172: }
173:
174: // return identical object if not reordered, as TypeSpecifier relies on object identity
175: if ($reorderedArgs === $new->getArgs()) {
176: return $new;
177: }
178:
179: return new New_(
180: $new->class,
181: $reorderedArgs,
182: $new->getAttributes(),
183: );
184: }
185:
186: /**
187: * @param Arg[] $callArgs
188: * @return ?list<Arg>
189: */
190: public static function reorderArgs(ParametersAcceptor $parametersAcceptor, array $callArgs): ?array
191: {
192: if (count($callArgs) === 0) {
193: return [];
194: }
195:
196: $signatureParameters = $parametersAcceptor->getParameters();
197:
198: $hasNamedArgs = false;
199: foreach ($callArgs as $arg) {
200: if ($arg->name !== null) {
201: $hasNamedArgs = true;
202: break;
203: }
204: }
205: if (!$hasNamedArgs) {
206: return array_values($callArgs);
207: }
208:
209: $hasVariadic = false;
210: $argumentPositions = [];
211: foreach ($signatureParameters as $i => $parameter) {
212: if ($hasVariadic) {
213: // variadic parameter must be last
214: return null;
215: }
216:
217: $hasVariadic = $parameter->isVariadic();
218: $argumentPositions[$parameter->getName()] = $i;
219: }
220:
221: $reorderedArgs = [];
222: $additionalNamedArgs = [];
223: $appendArgs = [];
224: foreach ($callArgs as $i => $arg) {
225: if ($arg->name === null) {
226: // add regular args as is
227:
228: $attributes = $arg->getAttributes();
229: $attributes[self::ORIGINAL_ARG_ATTRIBUTE] = $arg;
230: $reorderedArgs[$i] = new Arg(
231: $arg->value,
232: $arg->byRef,
233: $arg->unpack,
234: $attributes,
235: null,
236: );
237: } elseif (array_key_exists($arg->name->toString(), $argumentPositions)) {
238: $argName = $arg->name->toString();
239: // order named args into the position the signature expects them
240: $attributes = $arg->getAttributes();
241: $attributes[self::ORIGINAL_ARG_ATTRIBUTE] = $arg;
242: $reorderedArgs[$argumentPositions[$argName]] = new Arg(
243: $arg->value,
244: $arg->byRef,
245: $arg->unpack,
246: $attributes,
247: null,
248: );
249: } else {
250: if (!$hasVariadic) {
251: $attributes = $arg->getAttributes();
252: $attributes[self::ORIGINAL_ARG_ATTRIBUTE] = $arg;
253: $appendArgs[] = new Arg(
254: $arg->value,
255: $arg->byRef,
256: $arg->unpack,
257: $attributes,
258: null,
259: );
260: continue;
261: }
262:
263: $attributes = $arg->getAttributes();
264: $attributes[self::ORIGINAL_ARG_ATTRIBUTE] = $arg;
265: $additionalNamedArgs[] = new Arg(
266: $arg->value,
267: $arg->byRef,
268: $arg->unpack,
269: $attributes,
270: null,
271: );
272: }
273: }
274:
275: // replace variadic parameter with additional named args, except if it is already set
276: $additionalNamedArgsOffset = count($argumentPositions) - 1;
277: if (array_key_exists($additionalNamedArgsOffset, $reorderedArgs)) {
278: $additionalNamedArgsOffset++;
279: }
280:
281: foreach ($additionalNamedArgs as $i => $additionalNamedArg) {
282: $reorderedArgs[$additionalNamedArgsOffset + $i] = $additionalNamedArg;
283: }
284:
285: if (count($reorderedArgs) === 0) {
286: foreach ($appendArgs as $arg) {
287: $reorderedArgs[] = $arg;
288: }
289: return $reorderedArgs;
290: }
291:
292: // fill up all holes with default values until the last given argument
293: for ($j = 0; $j < max(array_keys($reorderedArgs)); $j++) {
294: if (array_key_exists($j, $reorderedArgs)) {
295: continue;
296: }
297: if (!array_key_exists($j, $signatureParameters)) {
298: throw new ShouldNotHappenException('Parameter signatures cannot have holes');
299: }
300:
301: $parameter = $signatureParameters[$j];
302:
303: // we can only fill up optional parameters with default values
304: if (!$parameter->isOptional()) {
305: return null;
306: }
307:
308: $defaultValue = $parameter->getDefaultValue();
309: if ($defaultValue === null) {
310: if (!$parameter->isVariadic()) {
311: throw new ShouldNotHappenException(sprintf('An optional parameter $%s must have a default value', $parameter->getName()));
312: }
313: $defaultValue = new ConstantArrayType([], []);
314: }
315:
316: $reorderedArgs[$j] = new Arg(
317: new TypeExpr($defaultValue),
318: );
319: }
320:
321: ksort($reorderedArgs);
322:
323: foreach ($appendArgs as $arg) {
324: $reorderedArgs[] = $arg;
325: }
326:
327: if (!array_is_list($reorderedArgs)) {
328: $reorderedArgs = array_values($reorderedArgs);
329: }
330:
331: return $reorderedArgs;
332: }
333:
334: }
335: