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