Skip to content

Commit

Permalink
feat: handle integer range type
Browse files Browse the repository at this point in the history
Integer range can be used as follows:

```php
final class SomeClass
{
    /** @var int<42, 1337> */
    public int $intRange; // accepts any int between 42 and 1337

    /** @var int<-1337, 1337> */
    public int $negativeIntRange; // also works with negative values

    /** @var int<min, 1337> */
    public int $minIntRange; // `min` can be used…

    /** @var int<0, max> */
    public int $maxIntRange; // …as well as `max`
}
```

Note that `min` and `max` will check the range with PHP's internal
constants `PHP_INT_MIN` and `PHP_INT_MAX`.
  • Loading branch information
romm committed Dec 7, 2021
1 parent 185edf6 commit 9f99a2a
Show file tree
Hide file tree
Showing 19 changed files with 708 additions and 19 deletions.
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,15 @@ final class SomeClass
/** @var negative-int */
private int $negativeInteger,

/** @var int<-42, 1337> */
private int $integerRange,

/** @var int<min, 0> */
private int $integerRangeWithMinRange,

/** @var int<0, max> */
private int $integerRangeWithMaxRange,

private string $string,

/** @var non-empty-string */
Expand Down
3 changes: 3 additions & 0 deletions src/Definition/Repository/Cache/Compiler/TypeCompiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use CuyZ\Valinor\Type\Types\ArrayType;
use CuyZ\Valinor\Type\Types\EnumType;
use CuyZ\Valinor\Type\Types\FloatType;
use CuyZ\Valinor\Type\Types\IntegerRangeType;
use CuyZ\Valinor\Type\Types\IntegerValueType;
use CuyZ\Valinor\Type\Types\InterfaceType;
use CuyZ\Valinor\Type\Types\IntersectionType;
Expand Down Expand Up @@ -57,6 +58,8 @@ public function compile(Type $type): string
case $type instanceof UndefinedObjectType:
case $type instanceof MixedType:
return "$class::get()";
case $type instanceof IntegerRangeType:
return "new $class({$type->min()}, {$type->max()})";
case $type instanceof StringValueType:
case $type instanceof IntegerValueType:
$value = var_export($type->value(), true);
Expand Down
21 changes: 21 additions & 0 deletions src/Type/Parser/Exception/Scalar/IntegerRangeInvalidMaxValue.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace CuyZ\Valinor\Type\Parser\Exception\Scalar;

use CuyZ\Valinor\Type\Parser\Exception\InvalidType;
use CuyZ\Valinor\Type\Type;
use CuyZ\Valinor\Type\Types\IntegerValueType;
use RuntimeException;

final class IntegerRangeInvalidMaxValue extends RuntimeException implements InvalidType
{
public function __construct(IntegerValueType $min, Type $type)
{
parent::__construct(
"Invalid type `$type` for max value of integer range `int<$min, ?>`, it must be either `max` or an integer value.",
1638788172
);
}
}
20 changes: 20 additions & 0 deletions src/Type/Parser/Exception/Scalar/IntegerRangeInvalidMinValue.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

namespace CuyZ\Valinor\Type\Parser\Exception\Scalar;

use CuyZ\Valinor\Type\Parser\Exception\InvalidType;
use CuyZ\Valinor\Type\Type;
use RuntimeException;

final class IntegerRangeInvalidMinValue extends RuntimeException implements InvalidType
{
public function __construct(Type $type)
{
parent::__construct(
"Invalid type `$type` for min value of integer range, it must be either `min` or an integer value.",
1638787807
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

namespace CuyZ\Valinor\Type\Parser\Exception\Scalar;

use CuyZ\Valinor\Type\Parser\Exception\InvalidType;
use CuyZ\Valinor\Type\Types\IntegerValueType;
use RuntimeException;

final class IntegerRangeMissingClosingBracket extends RuntimeException implements InvalidType
{
public function __construct(IntegerValueType $min, IntegerValueType $max)
{
parent::__construct(
"Missing closing bracket in integer range signature `int<$min, $max>`.",
1638788306
);
}
}
20 changes: 20 additions & 0 deletions src/Type/Parser/Exception/Scalar/IntegerRangeMissingComma.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

namespace CuyZ\Valinor\Type\Parser\Exception\Scalar;

use CuyZ\Valinor\Type\Parser\Exception\InvalidType;
use CuyZ\Valinor\Type\Types\IntegerValueType;
use RuntimeException;

final class IntegerRangeMissingComma extends RuntimeException implements InvalidType
{
public function __construct(IntegerValueType $min)
{
parent::__construct(
"Missing comma in integer range signature `int<$min, ?>`.",
1638787915
);
}
}
20 changes: 20 additions & 0 deletions src/Type/Parser/Exception/Scalar/IntegerRangeMissingMaxValue.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

namespace CuyZ\Valinor\Type\Parser\Exception\Scalar;

use CuyZ\Valinor\Type\Parser\Exception\InvalidType;
use CuyZ\Valinor\Type\Types\IntegerValueType;
use RuntimeException;

final class IntegerRangeMissingMaxValue extends RuntimeException implements InvalidType
{
public function __construct(IntegerValueType $min)
{
parent::__construct(
"Missing max value for integer range, its signature must match `int<$min, max>`.",
1638788092
);
}
}
19 changes: 19 additions & 0 deletions src/Type/Parser/Exception/Scalar/IntegerRangeMissingMinValue.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

namespace CuyZ\Valinor\Type\Parser\Exception\Scalar;

use CuyZ\Valinor\Type\Parser\Exception\InvalidType;
use RuntimeException;

final class IntegerRangeMissingMinValue extends RuntimeException implements InvalidType
{
public function __construct()
{
parent::__construct(
'Missing min value for integer range, its signature must match `int<min, max>`.',
1638787061
);
}
}
19 changes: 19 additions & 0 deletions src/Type/Parser/Exception/Scalar/ReversedValuesForIntegerRange.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

namespace CuyZ\Valinor\Type\Parser\Exception\Scalar;

use CuyZ\Valinor\Type\Parser\Exception\InvalidType;
use RuntimeException;

final class ReversedValuesForIntegerRange extends RuntimeException implements InvalidType
{
public function __construct(int $min, int $max)
{
parent::__construct(
"The min value must be less than the max for integer range `int<$min, $max>`.",
1638787061
);
}
}
19 changes: 19 additions & 0 deletions src/Type/Parser/Exception/Scalar/SameValueForIntegerRange.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

namespace CuyZ\Valinor\Type\Parser\Exception\Scalar;

use CuyZ\Valinor\Type\Parser\Exception\InvalidType;
use RuntimeException;

final class SameValueForIntegerRange extends RuntimeException implements InvalidType
{
public function __construct(int $value)
{
parent::__construct(
"The min and max values for integer range must be different, `$value` was given.",
1638786927
);
}
}
4 changes: 4 additions & 0 deletions src/Type/Parser/Lexer/NativeLexer.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use CuyZ\Valinor\Type\Parser\Lexer\Token\ColonToken;
use CuyZ\Valinor\Type\Parser\Lexer\Token\CommaToken;
use CuyZ\Valinor\Type\Parser\Lexer\Token\EnumNameToken;
use CuyZ\Valinor\Type\Parser\Lexer\Token\IntegerToken;
use CuyZ\Valinor\Type\Parser\Lexer\Token\IntegerValueToken;
use CuyZ\Valinor\Type\Parser\Lexer\Token\IntersectionToken;
use CuyZ\Valinor\Type\Parser\Lexer\Token\IterableToken;
Expand Down Expand Up @@ -68,6 +69,9 @@ public function tokenize(string $symbol): Token
return NullableToken::get();
case ',':
return CommaToken::get();
case 'int':
case 'integer':
return IntegerToken::get();
case 'array':
return ArrayToken::array();
case 'non-empty-array':
Expand Down
72 changes: 72 additions & 0 deletions src/Type/Parser/Lexer/Token/IntegerToken.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php

declare(strict_types=1);

namespace CuyZ\Valinor\Type\Parser\Lexer\Token;

use CuyZ\Valinor\Type\Parser\Exception\Scalar\IntegerRangeInvalidMaxValue;
use CuyZ\Valinor\Type\Parser\Exception\Scalar\IntegerRangeInvalidMinValue;
use CuyZ\Valinor\Type\Parser\Exception\Scalar\IntegerRangeMissingClosingBracket;
use CuyZ\Valinor\Type\Parser\Exception\Scalar\IntegerRangeMissingComma;
use CuyZ\Valinor\Type\Parser\Exception\Scalar\IntegerRangeMissingMaxValue;
use CuyZ\Valinor\Type\Parser\Exception\Scalar\IntegerRangeMissingMinValue;
use CuyZ\Valinor\Type\Parser\Lexer\TokenStream;
use CuyZ\Valinor\Type\Type;
use CuyZ\Valinor\Type\Types\IntegerRangeType;
use CuyZ\Valinor\Type\Types\IntegerValueType;
use CuyZ\Valinor\Type\Types\NativeIntegerType;
use CuyZ\Valinor\Utility\IsSingleton;

final class IntegerToken implements TraversingToken
{
use IsSingleton;

public function traverse(TokenStream $stream): Type
{
if ($stream->done() || ! $stream->next() instanceof OpeningBracketToken) {
return NativeIntegerType::get();
}

$stream->forward();

if ($stream->done()) {
throw new IntegerRangeMissingMinValue();
}

if ($stream->next() instanceof UnknownSymbolToken) {
$min = new IntegerValueType(PHP_INT_MIN);
$stream->forward();
} else {
$min = $stream->read();
}

if (! $min instanceof IntegerValueType) {
throw new IntegerRangeInvalidMinValue($min);
}

if ($stream->done() || ! $stream->forward() instanceof CommaToken) {
throw new IntegerRangeMissingComma($min);
}

if ($stream->done()) {
throw new IntegerRangeMissingMaxValue($min);
}

if ($stream->next() instanceof UnknownSymbolToken) {
$max = new IntegerValueType(PHP_INT_MAX);
$stream->forward();
} else {
$max = $stream->read();
}

if (! $max instanceof IntegerValueType) {
throw new IntegerRangeInvalidMaxValue($min, $max);
}

if ($stream->done() || ! $stream->forward() instanceof ClosingBracketToken) {
throw new IntegerRangeMissingClosingBracket($min, $max);
}

return new IntegerRangeType($min->value(), $max->value());
}
}
4 changes: 0 additions & 4 deletions src/Type/Parser/Lexer/Token/NativeToken.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
use CuyZ\Valinor\Type\Types\BooleanType;
use CuyZ\Valinor\Type\Types\FloatType;
use CuyZ\Valinor\Type\Types\MixedType;
use CuyZ\Valinor\Type\Types\NativeIntegerType;
use CuyZ\Valinor\Type\Types\NativeStringType;
use CuyZ\Valinor\Type\Types\NegativeIntegerType;
use CuyZ\Valinor\Type\Types\NonEmptyStringType;
Expand Down Expand Up @@ -61,9 +60,6 @@ private static function type(string $symbol): ?Type
return MixedType::get();
case 'float':
return FloatType::get();
case 'int':
case 'integer':
return NativeIntegerType::get();
case 'positive-int':
return PositiveIntegerType::get();
case 'negative-int':
Expand Down
19 changes: 19 additions & 0 deletions src/Type/Types/Exception/InvalidIntegerRangeValue.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

namespace CuyZ\Valinor\Type\Types\Exception;

use CuyZ\Valinor\Type\Types\IntegerRangeType;
use RuntimeException;

final class InvalidIntegerRangeValue extends RuntimeException implements CastError
{
public function __construct(int $value, IntegerRangeType $type)
{
parent::__construct(
"Invalid value `$value`: it must be an integer between {$type->min()} and {$type->max()}.",
1638785150
);
}
}
Loading

0 comments on commit 9f99a2a

Please sign in to comment.