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