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 ClassStringType) {
146: return $this->type->isSuperTypeOf(new ObjectWithoutClassType());
147: } elseif ($type instanceof StringType) {
148: return IsSuperTypeOfResult::createMaybe();
149: }
150:
151: return IsSuperTypeOfResult::createNo();
152: }
153:
154: public function traverse(callable $cb): Type
155: {
156: $newType = $cb($this->type);
157: if ($newType === $this->type) {
158: return $this;
159: }
160:
161: return new self($newType);
162: }
163:
164: public function traverseSimultaneously(Type $right, callable $cb): Type
165: {
166: $newType = $cb($this->type, $right->getClassStringObjectType());
167: if ($newType === $this->type) {
168: return $this;
169: }
170:
171: return new self($newType);
172: }
173:
174: public function inferTemplateTypes(Type $receivedType): TemplateTypeMap
175: {
176: if ($receivedType instanceof UnionType || $receivedType instanceof IntersectionType) {
177: return $receivedType->inferTemplateTypesOn($this);
178: }
179:
180: if ($receivedType instanceof ConstantStringType) {
181: $typeToInfer = new ObjectType($receivedType->getValue());
182: } elseif ($receivedType instanceof self) {
183: $typeToInfer = $receivedType->type;
184: } elseif ($receivedType->isClassString()->yes()) {
185: $typeToInfer = $this->type;
186: if ($typeToInfer instanceof TemplateType) {
187: $typeToInfer = $typeToInfer->getBound();
188: }
189:
190: $typeToInfer = TypeCombinator::intersect($typeToInfer, new ObjectWithoutClassType());
191: } else {
192: return TemplateTypeMap::createEmpty();
193: }
194:
195: return $this->type->inferTemplateTypes($typeToInfer);
196: }
197:
198: public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array
199: {
200: $variance = $positionVariance->compose(TemplateTypeVariance::createCovariant());
201:
202: return $this->type->getReferencedTemplateTypes($variance);
203: }
204:
205: public function equals(Type $type): bool
206: {
207: if (!$type instanceof self) {
208: return false;
209: }
210:
211: if (!parent::equals($type)) {
212: return false;
213: }
214:
215: if (!$this->type->equals($type->type)) {
216: return false;
217: }
218:
219: return true;
220: }
221:
222: public function toPhpDocNode(): TypeNode
223: {
224: return new GenericTypeNode(
225: new IdentifierTypeNode('class-string'),
226: [
227: $this->type->toPhpDocNode(),
228: ],
229: );
230: }
231:
232: public function tryRemove(Type $typeToRemove): ?Type
233: {
234: if ($typeToRemove instanceof ConstantStringType && $typeToRemove->isClassString()->yes()) {
235: $generic = $this->getGenericType();
236:
237: $genericObjectClassNames = $generic->getObjectClassNames();
238: $reflectionProvider = ReflectionProviderStaticAccessor::getInstance();
239:
240: if (count($genericObjectClassNames) === 1) {
241: if ($reflectionProvider->hasClass($genericObjectClassNames[0])) {
242: $classReflection = $reflectionProvider->getClass($genericObjectClassNames[0]);
243: if ($classReflection->isFinal() && $genericObjectClassNames[0] === $typeToRemove->getValue()) {
244: return new NeverType();
245: }
246:
247: if ($classReflection->getAllowedSubTypes() !== null) {
248: $objectTypeToRemove = new ObjectType($typeToRemove->getValue());
249: $remainingType = TypeCombinator::remove($generic, $objectTypeToRemove);
250: if ($remainingType instanceof NeverType) {
251: return new NeverType();
252: }
253:
254: if (!$remainingType->equals($generic)) {
255: return new self($remainingType);
256: }
257: }
258: }
259: } elseif (count($genericObjectClassNames) > 1) {
260: $objectTypeToRemove = new ObjectType($typeToRemove->getValue());
261: if ($reflectionProvider->hasClass($typeToRemove->getValue())) {
262: $classReflection = $reflectionProvider->getClass($typeToRemove->getValue());
263: if ($classReflection->isFinal()) {
264: $remainingType = TypeCombinator::remove($generic, $objectTypeToRemove);
265: if ($remainingType instanceof NeverType) {
266: return new NeverType();
267: }
268:
269: return new self($remainingType);
270: }
271: }
272: }
273: }
274:
275: return parent::tryRemove($typeToRemove);
276: }
277:
278: public function hasTemplateOrLateResolvableType(): bool
279: {
280: return $this->type->hasTemplateOrLateResolvableType();
281: }
282:
283: }
284: