1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Reflection;
4:
5: use PHPStan\Analyser\OutOfClassScope;
6: use PHPStan\Reflection\Callables\CallableParametersAcceptor;
7: use PHPStan\Reflection\Php\ExtendedDummyParameter;
8: use PHPStan\TrinaryLogic;
9: use PHPStan\Type\CallableAssertionsHelper;
10: use PHPStan\Type\ConditionalTypeForParameter;
11: use PHPStan\Type\ErrorType;
12: use PHPStan\Type\Generic\TemplateTypeHelper;
13: use PHPStan\Type\Generic\TemplateTypeMap;
14: use PHPStan\Type\Generic\TemplateTypeVariance;
15: use PHPStan\Type\Generic\TemplateTypeVarianceMap;
16: use PHPStan\Type\MixedType;
17: use PHPStan\Type\NeverType;
18: use PHPStan\Type\Type;
19: use PHPStan\Type\TypeCombinator;
20: use PHPStan\Type\UnionType;
21: use function array_key_exists;
22: use function array_last;
23: use function array_map;
24: use function array_merge;
25: use function count;
26: use function is_int;
27:
28: final class GenericParametersAcceptorResolver
29: {
30:
31: /**
32: * @api
33: * @param array<int|string, Type> $argTypes
34: */
35: public static function resolve(array $argTypes, ParametersAcceptor $parametersAcceptor): ExtendedParametersAcceptor
36: {
37: $typeMap = TemplateTypeMap::createEmpty();
38: $passedArgs = [];
39:
40: $parameters = $parametersAcceptor->getParameters();
41: $namedArgTypes = [];
42: foreach ($argTypes as $i => $argType) {
43: if (is_int($i)) {
44: if (isset($parameters[$i])) {
45: $namedArgTypes[$parameters[$i]->getName()] = $argType;
46: continue;
47: }
48: if (count($parameters) > 0) {
49: $lastParameter = array_last($parameters);
50: if ($lastParameter->isVariadic()) {
51: $parameterName = $lastParameter->getName();
52: if (array_key_exists($parameterName, $namedArgTypes)) {
53: $namedArgTypes[$parameterName] = TypeCombinator::union($namedArgTypes[$parameterName], $argType);
54: continue;
55: }
56: $namedArgTypes[$parameterName] = $argType;
57: }
58: }
59: continue;
60: }
61:
62: $namedArgTypes[$i] = $argType;
63: }
64:
65: // type predicates of passed callables determine their template types exactly,
66: // so they are inferred first and substituted into the parameter types —
67: // the remaining template types are then inferred from what is left over,
68: // e.g. in partition(iterable<T0|T1> $values, callable(T0|T1): ($value is T0 ? true : false) $predicate)
69: // called with iterable<int|string> and is_int(...), T0 becomes int and T1 string
70: $predicateTypeMap = TemplateTypeMap::createEmpty();
71: foreach ($parameters as $param) {
72: if (!isset($namedArgTypes[$param->getName()])) {
73: continue;
74: }
75:
76: $predicateTypeMap = $predicateTypeMap->union(
77: self::inferPredicateTemplateTypes($param->getType(), $namedArgTypes[$param->getName()]),
78: );
79: }
80:
81: foreach ($parameters as $param) {
82: if (isset($namedArgTypes[$param->getName()])) {
83: $argType = $namedArgTypes[$param->getName()];
84: } elseif ($param->getDefaultValue() !== null) {
85: $argType = $param->getDefaultValue();
86: } elseif ($param->isVariadic()) {
87: $argType = new NeverType(true);
88: } else {
89: continue;
90: }
91:
92: $paramType = self::resolvePredicateTemplateTypes($param->getType(), $predicateTypeMap);
93: $typeMap = $typeMap->union($paramType->inferTemplateTypes($argType));
94: $passedArgs['$' . $param->getName()] = $argType;
95: }
96:
97: $typeMap = $typeMap->union($predicateTypeMap);
98:
99: $returnType = $parametersAcceptor->getReturnType();
100: if (
101: $returnType instanceof ConditionalTypeForParameter
102: && !$returnType->isNegated()
103: && array_key_exists($returnType->getParameterName(), $passedArgs)
104: ) {
105: $paramType = self::resolvePredicateTemplateTypes($returnType->getTarget(), $predicateTypeMap);
106: $argType = $passedArgs[$returnType->getParameterName()];
107: $typeMap = $typeMap->union($paramType->inferTemplateTypes($argType));
108: }
109:
110: $resolvedTemplateTypeMap = new TemplateTypeMap(array_merge(
111: $parametersAcceptor->getTemplateTypeMap()->map(static fn (string $name, Type $type): Type => new ErrorType())->getTypes(),
112: $typeMap->getTypes(),
113: ));
114:
115: $originalParametersAcceptor = $parametersAcceptor;
116:
117: if (!$parametersAcceptor instanceof ExtendedParametersAcceptor) {
118: $parametersAcceptor = new ExtendedFunctionVariant(
119: $parametersAcceptor->getTemplateTypeMap(),
120: $parametersAcceptor->getResolvedTemplateTypeMap(),
121: array_map(static fn (ParameterReflection $parameter): ExtendedParameterReflection => new ExtendedDummyParameter(
122: $parameter->getName(),
123: $parameter->getType(),
124: $parameter->isOptional(),
125: $parameter->passedByReference(),
126: $parameter->isVariadic(),
127: $parameter->getDefaultValue(),
128: new MixedType(),
129: $parameter->getType(),
130: null,
131: TrinaryLogic::createMaybe(),
132: null,
133: [],
134: null,
135: ), $parameters),
136: $parametersAcceptor->isVariadic(),
137: $returnType,
138: $returnType,
139: new MixedType(),
140: TemplateTypeVarianceMap::createEmpty(),
141: );
142: }
143:
144: $result = new ResolvedFunctionVariantWithOriginal(
145: $parametersAcceptor,
146: $resolvedTemplateTypeMap,
147: $parametersAcceptor->getCallSiteVarianceMap(),
148: $passedArgs,
149: );
150: if ($originalParametersAcceptor instanceof CallableParametersAcceptor) {
151: return new ResolvedFunctionVariantWithCallable(
152: $result,
153: $originalParametersAcceptor->getThrowPoints(),
154: $originalParametersAcceptor->isPure(),
155: $originalParametersAcceptor->getImpurePoints(),
156: $originalParametersAcceptor->getInvalidateExpressions(),
157: $originalParametersAcceptor->getUsedVariables(),
158: $originalParametersAcceptor->acceptsNamedArguments(),
159: $originalParametersAcceptor->mustUseReturnValue(),
160: $originalParametersAcceptor->getAsserts(),
161: $originalParametersAcceptor->isStaticClosure(),
162: );
163: }
164:
165: return $result;
166: }
167:
168: private static function inferPredicateTemplateTypes(Type $paramType, Type $argType): TemplateTypeMap
169: {
170: $typeMap = TemplateTypeMap::createEmpty();
171: if (!$argType->isCallable()->yes()) {
172: return $typeMap;
173: }
174:
175: foreach ($paramType instanceof UnionType ? $paramType->getTypes() : [$paramType] as $innerType) {
176: if (!$innerType instanceof CallableParametersAcceptor) {
177: continue;
178: }
179: if ($innerType->getAsserts()->getAll() === []) {
180: continue;
181: }
182:
183: foreach ($argType->getCallableParametersAcceptors(new OutOfClassScope()) as $receivedAcceptor) {
184: $typeMap = $typeMap->union(CallableAssertionsHelper::inferTemplateTypesOnAsserts($innerType, $receivedAcceptor));
185: }
186: }
187:
188: return $typeMap;
189: }
190:
191: private static function resolvePredicateTemplateTypes(Type $type, TemplateTypeMap $predicateTypeMap): Type
192: {
193: if ($predicateTypeMap->isEmpty()) {
194: return $type;
195: }
196:
197: return TemplateTypeHelper::resolveTemplateTypes(
198: $type,
199: $predicateTypeMap,
200: TemplateTypeVarianceMap::createEmpty(),
201: TemplateTypeVariance::createInvariant(),
202: );
203: }
204:
205: }
206: