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