8000 [TypeInfo] Add `IntRangeType` to hold specific type details for `int` builtin types by DerManoMann · Pull Request #59676 · symfony/symfony · GitHub
[go: up one dir, main page]

Skip to content

[TypeInfo] Add IntRangeType to hold specific type details for int builtin types #59676

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 10 commits into from
Original file line number Diff line number Diff line change
Expand Up @@ -963,15 +963,22 @@ public static function pseudoTypesProvider(): iterable
yield ['traitString', Type::string()];
yield ['interfaceString', Type::string()];
yield ['literalString', Type::string()];
yield ['positiveInt', Type::int()];
yield ['negativeInt', Type::int()];
yield ['nonEmptyArray', Type::array()];
yield ['nonEmptyList', Type::list()];
yield ['scalar', Type::union(Type::int(), Type::float(), Type::string(), Type::bool())];
yield ['number', Type::union(Type::int(), Type::float())];
yield ['numeric', Type::union(Type::int(), Type::float(), Type::string())];
yield ['arrayKey', Type::union(Type::int(), Type::string())];
yield ['double', Type::float()];

// BC layer for symfony/type-info < 7.3
if (method_exists(Type::class, 'intRange')) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (method_exists(Type::class, 'intRange')) {
// BC layer for symfony/type-info < 7.3
if (method_exists(Type::class, 'intRange')) {

yield ['positiveInt', Type::intRange(1)];
yield ['negativeInt', Type::intRange(\PHP_INT_MIN, -1)];
} else {
yield ['positiveInt', Type::int()];
yield ['negativeInt', Type::int()];
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It misses the else

}

public function testDummyNamespace()
Expand Down Expand Up @@ -1001,9 +1008,16 @@ public function testExtractorIntRangeType(string $property, ?Type $type)
*/
public static function intRangeTypeProvider(): iterable
{
yield ['a', Type::int()];
yield ['b', Type::nullable(Type::int())];
yield ['c', Type::int()];
// BC layer for symfony/type-info < 7.3
if (method_exists(Type::class, 'intRange')) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (method_exists(Type::class, 'intRange')) {
// BC layer for symfony/type-info < 7.3
if (method_exists(Type::class, 'intRange')) {

yield ['a', Type::intRange(0, 100)];
yield ['b', Type::nullable(Type::intRange(\PHP_INT_MIN, 100))];
yield ['c', Type::intRange(50, \PHP_INT_MAX)];
} else {
yield ['a', Type::int()];
yield ['b', Type::nullable(Type::int())];
yield ['c', Type::int()];
}
}

/**
Expand Down
1 change: 1 addition & 0 deletions src/Symfony/Component/TypeInfo/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ CHANGELOG
* Deprecate constructing a `CollectionType` instance as a list that is not an array
* Deprecate the third `$asList` argument of `TypeFactoryTrait::iterable()`, use `TypeFactoryTrait::list()` instead
* Add type alias support in `TypeContext` and `StringTypeResolver`
* Add `IntRangeType` class that represents a range of integer values

7.2
---
Expand Down
43 changes: 43 additions & 0 deletions src/Symfony/Component/TypeInfo/Tests/Type/IntRangeTypeTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\TypeInfo\Tests\Type;

use PHPUnit\Framework\TestCase;
use Symfony\Component\TypeInfo\Type;
use Symfony\Component\TypeInfo\Type\IntRangeType;

class IntRangeTypeTest extends TestCase
{
public function testToString()
{
$this->assertSame('int<0, max>', (string) new IntRangeType(from: 0));
$this->assertSame('int<min, 0>', (string) new IntRangeType(to: 0));
$this->assertSame('int<1, max>', (string) new IntRangeType(from: 1));
$this->assertSame('int<min, -1>', (string) new IntRangeType(to: -1));
$this->assertSame('int<-3, 5>', (string) new IntRangeType(from: -3, to: 5));
$this->assertSame('int<min, max>', (string) new IntRangeType());
$this->assertSame('int<min, 5>', (string) new IntRangeType(to: 5));
}

public function testAccepts()
{
$this->assertFalse((new IntRangeType(from: 0, to: 5))->accepts('string'));
$this->assertFalse((new IntRangeType(from: 0, to: 5))->accepts([]));

$this->assertFalse((new IntRangeType(from: -1, to: 5))->accepts(-3));
$this->assertTrue((new IntRangeType(from: -1, to: 5))->accepts(0));
$this->assertTrue((new IntRangeType(from: -1, to: 5))->accepts(2));
$this->assertFalse((new IntRangeType(from: -1, to: 5))->accepts(6));

$this->assertFalse(Type::union(Type::intRange(to: -1), Type::intRange(from: 1))->accepts(0));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -98,11 +98,6 @@ public static function resolveDataProvider(): iterable
yield [Type::false(), 'false'];
yield [Type::int(), 'int'];
yield [Type::int(), 'integer'];
yield [Type::int(), 'positive-int'];
yield [Type::int(), 'negative-int'];
yield [Type::int(), 'non-positive-int'];
yield [Type::int(), 'non-negative-int'];
yield [Type::int(), 'non-zero-int'];
yield [Type::float(), 'float'];
yield [Type::float(), 'double'];
yield [Type::string(), 'string'];
Expand Down Expand Up @@ -146,13 +141,21 @@ public static function resolveDataProvider(): iterable
yield [Type::template('T', Type::union(Type::int(), Type::string())), 'T', $typeContextFactory->createFromClassName(DummyWithTemplates::class)];
yield [Type::template('V'), 'V', $typeContextFactory->createFromReflection(new \ReflectionMethod(DummyWithTemplates::class, 'getPrice'))];

// int range
yield [Type::intRange(from: 1), 'positive-int'];
yield [Type::intRange(from: 0), 'non-negative-int'];
yield [Type::intRange(to: -1), 'negative-int'];
yield [Type::intRange(to: 0), 'non-positive-int'];
yield [Type::union(Type::intRange(to: -1), Type::intRange(from: 1)), 'non-zero-int'];
yield [Type::intRange(0, 100), 'int<0, 100>'];
yield [Type::intRange(), 'int<min, max>'];

// nullable
yield [Type::nullable(Type::int()), '?int'];

// generic
yield [Type::generic(Type::object(\DateTime::class), Type::string(), Type::bool()), \DateTime::class.'<string, bool>'];
yield [Type::generic(Type::object(\DateTime::class), Type::generic(Type::object(\Stringable::class), Type::bool())), \sprintf('%s<%s<bool>>', \DateTime::class, \Stringable::class)];
yield [Type::int(), 'int<0, 100>'];

// union
yield [Type::union(Type::int(), Type::string()), 'int|string'];
Expand Down
2 changes: 1 addition & 1 deletion src/Symfony/Component/TypeInfo/Type/BuiltinType.php
F438
Original file line numberDiff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
*
* @template T of TypeIdentifier
*/
final class BuiltinType extends Type
class BuiltinType extends Type
{
/**
* @param T $typeIdentifier
Expand Down
54 changes: 54 additions & 0 deletions src/Symfony/Component/TypeInfo/Type/IntRangeType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\TypeInfo\Type;

use Symfony\Component\TypeInfo\TypeIdentifier;

/**
* Int range type.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* Int range type.
* Represents a range of integer values.

*
* @author Martin Rademacher <mano@radebatz.net>
*
* @extends BuiltinType<TypeIdentifier::INT>
*/
final class IntRangeType extends BuiltinType
{
public function __construct(
private int $from = \PHP_INT_MIN,
private int $to = \PHP_INT_MAX,
) {
parent::__construct(TypeIdentifier::INT);
}

public function getFrom(): int
{
return $this->from;
}

public function getTo(): int
{
return $this->to;
}

public function accepts(mixed $value): bool
{
return parent::accepts($value) && $this->from <= $value && $value <= $this->to;
}

public function __toString(): string
{
$min = \PHP_INT_MIN === $this->from ? 'min' : $this->from;
$max = \PHP_INT_MAX === $this->to ? 'max' : $this->to;

return \sprintf('int<%s, %s>', $min, $max);
}
}
6 changes: 6 additions & 0 deletions src/Symfony/Component/TypeInfo/TypeFactoryTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use Symfony\Component\TypeInfo\Type\EnumType;
use Symfony\Component\TypeInfo\Type\GenericType;
use Symfony\Component\TypeInfo\Type\IntersectionType;
use Symfony\Component\TypeInfo\Type\IntRangeType;
use Symfony\Component\TypeInfo\Type\NullableType;
use Symfony\Component\TypeInfo\Type\ObjectType;
use Symfony\Component\TypeInfo\Type\TemplateType;
Expand Down Expand Up @@ -54,6 +55,11 @@ public static function int(): BuiltinType
return self::builtin(TypeIdentifier::INT);
}

public static function intRange(int $from = \PHP_INT_MIN, int $to = \PHP_INT_MAX): IntRangeType
{
return new IntRangeType($from, $to);
}

/**
* @return BuiltinType<TypeIdentifier::FLOAT>
*/
Expand Down
29 changes: 26 additions & 3 deletions src/Symfony/Component/TypeInfo/TypeResolver/StringTypeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,12 @@ private function getTypeFromNode(TypeNode $node, ?TypeContext $typeContext): Typ
'bool', 'boolean' => Type::bool(),
'true' => Type::true(),
'false' => Type::false(),
'int', 'integer', 'positive-int', 'negative-int', 'non-positive-int', 'non-negative-int', 'non-zero-int' => Type::int(),
'int', 'integer' => Type::int(),
'positive-int' => Type::intRange(from: 1),
'negative-int' => Type::intRange(to: -1),
'non-positive-int' => Type::intRange(to: 0),
'non-negative-int' => Type::intRange(from: 0),
'non-zero-int' => Type::union(Type::intRange(to: -1), Type::intRange(from: 1)),
'float', 'double' => Type::float(),
'string',
'class-string',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we'll need to handle class-string<Foo>, and interface-string<Bar> in a specific way.
IMHO, we should split the work in 2 PRs, on for the integers stuff, and on other for the strings stuff.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I was wondering about that. Seems wrong to leave things half finished.
Happy to create a second PR. Not sure what the pattern is here - wait until this is merged, or base thge second off this branch?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I meant to move all the string related stuff (ie: ExplicitString) in another PR. By doing that, you won't have to base the second PR on the first.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True. Getting late here...

Expand Down Expand Up @@ -184,9 +189,27 @@ private function getTypeFromNode(TypeNode $node, ?TypeContext $typeContext): Typ
if ($node instanceof GenericTypeNode) {
$type = $this->getTypeFromNode($node->type, $typeContext);

// handle integer ranges as simple integers
// handle integer ranges
if ($type->isIdentifiedBy(TypeIdentifier::INT)) {
return $type;
$getBoundaryFromNode = function (TypeNode $node) {
if ($node instanceof IdentifierTypeNode) {
return match ($node->name) {
'min' => \PHP_INT_MIN,
'max' => \PHP_INT_MAX,
default => throw new \DomainException(\sprintf('Invalid int range value "%s".', $node->name)),
};
}

if ($node->constExpr instanceof ConstExprIntegerNode) {
return (int) $node->constExpr->value;
}

throw new \DomainException(\sprintf('Invalid int range expression "%s".', \get_class($node->constExpr)));
};

$boundaries = array_map(static fn (TypeNode $t): int => $getBoundaryFromNode($t), $node->genericTypes);

return Type::intRange($boundaries[0], $boundaries[1]);
}

$variableTypes = array_map(fn (TypeNode $t): Type => $this->getTypeFromNode($t, $typeContext), $node->genericTypes);
Expand Down
Loading
0