Skip to content

Commit 9c0394b

Browse files
committed
add support for "int<min,max>", "negative-int" and "numeric"
integer ranges: - PHPStan: https://phpstan.org/writing-php-code/phpdoc-types#integer-ranges - PhpStorm: https://youtrack.jetbrains.com/issue/WI-63623
1 parent 2a7ec2a commit 9c0394b

10 files changed

+489
-3
lines changed

src/PseudoTypes/IntegerRange.php

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* This file is part of phpDocumentor.
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*
11+
* @link http://phpdoc.org
12+
*/
13+
14+
namespace phpDocumentor\Reflection\PseudoTypes;
15+
16+
use phpDocumentor\Reflection\PseudoType;
17+
use phpDocumentor\Reflection\Type;
18+
use phpDocumentor\Reflection\Types\Integer;
19+
20+
/**
21+
* Value Object representing the type 'int'.
22+
*
23+
* @psalm-immutable
24+
*/
25+
final class IntegerRange extends Integer implements PseudoType
26+
{
27+
/** @var string */
28+
private $minValue;
29+
30+
/** @var string */
31+
private $maxValue;
32+
33+
public function __construct(string $minValue, string $maxValue)
34+
{
35+
$this->minValue = $minValue;
36+
$this->maxValue = $maxValue;
37+
}
38+
39+
public function underlyingType(): Type
40+
{
41+
return new Integer();
42+
}
43+
44+
public function getMinValue(): string
45+
{
46+
return $this->minValue;
47+
}
48+
49+
public function getMaxValue(): string
50+
{
51+
return $this->maxValue;
52+
}
53+
54+
/**
55+
* Returns a rendered output of the Type as it would be used in a DocBlock.
56+
*/
57+
public function __toString(): string
58+
{
59+
return 'int<' . $this->minValue . ', ' . $this->maxValue . '>';
60+
}
61+
}

src/PseudoTypes/NegativeInteger.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* This file is part of phpDocumentor.
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*
11+
* @link http://phpdoc.org
12+
*/
13+
14+
namespace phpDocumentor\Reflection\PseudoTypes;
15+
16+
use phpDocumentor\Reflection\PseudoType;
17+
use phpDocumentor\Reflection\Type;
18+
use phpDocumentor\Reflection\Types\Integer;
19+
20+
/**
21+
* Value Object representing the type 'int'.
22+
*
23+
* @psalm-immutable
24+
*/
25+
final class NegativeInteger extends Integer implements PseudoType
26+
{
27+
public function underlyingType(): Type
28+
{
29+
return new Integer();
30+
}
31+
32+
/**
33+
* Returns a rendered output of the Type as it would be used in a DocBlock.
34+
*/
35+
public function __toString(): string
36+
{
37+
return 'negative-int';
38+
}
39+
}

src/PseudoTypes/Numeric_.php

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* This file is part of phpDocumentor.
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*
11+
* @link http://phpdoc.org
12+
*/
13+
14+
namespace phpDocumentor\Reflection\PseudoTypes;
15+
16+
use phpDocumentor\Reflection\PseudoType;
17+
use phpDocumentor\Reflection\Type;
18+
use phpDocumentor\Reflection\Types\AggregatedType;
19+
use phpDocumentor\Reflection\Types\Compound;
20+
use phpDocumentor\Reflection\Types\Float_;
21+
use phpDocumentor\Reflection\Types\Integer;
22+
23+
/**
24+
* Value Object representing the 'numeric' pseudo-type, which is either a numeric-string, integer or float.
25+
*
26+
* @psalm-immutable
27+
*/
28+
final class Numeric_ extends AggregatedType implements PseudoType
29+
{
30+
public function __construct()
31+
{
32+
AggregatedType::__construct([new NumericString(), new Integer(), new Float_()], '|');
33+
}
34+
35+
public function underlyingType(): Type
36+
{
37+
return new Compound([new NumericString(), new Integer(), new Float_()]);
38+
}
39+
40+
/**
41+
* Returns a rendered output of the Type as it would be used in a DocBlock.
42+
*/
43+
public function __toString(): string
44+
{
45+
return 'numeric';
46+
}
47+
}

src/PseudoTypes/PositiveInteger.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
use phpDocumentor\Reflection\Types\Integer;
1919

2020
/**
21-
* Value Object representing the type 'string'.
21+
* Value Object representing the type 'int'.
2222
*
2323
* @psalm-immutable
2424
*/

src/TypeResolver.php

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
use ArrayIterator;
1717
use InvalidArgumentException;
18+
use phpDocumentor\Reflection\PseudoTypes\IntegerRange;
1819
use phpDocumentor\Reflection\PseudoTypes\List_;
1920
use phpDocumentor\Reflection\Types\Array_;
2021
use phpDocumentor\Reflection\Types\ArrayKey;
@@ -40,6 +41,7 @@
4041
use function count;
4142
use function end;
4243
use function in_array;
44+
use function is_numeric;
4345
use function key;
4446
use function preg_split;
4547
use function strpos;
@@ -82,10 +84,12 @@ final class TypeResolver
8284
'non-empty-lowercase-string' => PseudoTypes\NonEmptyLowercaseString::class,
8385
'non-empty-string' => PseudoTypes\NonEmptyString::class,
8486
'numeric-string' => PseudoTypes\NumericString::class,
87+
'numeric' => PseudoTypes\Numeric_::class,
8588
'trait-string' => PseudoTypes\TraitString::class,
8689
'int' => Types\Integer::class,
8790
'integer' => Types\Integer::class,
8891
'positive-int' => PseudoTypes\PositiveInteger::class,
92+
'negative-int' => PseudoTypes\NegativeInteger::class,
8993
'bool' => Types\Boolean::class,
9094
'boolean' => Types\Boolean::class,
9195
'real' => Types\Float_::class,
@@ -257,6 +261,8 @@ private function parseTypes(ArrayIterator $tokens, Context $context, int $parser
257261
if ($classType !== null) {
258262
if ((string) $classType === 'class-string') {
259263
$types[] = $this->resolveClassString($tokens, $context);
264+
} elseif ((string) $classType === 'int') {
265+
$types[] = $this->resolveIntRange($tokens);
260266
} elseif ((string) $classType === 'interface-string') {
261267
$types[] = $this->resolveInterfaceString($tokens, $context);
262268
} else {
@@ -479,6 +485,75 @@ private function resolveClassString(ArrayIterator $tokens, Context $context): Ty
479485
return new ClassString($classType->getFqsen());
480486
}
481487

488+
/**
489+
* Resolves integer ranges
490+
*
491+
* @param ArrayIterator<int, (string|null)> $tokens
492+
*/
493+
private function resolveIntRange(ArrayIterator $tokens): Type
494+
{
495+
$tokens->next();
496+
497+
$token = '';
498+
$minValue = null;
499+
$maxValue = null;
500+
$commaFound = false;
501+
$tokenCounter = 0;
502+
while ($tokens->valid()) {
503+
$tokenCounter++;
504+
$token = $tokens->current();
505+
if ($token === null) {
506+
throw new RuntimeException(
507+
'Unexpected nullable character'
508+
);
509+
}
510+
511+
$token = trim($token);
512+
513+
if ($token === '>') {
514+
break;
515+
}
516+
517+
if ($token === ',') {
518+
$commaFound = true;
519+
}
520+
521+
if ($commaFound === false && $minValue === null) {
522+
if (is_numeric($token) || $token === 'max' || $token === 'min') {
523+
$minValue = $token;
524+
}
525+
}
526+
527+
if ($commaFound === true && $maxValue === null) {
528+
if (is_numeric($token) || $token === 'max' || $token === 'min') {
529+
$maxValue = $token;
530+
}
531+
}
532+
533+
$tokens->next();
534+
}
535+
536+
if ($token !== '>') {
537+
if (empty($token)) {
538+
throw new RuntimeException(
539+
'interface-string: ">" is missing'
540+
);
541+
}
542+
543+
throw new RuntimeException(
544+
'Unexpected character "' . $token . '", ">" is missing'
545+
);
546+
}
547+
548+
if (!$minValue || !$maxValue || $tokenCounter > 4) {
549+
throw new RuntimeException(
550+
'int<min,max> has not the correct format'
551+
);
552+
}
553+
554+
return new IntegerRange($minValue, $maxValue);
555+
}
556+
482557
/**
483558
* Resolves class string
484559
*

tests/unit/CollectionResolverTest.php

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -231,10 +231,16 @@ public function testBadArrayCollectionKey(): void
231231
public function testGoodArrayCollectionKey(): void
232232
{
233233
$fixture = new TypeResolver();
234-
$fixture->resolve('array<array-key,string>', new Context(''));
234+
$resolvedType = $fixture->resolve('array<array-key,string>', new Context(''));
235+
236+
$this->assertInstanceOf(Array_::class, $resolvedType);
237+
$this->assertSame('array<array-key,string>', (string) $resolvedType);
235238

236239
$fixture = new TypeResolver();
237-
$fixture->resolve('array<class-string,string>', new Context(''));
240+
$resolvedType = $fixture->resolve('array<class-string,string>', new Context(''));
241+
242+
$this->assertInstanceOf(Array_::class, $resolvedType);
243+
$this->assertSame('array<class-string,string>', (string) $resolvedType);
238244
}
239245

240246
/**

0 commit comments

Comments
 (0)