1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Type\Generic;
4:
5: use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
6: use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
7: use PHPStan\PhpDocParser\Ast\Type\TypeNode;
8: use PHPStan\Reflection\ReflectionProviderStaticAccessor;
9: use PHPStan\Type\AcceptsResult;
10: use PHPStan\Type\ClassNameToObjectTypeResult;
11: use PHPStan\Type\ClassStringType;
12: use PHPStan\Type\CompoundType;
13: use PHPStan\Type\Constant\ConstantStringType;
14: use PHPStan\Type\InstanceofDeprecated;
15: use PHPStan\Type\IntersectionType;
16: use PHPStan\Type\IsSuperTypeOfResult;
17: use PHPStan\Type\MixedType;
18: use PHPStan\Type\NeverType;
19: use PHPStan\Type\ObjectType;
20: use PHPStan\Type\ObjectWithoutClassType;
21: use PHPStan\Type\StaticType;
22: use PHPStan\Type\StringType;
23: use PHPStan\Type\Type;
24: use PHPStan\Type\TypeCombinator;
25: use PHPStan\Type\UnionType;
26: use PHPStan\Type\VerbosityLevel;
27: use function count;
28: use function sprintf;
29:
30: /** @api */
31: #[InstanceofDeprecated(insteadUse: 'Type::isClassStringType() and Type::getClassStringObjectType()')]
32: class GenericClassStringType extends ClassStringType
33: {
34:
35: /** @api */
36: public function __construct(private Type $type)
37: {
38: parent::__construct();
39: }
40:
41: public function getReferencedClasses(): array
42: {
43: return $this->type->getReferencedClasses();
44: }
45:
46: public function getGenericType(): Type
47: {
48: return $this->type;
49: }
50:
51: public function toObjectTypeForInstanceofCheck(): ClassNameToObjectTypeResult
52: {
53: // `class-string<X>` narrows to `X` for the comparison target, but
54: // the actual runtime class can be any subclass of `X` — keep
55: // uncertainty so the caller falls back to `BooleanType` instead
56: // of a definite yes when `$x instanceof Y` and `Y === X`.
57: return new ClassNameToObjectTypeResult($this->getGenericType(), true);
58: }
59:
60: public function toObjectTypeForIsACheck(Type $objectOrClassType, bool $allowString, bool $allowSameClass): ClassNameToObjectTypeResult
61: {
62: if ($allowString) {
63: return new ClassNameToObjectTypeResult(
64: TypeCombinator::union($this->getGenericType(), $this),
65: false,
66: );
67: }
68:
69: return new ClassNameToObjectTypeResult($this->getGenericType(), false);
70: }
71:
72: public function getClassStringObjectType(): Type
73: {
74: return $this->getGenericType();
75: }
76:
77: public function getObjectTypeOrClassStringObjectType(): Type
78: {
79: return $this->getClassStringObjectType();
80: }
81:
82: public function describe(VerbosityLevel $level): string
83: {
84: return sprintf('%s<%s>', parent::describe($level), $this->type->describe($level));
85: }
86:
87: public function accepts(Type $type, bool $strictTypes): AcceptsResult
88: {
89: if ($type instanceof CompoundType) {
90: return $type->isAcceptedBy($this, $strictTypes);
91: }
92:
93: if ($type instanceof ConstantStringType) {
94: if (!$type->isClassString()->yes()) {
95: return AcceptsResult::createNo();
96: }
97:
98: $objectType = new ObjectType($type->getValue());
99: } elseif ($type instanceof self) {
100: $objectType = $type->type;
101: } elseif ($type instanceof ClassStringType) {
102: $objectType = new ObjectWithoutClassType();
103: } elseif ($type instanceof StringType) {
104: return AcceptsResult::createMaybe();
105: } else {
106: return AcceptsResult::createNo();
107: }
108:
109: return $this->type->accepts($objectType, $strictTypes);
110: }
111:
112: public function isSuperTypeOf(Type $type): IsSuperTypeOfResult
113: {
114: if ($type instanceof CompoundType) {
115: return $type->isSubTypeOf($this);
116: }
117:
118: if ($type instanceof ConstantStringType) {
119: $genericType = $this->type;
120: if ($genericType instanceof MixedType) {
121: return IsSuperTypeOfResult::createYes();
122: }
123:
124: if ($genericType instanceof StaticType) {
125: $genericType = $genericType->getStaticObjectType();
126: }
127:
128: // We are transforming constant class-string to ObjectType. But we need to filter out
129: // an uncertainty originating in possible ObjectType's class subtypes.
130: $objectType = new ObjectType($type->getValue());
131:
132: // Do not use TemplateType's isSuperTypeOf handling directly because it takes ObjectType
133: // uncertainty into account.
134: if ($genericType instanceof TemplateType) {
135: $isSuperType = $genericType->getBound()->isSuperTypeOf($objectType);
136: } else {
137: $isSuperType = $genericType->isSuperTypeOf($objectType);
138: }
139:
140: if (!$type->isClassString()->yes()) {
141: $isSuperType = $isSuperType->and(IsSuperTypeOfResult::createMaybe());
142: }
143:
144: return $isSuperType;
145: } elseif ($type instanceof self) {
146: return $this->type->isSuperTypeOf($type->type);
147: } elseif ($type instanceof ClassStringType) {
148: return $this->type->isSuperTypeOf(new ObjectWithoutClassType());
149: } elseif ($type instanceof StringType) {
150: return IsSuperTypeOfResult::createMaybe();
151: }
152:
153: return IsSuperTypeOfResult::createNo();
154: }
155:
156: public function traverse(callable $cb): Type
157: {
158: $newType = $cb($this->type);
159: if ($newType === $this->type) {
160: return $this;
161: }
162:
163: return new self($newType);
164: }
165:
166: public function traverseSimultaneously(Type $right, callable $cb): Type
167: {
168: $newType = $cb($this->type, $right->getClassStringObjectType());
169: if ($newType === $this->type) {
170: return $this;
171: }
172:
173: return new self($newType);
174: }
175:
176: public function inferTemplateTypes(Type $receivedType): TemplateTypeMap
177: {
178: if ($receivedType instanceof UnionType || $receivedType instanceof IntersectionType) {
179: return $receivedType->inferTemplateTypesOn($this);
180: }
181:
182: if ($receivedType instanceof ConstantStringType) {
183: $typeToInfer = new ObjectType($receivedType->getValue());
184: } elseif ($receivedType instanceof self) {
185: $typeToInfer = $receivedType->type;
186: } elseif ($receivedType->isClassString()->yes()) {
187: $typeToInfer = $this->type;
188: if ($typeToInfer instanceof TemplateType) {
189: $typeToInfer = $typeToInfer->getBound();
190: }
191:
192: $typeToInfer = TypeCombinator::intersect($typeToInfer, new ObjectWithoutClassType());
193: } else {
194: return TemplateTypeMap::createEmpty();
195: }
196:
197: return $this->type->inferTemplateTypes($typeToInfer);
198: }
199:
200: public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array
201: {
202: $variance = $positionVariance->compose(TemplateTypeVariance::createCovariant());
203:
204: return $this->type->getReferencedTemplateTypes($variance);
205: }
206:
207: public function equals(Type $type): bool
208: {
209: if (!$type instanceof self) {
210: return false;
211: }
212:
213: if (!parent::equals($type)) {
214: return false;
215: }
216:
217: if (!$this->type->equals($type->type)) {
218: return false;
219: }
220:
221: return true;
222: }
223:
224: public function toPhpDocNode(): TypeNode
225: {
226: return new GenericTypeNode(
227: new IdentifierTypeNode('class-string'),
228: [
229: $this->type->toPhpDocNode(),
230: ],
231: );
232: }
233:
234: public function tryRemove(Type $typeToRemove): ?Type
235: {
236: if ($typeToRemove instanceof ConstantStringType && $typeToRemove->isClassString()->yes()) {
237: $generic = $this->getGenericType();
238:
239: $genericObjectClassNames = $generic->getObjectClassNames();
240: $reflectionProvider = ReflectionProviderStaticAccessor::getInstance();
241:
242: if (count($genericObjectClassNames) === 1) {
243: if ($reflectionProvider->hasClass($genericObjectClassNames[0])) {
244: $classReflection = $reflectionProvider->getClass($genericObjectClassNames[0]);
245: if ($classReflection->isFinal() && $genericObjectClassNames[0] === $typeToRemove->getValue()) {
246: return new NeverType();
247: }
248:
249: if ($classReflection->getAllowedSubTypes() !== null) {
250: $objectTypeToRemove = new ObjectType($typeToRemove->getValue());
251: $remainingType = TypeCombinator::remove($generic, $objectTypeToRemove);
252: if ($remainingType instanceof NeverType) {
253: return new NeverType();
254: }
255:
256: if (!$remainingType->equals($generic)) {
257: return new self($remainingType);
258: }
259: }
260: }
261: } elseif (count($genericObjectClassNames) > 1) {
262: $objectTypeToRemove = new ObjectType($typeToRemove->getValue());
263: if ($reflectionProvider->hasClass($typeToRemove->getValue())) {
264: $classReflection = $reflectionProvider->getClass($typeToRemove->getValue());
265: if ($classReflection->isFinal()) {
266: $remainingType = TypeCombinator::remove($generic, $objectTypeToRemove);
267: if ($remainingType instanceof NeverType) {
268: return new NeverType();
269: }
270:
271: return new self($remainingType);
272: }
273: }
274: }
275: }
276:
277: return parent::tryRemove($typeToRemove);
278: }
279:
280: public function hasTemplateOrLateResolvableType(): bool
281: {
282: return $this->type->hasTemplateOrLateResolvableType();
283: }
284:
285: }
286: