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