1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Type;
4:
5: use Closure;
6: use PHPStan\Analyser\OutOfClassScope;
7: use PHPStan\Php\PhpVersion;
8: use PHPStan\PhpDoc\Tag\TemplateTag;
9: use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode;
10: use PHPStan\PhpDocParser\Ast\Type\CallableTypeNode;
11: use PHPStan\PhpDocParser\Ast\Type\CallableTypeParameterNode;
12: use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
13: use PHPStan\PhpDocParser\Ast\Type\TypeNode;
14: use PHPStan\PhpDocParser\Printer\Printer;
15: use PHPStan\Reflection\Callables\CallableParametersAcceptor;
16: use PHPStan\Reflection\Callables\SimpleImpurePoint;
17: use PHPStan\Reflection\Callables\SimpleThrowPoint;
18: use PHPStan\Reflection\ClassMemberAccessAnswerer;
19: use PHPStan\Reflection\Native\NativeParameterReflection;
20: use PHPStan\Reflection\ParameterReflection;
21: use PHPStan\Reflection\ParametersAcceptor;
22: use PHPStan\Reflection\ParametersAcceptorSelector;
23: use PHPStan\Reflection\PassedByReference;
24: use PHPStan\Reflection\Php\DummyParameter;
25: use PHPStan\ShouldNotHappenException;
26: use PHPStan\TrinaryLogic;
27: use PHPStan\Type\Generic\TemplateType;
28: use PHPStan\Type\Generic\TemplateTypeHelper;
29: use PHPStan\Type\Generic\TemplateTypeMap;
30: use PHPStan\Type\Generic\TemplateTypeVariance;
31: use PHPStan\Type\Generic\TemplateTypeVarianceMap;
32: use PHPStan\Type\Traits\MaybeArrayTypeTrait;
33: use PHPStan\Type\Traits\MaybeIterableTypeTrait;
34: use PHPStan\Type\Traits\MaybeObjectTypeTrait;
35: use PHPStan\Type\Traits\MaybeOffsetAccessibleTypeTrait;
36: use PHPStan\Type\Traits\NonGeneralizableTypeTrait;
37: use PHPStan\Type\Traits\NonRemoveableTypeTrait;
38: use PHPStan\Type\Traits\TruthyBooleanTypeTrait;
39: use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait;
40: use function array_map;
41: use function array_merge;
42: use function count;
43:
44: /** @api */
45: class CallableType implements CompoundType, CallableParametersAcceptor
46: {
47:
48: use MaybeArrayTypeTrait;
49: use MaybeIterableTypeTrait;
50: use MaybeObjectTypeTrait;
51: use MaybeOffsetAccessibleTypeTrait;
52: use TruthyBooleanTypeTrait;
53: use UndecidedComparisonCompoundTypeTrait;
54: use NonRemoveableTypeTrait;
55: use NonGeneralizableTypeTrait;
56:
57: /** @var list<ParameterReflection> */
58: private array $parameters;
59:
60: private Type $returnType;
61:
62: private bool $isCommonCallable;
63:
64: private TemplateTypeMap $templateTypeMap;
65:
66: private TemplateTypeMap $resolvedTemplateTypeMap;
67:
68: private TrinaryLogic $isPure;
69:
70: /**
71: * @api
72: * @param list<ParameterReflection>|null $parameters
73: * @param array<non-empty-string, TemplateTag> $templateTags
74: */
75: public function __construct(
76: ?array $parameters = null,
77: ?Type $returnType = null,
78: private bool $variadic = true,
79: ?TemplateTypeMap $templateTypeMap = null,
80: ?TemplateTypeMap $resolvedTemplateTypeMap = null,
81: private array $templateTags = [],
82: ?TrinaryLogic $isPure = null,
83: )
84: {
85: $this->parameters = $parameters ?? [];
86: $this->returnType = $returnType ?? new MixedType();
87: $this->isCommonCallable = $parameters === null && $returnType === null;
88: $this->templateTypeMap = $templateTypeMap ?? TemplateTypeMap::createEmpty();
89: $this->resolvedTemplateTypeMap = $resolvedTemplateTypeMap ?? TemplateTypeMap::createEmpty();
90: $this->isPure = $isPure ?? TrinaryLogic::createMaybe();
91: }
92:
93: /**
94: * @return array<non-empty-string, TemplateTag>
95: */
96: public function getTemplateTags(): array
97: {
98: return $this->templateTags;
99: }
100:
101: public function isPure(): TrinaryLogic
102: {
103: return $this->isPure;
104: }
105:
106: public function getReferencedClasses(): array
107: {
108: $classes = [];
109: foreach ($this->parameters as $parameter) {
110: $classes = array_merge($classes, $parameter->getType()->getReferencedClasses());
111: }
112:
113: return array_merge($classes, $this->returnType->getReferencedClasses());
114: }
115:
116: public function getObjectClassNames(): array
117: {
118: return [];
119: }
120:
121: public function getObjectClassReflections(): array
122: {
123: return [];
124: }
125:
126: public function getConstantStrings(): array
127: {
128: return [];
129: }
130:
131: public function accepts(Type $type, bool $strictTypes): AcceptsResult
132: {
133: if ($type instanceof CompoundType && !$type instanceof self) {
134: return $type->isAcceptedBy($this, $strictTypes);
135: }
136:
137: return $this->isSuperTypeOfInternal($type, true)->toAcceptsResult();
138: }
139:
140: public function isSuperTypeOf(Type $type): IsSuperTypeOfResult
141: {
142: if ($type instanceof CompoundType && !$type instanceof self) {
143: return $type->isSubTypeOf($this);
144: }
145:
146: return $this->isSuperTypeOfInternal($type, false);
147: }
148:
149: private function isSuperTypeOfInternal(Type $type, bool $treatMixedAsAny): IsSuperTypeOfResult
150: {
151: $isCallable = new IsSuperTypeOfResult($type->isCallable(), []);
152: if ($isCallable->no()) {
153: return $isCallable;
154: }
155:
156: static $scope;
157: if ($scope === null) {
158: $scope = new OutOfClassScope();
159: }
160:
161: if ($this->isCommonCallable) {
162: if ($this->isPure()->yes()) {
163: $typePure = TrinaryLogic::createYes();
164: foreach ($type->getCallableParametersAcceptors($scope) as $variant) {
165: $typePure = $typePure->and($variant->isPure());
166: }
167:
168: return $isCallable->and(new IsSuperTypeOfResult($typePure, []));
169: }
170:
171: return $isCallable;
172: }
173:
174: $parameterTypes = array_map(static fn ($parameter) => $parameter->getType(), $this->getParameters());
175:
176: $variantsResult = null;
177: foreach ($type->getCallableParametersAcceptors($scope) as $variant) {
178: $variant = ParametersAcceptorSelector::selectFromTypes($parameterTypes, [$variant], false);
179: if (!$variant instanceof CallableParametersAcceptor) {
180: return IsSuperTypeOfResult::createNo([]);
181: }
182: $isSuperType = CallableTypeHelper::isParametersAcceptorSuperTypeOf($this, $variant, $treatMixedAsAny);
183: if ($variantsResult === null) {
184: $variantsResult = $isSuperType;
185: } else {
186: $variantsResult = $variantsResult->or($isSuperType);
187: }
188: }
189:
190: if ($variantsResult === null) {
191: throw new ShouldNotHappenException();
192: }
193:
194: return $isCallable->and($variantsResult);
195: }
196:
197: public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult
198: {
199: if ($otherType instanceof IntersectionType || $otherType instanceof UnionType) {
200: return $otherType->isSuperTypeOf($this);
201: }
202:
203: return (new IsSuperTypeOfResult($otherType->isCallable(), []))
204: ->and($otherType instanceof self ? IsSuperTypeOfResult::createYes() : IsSuperTypeOfResult::createMaybe());
205: }
206:
207: public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult
208: {
209: return $this->isSubTypeOf($acceptingType)->toAcceptsResult();
210: }
211:
212: public function equals(Type $type): bool
213: {
214: if (!$type instanceof self) {
215: return false;
216: }
217:
218: return $this->describe(VerbosityLevel::precise()) === $type->describe(VerbosityLevel::precise());
219: }
220:
221: public function describe(VerbosityLevel $level): string
222: {
223: return $level->handle(
224: static fn (): string => 'callable',
225: function (): string {
226: $printer = new Printer();
227: $selfWithoutParameterNames = new self(
228: array_map(static fn (ParameterReflection $p): ParameterReflection => new DummyParameter(
229: '',
230: $p->getType(),
231: $p->isOptional() && !$p->isVariadic(),
232: PassedByReference::createNo(),
233: $p->isVariadic(),
234: $p->getDefaultValue(),
235: ), $this->parameters),
236: $this->returnType,
237: $this->variadic,
238: $this->templateTypeMap,
239: $this->resolvedTemplateTypeMap,
240: $this->templateTags,
241: $this->isPure,
242: );
243:
244: return $printer->print($selfWithoutParameterNames->toPhpDocNode());
245: },
246: );
247: }
248:
249: public function isCallable(): TrinaryLogic
250: {
251: return TrinaryLogic::createYes();
252: }
253:
254: public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array
255: {
256: return [$this];
257: }
258:
259: public function getThrowPoints(): array
260: {
261: return [
262: SimpleThrowPoint::createImplicit(),
263: ];
264: }
265:
266: public function getImpurePoints(): array
267: {
268: $pure = $this->isPure();
269: if ($pure->yes()) {
270: return [];
271: }
272:
273: return [
274: new SimpleImpurePoint(
275: 'functionCall',
276: 'call to a callable',
277: $pure->no(),
278: ),
279: ];
280: }
281:
282: public function getInvalidateExpressions(): array
283: {
284: return [];
285: }
286:
287: public function getUsedVariables(): array
288: {
289: return [];
290: }
291:
292: public function acceptsNamedArguments(): TrinaryLogic
293: {
294: return TrinaryLogic::createYes();
295: }
296:
297: public function toNumber(): Type
298: {
299: return new ErrorType();
300: }
301:
302: public function toAbsoluteNumber(): Type
303: {
304: return new ErrorType();
305: }
306:
307: public function toString(): Type
308: {
309: return new ErrorType();
310: }
311:
312: public function toInteger(): Type
313: {
314: return new ErrorType();
315: }
316:
317: public function toFloat(): Type
318: {
319: return new ErrorType();
320: }
321:
322: public function toArray(): Type
323: {
324: return new ArrayType(new MixedType(), new MixedType());
325: }
326:
327: public function toArrayKey(): Type
328: {
329: return new ErrorType();
330: }
331:
332: public function toCoercedArgumentType(bool $strictTypes): Type
333: {
334: return TypeCombinator::union($this, new StringType(), new ArrayType(new MixedType(true), new MixedType(true)), new ObjectType(Closure::class));
335: }
336:
337: public function isOffsetAccessLegal(): TrinaryLogic
338: {
339: return TrinaryLogic::createMaybe();
340: }
341:
342: public function getTemplateTypeMap(): TemplateTypeMap
343: {
344: return $this->templateTypeMap;
345: }
346:
347: public function getResolvedTemplateTypeMap(): TemplateTypeMap
348: {
349: return $this->resolvedTemplateTypeMap;
350: }
351:
352: public function getCallSiteVarianceMap(): TemplateTypeVarianceMap
353: {
354: return TemplateTypeVarianceMap::createEmpty();
355: }
356:
357: /**
358: * @return list<ParameterReflection>
359: */
360: public function getParameters(): array
361: {
362: return $this->parameters;
363: }
364:
365: public function isVariadic(): bool
366: {
367: return $this->variadic;
368: }
369:
370: public function getReturnType(): Type
371: {
372: return $this->returnType;
373: }
374:
375: public function inferTemplateTypes(Type $receivedType): TemplateTypeMap
376: {
377: if ($receivedType instanceof UnionType || $receivedType instanceof IntersectionType) {
378: return $receivedType->inferTemplateTypesOn($this);
379: }
380:
381: if (! $receivedType->isCallable()->yes()) {
382: return TemplateTypeMap::createEmpty();
383: }
384:
385: $parametersAcceptors = $receivedType->getCallableParametersAcceptors(new OutOfClassScope());
386:
387: $typeMap = TemplateTypeMap::createEmpty();
388:
389: foreach ($parametersAcceptors as $parametersAcceptor) {
390: $typeMap = $typeMap->union($this->inferTemplateTypesOnParametersAcceptor($parametersAcceptor));
391: }
392:
393: return $typeMap;
394: }
395:
396: private function inferTemplateTypesOnParametersAcceptor(ParametersAcceptor $parametersAcceptor): TemplateTypeMap
397: {
398: $parameterTypes = array_map(static fn ($parameter) => $parameter->getType(), $this->getParameters());
399: $parametersAcceptor = ParametersAcceptorSelector::selectFromTypes($parameterTypes, [$parametersAcceptor], false);
400: $args = $parametersAcceptor->getParameters();
401: $returnType = $parametersAcceptor->getReturnType();
402:
403: $typeMap = TemplateTypeMap::createEmpty();
404: foreach ($this->getParameters() as $i => $param) {
405: $paramType = $param->getType();
406: if (isset($args[$i])) {
407: $argType = $args[$i]->getType();
408: } elseif ($paramType instanceof TemplateType) {
409: $argType = TemplateTypeHelper::resolveToBounds($paramType);
410: } else {
411: $argType = new NeverType();
412: }
413:
414: $typeMap = $typeMap->union($paramType->inferTemplateTypes($argType)->convertToLowerBoundTypes());
415: }
416:
417: return $typeMap->union($this->getReturnType()->inferTemplateTypes($returnType));
418: }
419:
420: public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array
421: {
422: $references = $this->getReturnType()->getReferencedTemplateTypes(
423: $positionVariance->compose(TemplateTypeVariance::createCovariant()),
424: );
425:
426: $paramVariance = $positionVariance->compose(TemplateTypeVariance::createContravariant());
427:
428: foreach ($this->getParameters() as $param) {
429: foreach ($param->getType()->getReferencedTemplateTypes($paramVariance) as $reference) {
430: $references[] = $reference;
431: }
432: }
433:
434: return $references;
435: }
436:
437: public function traverse(callable $cb): Type
438: {
439: if ($this->isCommonCallable) {
440: return $this;
441: }
442:
443: $parameters = array_map(static function (ParameterReflection $param) use ($cb): NativeParameterReflection {
444: $defaultValue = $param->getDefaultValue();
445: return new NativeParameterReflection(
446: $param->getName(),
447: $param->isOptional(),
448: $cb($param->getType()),
449: $param->passedByReference(),
450: $param->isVariadic(),
451: $defaultValue !== null ? $cb($defaultValue) : null,
452: );
453: }, $this->getParameters());
454:
455: return new self(
456: $parameters,
457: $cb($this->getReturnType()),
458: $this->isVariadic(),
459: $this->templateTypeMap,
460: $this->resolvedTemplateTypeMap,
461: $this->templateTags,
462: $this->isPure,
463: );
464: }
465:
466: public function traverseSimultaneously(Type $right, callable $cb): Type
467: {
468: if ($this->isCommonCallable) {
469: return $this;
470: }
471:
472: if (!$right->isCallable()->yes()) {
473: return $this;
474: }
475:
476: $rightAcceptors = $right->getCallableParametersAcceptors(new OutOfClassScope());
477: if (count($rightAcceptors) !== 1) {
478: return $this;
479: }
480:
481: $rightParameters = $rightAcceptors[0]->getParameters();
482: if (count($this->getParameters()) !== count($rightParameters)) {
483: return $this;
484: }
485:
486: $parameters = [];
487: foreach ($this->getParameters() as $i => $leftParam) {
488: $rightParam = $rightParameters[$i];
489: $leftDefaultValue = $leftParam->getDefaultValue();
490: $rightDefaultValue = $rightParam->getDefaultValue();
491: $defaultValue = $leftDefaultValue;
492: if ($leftDefaultValue !== null && $rightDefaultValue !== null) {
493: $defaultValue = $cb($leftDefaultValue, $rightDefaultValue);
494: }
495: $parameters[] = new NativeParameterReflection(
496: $leftParam->getName(),
497: $leftParam->isOptional(),
498: $cb($leftParam->getType(), $rightParam->getType()),
499: $leftParam->passedByReference(),
500: $leftParam->isVariadic(),
501: $defaultValue,
502: );
503: }
504:
505: return new self(
506: $parameters,
507: $cb($this->getReturnType(), $rightAcceptors[0]->getReturnType()),
508: $this->isVariadic(),
509: $this->templateTypeMap,
510: $this->resolvedTemplateTypeMap,
511: $this->templateTags,
512: $this->isPure,
513: );
514: }
515:
516: public function isOversizedArray(): TrinaryLogic
517: {
518: return TrinaryLogic::createNo();
519: }
520:
521: public function isNull(): TrinaryLogic
522: {
523: return TrinaryLogic::createNo();
524: }
525:
526: public function isConstantValue(): TrinaryLogic
527: {
528: return TrinaryLogic::createNo();
529: }
530:
531: public function isConstantScalarValue(): TrinaryLogic
532: {
533: return TrinaryLogic::createNo();
534: }
535:
536: public function getConstantScalarTypes(): array
537: {
538: return [];
539: }
540:
541: public function getConstantScalarValues(): array
542: {
543: return [];
544: }
545:
546: public function isTrue(): TrinaryLogic
547: {
548: return TrinaryLogic::createNo();
549: }
550:
551: public function isFalse(): TrinaryLogic
552: {
553: return TrinaryLogic::createNo();
554: }
555:
556: public function isBoolean(): TrinaryLogic
557: {
558: return TrinaryLogic::createNo();
559: }
560:
561: public function isFloat(): TrinaryLogic
562: {
563: return TrinaryLogic::createNo();
564: }
565:
566: public function isInteger(): TrinaryLogic
567: {
568: return TrinaryLogic::createNo();
569: }
570:
571: public function isString(): TrinaryLogic
572: {
573: return TrinaryLogic::createMaybe();
574: }
575:
576: public function isNumericString(): TrinaryLogic
577: {
578: return TrinaryLogic::createNo();
579: }
580:
581: public function isNonEmptyString(): TrinaryLogic
582: {
583: return TrinaryLogic::createMaybe();
584: }
585:
586: public function isNonFalsyString(): TrinaryLogic
587: {
588: return TrinaryLogic::createMaybe();
589: }
590:
591: public function isLiteralString(): TrinaryLogic
592: {
593: return TrinaryLogic::createMaybe();
594: }
595:
596: public function isLowercaseString(): TrinaryLogic
597: {
598: return TrinaryLogic::createMaybe();
599: }
600:
601: public function isClassString(): TrinaryLogic
602: {
603: return TrinaryLogic::createMaybe();
604: }
605:
606: public function isUppercaseString(): TrinaryLogic
607: {
608: return TrinaryLogic::createMaybe();
609: }
610:
611: public function getClassStringObjectType(): Type
612: {
613: return new ObjectWithoutClassType();
614: }
615:
616: public function getObjectTypeOrClassStringObjectType(): Type
617: {
618: return new ObjectWithoutClassType();
619: }
620:
621: public function isVoid(): TrinaryLogic
622: {
623: return TrinaryLogic::createNo();
624: }
625:
626: public function isScalar(): TrinaryLogic
627: {
628: return TrinaryLogic::createMaybe();
629: }
630:
631: public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType
632: {
633: return new BooleanType();
634: }
635:
636: public function getEnumCases(): array
637: {
638: return [];
639: }
640:
641: public function isCommonCallable(): bool
642: {
643: return $this->isCommonCallable;
644: }
645:
646: public function exponentiate(Type $exponent): Type
647: {
648: return new ErrorType();
649: }
650:
651: public function getFiniteTypes(): array
652: {
653: return [];
654: }
655:
656: public function toPhpDocNode(): TypeNode
657: {
658: if ($this->isCommonCallable) {
659: return new IdentifierTypeNode($this->isPure()->yes() ? 'pure-callable' : 'callable');
660: }
661:
662: $parameters = [];
663: foreach ($this->parameters as $parameter) {
664: $parameters[] = new CallableTypeParameterNode(
665: $parameter->getType()->toPhpDocNode(),
666: !$parameter->passedByReference()->no(),
667: $parameter->isVariadic(),
668: $parameter->getName() === '' ? '' : '$' . $parameter->getName(),
669: $parameter->isOptional(),
670: );
671: }
672:
673: $templateTags = [];
674: foreach ($this->templateTags as $templateName => $templateTag) {
675: $templateTags[] = new TemplateTagValueNode(
676: $templateName,
677: $templateTag->getBound()->toPhpDocNode(),
678: '',
679: );
680: }
681:
682: return new CallableTypeNode(
683: new IdentifierTypeNode($this->isPure->yes() ? 'pure-callable' : 'callable'),
684: $parameters,
685: $this->returnType->toPhpDocNode(),
686: $templateTags,
687: );
688: }
689:
690: }
691: