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