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