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: $hasNamedArgs = false;
197: foreach ($callArgs as $arg) {
198: if ($arg->name !== null) {
199: $hasNamedArgs = true;
200: break;
201: }
202: }
203: if (!$hasNamedArgs) {
204: return array_values($callArgs);
205: }
206:
207: $hasVariadic = false;
208: $argumentPositions = [];
209: $signatureParameters = $parametersAcceptor->getParameters();
210: foreach ($signatureParameters as $i => $parameter) {
211: if ($hasVariadic) {
212: // variadic parameter must be last
213: return null;
214: }
215:
216: $hasVariadic = $parameter->isVariadic();
217: $argumentPositions[$parameter->getName()] = $i;
218: }
219:
220: $reorderedArgs = [];
221: $additionalNamedArgs = [];
222: $appendArgs = [];
223: foreach ($callArgs as $i => $arg) {
224: if ($arg->name === null) {
225: // add regular args as is
226:
227: $attributes = $arg->getAttributes();
228: $attributes[self::ORIGINAL_ARG_ATTRIBUTE] = $arg;
229: $reorderedArgs[$i] = new Arg(
230: $arg->value,
231: $arg->byRef,
232: $arg->unpack,
233: $attributes,
234: null,
235: );
236: } elseif (array_key_exists($arg->name->toString(), $argumentPositions)) {
237: $argName = $arg->name->toString();
238: // order named args into the position the signature expects them
239: $attributes = $arg->getAttributes();
240: $attributes[self::ORIGINAL_ARG_ATTRIBUTE] = $arg;
241: if (array_key_exists($argumentPositions[$argName], $reorderedArgs)) {
242: continue;
243: }
244: $reorderedArgs[$argumentPositions[$argName]] = new Arg(
245: $arg->value,
246: $arg->byRef,
247: $arg->unpack,
248: $attributes,
249: null,
250: );
251: } else {
252: if (!$hasVariadic) {
253: $attributes = $arg->getAttributes();
254: $attributes[self::ORIGINAL_ARG_ATTRIBUTE] = $arg;
255: $appendArgs[] = new Arg(
256: $arg->value,
257: $arg->byRef,
258: $arg->unpack,
259: $attributes,
260: null,
261: );
262: continue;
263: }
264:
265: $attributes = $arg->getAttributes();
266: $attributes[self::ORIGINAL_ARG_ATTRIBUTE] = $arg;
267: $additionalNamedArgs[] = new Arg(
268: $arg->value,
269: $arg->byRef,
270: $arg->unpack,
271: $attributes,
272: null,
273: );
274: }
275: }
276:
277: // replace variadic parameter with additional named args, except if it is already set
278: $additionalNamedArgsOffset = count($argumentPositions) - 1;
279: if (array_key_exists($additionalNamedArgsOffset, $reorderedArgs)) {
280: $additionalNamedArgsOffset++;
281: }
282:
283: foreach ($additionalNamedArgs as $i => $additionalNamedArg) {
284: $reorderedArgs[$additionalNamedArgsOffset + $i] = $additionalNamedArg;
285: }
286:
287: if (count($reorderedArgs) === 0) {
288: foreach ($appendArgs as $arg) {
289: $reorderedArgs[] = $arg;
290: }
291: return $reorderedArgs;
292: }
293:
294: // fill up all holes with default values until the last given argument
295: for ($j = 0; $j < max(array_keys($reorderedArgs)); $j++) {
296: if (array_key_exists($j, $reorderedArgs)) {
297: continue;
298: }
299: if (!array_key_exists($j, $signatureParameters)) {
300: throw new ShouldNotHappenException('Parameter signatures cannot have holes');
301: }
302:
303: $parameter = $signatureParameters[$j];
304:
305: // we can only fill up optional parameters with default values
306: if (!$parameter->isOptional()) {
307: return null;
308: }
309:
310: $defaultValue = $parameter->getDefaultValue();
311: if ($defaultValue === null) {
312: if (!$parameter->isVariadic()) {
313: throw new ShouldNotHappenException(sprintf('An optional parameter $%s must have a default value', $parameter->getName()));
314: }
315: $defaultValue = new ConstantArrayType([], []);
316: }
317:
318: $reorderedArgs[$j] = new Arg(
319: new TypeExpr($defaultValue),
320: );
321: }
322:
323: ksort($reorderedArgs);
324:
325: foreach ($appendArgs as $arg) {
326: $reorderedArgs[] = $arg;
327: }
328:
329: if (!array_is_list($reorderedArgs)) {
330: $reorderedArgs = array_values($reorderedArgs);
331: }
332:
333: return $reorderedArgs;
334: }
335:
336: }
337: