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