1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Type\Accessory;
4:
5: use PHPStan\Php\PhpVersion;
6: use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
7: use PHPStan\PhpDocParser\Ast\Type\TypeNode;
8: use PHPStan\TrinaryLogic;
9: use PHPStan\Type\AcceptsResult;
10: use PHPStan\Type\BooleanType;
11: use PHPStan\Type\CompoundType;
12: use PHPStan\Type\Constant\ConstantIntegerType;
13: use PHPStan\Type\Constant\ConstantStringType;
14: use PHPStan\Type\Enum\EnumCaseObjectType;
15: use PHPStan\Type\ErrorType;
16: use PHPStan\Type\IntegerRangeType;
17: use PHPStan\Type\IntersectionType;
18: use PHPStan\Type\IsSuperTypeOfResult;
19: use PHPStan\Type\MixedType;
20: use PHPStan\Type\ObjectWithoutClassType;
21: use PHPStan\Type\Traits\MaybeArrayTypeTrait;
22: use PHPStan\Type\Traits\MaybeCallableTypeTrait;
23: use PHPStan\Type\Traits\MaybeIterableTypeTrait;
24: use PHPStan\Type\Traits\MaybeObjectTypeTrait;
25: use PHPStan\Type\Traits\MaybeStringTypeTrait;
26: use PHPStan\Type\Traits\NonGeneralizableTypeTrait;
27: use PHPStan\Type\Traits\NonGenericTypeTrait;
28: use PHPStan\Type\Traits\NonRemoveableTypeTrait;
29: use PHPStan\Type\Traits\TruthyBooleanTypeTrait;
30: use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait;
31: use PHPStan\Type\Type;
32: use PHPStan\Type\TypeCombinator;
33: use PHPStan\Type\UnionType;
34: use PHPStan\Type\VerbosityLevel;
35: use function sprintf;
36: use function strtolower;
37: use function strtoupper;
38: use const CASE_LOWER;
39: use const CASE_UPPER;
40:
41: class HasOffsetType implements CompoundType, AccessoryType
42: {
43:
44: use MaybeArrayTypeTrait;
45: use MaybeCallableTypeTrait;
46: use MaybeIterableTypeTrait;
47: use MaybeObjectTypeTrait;
48: use MaybeStringTypeTrait;
49: use TruthyBooleanTypeTrait;
50: use NonGenericTypeTrait;
51: use UndecidedComparisonCompoundTypeTrait;
52: use NonRemoveableTypeTrait;
53: use NonGeneralizableTypeTrait;
54:
55: /**
56: * @api
57: */
58: public function __construct(private ConstantStringType|ConstantIntegerType $offsetType)
59: {
60: }
61:
62: public function getOffsetType(): ConstantStringType|ConstantIntegerType
63: {
64: return $this->offsetType;
65: }
66:
67: public function getReferencedClasses(): array
68: {
69: return [];
70: }
71:
72: public function getObjectClassNames(): array
73: {
74: return [];
75: }
76:
77: public function getObjectClassReflections(): array
78: {
79: return [];
80: }
81:
82: public function accepts(Type $type, bool $strictTypes): AcceptsResult
83: {
84: if ($type instanceof CompoundType) {
85: return $type->isAcceptedBy($this, $strictTypes);
86: }
87:
88: return new AcceptsResult($type->isOffsetAccessible()->and($type->hasOffsetValueType($this->offsetType)), []);
89: }
90:
91: public function isSuperTypeOf(Type $type): IsSuperTypeOfResult
92: {
93: if ($this->equals($type)) {
94: return IsSuperTypeOfResult::createYes();
95: }
96: return new IsSuperTypeOfResult($type->isOffsetAccessible()->and($type->hasOffsetValueType($this->offsetType)), []);
97: }
98:
99: public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult
100: {
101: if ($otherType instanceof UnionType || $otherType instanceof IntersectionType) {
102: return $otherType->isSuperTypeOf($this);
103: }
104:
105: return new IsSuperTypeOfResult(
106: $otherType->isOffsetAccessible()->and($otherType->hasOffsetValueType($this->offsetType))->and($otherType instanceof self ? TrinaryLogic::createYes() : TrinaryLogic::createMaybe()),
107: [],
108: );
109: }
110:
111: public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult
112: {
113: return $this->isSubTypeOf($acceptingType)->toAcceptsResult();
114: }
115:
116: public function equals(Type $type): bool
117: {
118: return $type instanceof self
119: && $this->offsetType->equals($type->offsetType);
120: }
121:
122: public function describe(VerbosityLevel $level): string
123: {
124: return sprintf('hasOffset(%s)', $this->offsetType->describe($level));
125: }
126:
127: public function isOffsetAccessible(): TrinaryLogic
128: {
129: return TrinaryLogic::createYes();
130: }
131:
132: public function isOffsetAccessLegal(): TrinaryLogic
133: {
134: return TrinaryLogic::createYes();
135: }
136:
137: public function hasOffsetValueType(Type $offsetType): TrinaryLogic
138: {
139: if ($offsetType->isConstantScalarValue()->yes() && $offsetType->equals($this->offsetType)) {
140: return TrinaryLogic::createYes();
141: }
142:
143: return TrinaryLogic::createMaybe();
144: }
145:
146: public function getOffsetValueType(Type $offsetType): Type
147: {
148: return new MixedType();
149: }
150:
151: public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type
152: {
153: return $this;
154: }
155:
156: public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type
157: {
158: return $this;
159: }
160:
161: public function unsetOffset(Type $offsetType): Type
162: {
163: if ($this->offsetType->isSuperTypeOf($offsetType)->yes()) {
164: return new ErrorType();
165: }
166: return $this;
167: }
168:
169: public function chunkArray(Type $lengthType, TrinaryLogic $preserveKeys): Type
170: {
171: return new NonEmptyArrayType();
172: }
173:
174: public function fillKeysArray(Type $valueType): Type
175: {
176: return new NonEmptyArrayType();
177: }
178:
179: public function intersectKeyArray(Type $otherArraysType): Type
180: {
181: if ($otherArraysType->hasOffsetValueType($this->offsetType)->yes()) {
182: return $this;
183: }
184:
185: return new MixedType();
186: }
187:
188: public function reverseArray(TrinaryLogic $preserveKeys): Type
189: {
190: if ($preserveKeys->yes()) {
191: return $this;
192: }
193:
194: return new NonEmptyArrayType();
195: }
196:
197: public function shuffleArray(): Type
198: {
199: return new NonEmptyArrayType();
200: }
201:
202: public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type
203: {
204: if (
205: $this->offsetType->isSuperTypeOf($offsetType)->yes()
206: && ($lengthType->isNull()->yes() || IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($lengthType)->yes())
207: ) {
208: return $preserveKeys->yes()
209: ? TypeCombinator::intersect($this, new NonEmptyArrayType())
210: : new NonEmptyArrayType();
211: }
212:
213: return new MixedType();
214: }
215:
216: public function spliceArray(Type $offsetType, Type $lengthType, Type $replacementType): Type
217: {
218: if ((new ConstantIntegerType(0))->isSuperTypeOf($lengthType)->yes()) {
219: return $this;
220: }
221:
222: return new MixedType();
223: }
224:
225: public function makeListMaybe(): Type
226: {
227: // Having an offset doesn't conflict with list-being-maybe.
228: return $this;
229: }
230:
231: public function mapValueType(callable $cb): Type
232: {
233: // `HasOffsetType` only records that an offset exists, not its
234: // value; the assertion still holds after a value transformation.
235: return $this;
236: }
237:
238: public function mapKeyType(callable $cb): Type
239: {
240: // Match the prior `TypeTraverser`-based pattern that left
241: // accessories untouched while rewriting the array key type.
242: return $this;
243: }
244:
245: public function makeAllArrayKeysOptional(): Type
246: {
247: // "Has offset X" is no longer guaranteed when X is now optional.
248: return new MixedType();
249: }
250:
251: public function changeKeyCaseArray(?int $case): Type
252: {
253: // A string offset is itself case-folded; an int offset is unchanged.
254: if (!$this->offsetType instanceof ConstantStringType) {
255: return $this;
256: }
257:
258: $value = $this->offsetType->getValue();
259: if ($case === CASE_LOWER) {
260: return new self(new ConstantStringType(strtolower($value)));
261: }
262: if ($case === CASE_UPPER) {
263: return new self(new ConstantStringType(strtoupper($value)));
264: }
265:
266: // Unknown case → could be either fold; the accessory weakens to
267: // "no specific offset known".
268: return new MixedType();
269: }
270:
271: public function filterArrayRemovingFalsey(): Type
272: {
273: // We don't track the value at this offset, so we can't guarantee
274: // it survives a falsey filter. Drop the assertion.
275: return new MixedType();
276: }
277:
278: public function isIterableAtLeastOnce(): TrinaryLogic
279: {
280: return TrinaryLogic::createYes();
281: }
282:
283: public function isList(): TrinaryLogic
284: {
285: if ($this->offsetType->isString()->yes()) {
286: return TrinaryLogic::createNo();
287: }
288:
289: return TrinaryLogic::createMaybe();
290: }
291:
292: public function isNull(): TrinaryLogic
293: {
294: return TrinaryLogic::createNo();
295: }
296:
297: public function isConstantValue(): TrinaryLogic
298: {
299: return TrinaryLogic::createNo();
300: }
301:
302: public function isConstantScalarValue(): TrinaryLogic
303: {
304: return TrinaryLogic::createNo();
305: }
306:
307: public function getConstantScalarTypes(): array
308: {
309: return [];
310: }
311:
312: public function getConstantScalarValues(): array
313: {
314: return [];
315: }
316:
317: public function isTrue(): TrinaryLogic
318: {
319: return TrinaryLogic::createNo();
320: }
321:
322: public function isFalse(): TrinaryLogic
323: {
324: return TrinaryLogic::createNo();
325: }
326:
327: public function isBoolean(): TrinaryLogic
328: {
329: return TrinaryLogic::createNo();
330: }
331:
332: public function isFloat(): TrinaryLogic
333: {
334: return TrinaryLogic::createNo();
335: }
336:
337: public function isInteger(): TrinaryLogic
338: {
339: return TrinaryLogic::createNo();
340: }
341:
342: public function getClassStringObjectType(): Type
343: {
344: return new ObjectWithoutClassType();
345: }
346:
347: public function getObjectTypeOrClassStringObjectType(): Type
348: {
349: return new ObjectWithoutClassType();
350: }
351:
352: public function isVoid(): TrinaryLogic
353: {
354: return TrinaryLogic::createNo();
355: }
356:
357: public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType
358: {
359: return new BooleanType();
360: }
361:
362: public function getKeysArrayFiltered(Type $filterValueType, TrinaryLogic $strict): Type
363: {
364: return $this->getKeysArray();
365: }
366:
367: public function getKeysArray(): Type
368: {
369: return new NonEmptyArrayType();
370: }
371:
372: public function getValuesArray(): Type
373: {
374: return new NonEmptyArrayType();
375: }
376:
377: public function toNumber(): Type
378: {
379: return new ErrorType();
380: }
381:
382: public function toBitwiseNotType(): Type
383: {
384: return new ErrorType();
385: }
386:
387: public function toAbsoluteNumber(): Type
388: {
389: return new ErrorType();
390: }
391:
392: public function toInteger(): Type
393: {
394: return new ErrorType();
395: }
396:
397: public function toFloat(): Type
398: {
399: return new ErrorType();
400: }
401:
402: public function toString(): Type
403: {
404: return new ErrorType();
405: }
406:
407: public function toArray(): Type
408: {
409: return new MixedType();
410: }
411:
412: public function toArrayKey(): Type
413: {
414: return new ErrorType();
415: }
416:
417: public function toCoercedArgumentType(bool $strictTypes): Type
418: {
419: return $this;
420: }
421:
422: public function getEnumCases(): array
423: {
424: return [];
425: }
426:
427: public function getEnumCaseObject(): ?EnumCaseObjectType
428: {
429: return null;
430: }
431:
432: public function traverse(callable $cb): Type
433: {
434: return $this;
435: }
436:
437: public function traverseSimultaneously(Type $right, callable $cb): Type
438: {
439: return $this;
440: }
441:
442: public function exponentiate(Type $exponent): Type
443: {
444: return new ErrorType();
445: }
446:
447: public function getFiniteTypes(): array
448: {
449: return [];
450: }
451:
452: public function toPhpDocNode(): TypeNode
453: {
454: return new IdentifierTypeNode(''); // no PHPDoc representation
455: }
456:
457: public function hasTemplateOrLateResolvableType(): bool
458: {
459: return $this->offsetType->hasTemplateOrLateResolvableType();
460: }
461:
462: }
463: