Skip to content

Commit d063595

Browse files
committed
isDecimalIntegerString test
1 parent 818433d commit d063595

13 files changed

+298
-21
lines changed

phpstan-baseline.neon

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1716,7 +1716,7 @@ parameters:
17161716
-
17171717
rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated.
17181718
identifier: phpstanApi.instanceofType
1719-
count: 3
1719+
count: 4
17201720
path: src/Type/TypeCombinator.php
17211721

17221722
-

src/Type/Accessory/AccessoryDecimalIntegerStringType.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,7 @@ public function isNonEmptyString(): TrinaryLogic
347347

348348
public function isNonFalsyString(): TrinaryLogic
349349
{
350-
return $this->inverse ? TrinaryLogic::createNo() : TrinaryLogic::createMaybe();
350+
return TrinaryLogic::createMaybe();
351351
}
352352

353353
public function isLiteralString(): TrinaryLogic

src/Type/Accessory/AccessoryNonFalsyStringType.php

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -347,10 +347,6 @@ public function isScalar(): TrinaryLogic
347347

348348
public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType
349349
{
350-
if ($type->isString()->yes() && $type->isNonFalsyString()->no()) {
351-
return new ConstantBooleanType(false);
352-
}
353-
354350
$falseyTypes = StaticTypeFactory::falsey();
355351
if ($falseyTypes->isSuperTypeOf($type)->yes()) {
356352
return new ConstantBooleanType(false);

src/Type/ArrayType.php

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
use PHPStan\ShouldNotHappenException;
1313
use PHPStan\TrinaryLogic;
1414
use PHPStan\Type\Accessory\AccessoryArrayListType;
15-
use PHPStan\Type\Accessory\AccessoryDecimalIntegerStringType;
1615
use PHPStan\Type\Accessory\HasOffsetValueType;
1716
use PHPStan\Type\Accessory\NonEmptyArrayType;
1817
use PHPStan\Type\Constant\ConstantArrayType;
@@ -33,7 +32,6 @@
3332
use PHPStan\Type\Traits\UndecidedComparisonTypeTrait;
3433
use function array_merge;
3534
use function count;
36-
use function in_array;
3735
use function sprintf;
3836

3937
/** @api */
@@ -52,15 +50,11 @@ class ArrayType implements Type
5250
/** @api */
5351
public function __construct(Type $keyType, private Type $itemType)
5452
{
55-
$desc = $keyType->describe(VerbosityLevel::value());
56-
if (in_array($desc, ['(int|string)', '(int|non-decimal-int-string)'], true)) {
53+
if ($keyType->describe(VerbosityLevel::value()) === '(int|string)') {
5754
$keyType = new MixedType();
5855
}
5956
if ($keyType instanceof StrictMixedType && !$keyType instanceof TemplateStrictMixedType) {
60-
$keyType = new UnionType([
61-
new StringType(),
62-
new IntegerType(),
63-
]);
57+
$keyType = new UnionType([new StringType(), new IntegerType()]);
6458
}
6559

6660
$this->keyType = $keyType;
@@ -121,7 +115,7 @@ public function isSuperTypeOf(Type $type): IsSuperTypeOfResult
121115
{
122116
if ($type instanceof self || $type instanceof ConstantArrayType) {
123117
return $this->getItemType()->isSuperTypeOf($type->getItemType())
124-
->and($this->getKeyType()->isSuperTypeOf($type->getKeyType()));
118+
->and($this->getIterableKeyType()->isSuperTypeOf($type->getIterableKeyType()));
125119
}
126120

127121
if ($type instanceof CompoundType) {
@@ -206,10 +200,10 @@ public function getIterableKeyType(): Type
206200
{
207201
$keyType = $this->keyType;
208202
if ($keyType instanceof MixedType && !$keyType instanceof TemplateMixedType) {
209-
return new BenevolentUnionType([new IntegerType(), new IntersectionType([new StringType(), new AccessoryDecimalIntegerStringType(inverse: true)])]);
203+
return new BenevolentUnionType([new IntegerType(), new StringType()]);
210204
}
211205
if ($keyType instanceof StrictMixedType) {
212-
return new BenevolentUnionType([new IntegerType(), new IntersectionType([new StringType(), new AccessoryDecimalIntegerStringType(inverse: true)])]);
206+
return new BenevolentUnionType([new IntegerType(), new StringType()]);
213207
}
214208

215209
return $keyType;

src/Type/Constant/ConstantStringType.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -331,7 +331,7 @@ public function isNumericString(): TrinaryLogic
331331

332332
public function isDecimalIntegerString(): TrinaryLogic
333333
{
334-
return parent::isDecimalIntegerString();
334+
return TrinaryLogic::createFromBoolean((string) (int) $this->value === $this->value);
335335
}
336336

337337
public function isNonEmptyString(): TrinaryLogic

src/Type/TypeCombinator.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use PHPStan\TrinaryLogic;
66
use PHPStan\Type\Accessory\AccessoryArrayListType;
7+
use PHPStan\Type\Accessory\AccessoryDecimalIntegerStringType;
78
use PHPStan\Type\Accessory\AccessoryLowercaseStringType;
89
use PHPStan\Type\Accessory\AccessoryNonEmptyStringType;
910
use PHPStan\Type\Accessory\AccessoryType;
@@ -26,6 +27,7 @@
2627
use PHPStan\Type\Generic\TemplateType;
2728
use PHPStan\Type\Generic\TemplateTypeFactory;
2829
use PHPStan\Type\Generic\TemplateUnionType;
30+
use function array_filter;
2931
use function array_key_exists;
3032
use function array_key_first;
3133
use function array_merge;
@@ -562,6 +564,24 @@ private static function compareTypesInUnion(Type $a, Type $b): ?array
562564
}
563565
}
564566

567+
// numeric-string | non-decimal-int-string → string (preserving common accessories)
568+
// Works because decimal-int-string ⊂ numeric-string, so together they cover all strings
569+
if ($a->isString()->yes() && $b->isString()->yes()) {
570+
$decimalIntString = new IntersectionType([new StringType(), new AccessoryDecimalIntegerStringType()]);
571+
if ($b->isDecimalIntegerString()->no()) {
572+
$bBase = self::removeDecimalIntStringAccessory($b);
573+
if ($bBase->isSuperTypeOf($a)->yes() && $a->isSuperTypeOf($decimalIntString)->yes()) {
574+
return [null, $bBase];
575+
}
576+
}
577+
if ($a->isDecimalIntegerString()->no()) {
578+
$aBase = self::removeDecimalIntStringAccessory($a);
579+
if ($aBase->isSuperTypeOf($b)->yes() && $b->isSuperTypeOf($decimalIntString)->yes()) {
580+
return [$aBase, null];
581+
}
582+
}
583+
}
584+
565585
return null;
566586
}
567587

@@ -581,6 +601,18 @@ private static function getAccessoryCaseStringTypes(Type $type): array
581601
return $accessory;
582602
}
583603

604+
private static function removeDecimalIntStringAccessory(Type $type): Type
605+
{
606+
if (!$type instanceof IntersectionType) {
607+
return $type;
608+
}
609+
610+
return self::intersect(...array_filter(
611+
$type->getTypes(),
612+
static fn (Type $t): bool => !$t instanceof AccessoryDecimalIntegerStringType,
613+
));
614+
}
615+
584616
private static function unionWithSubtractedType(
585617
Type $type,
586618
?Type $subtractedType,

src/Type/VerbosityLevel.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace PHPStan\Type;
44

55
use PHPStan\Type\Accessory\AccessoryArrayListType;
6+
use PHPStan\Type\Accessory\AccessoryDecimalIntegerStringType;
67
use PHPStan\Type\Accessory\AccessoryLiteralStringType;
78
use PHPStan\Type\Accessory\AccessoryLowercaseStringType;
89
use PHPStan\Type\Accessory\AccessoryNonEmptyStringType;
@@ -156,6 +157,7 @@ public static function getRecommendedLevelByType(Type $acceptingType, ?Type $acc
156157
|| $type instanceof AccessoryNonFalsyStringType
157158
|| $type instanceof AccessoryLiteralStringType
158159
|| $type instanceof AccessoryNumericStringType
160+
|| $type instanceof AccessoryDecimalIntegerStringType
159161
|| $type instanceof NonEmptyArrayType
160162
|| $type instanceof AccessoryArrayListType
161163
) {

tests/PHPStan/Analyser/nsrt/decimal-int-string.php

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ public function doFoo(string $s): void
1717
assertType('non-empty-array<int, 1>', $a);
1818

1919
assertType('bool', (bool) $s);
20+
21+
assertType('int', $s + $s);
2022
}
2123

2224
/**
@@ -28,7 +30,19 @@ public function doBar(string $s): void
2830
$a = [$s => 1];
2931
assertType('non-empty-array<non-decimal-int-string, 1>', $a);
3032

31-
assertType('true', (bool) $s);
33+
assertType('bool', (bool) $s);
34+
35+
assertType('float|int', $s + $s);
36+
}
37+
38+
/**
39+
* @param non-decimal-int-string $s
40+
*/
41+
public function emptyStringIsNonDecimal(string $s): void
42+
{
43+
if ($s === '') {
44+
assertType("''", $s); // '' is a valid non-decimal-int-string
45+
}
3246
}
3347

3448
}

tests/PHPStan/Type/Constant/ConstantStringTypeTest.php

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,63 @@ public function testSetInvalidValue(): void
185185
$this->assertInstanceOf(ErrorType::class, $result);
186186
}
187187

188-
public function test
188+
public static function dataIsDecimalIntegerString(): iterable
189+
{
190+
yield [
191+
'0',
192+
TrinaryLogic::createYes(),
193+
];
194+
yield [
195+
'1',
196+
TrinaryLogic::createYes(),
197+
];
198+
yield [
199+
'1234',
200+
TrinaryLogic::createYes(),
201+
];
202+
yield [
203+
'-1',
204+
TrinaryLogic::createYes(),
205+
];
206+
yield [
207+
'+1',
208+
TrinaryLogic::createNo(),
209+
];
210+
yield [
211+
'00',
212+
TrinaryLogic::createNo(),
213+
];
214+
yield [
215+
'01',
216+
TrinaryLogic::createNo(),
217+
];
218+
yield [
219+
'18E+3',
220+
TrinaryLogic::createNo(),
221+
];
222+
yield [
223+
'1.2',
224+
TrinaryLogic::createNo(),
225+
];
226+
yield [
227+
'1,3',
228+
TrinaryLogic::createNo(),
229+
];
230+
yield [
231+
'foo',
232+
TrinaryLogic::createNo(),
233+
];
234+
yield [
235+
'1foo',
236+
TrinaryLogic::createNo(),
237+
];
238+
}
239+
240+
#[DataProvider('dataIsDecimalIntegerString')]
241+
public function testIsDecimalIntegerString(string $value, TrinaryLogic $expected): void
242+
{
243+
$type = new ConstantStringType($value);
244+
$this->assertSame($expected->describe(), $type->isDecimalIntegerString()->describe());
245+
}
189246

190247
}

tests/PHPStan/Type/IntersectionTypeTest.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use PHPStan\Testing\PHPStanTestCase;
99
use PHPStan\TrinaryLogic;
1010
use PHPStan\Type\Accessory\AccessoryArrayListType;
11+
use PHPStan\Type\Accessory\AccessoryDecimalIntegerStringType;
1112
use PHPStan\Type\Accessory\AccessoryLowercaseStringType;
1213
use PHPStan\Type\Accessory\AccessoryUppercaseStringType;
1314
use PHPStan\Type\Accessory\HasOffsetType;
@@ -747,6 +748,30 @@ public static function dataDescribe(): iterable
747748
VerbosityLevel::precise(),
748749
'uppercase-string',
749750
];
751+
752+
yield [
753+
new IntersectionType([new StringType(), new AccessoryDecimalIntegerStringType()]),
754+
VerbosityLevel::typeOnly(),
755+
'string',
756+
];
757+
758+
yield [
759+
new IntersectionType([new StringType(), new AccessoryDecimalIntegerStringType(inverse: true)]),
760+
VerbosityLevel::typeOnly(),
761+
'string',
762+
];
763+
764+
yield [
765+
new IntersectionType([new StringType(), new AccessoryDecimalIntegerStringType()]),
766+
VerbosityLevel::value(),
767+
'decimal-int-string',
768+
];
769+
770+
yield [
771+
new IntersectionType([new StringType(), new AccessoryDecimalIntegerStringType(inverse: true)]),
772+
VerbosityLevel::value(),
773+
'non-decimal-int-string',
774+
];
750775
}
751776

752777
#[DataProvider('dataDescribe')]

0 commit comments

Comments
 (0)