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