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