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