Skip to content

Commit 8204dce

Browse files
committed
PhpDocTypeUtils: support integer ranges
1 parent d2fba08 commit 8204dce

File tree

2 files changed

+237
-1
lines changed

2 files changed

+237
-1
lines changed

src/Compiler/Type/PhpDocTypeUtils.php

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@
5555
use function str_contains;
5656
use function strcasecmp;
5757
use function strtolower;
58+
use const PHP_INT_MAX;
59+
use const PHP_INT_MIN;
5860

5961
class PhpDocTypeUtils
6062
{
@@ -147,6 +149,10 @@ public static function toNativeType(TypeNode $type, ?bool &$phpDocUseful): Compl
147149
$phpDocUseful = true;
148150
return match ($type->name) {
149151
'list' => new Identifier('array'),
152+
'positive-int',
153+
'negative-int',
154+
'non-positive-int',
155+
'non-negative-int' => new Identifier('int'),
150156
default => new Identifier('mixed'),
151157
};
152158
}
@@ -380,6 +386,7 @@ public static function isSubTypeOf(TypeNode $a, TypeNode $b): bool
380386

381387
'int' => match (true) {
382388
$a instanceof IdentifierTypeNode => $a->name === 'int',
389+
$a instanceof GenericTypeNode => $a->type->name === 'int',
383390
$a instanceof ConstTypeNode => match (true) {
384391
$a->constExpr instanceof ConstExprIntegerNode => true,
385392
$a->constExpr instanceof ConstFetchNode => is_int(constant((string) $a->constExpr)),
@@ -445,6 +452,39 @@ public static function isSubTypeOf(TypeNode $a, TypeNode $b): bool
445452
}
446453

447454
if ($b instanceof GenericTypeNode) {
455+
if ($b->type->name === 'int' && count($b->genericTypes) === 2) {
456+
$bLowerBound = self::resolveIntegerBoundary($b->genericTypes[0], 'min', PHP_INT_MIN);
457+
$bUpperBound = self::resolveIntegerBoundary($b->genericTypes[1], 'max', PHP_INT_MAX);
458+
459+
if ($a instanceof GenericTypeNode && count($a->genericTypes) === 2) {
460+
$aLowerBound = self::resolveIntegerBoundary($a->genericTypes[0], 'min', PHP_INT_MIN);
461+
$aUpperBound = self::resolveIntegerBoundary($a->genericTypes[1], 'max', PHP_INT_MAX);
462+
463+
} elseif ($a instanceof ConstTypeNode) {
464+
if ($a->constExpr instanceof ConstExprIntegerNode) {
465+
$bound = (int) $a->constExpr->value;
466+
$aLowerBound = $bound;
467+
$aUpperBound = $bound;
468+
} elseif ($a->constExpr instanceof ConstFetchNode) {
469+
$bound = constant((string) $a->constExpr);
470+
471+
if (!is_int($bound)) {
472+
return false;
473+
}
474+
475+
$aLowerBound = $bound;
476+
$aUpperBound = $bound;
477+
478+
} else {
479+
return false;
480+
}
481+
} else {
482+
return false;
483+
}
484+
485+
return $aLowerBound >= $bLowerBound && $aUpperBound <= $bUpperBound;
486+
}
487+
448488
return match (true) {
449489
$a instanceof GenericTypeNode => self::isSubTypeOfGeneric($a, $b),
450490
$a instanceof IdentifierTypeNode => self::isSubTypeOfGeneric(new GenericTypeNode($a, []), $b),
@@ -652,8 +692,24 @@ private static function normalizeType(TypeNode $type): TypeNode
652692
new IdentifierTypeNode('array'),
653693
new IdentifierTypeNode(Traversable::class),
654694
]),
695+
'negative-int' => new GenericTypeNode(new IdentifierTypeNode('int'), [
696+
new IdentifierTypeNode('min'),
697+
new ConstTypeNode(new ConstExprIntegerNode('-1')),
698+
]),
699+
'non-negative-int' => new GenericTypeNode(new IdentifierTypeNode('int'), [
700+
new ConstTypeNode(new ConstExprIntegerNode('0')),
701+
new IdentifierTypeNode('max'),
702+
]),
703+
'non-positive-int' => new GenericTypeNode(new IdentifierTypeNode('int'), [
704+
new IdentifierTypeNode('min'),
705+
new ConstTypeNode(new ConstExprIntegerNode('0')),
706+
]),
655707
'noreturn' => new IdentifierTypeNode('never'),
656708
'number' => new UnionTypeNode([new IdentifierTypeNode('int'), new IdentifierTypeNode('float')]),
709+
'positive-int' => new GenericTypeNode(new IdentifierTypeNode('int'), [
710+
new ConstTypeNode(new ConstExprIntegerNode('1')),
711+
new IdentifierTypeNode('max'),
712+
]),
657713
'scalar' => new UnionTypeNode([
658714
new IdentifierTypeNode('int'),
659715
new IdentifierTypeNode('float'),
@@ -729,6 +785,17 @@ private static function normalizeType(TypeNode $type): TypeNode
729785
]);
730786
}
731787

788+
if (
789+
strtolower($type->type->name) === 'int'
790+
&& count($type->genericTypes) === 2
791+
&& $type->genericTypes[0] instanceof IdentifierTypeNode
792+
&& $type->genericTypes[1] instanceof IdentifierTypeNode
793+
&& strtolower($type->genericTypes[0]->name) === 'min'
794+
&& strtolower($type->genericTypes[1]->name) === 'max'
795+
) {
796+
return new IdentifierTypeNode('int');
797+
}
798+
732799
if (self::isKeyword($type->type)) {
733800
return new GenericTypeNode(new IdentifierTypeNode(strtolower($type->type->name)), $type->genericTypes);
734801
}
@@ -737,4 +804,17 @@ private static function normalizeType(TypeNode $type): TypeNode
737804
return $type;
738805
}
739806

807+
private static function resolveIntegerBoundary(TypeNode $boundaryType, string $extremeName, int $extremeValue): int
808+
{
809+
if ($boundaryType instanceof ConstTypeNode && $boundaryType->constExpr instanceof ConstExprIntegerNode) {
810+
return (int) $boundaryType->constExpr->value;
811+
}
812+
813+
if ($boundaryType instanceof IdentifierTypeNode && $boundaryType->name === $extremeName) {
814+
return $extremeValue;
815+
}
816+
817+
throw new LogicException('Invalid integer boundary type');
818+
}
819+
740820
}

tests/Compiler/Type/PhpDocTypeUtilsTest.php

Lines changed: 157 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,25 @@ public static function provideToNativeTypeData(): iterable
170170

171171
yield 'positive-int' => [
172172
new IdentifierTypeNode('positive-int'),
173-
new Identifier('mixed'),
173+
new Identifier('int'),
174+
true,
175+
];
176+
177+
yield 'negative-int' => [
178+
new IdentifierTypeNode('negative-int'),
179+
new Identifier('int'),
180+
true,
181+
];
182+
183+
yield 'non-positive-int' => [
184+
new IdentifierTypeNode('non-positive-int'),
185+
new Identifier('int'),
186+
true,
187+
];
188+
189+
yield 'non-negative-int' => [
190+
new IdentifierTypeNode('non-negative-int'),
191+
new Identifier('int'),
174192
true,
175193
];
176194

@@ -795,6 +813,12 @@ private static function provideIsSubTypeOfDataInner(): iterable
795813
'true' => [
796814
'int',
797815
'integer',
816+
'int<1, 10>',
817+
'int<min, max>',
818+
'int<min, 10>',
819+
'int<1, max>',
820+
'positive-int',
821+
'negative-int',
798822
'1',
799823
],
800824

@@ -804,6 +828,100 @@ private static function provideIsSubTypeOfDataInner(): iterable
804828
],
805829
];
806830

831+
yield 'int<3, 5>' => [
832+
'true' => [
833+
'int<3, 5>',
834+
'int<3, 4>',
835+
'int<4, 5>',
836+
'3',
837+
'4',
838+
'5',
839+
],
840+
841+
'false' => [
842+
'int',
843+
'int<1, 2>',
844+
'int<6, 10>',
845+
'int<min, 4>',
846+
'int<4, max>',
847+
'positive-int',
848+
'negative-int',
849+
'2',
850+
'6',
851+
],
852+
];
853+
854+
yield 'int<min, 5>' => [
855+
'true' => [
856+
'int<min, 4>',
857+
'int<min, 5>',
858+
'int<-7, 5>',
859+
'int<0, 5>',
860+
'int<5, 5>',
861+
'negative-int',
862+
'-7',
863+
'0',
864+
'5',
865+
],
866+
867+
'false' => [
868+
'int',
869+
'int<0, 10>',
870+
'int<0, max>',
871+
'int<min, 6>',
872+
'positive-int',
873+
'6',
874+
],
875+
];
876+
877+
yield 'int<3, max>' => [
878+
'true' => [
879+
'int<3, 3>',
880+
'int<3, 4>',
881+
'int<3, 5>',
882+
'int<3, max>',
883+
'int<4, 4>',
884+
'int<4, 5>',
885+
'int<4, max>',
886+
'3',
887+
'4',
888+
'5',
889+
'6',
890+
],
891+
892+
'false' => [
893+
'int',
894+
'int<0, 5>',
895+
'int<2, max>',
896+
'int<min, max>',
897+
'positive-int',
898+
'negative-int',
899+
'-2',
900+
'0',
901+
'2',
902+
],
903+
];
904+
905+
yield 'int<min, max>' => [
906+
'true' => [
907+
'int',
908+
'int<min, max>',
909+
'int<min, 3>',
910+
'int<3, max>',
911+
'positive-int',
912+
'negative-int',
913+
'-3',
914+
'0',
915+
'3',
916+
],
917+
918+
'false' => [
919+
'string',
920+
'array',
921+
'null',
922+
],
923+
];
924+
807925
yield 'iterable' => [
808926
'true' => [
809927
'iterable',
@@ -895,6 +1013,24 @@ private static function provideIsSubTypeOfDataInner(): iterable
8951013
'false' => [],
8961014
];
8971015

1016+
yield 'negative-int' => [
1017+
'true' => [
1018+
'negative-int',
1019+
'int<min, -1>',
1020+
'-1',
1021+
'-2',
1022+
],
1023+
1024+
'false' => [
1025+
'int',
1026+
'int<0, max>',
1027+
'int<min, max>',
1028+
'int<min, 0>',
1029+
'0',
1030+
'1',
1031+
],
1032+
];
1033+
8981034
yield 'never' => [
8991035
'true' => [
9001036
'never',
@@ -948,6 +1084,26 @@ private static function provideIsSubTypeOfDataInner(): iterable
9481084
],
9491085
];
9501086

1087+
yield 'positive-int' => [
1088+
'true' => [
1089+
'positive-int',
1090+
'int<1, max>',
1091+
'int<1, 10>',
1092+
'int<1, 1>',
1093+
'1',
1094+
'10',
1095+
],
1096+
1097+
'false' => [
1098+
'int',
1099+
'int<0, max>',
1100+
'int<min, max>',
1101+
'int<min, 1>',
1102+
'0',
1103+
'-1',
1104+
],
1105+
];
1106+
9511107
yield 'resource' => [
9521108
'true' => [
9531109
'resource',

0 commit comments

Comments
 (0)