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