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\Reflection\ClassMemberAccessAnswerer;
9: use PHPStan\Reflection\TrivialParametersAcceptor;
10: use PHPStan\ShouldNotHappenException;
11: use PHPStan\TrinaryLogic;
12: use PHPStan\Type\AcceptsResult;
13: use PHPStan\Type\BenevolentUnionType;
14: use PHPStan\Type\BooleanType;
15: use PHPStan\Type\CompoundType;
16: use PHPStan\Type\Constant\ConstantArrayType;
17: use PHPStan\Type\Constant\ConstantBooleanType;
18: use PHPStan\Type\Constant\ConstantIntegerType;
19: use PHPStan\Type\ErrorType;
20: use PHPStan\Type\FloatType;
21: use PHPStan\Type\GeneralizePrecision;
22: use PHPStan\Type\IntegerType;
23: use PHPStan\Type\IntersectionType;
24: use PHPStan\Type\IsSuperTypeOfResult;
25: use PHPStan\Type\StringType;
26: use PHPStan\Type\Traits\NonArrayTypeTrait;
27: use PHPStan\Type\Traits\NonGenericTypeTrait;
28: use PHPStan\Type\Traits\NonIterableTypeTrait;
29: use PHPStan\Type\Traits\NonObjectTypeTrait;
30: use PHPStan\Type\Traits\NonRemoveableTypeTrait;
31: use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait;
32: use PHPStan\Type\Type;
33: use PHPStan\Type\TypeCombinator;
34: use PHPStan\Type\UnionType;
35: use PHPStan\Type\VerbosityLevel;
36:
37: /**
38: * This accessory type is coupled with `Type::isDecimalIntegerString()` method.
39: *
40: * When inverse=false, this represents strings containing decimal integers.
41: * These are guaranteed to be cast to an integer in an array key.
42: * Examples of constant values covered by this type: "0", "1", "1234", "-1"
43: *
44: * When inverse=true, this represents strings containing non-decimal integers and other text.
45: * These are guaranteed to stay as string in an array key.
46: * Examples of constant values covered by this type: "+1", "00", "18E+3", "1.2", "1,3", "foo"
47: *
48: * @api
49: */
50: class AccessoryDecimalIntegerStringType implements CompoundType, AccessoryType
51: {
52:
53: use NonArrayTypeTrait;
54: use NonObjectTypeTrait;
55: use NonIterableTypeTrait;
56: use UndecidedComparisonCompoundTypeTrait;
57: use NonGenericTypeTrait;
58: use NonRemoveableTypeTrait;
59:
60: /** @api */
61: public function __construct(private bool $inverse = false)
62: {
63: }
64:
65: public function getReferencedClasses(): array
66: {
67: return [];
68: }
69:
70: public function getObjectClassNames(): array
71: {
72: return [];
73: }
74:
75: public function getObjectClassReflections(): array
76: {
77: return [];
78: }
79:
80: public function getConstantStrings(): array
81: {
82: return [];
83: }
84:
85: public function accepts(Type $type, bool $strictTypes): AcceptsResult
86: {
87: $isDecimalIntegerString = $type->isDecimalIntegerString();
88:
89: if (
90: $type->isString()->yes()
91: && ($this->inverse ? $isDecimalIntegerString->no() : $isDecimalIntegerString->yes())
92: ) {
93: return AcceptsResult::createYes();
94: }
95:
96: if ($type instanceof CompoundType) {
97: return $type->isAcceptedBy($this, $strictTypes);
98: }
99:
100: $result = $type->isString()->and($this->inverse ? $isDecimalIntegerString->negate() : $isDecimalIntegerString);
101:
102: return new AcceptsResult($result, []);
103: }
104:
105: public function isSuperTypeOf(Type $type): IsSuperTypeOfResult
106: {
107: if ($type instanceof CompoundType) {
108: return $type->isSubTypeOf($this);
109: }
110:
111: if ($this->equals($type)) {
112: return IsSuperTypeOfResult::createYes();
113: }
114:
115: $isDecimalIntegerString = $type->isDecimalIntegerString();
116: $result = $type->isString()->and($this->inverse ? $isDecimalIntegerString->negate() : $isDecimalIntegerString);
117:
118: return new IsSuperTypeOfResult($result, []);
119: }
120:
121: public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult
122: {
123: if ($otherType instanceof UnionType || $otherType instanceof IntersectionType) {
124: return $otherType->isSuperTypeOf($this);
125: }
126:
127: if (
128: (
129: $otherType instanceof AccessoryNumericStringType
130: || $otherType instanceof AccessoryLowercaseStringType
131: || $otherType instanceof AccessoryUppercaseStringType
132: )
133: && !$this->inverse
134: ) {
135: return IsSuperTypeOfResult::createYes();
136: }
137:
138: $otherTypeResult = $otherType->isString()->and($this->inverse ? $otherType->isDecimalIntegerString()->negate() : $otherType->isDecimalIntegerString());
139:
140: return new IsSuperTypeOfResult(
141: $otherTypeResult->and($otherType->equals($this) ? TrinaryLogic::createYes() : TrinaryLogic::createMaybe()),
142: [],
143: );
144: }
145:
146: public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult
147: {
148: return $this->isSubTypeOf($acceptingType)->toAcceptsResult();
149: }
150:
151: public function equals(Type $type): bool
152: {
153: return $type instanceof self && $this->inverse === $type->inverse;
154: }
155:
156: public function describe(VerbosityLevel $level): string
157: {
158: return $this->inverse ? 'non-decimal-int-string' : 'decimal-int-string';
159: }
160:
161: public function isOffsetAccessible(): TrinaryLogic
162: {
163: return TrinaryLogic::createYes();
164: }
165:
166: public function isOffsetAccessLegal(): TrinaryLogic
167: {
168: return TrinaryLogic::createYes();
169: }
170:
171: public function hasOffsetValueType(Type $offsetType): TrinaryLogic
172: {
173: return $offsetType->isInteger()->and(TrinaryLogic::createMaybe());
174: }
175:
176: public function getOffsetValueType(Type $offsetType): Type
177: {
178: if ($this->hasOffsetValueType($offsetType)->no()) {
179: return new ErrorType();
180: }
181:
182: return new StringType();
183: }
184:
185: public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type
186: {
187: $stringOffset = (new StringType())->setOffsetValueType($offsetType, $valueType, $unionValues);
188:
189: if ($stringOffset instanceof ErrorType) {
190: return $stringOffset;
191: }
192:
193: return $this;
194: }
195:
196: public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type
197: {
198: return $this;
199: }
200:
201: public function unsetOffset(Type $offsetType): Type
202: {
203: return new ErrorType();
204: }
205:
206: public function toNumber(): Type
207: {
208: if ($this->inverse) {
209: return new UnionType([
210: $this->toInteger(),
211: $this->toFloat(),
212: ]);
213: }
214:
215: return $this->toInteger();
216: }
217:
218: public function toAbsoluteNumber(): Type
219: {
220: return $this->toNumber()->toAbsoluteNumber();
221: }
222:
223: public function toBitwiseNotType(): Type
224: {
225: // Decimal integer strings are non-empty when not inverted
226: // (`"0"` / `"123"` are still at least one character). `~$s`
227: // returns a string of the same length, so the non-empty flag
228: // survives. The decimal-integer property doesn't survive the
229: // bitwise-not, hence we drop the accessory.
230: return $this->isNonEmptyString()->yes()
231: ? new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()])
232: : new StringType();
233: }
234:
235: public function toBoolean(): BooleanType
236: {
237: return $this->isNonFalsyString()->negate()->toBooleanType();
238: }
239:
240: public function toInteger(): Type
241: {
242: return new IntegerType();
243: }
244:
245: public function toFloat(): Type
246: {
247: return new FloatType();
248: }
249:
250: public function toString(): Type
251: {
252: return $this;
253: }
254:
255: public function toArray(): Type
256: {
257: return new ConstantArrayType(
258: [new ConstantIntegerType(0)],
259: [$this],
260: [1],
261: isList: TrinaryLogic::createYes(),
262: );
263: }
264:
265: public function toArrayKey(): Type
266: {
267: if ($this->inverse) {
268: return $this;
269: }
270:
271: return new IntegerType();
272: }
273:
274: public function toCoercedArgumentType(bool $strictTypes): Type
275: {
276: if (!$strictTypes) {
277: return TypeCombinator::union($this->toInteger(), $this->toFloat(), $this, $this->toBoolean());
278: }
279:
280: return $this;
281: }
282:
283: public function isNull(): TrinaryLogic
284: {
285: return TrinaryLogic::createNo();
286: }
287:
288: public function isConstantValue(): TrinaryLogic
289: {
290: return TrinaryLogic::createMaybe();
291: }
292:
293: public function isConstantScalarValue(): TrinaryLogic
294: {
295: return TrinaryLogic::createMaybe();
296: }
297:
298: public function getConstantScalarTypes(): array
299: {
300: return [];
301: }
302:
303: public function getConstantScalarValues(): array
304: {
305: return [];
306: }
307:
308: public function isCallable(): TrinaryLogic
309: {
310: return $this->inverse ? TrinaryLogic::createMaybe() : TrinaryLogic::createNo();
311: }
312:
313: public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array
314: {
315: if ($this->inverse) {
316: return [new TrivialParametersAcceptor()];
317: }
318:
319: throw new ShouldNotHappenException();
320: }
321:
322: public function isTrue(): TrinaryLogic
323: {
324: return TrinaryLogic::createNo();
325: }
326:
327: public function isFalse(): TrinaryLogic
328: {
329: return TrinaryLogic::createNo();
330: }
331:
332: public function isBoolean(): TrinaryLogic
333: {
334: return TrinaryLogic::createNo();
335: }
336:
337: public function isFloat(): TrinaryLogic
338: {
339: return TrinaryLogic::createNo();
340: }
341:
342: public function isInteger(): TrinaryLogic
343: {
344: return TrinaryLogic::createNo();
345: }
346:
347: public function isString(): TrinaryLogic
348: {
349: return TrinaryLogic::createYes();
350: }
351:
352: public function isNumericString(): TrinaryLogic
353: {
354: return $this->inverse ? TrinaryLogic::createMaybe() : TrinaryLogic::createYes();
355: }
356:
357: public function isDecimalIntegerString(): TrinaryLogic
358: {
359: return TrinaryLogic::createFromBoolean(!$this->inverse);
360: }
361:
362: public function isNonEmptyString(): TrinaryLogic
363: {
364: return $this->inverse ? TrinaryLogic::createMaybe() : TrinaryLogic::createYes();
365: }
366:
367: public function isNonFalsyString(): TrinaryLogic
368: {
369: return TrinaryLogic::createMaybe();
370: }
371:
372: public function isLiteralString(): TrinaryLogic
373: {
374: return TrinaryLogic::createMaybe();
375: }
376:
377: public function isLowercaseString(): TrinaryLogic
378: {
379: return $this->inverse ? TrinaryLogic::createMaybe() : TrinaryLogic::createYes();
380: }
381:
382: public function isUppercaseString(): TrinaryLogic
383: {
384: return $this->inverse ? TrinaryLogic::createMaybe() : TrinaryLogic::createYes();
385: }
386:
387: public function isClassString(): TrinaryLogic
388: {
389: return TrinaryLogic::createNo();
390: }
391:
392: public function getClassStringObjectType(): Type
393: {
394: return new ErrorType();
395: }
396:
397: public function getObjectTypeOrClassStringObjectType(): Type
398: {
399: return new ErrorType();
400: }
401:
402: public function isVoid(): TrinaryLogic
403: {
404: return TrinaryLogic::createNo();
405: }
406:
407: public function isScalar(): TrinaryLogic
408: {
409: return TrinaryLogic::createYes();
410: }
411:
412: public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType
413: {
414: if ($type->isNull()->yes()) {
415: return new ConstantBooleanType(false);
416: }
417:
418: if ($type->isString()->yes()) {
419: if ($this->inverse) {
420: if ($type->isDecimalIntegerString()->yes()) {
421: return new ConstantBooleanType(false);
422: }
423: } elseif ($type->isDecimalIntegerString()->no()) {
424: return new ConstantBooleanType(false);
425: }
426: }
427:
428: return new BooleanType();
429: }
430:
431: public function traverse(callable $cb): Type
432: {
433: return $this;
434: }
435:
436: public function traverseSimultaneously(Type $right, callable $cb): Type
437: {
438: return $this;
439: }
440:
441: public function generalize(GeneralizePrecision $precision): Type
442: {
443: return new StringType();
444: }
445:
446: public function exponentiate(Type $exponent): Type
447: {
448: return new BenevolentUnionType([
449: new FloatType(),
450: new IntegerType(),
451: ]);
452: }
453:
454: public function getFiniteTypes(): array
455: {
456: return [];
457: }
458:
459: public function toPhpDocNode(): TypeNode
460: {
461: return new IdentifierTypeNode($this->inverse ? 'non-decimal-int-string' : 'decimal-int-string');
462: }
463:
464: public function hasTemplateOrLateResolvableType(): bool
465: {
466: return false;
467: }
468:
469: }
470: