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