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