1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Type;
4:
5: use ArrayAccess;
6: use PHPStan\Reflection\ClassMemberAccessAnswerer;
7: use PHPStan\Reflection\ConstantReflection;
8: use PHPStan\Reflection\Dummy\DummyConstantReflection;
9: use PHPStan\Reflection\Dummy\DummyMethodReflection;
10: use PHPStan\Reflection\Dummy\DummyPropertyReflection;
11: use PHPStan\Reflection\ExtendedMethodReflection;
12: use PHPStan\Reflection\ParametersAcceptor;
13: use PHPStan\Reflection\PropertyReflection;
14: use PHPStan\Reflection\TrivialParametersAcceptor;
15: use PHPStan\Reflection\Type\CallbackUnresolvedMethodPrototypeReflection;
16: use PHPStan\Reflection\Type\CallbackUnresolvedPropertyPrototypeReflection;
17: use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection;
18: use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection;
19: use PHPStan\TrinaryLogic;
20: use PHPStan\Type\Accessory\AccessoryArrayListType;
21: use PHPStan\Type\Accessory\AccessoryLiteralStringType;
22: use PHPStan\Type\Accessory\AccessoryNonEmptyStringType;
23: use PHPStan\Type\Accessory\AccessoryNonFalsyStringType;
24: use PHPStan\Type\Accessory\AccessoryNumericStringType;
25: use PHPStan\Type\Accessory\OversizedArrayType;
26: use PHPStan\Type\Constant\ConstantArrayType;
27: use PHPStan\Type\Constant\ConstantBooleanType;
28: use PHPStan\Type\Generic\TemplateMixedType;
29: use PHPStan\Type\Generic\TemplateType;
30: use PHPStan\Type\Traits\NonGeneralizableTypeTrait;
31: use PHPStan\Type\Traits\NonGenericTypeTrait;
32: use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait;
33: use function sprintf;
34:
35: /** @api */
36: class MixedType implements CompoundType, SubtractableType
37: {
38:
39: use NonGenericTypeTrait;
40: use UndecidedComparisonCompoundTypeTrait;
41: use NonGeneralizableTypeTrait;
42:
43: private ?Type $subtractedType;
44:
45: /** @api */
46: public function __construct(
47: private bool $isExplicitMixed = false,
48: ?Type $subtractedType = null,
49: )
50: {
51: if ($subtractedType instanceof NeverType) {
52: $subtractedType = null;
53: }
54:
55: $this->subtractedType = $subtractedType;
56: }
57:
58: /**
59: * @return string[]
60: */
61: public function getReferencedClasses(): array
62: {
63: return [];
64: }
65:
66: public function getArrays(): array
67: {
68: return [];
69: }
70:
71: public function getConstantArrays(): array
72: {
73: return [];
74: }
75:
76: public function getConstantStrings(): array
77: {
78: return [];
79: }
80:
81: public function accepts(Type $type, bool $strictTypes): TrinaryLogic
82: {
83: return TrinaryLogic::createYes();
84: }
85:
86: public function isSuperTypeOfMixed(MixedType $type): TrinaryLogic
87: {
88: if ($this->subtractedType === null) {
89: if ($this->isExplicitMixed) {
90: if ($type->isExplicitMixed) {
91: return TrinaryLogic::createYes();
92: }
93: return TrinaryLogic::createMaybe();
94: }
95:
96: return TrinaryLogic::createYes();
97: }
98:
99: if ($type->subtractedType === null) {
100: return TrinaryLogic::createMaybe();
101: }
102:
103: $isSuperType = $type->subtractedType->isSuperTypeOf($this->subtractedType);
104: if ($isSuperType->yes()) {
105: if ($this->isExplicitMixed) {
106: if ($type->isExplicitMixed) {
107: return TrinaryLogic::createYes();
108: }
109: return TrinaryLogic::createMaybe();
110: }
111:
112: return TrinaryLogic::createYes();
113: }
114:
115: return TrinaryLogic::createMaybe();
116: }
117:
118: public function isSuperTypeOf(Type $type): TrinaryLogic
119: {
120: if ($this->subtractedType === null || $type instanceof NeverType) {
121: return TrinaryLogic::createYes();
122: }
123:
124: if ($type instanceof self) {
125: if ($type->subtractedType === null) {
126: return TrinaryLogic::createMaybe();
127: }
128: $isSuperType = $type->subtractedType->isSuperTypeOf($this->subtractedType);
129: if ($isSuperType->yes()) {
130: return TrinaryLogic::createYes();
131: }
132:
133: return TrinaryLogic::createMaybe();
134: }
135:
136: return $this->subtractedType->isSuperTypeOf($type)->negate();
137: }
138:
139: public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type
140: {
141: return new self($this->isExplicitMixed);
142: }
143:
144: public function unsetOffset(Type $offsetType): Type
145: {
146: if ($this->subtractedType !== null) {
147: return new self($this->isExplicitMixed, TypeCombinator::remove($this->subtractedType, new ConstantArrayType([], [])));
148: }
149: return $this;
150: }
151:
152: public function getKeysArray(): Type
153: {
154: if ($this->isArray()->no()) {
155: return new ErrorType();
156: }
157:
158: return AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), new UnionType([new IntegerType(), new StringType()])));
159: }
160:
161: public function getValuesArray(): Type
162: {
163: if ($this->isArray()->no()) {
164: return new ErrorType();
165: }
166:
167: return AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), new MixedType($this->isExplicitMixed)));
168: }
169:
170: public function fillKeysArray(Type $valueType): Type
171: {
172: if ($this->isArray()->no()) {
173: return new ErrorType();
174: }
175:
176: return new ArrayType($this->getIterableValueType(), $valueType);
177: }
178:
179: public function flipArray(): Type
180: {
181: if ($this->isArray()->no()) {
182: return new ErrorType();
183: }
184:
185: return new ArrayType(new MixedType($this->isExplicitMixed), new MixedType($this->isExplicitMixed));
186: }
187:
188: public function intersectKeyArray(Type $otherArraysType): Type
189: {
190: if ($this->isArray()->no()) {
191: return new ErrorType();
192: }
193:
194: return new ArrayType(new MixedType($this->isExplicitMixed), new MixedType($this->isExplicitMixed));
195: }
196:
197: public function popArray(): Type
198: {
199: if ($this->isArray()->no()) {
200: return new ErrorType();
201: }
202:
203: return new ArrayType(new MixedType($this->isExplicitMixed), new MixedType($this->isExplicitMixed));
204: }
205:
206: public function searchArray(Type $needleType): Type
207: {
208: if ($this->isArray()->no()) {
209: return new ErrorType();
210: }
211:
212: return TypeCombinator::union(new IntegerType(), new StringType(), new ConstantBooleanType(false));
213: }
214:
215: public function shiftArray(): Type
216: {
217: if ($this->isArray()->no()) {
218: return new ErrorType();
219: }
220:
221: return new ArrayType(new MixedType($this->isExplicitMixed), new MixedType($this->isExplicitMixed));
222: }
223:
224: public function shuffleArray(): Type
225: {
226: if ($this->isArray()->no()) {
227: return new ErrorType();
228: }
229:
230: return AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), new MixedType($this->isExplicitMixed)));
231: }
232:
233: public function isCallable(): TrinaryLogic
234: {
235: if ($this->subtractedType !== null) {
236: if ($this->subtractedType->isSuperTypeOf(new CallableType())->yes()) {
237: return TrinaryLogic::createNo();
238: }
239: }
240:
241: return TrinaryLogic::createMaybe();
242: }
243:
244: /**
245: * @return ParametersAcceptor[]
246: */
247: public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array
248: {
249: return [new TrivialParametersAcceptor()];
250: }
251:
252: public function equals(Type $type): bool
253: {
254: if (!$type instanceof self) {
255: return false;
256: }
257:
258: if ($this->subtractedType === null) {
259: if ($type->subtractedType === null) {
260: return true;
261: }
262:
263: return false;
264: }
265:
266: if ($type->subtractedType === null) {
267: return false;
268: }
269:
270: return $this->subtractedType->equals($type->subtractedType);
271: }
272:
273: public function isSubTypeOf(Type $otherType): TrinaryLogic
274: {
275: if ($otherType instanceof self && !$otherType instanceof TemplateMixedType) {
276: return TrinaryLogic::createYes();
277: }
278:
279: if ($this->subtractedType !== null) {
280: $isSuperType = $this->subtractedType->isSuperTypeOf($otherType);
281: if ($isSuperType->yes()) {
282: return TrinaryLogic::createNo();
283: }
284: }
285:
286: return TrinaryLogic::createMaybe();
287: }
288:
289: public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic
290: {
291: $isSuperType = $this->isSuperTypeOf($acceptingType);
292: if ($isSuperType->no()) {
293: return $isSuperType;
294: }
295: return TrinaryLogic::createYes();
296: }
297:
298: public function canAccessProperties(): TrinaryLogic
299: {
300: return TrinaryLogic::createYes();
301: }
302:
303: public function hasProperty(string $propertyName): TrinaryLogic
304: {
305: return TrinaryLogic::createYes();
306: }
307:
308: public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): PropertyReflection
309: {
310: return $this->getUnresolvedPropertyPrototype($propertyName, $scope)->getTransformedProperty();
311: }
312:
313: public function getUnresolvedPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection
314: {
315: $property = new DummyPropertyReflection();
316: return new CallbackUnresolvedPropertyPrototypeReflection(
317: $property,
318: $property->getDeclaringClass(),
319: false,
320: static fn (Type $type): Type => $type,
321: );
322: }
323:
324: public function canCallMethods(): TrinaryLogic
325: {
326: return TrinaryLogic::createYes();
327: }
328:
329: public function hasMethod(string $methodName): TrinaryLogic
330: {
331: return TrinaryLogic::createYes();
332: }
333:
334: public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): ExtendedMethodReflection
335: {
336: return $this->getUnresolvedMethodPrototype($methodName, $scope)->getTransformedMethod();
337: }
338:
339: public function getUnresolvedMethodPrototype(string $methodName, ClassMemberAccessAnswerer $scope): UnresolvedMethodPrototypeReflection
340: {
341: $method = new DummyMethodReflection($methodName);
342: return new CallbackUnresolvedMethodPrototypeReflection(
343: $method,
344: $method->getDeclaringClass(),
345: false,
346: static fn (Type $type): Type => $type,
347: );
348: }
349:
350: public function canAccessConstants(): TrinaryLogic
351: {
352: return TrinaryLogic::createYes();
353: }
354:
355: public function hasConstant(string $constantName): TrinaryLogic
356: {
357: return TrinaryLogic::createYes();
358: }
359:
360: public function getConstant(string $constantName): ConstantReflection
361: {
362: return new DummyConstantReflection($constantName);
363: }
364:
365: public function isCloneable(): TrinaryLogic
366: {
367: return TrinaryLogic::createYes();
368: }
369:
370: public function describe(VerbosityLevel $level): string
371: {
372: return $level->handle(
373: static fn (): string => 'mixed',
374: static fn (): string => 'mixed',
375: function () use ($level): string {
376: $description = 'mixed';
377: if ($this->subtractedType !== null) {
378: $description .= sprintf('~%s', $this->subtractedType->describe($level));
379: }
380:
381: return $description;
382: },
383: function () use ($level): string {
384: $description = 'mixed';
385: if ($this->subtractedType !== null) {
386: $description .= sprintf('~%s', $this->subtractedType->describe($level));
387: }
388:
389: if ($this->isExplicitMixed) {
390: $description .= '=explicit';
391: } else {
392: $description .= '=implicit';
393: }
394:
395: return $description;
396: },
397: );
398: }
399:
400: public function toBoolean(): BooleanType
401: {
402: if ($this->subtractedType !== null && StaticTypeFactory::falsey()->equals($this->subtractedType)) {
403: return new ConstantBooleanType(true);
404: }
405:
406: return new BooleanType();
407: }
408:
409: public function toNumber(): Type
410: {
411: return new UnionType([
412: $this->toInteger(),
413: $this->toFloat(),
414: ]);
415: }
416:
417: public function toInteger(): Type
418: {
419: return new IntegerType();
420: }
421:
422: public function toFloat(): Type
423: {
424: return new FloatType();
425: }
426:
427: public function toString(): Type
428: {
429: return new StringType();
430: }
431:
432: public function toArray(): Type
433: {
434: $mixed = new self($this->isExplicitMixed);
435:
436: return new ArrayType($mixed, $mixed);
437: }
438:
439: public function toArrayKey(): Type
440: {
441: return new UnionType([new IntegerType(), new StringType()]);
442: }
443:
444: public function isIterable(): TrinaryLogic
445: {
446: if ($this->subtractedType !== null) {
447: if ($this->subtractedType->isSuperTypeOf(new IterableType(new MixedType(), new MixedType()))->yes()) {
448: return TrinaryLogic::createNo();
449: }
450: }
451:
452: return TrinaryLogic::createMaybe();
453: }
454:
455: public function isIterableAtLeastOnce(): TrinaryLogic
456: {
457: return $this->isIterable();
458: }
459:
460: public function getArraySize(): Type
461: {
462: if ($this->isIterable()->no()) {
463: return new ErrorType();
464: }
465:
466: return IntegerRangeType::fromInterval(0, null);
467: }
468:
469: public function getIterableKeyType(): Type
470: {
471: return new self($this->isExplicitMixed);
472: }
473:
474: public function getFirstIterableKeyType(): Type
475: {
476: return new self($this->isExplicitMixed);
477: }
478:
479: public function getLastIterableKeyType(): Type
480: {
481: return new self($this->isExplicitMixed);
482: }
483:
484: public function getIterableValueType(): Type
485: {
486: return new self($this->isExplicitMixed);
487: }
488:
489: public function getFirstIterableValueType(): Type
490: {
491: return new self($this->isExplicitMixed);
492: }
493:
494: public function getLastIterableValueType(): Type
495: {
496: return new self($this->isExplicitMixed);
497: }
498:
499: public function isOffsetAccessible(): TrinaryLogic
500: {
501: if ($this->subtractedType !== null) {
502: $offsetAccessibles = new UnionType([
503: new StringType(),
504: new ArrayType(new MixedType(), new MixedType()),
505: new ObjectType(ArrayAccess::class),
506: ]);
507:
508: if ($this->subtractedType->isSuperTypeOf($offsetAccessibles)->yes()) {
509: return TrinaryLogic::createNo();
510: }
511: }
512: return TrinaryLogic::createMaybe();
513: }
514:
515: public function hasOffsetValueType(Type $offsetType): TrinaryLogic
516: {
517: if ($this->isOffsetAccessible()->no()) {
518: return TrinaryLogic::createNo();
519: }
520:
521: return TrinaryLogic::createMaybe();
522: }
523:
524: public function getOffsetValueType(Type $offsetType): Type
525: {
526: return new self($this->isExplicitMixed);
527: }
528:
529: public function isExplicitMixed(): bool
530: {
531: return $this->isExplicitMixed;
532: }
533:
534: public function subtract(Type $type): Type
535: {
536: if ($type instanceof self && !$type instanceof TemplateType) {
537: return new NeverType();
538: }
539: if ($this->subtractedType !== null) {
540: $type = TypeCombinator::union($this->subtractedType, $type);
541: }
542:
543: return new self($this->isExplicitMixed, $type);
544: }
545:
546: public function getTypeWithoutSubtractedType(): Type
547: {
548: return new self($this->isExplicitMixed);
549: }
550:
551: public function changeSubtractedType(?Type $subtractedType): Type
552: {
553: return new self($this->isExplicitMixed, $subtractedType);
554: }
555:
556: public function getSubtractedType(): ?Type
557: {
558: return $this->subtractedType;
559: }
560:
561: public function traverse(callable $cb): Type
562: {
563: return $this;
564: }
565:
566: public function isArray(): TrinaryLogic
567: {
568: if ($this->subtractedType !== null) {
569: if ($this->subtractedType->isSuperTypeOf(new ArrayType(new MixedType(), new MixedType()))->yes()) {
570: return TrinaryLogic::createNo();
571: }
572: }
573:
574: return TrinaryLogic::createMaybe();
575: }
576:
577: public function isConstantArray(): TrinaryLogic
578: {
579: return $this->isArray();
580: }
581:
582: public function isOversizedArray(): TrinaryLogic
583: {
584: if ($this->subtractedType !== null) {
585: $oversizedArray = TypeCombinator::intersect(
586: new ArrayType(new MixedType(), new MixedType()),
587: new OversizedArrayType(),
588: );
589:
590: if ($this->subtractedType->isSuperTypeOf($oversizedArray)->yes()) {
591: return TrinaryLogic::createNo();
592: }
593: }
594:
595: return TrinaryLogic::createMaybe();
596: }
597:
598: public function isList(): TrinaryLogic
599: {
600: if ($this->subtractedType !== null) {
601: $list = TypeCombinator::intersect(
602: new ArrayType(new IntegerType(), new MixedType()),
603: new AccessoryArrayListType(),
604: );
605:
606: if ($this->subtractedType->isSuperTypeOf($list)->yes()) {
607: return TrinaryLogic::createNo();
608: }
609: }
610:
611: return TrinaryLogic::createMaybe();
612: }
613:
614: public function isNull(): TrinaryLogic
615: {
616: if ($this->subtractedType !== null) {
617: if ($this->subtractedType->isSuperTypeOf(new NullType())->yes()) {
618: return TrinaryLogic::createNo();
619: }
620: }
621:
622: return TrinaryLogic::createMaybe();
623: }
624:
625: public function isTrue(): TrinaryLogic
626: {
627: if ($this->subtractedType !== null) {
628: if ($this->subtractedType->isSuperTypeOf(new ConstantBooleanType(true))->yes()) {
629: return TrinaryLogic::createNo();
630: }
631: }
632:
633: return TrinaryLogic::createMaybe();
634: }
635:
636: public function isFalse(): TrinaryLogic
637: {
638: if ($this->subtractedType !== null) {
639: if ($this->subtractedType->isSuperTypeOf(new ConstantBooleanType(false))->yes()) {
640: return TrinaryLogic::createNo();
641: }
642: }
643:
644: return TrinaryLogic::createMaybe();
645: }
646:
647: public function isBoolean(): TrinaryLogic
648: {
649: if ($this->subtractedType !== null) {
650: if ($this->subtractedType->isSuperTypeOf(new BooleanType())->yes()) {
651: return TrinaryLogic::createNo();
652: }
653: }
654:
655: return TrinaryLogic::createMaybe();
656: }
657:
658: public function isFloat(): TrinaryLogic
659: {
660: if ($this->subtractedType !== null) {
661: if ($this->subtractedType->isSuperTypeOf(new FloatType())->yes()) {
662: return TrinaryLogic::createNo();
663: }
664: }
665:
666: return TrinaryLogic::createMaybe();
667: }
668:
669: public function isInteger(): TrinaryLogic
670: {
671: if ($this->subtractedType !== null) {
672: if ($this->subtractedType->isSuperTypeOf(new IntegerType())->yes()) {
673: return TrinaryLogic::createNo();
674: }
675: }
676:
677: return TrinaryLogic::createMaybe();
678: }
679:
680: public function isString(): TrinaryLogic
681: {
682: if ($this->subtractedType !== null) {
683: if ($this->subtractedType->isSuperTypeOf(new StringType())->yes()) {
684: return TrinaryLogic::createNo();
685: }
686: }
687: return TrinaryLogic::createMaybe();
688: }
689:
690: public function isNumericString(): TrinaryLogic
691: {
692: if ($this->subtractedType !== null) {
693: $numericString = TypeCombinator::intersect(
694: new StringType(),
695: new AccessoryNumericStringType(),
696: );
697:
698: if ($this->subtractedType->isSuperTypeOf($numericString)->yes()) {
699: return TrinaryLogic::createNo();
700: }
701: }
702:
703: return TrinaryLogic::createMaybe();
704: }
705:
706: public function isNonEmptyString(): TrinaryLogic
707: {
708: if ($this->subtractedType !== null) {
709: $nonEmptyString = TypeCombinator::intersect(
710: new StringType(),
711: new AccessoryNonEmptyStringType(),
712: );
713:
714: if ($this->subtractedType->isSuperTypeOf($nonEmptyString)->yes()) {
715: return TrinaryLogic::createNo();
716: }
717: }
718:
719: return TrinaryLogic::createMaybe();
720: }
721:
722: public function isNonFalsyString(): TrinaryLogic
723: {
724: if ($this->subtractedType !== null) {
725: $nonFalsyString = TypeCombinator::intersect(
726: new StringType(),
727: new AccessoryNonFalsyStringType(),
728: );
729:
730: if ($this->subtractedType->isSuperTypeOf($nonFalsyString)->yes()) {
731: return TrinaryLogic::createNo();
732: }
733: }
734:
735: return TrinaryLogic::createMaybe();
736: }
737:
738: public function isLiteralString(): TrinaryLogic
739: {
740: if ($this->subtractedType !== null) {
741: $literalString = TypeCombinator::intersect(
742: new StringType(),
743: new AccessoryLiteralStringType(),
744: );
745:
746: if ($this->subtractedType->isSuperTypeOf($literalString)->yes()) {
747: return TrinaryLogic::createNo();
748: }
749: }
750:
751: return TrinaryLogic::createMaybe();
752: }
753:
754: public function isClassStringType(): TrinaryLogic
755: {
756: if ($this->subtractedType !== null) {
757: if ($this->subtractedType->isSuperTypeOf(new StringType())->yes()) {
758: return TrinaryLogic::createNo();
759: }
760: if ($this->subtractedType->isSuperTypeOf(new ClassStringType())->yes()) {
761: return TrinaryLogic::createNo();
762: }
763: }
764:
765: return TrinaryLogic::createMaybe();
766: }
767:
768: public function isVoid(): TrinaryLogic
769: {
770: if ($this->subtractedType !== null) {
771: if ($this->subtractedType->isSuperTypeOf(new VoidType())->yes()) {
772: return TrinaryLogic::createNo();
773: }
774: }
775:
776: return TrinaryLogic::createMaybe();
777: }
778:
779: public function isScalar(): TrinaryLogic
780: {
781: if ($this->subtractedType !== null) {
782: if ($this->subtractedType->isSuperTypeOf(new UnionType([new BooleanType(), new FloatType(), new IntegerType(), new StringType()]))->yes()) {
783: return TrinaryLogic::createNo();
784: }
785: }
786:
787: return TrinaryLogic::createMaybe();
788: }
789:
790: public function tryRemove(Type $typeToRemove): ?Type
791: {
792: if ($this->isSuperTypeOf($typeToRemove)->yes()) {
793: return $this->subtract($typeToRemove);
794: }
795:
796: return null;
797: }
798:
799: /**
800: * @param mixed[] $properties
801: */
802: public static function __set_state(array $properties): Type
803: {
804: return new self(
805: $properties['isExplicitMixed'],
806: $properties['subtractedType'] ?? null,
807: );
808: }
809:
810: }
811: