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