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