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