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