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