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 isOffsetAccessLegal(): TrinaryLogic
344: {
345: return TrinaryLogic::createMaybe();
346: }
347:
348: public function getTemplateTypeMap(): TemplateTypeMap
349: {
350: return $this->templateTypeMap;
351: }
352:
353: public function getResolvedTemplateTypeMap(): TemplateTypeMap
354: {
355: return $this->resolvedTemplateTypeMap;
356: }
357:
358: public function getCallSiteVarianceMap(): TemplateTypeVarianceMap
359: {
360: return TemplateTypeVarianceMap::createEmpty();
361: }
362:
363: /**
364: * @return list<ParameterReflection>
365: */
366: public function getParameters(): array
367: {
368: return $this->parameters;
369: }
370:
371: public function isVariadic(): bool
372: {
373: return $this->variadic;
374: }
375:
376: public function getReturnType(): Type
377: {
378: return $this->returnType;
379: }
380:
381: public function inferTemplateTypes(Type $receivedType): TemplateTypeMap
382: {
383: if ($receivedType instanceof UnionType || $receivedType instanceof IntersectionType) {
384: return $receivedType->inferTemplateTypesOn($this);
385: }
386:
387: if (! $receivedType->isCallable()->yes()) {
388: return TemplateTypeMap::createEmpty();
389: }
390:
391: $parametersAcceptors = $receivedType->getCallableParametersAcceptors(new OutOfClassScope());
392:
393: $typeMap = TemplateTypeMap::createEmpty();
394:
395: foreach ($parametersAcceptors as $parametersAcceptor) {
396: $typeMap = $typeMap->union($this->inferTemplateTypesOnParametersAcceptor($parametersAcceptor));
397: }
398:
399: return $typeMap;
400: }
401:
402: private function inferTemplateTypesOnParametersAcceptor(ParametersAcceptor $parametersAcceptor): TemplateTypeMap
403: {
404: $parameterTypes = array_map(static fn ($parameter) => $parameter->getType(), $this->getParameters());
405: $parametersAcceptor = ParametersAcceptorSelector::selectFromTypes($parameterTypes, [$parametersAcceptor], false);
406: $args = $parametersAcceptor->getParameters();
407: $returnType = $parametersAcceptor->getReturnType();
408:
409: $typeMap = TemplateTypeMap::createEmpty();
410: foreach ($this->getParameters() as $i => $param) {
411: $paramType = $param->getType();
412: if (isset($args[$i])) {
413: $argType = $args[$i]->getType();
414: } elseif ($paramType instanceof TemplateType) {
415: $argType = TemplateTypeHelper::resolveToBounds($paramType);
416: } else {
417: $argType = new NeverType();
418: }
419:
420: $typeMap = $typeMap->union($paramType->inferTemplateTypes($argType)->convertToLowerBoundTypes());
421: }
422:
423: return $typeMap->union($this->getReturnType()->inferTemplateTypes($returnType));
424: }
425:
426: public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array
427: {
428: $references = $this->getReturnType()->getReferencedTemplateTypes(
429: $positionVariance->compose(TemplateTypeVariance::createCovariant()),
430: );
431:
432: $paramVariance = $positionVariance->compose(TemplateTypeVariance::createContravariant());
433:
434: foreach ($this->getParameters() as $param) {
435: foreach ($param->getType()->getReferencedTemplateTypes($paramVariance) as $reference) {
436: $references[] = $reference;
437: }
438: }
439:
440: return $references;
441: }
442:
443: public function traverse(callable $cb): Type
444: {
445: if ($this->isCommonCallable) {
446: return $this;
447: }
448:
449: $parameters = array_map(static function (ParameterReflection $param) use ($cb): NativeParameterReflection {
450: $defaultValue = $param->getDefaultValue();
451: return new NativeParameterReflection(
452: $param->getName(),
453: $param->isOptional(),
454: $cb($param->getType()),
455: $param->passedByReference(),
456: $param->isVariadic(),
457: $defaultValue !== null ? $cb($defaultValue) : null,
458: );
459: }, $this->getParameters());
460:
461: return new self(
462: $parameters,
463: $cb($this->getReturnType()),
464: $this->isVariadic(),
465: $this->templateTypeMap,
466: $this->resolvedTemplateTypeMap,
467: $this->templateTags,
468: $this->isPure,
469: );
470: }
471:
472: public function traverseSimultaneously(Type $right, callable $cb): Type
473: {
474: if ($this->isCommonCallable) {
475: return $this;
476: }
477:
478: if (!$right->isCallable()->yes()) {
479: return $this;
480: }
481:
482: $rightAcceptors = $right->getCallableParametersAcceptors(new OutOfClassScope());
483: if (count($rightAcceptors) !== 1) {
484: return $this;
485: }
486:
487: $rightParameters = $rightAcceptors[0]->getParameters();
488: if (count($this->getParameters()) !== count($rightParameters)) {
489: return $this;
490: }
491:
492: $parameters = [];
493: foreach ($this->getParameters() as $i => $leftParam) {
494: $rightParam = $rightParameters[$i];
495: $leftDefaultValue = $leftParam->getDefaultValue();
496: $rightDefaultValue = $rightParam->getDefaultValue();
497: $defaultValue = $leftDefaultValue;
498: if ($leftDefaultValue !== null && $rightDefaultValue !== null) {
499: $defaultValue = $cb($leftDefaultValue, $rightDefaultValue);
500: }
501: $parameters[] = new NativeParameterReflection(
502: $leftParam->getName(),
503: $leftParam->isOptional(),
504: $cb($leftParam->getType(), $rightParam->getType()),
505: $leftParam->passedByReference(),
506: $leftParam->isVariadic(),
507: $defaultValue,
508: );
509: }
510:
511: return new self(
512: $parameters,
513: $cb($this->getReturnType(), $rightAcceptors[0]->getReturnType()),
514: $this->isVariadic(),
515: $this->templateTypeMap,
516: $this->resolvedTemplateTypeMap,
517: $this->templateTags,
518: $this->isPure,
519: );
520: }
521:
522: public function isOversizedArray(): TrinaryLogic
523: {
524: return TrinaryLogic::createNo();
525: }
526:
527: public function isNull(): TrinaryLogic
528: {
529: return TrinaryLogic::createNo();
530: }
531:
532: public function isConstantValue(): TrinaryLogic
533: {
534: return TrinaryLogic::createNo();
535: }
536:
537: public function isConstantScalarValue(): TrinaryLogic
538: {
539: return TrinaryLogic::createNo();
540: }
541:
542: public function getConstantScalarTypes(): array
543: {
544: return [];
545: }
546:
547: public function getConstantScalarValues(): array
548: {
549: return [];
550: }
551:
552: public function isTrue(): TrinaryLogic
553: {
554: return TrinaryLogic::createNo();
555: }
556:
557: public function isFalse(): TrinaryLogic
558: {
559: return TrinaryLogic::createNo();
560: }
561:
562: public function isBoolean(): TrinaryLogic
563: {
564: return TrinaryLogic::createNo();
565: }
566:
567: public function isFloat(): TrinaryLogic
568: {
569: return TrinaryLogic::createNo();
570: }
571:
572: public function isInteger(): TrinaryLogic
573: {
574: return TrinaryLogic::createNo();
575: }
576:
577: public function isString(): TrinaryLogic
578: {
579: return TrinaryLogic::createMaybe();
580: }
581:
582: public function isNumericString(): TrinaryLogic
583: {
584: return TrinaryLogic::createNo();
585: }
586:
587: public function isNonEmptyString(): TrinaryLogic
588: {
589: return TrinaryLogic::createMaybe();
590: }
591:
592: public function isNonFalsyString(): TrinaryLogic
593: {
594: return TrinaryLogic::createMaybe();
595: }
596:
597: public function isLiteralString(): TrinaryLogic
598: {
599: return TrinaryLogic::createMaybe();
600: }
601:
602: public function isLowercaseString(): TrinaryLogic
603: {
604: return TrinaryLogic::createMaybe();
605: }
606:
607: public function isClassString(): TrinaryLogic
608: {
609: return TrinaryLogic::createMaybe();
610: }
611:
612: public function isUppercaseString(): TrinaryLogic
613: {
614: return TrinaryLogic::createMaybe();
615: }
616:
617: public function getClassStringObjectType(): Type
618: {
619: return new ObjectWithoutClassType();
620: }
621:
622: public function getObjectTypeOrClassStringObjectType(): Type
623: {
624: return new ObjectWithoutClassType();
625: }
626:
627: public function isVoid(): TrinaryLogic
628: {
629: return TrinaryLogic::createNo();
630: }
631:
632: public function isScalar(): TrinaryLogic
633: {
634: return TrinaryLogic::createMaybe();
635: }
636:
637: public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType
638: {
639: return new BooleanType();
640: }
641:
642: public function getEnumCases(): array
643: {
644: return [];
645: }
646:
647: public function isCommonCallable(): bool
648: {
649: return $this->isCommonCallable;
650: }
651:
652: public function exponentiate(Type $exponent): Type
653: {
654: return new ErrorType();
655: }
656:
657: public function getFiniteTypes(): array
658: {
659: return [];
660: }
661:
662: public function toPhpDocNode(): TypeNode
663: {
664: if ($this->isCommonCallable) {
665: return new IdentifierTypeNode($this->isPure()->yes() ? 'pure-callable' : 'callable');
666: }
667:
668: $parameters = [];
669: foreach ($this->parameters as $parameter) {
670: $parameters[] = new CallableTypeParameterNode(
671: $parameter->getType()->toPhpDocNode(),
672: !$parameter->passedByReference()->no(),
673: $parameter->isVariadic(),
674: $parameter->getName() === '' ? '' : '$' . $parameter->getName(),
675: $parameter->isOptional(),
676: );
677: }
678:
679: $templateTags = [];
680: foreach ($this->templateTags as $templateName => $templateTag) {
681: $templateTags[] = new TemplateTagValueNode(
682: $templateName,
683: $templateTag->getBound()->toPhpDocNode(),
684: '',
685: );
686: }
687:
688: return new CallableTypeNode(
689: new IdentifierTypeNode($this->isPure->yes() ? 'pure-callable' : 'callable'),
690: $parameters,
691: $this->returnType->toPhpDocNode(),
692: $templateTags,
693: );
694: }
695:
696: }
697: