55
55
use function str_contains ;
56
56
use function strcasecmp ;
57
57
use function strtolower ;
58
+ use const PHP_INT_MAX ;
59
+ use const PHP_INT_MIN ;
58
60
59
61
class PhpDocTypeUtils
60
62
{
@@ -147,6 +149,10 @@ public static function toNativeType(TypeNode $type, ?bool &$phpDocUseful): Compl
147
149
$ phpDocUseful = true ;
148
150
return match ($ type ->name ) {
149
151
'list ' => new Identifier ('array ' ),
152
+ 'positive-int ' ,
153
+ 'negative-int ' ,
154
+ 'non-positive-int ' ,
155
+ 'non-negative-int ' => new Identifier ('int ' ),
150
156
default => new Identifier ('mixed ' ),
151
157
};
152
158
}
@@ -380,6 +386,7 @@ public static function isSubTypeOf(TypeNode $a, TypeNode $b): bool
380
386
381
387
'int ' => match (true ) {
382
388
$ a instanceof IdentifierTypeNode => $ a ->name === 'int ' ,
389
+ $ a instanceof GenericTypeNode => $ a ->type ->name === 'int ' ,
383
390
$ a instanceof ConstTypeNode => match (true ) {
384
391
$ a ->constExpr instanceof ConstExprIntegerNode => true ,
385
392
$ a ->constExpr instanceof ConstFetchNode => is_int (constant ((string ) $ a ->constExpr )),
@@ -445,6 +452,39 @@ public static function isSubTypeOf(TypeNode $a, TypeNode $b): bool
445
452
}
446
453
447
454
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
+
448
488
return match (true ) {
449
489
$ a instanceof GenericTypeNode => self ::isSubTypeOfGeneric ($ a , $ b ),
450
490
$ a instanceof IdentifierTypeNode => self ::isSubTypeOfGeneric (new GenericTypeNode ($ a , []), $ b ),
@@ -652,8 +692,24 @@ private static function normalizeType(TypeNode $type): TypeNode
652
692
new IdentifierTypeNode ('array ' ),
653
693
new IdentifierTypeNode (Traversable::class),
654
694
]),
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
+ ]),
655
707
'noreturn ' => new IdentifierTypeNode ('never ' ),
656
708
'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
+ ]),
657
713
'scalar ' => new UnionTypeNode ([
658
714
new IdentifierTypeNode ('int ' ),
659
715
new IdentifierTypeNode ('float ' ),
@@ -729,6 +785,17 @@ private static function normalizeType(TypeNode $type): TypeNode
729
785
]);
730
786
}
731
787
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
+
732
799
if (self ::isKeyword ($ type ->type )) {
733
800
return new GenericTypeNode (new IdentifierTypeNode (strtolower ($ type ->type ->name )), $ type ->genericTypes );
734
801
}
@@ -737,4 +804,17 @@ private static function normalizeType(TypeNode $type): TypeNode
737
804
return $ type ;
738
805
}
739
806
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
+
740
820
}
0 commit comments