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