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