From 16d885de9fd9fc7e62fe9a56d26698014f334369 Mon Sep 17 00:00:00 2001 From: DerManoMann Date: Mon, 3 Feb 2025 09:58:23 +1300 Subject: [PATCH 01/10] Add `IntRangeType` and `ExplicitStringType` --- src/Symfony/Component/TypeInfo/CHANGELOG.md | 1 + .../TypeInfo/Tests/Type/IntRangeTypeTest.php | 60 +++++++++++++++ .../TypeResolver/StringTypeResolverTest.php | 28 +++---- .../Component/TypeInfo/Type/BuiltinType.php | 2 +- .../TypeInfo/Type/ExplicitStringType.php | 34 +++++++++ .../Component/TypeInfo/Type/IntRangeType.php | 73 +++++++++++++++++++ .../Component/TypeInfo/TypeFactoryTrait.php | 12 +++ .../TypeResolver/StringTypeResolver.php | 33 +++++++-- 8 files changed, 219 insertions(+), 24 deletions(-) create mode 100644 src/Symfony/Component/TypeInfo/Tests/Type/IntRangeTypeTest.php create mode 100644 src/Symfony/Component/TypeInfo/Type/ExplicitStringType.php create mode 100644 src/Symfony/Component/TypeInfo/Type/IntRangeType.php diff --git a/src/Symfony/Component/TypeInfo/CHANGELOG.md b/src/Symfony/Component/TypeInfo/CHANGELOG.md index 5eaf445c6b8a0..eea6174301ca4 100644 --- a/src/Symfony/Component/TypeInfo/CHANGELOG.md +++ b/src/Symfony/Component/TypeInfo/CHANGELOG.md @@ -4,6 +4,7 @@ CHANGELOG 7.3 --- + * Add `IntRangeType` and `ExplicitStringType` classes to capture more specific type detals for `int` and `string` builtin types * Add `Type::accepts()` method * Add `TypeFactoryTrait::fromValue()` method * Deprecate constructing a `CollectionType` instance as a list that is not an array diff --git a/src/Symfony/Component/TypeInfo/Tests/Type/IntRangeTypeTest.php b/src/Symfony/Component/TypeInfo/Tests/Type/IntRangeTypeTest.php new file mode 100644 index 0000000000000..1be174a8db5dc --- /dev/null +++ b/src/Symfony/Component/TypeInfo/Tests/Type/IntRangeTypeTest.php @@ -0,0 +1,60 @@ + + * + * 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\BuiltinType; +use Symfony\Component\TypeInfo\Type\IntRangeType; +use Symfony\Component\TypeInfo\TypeIdentifier; + +class IntRangeTypeTest extends TestCase +{ + public function testToString() + { + $this->assertSame('non-negative-int', (string) new IntRangeType(0, \PHP_INT_MAX)); + $this->assertSame('non-positive-int', (string) new IntRangeType(\PHP_INT_MIN, 0)); + $this->assertSame('positive-int', (string) new IntRangeType(1, \PHP_INT_MAX)); + $this->assertSame('negative-int', (string) new IntRangeType(\PHP_INT_MIN, -1)); + $this->assertSame('int<-3, 5>', (string) new IntRangeType(-3, 5)); + $this->assertSame('int', (string) new IntRangeType(\PHP_INT_MIN, \PHP_INT_MAX)); + $this->assertSame('int', (string) new IntRangeType(\PHP_INT_MIN, 5)); + } + + public function testIsIdentifiedBy() + { + $this->assertFalse((new IntRangeType(0, 5))->isIdentifiedBy(TypeIdentifier::ARRAY)); + $this->assertTrue((new BuiltinType(TypeIdentifier::INT))->isIdentifiedBy(TypeIdentifier::INT)); + + $this->assertFalse((new IntRangeType(0, 5))->isIdentifiedBy('array')); + $this->assertTrue((new IntRangeType(0, 5))->isIdentifiedBy('int')); + + $this->assertTrue((new IntRangeType(0, 5))->isIdentifiedBy('string', 'int')); + } + + public function testIsNullable() + { + $this->assertFalse((new IntRangeType(0, 5))->isNullable()); + } + + public function testAccepts() + { + $this->assertFalse((new IntRangeType(0, 5))->accepts('string')); + $this->assertFalse((new IntRangeType(0, 5))->accepts([])); + + $this->assertFalse((new IntRangeType(-1, 5))->accepts(-3)); + $this->assertTrue((new IntRangeType(-1, 5))->accepts(0)); + $this->assertTrue((new IntRangeType(-1, 5))->accepts(2)); + $this->assertFalse((new IntRangeType(-1, 5))->accepts(6)); + + $this->assertFalse((new IntRangeType(\PHP_INT_MIN, \PHP_INT_MAX, false))->accepts(0)); + } +} diff --git a/src/Symfony/Component/TypeInfo/Tests/TypeResolver/StringTypeResolverTest.php b/src/Symfony/Component/TypeInfo/Tests/TypeResolver/StringTypeResolverTest.php index bbc1ffc93b738..eca82e1e1787a 100644 --- a/src/Symfony/Component/TypeInfo/Tests/TypeResolver/StringTypeResolverTest.php +++ b/src/Symfony/Component/TypeInfo/Tests/TypeResolver/StringTypeResolverTest.php @@ -98,26 +98,17 @@ 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::intRange(1, \PHP_INT_MAX), 'positive-int']; + yield [Type::intRange(\PHP_INT_MIN, -1), 'negative-int']; + yield [Type::intRange(\PHP_INT_MIN, 0), 'non-positive-int']; + yield [Type::intRange(0, \PHP_INT_MAX), 'non-negative-int']; + yield [Type::intRange(\PHP_INT_MIN, \PHP_INT_MAX, false), 'non-zero-int']; yield [Type::float(), 'float']; yield [Type::float(), 'double']; yield [Type::string(), 'string']; - yield [Type::string(), 'class-string']; - yield [Type::string(), 'trait-string']; - yield [Type::string(), 'interface-string']; - yield [Type::string(), 'callable-string']; - yield [Type::string(), 'numeric-string']; - yield [Type::string(), 'lowercase-string']; - yield [Type::string(), 'non-empty-lowercase-string']; - yield [Type::string(), 'non-empty-string']; - yield [Type::string(), 'non-falsy-string']; - yield [Type::string(), 'truthy-string']; - yield [Type::string(), 'literal-string']; - yield [Type::string(), 'html-escaped-string']; + yield [Type::explicitString('class-string'), 'class-string']; + yield [Type::explicitString('literal-string'), 'literal-string']; + yield [Type::explicitString('html-escaped-string'), 'html-escaped-string']; yield [Type::resource(), 'resource']; yield [Type::object(), 'object']; yield [Type::callable(), 'callable']; @@ -152,7 +143,8 @@ public static function resolveDataProvider(): iterable // generic yield [Type::generic(Type::object(\DateTime::class), Type::string(), Type::bool()), \DateTime::class.'']; yield [Type::generic(Type::object(\DateTime::class), Type::generic(Type::object(\Stringable::class), Type::bool())), \sprintf('%s<%s>', \DateTime::class, \Stringable::class)]; - yield [Type::int(), 'int<0, 100>']; + yield [Type::intRange(0, 100), 'int<0, 100>']; + yield [Type::intRange(\PHP_INT_MIN, \PHP_INT_MAX), 'int']; // union yield [Type::union(Type::int(), Type::string()), 'int|string']; diff --git a/src/Symfony/Component/TypeInfo/Type/BuiltinType.php b/src/Symfony/Component/TypeInfo/Type/BuiltinType.php index 71ff78b3d94ab..110196aa4a0f7 100644 --- a/src/Symfony/Component/TypeInfo/Type/BuiltinType.php +++ b/src/Symfony/Component/TypeInfo/Type/BuiltinType.php @@ -20,7 +20,7 @@ * * @template T of TypeIdentifier */ -final class BuiltinType extends Type +class BuiltinType extends Type { /** * @param T $typeIdentifier diff --git a/src/Symfony/Component/TypeInfo/Type/ExplicitStringType.php b/src/Symfony/Component/TypeInfo/Type/ExplicitStringType.php new file mode 100644 index 0000000000000..2f6458362ae7b --- /dev/null +++ b/src/Symfony/Component/TypeInfo/Type/ExplicitStringType.php @@ -0,0 +1,34 @@ + + * + * 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; + +/** + * Explicit string type. + * + * @author Martin Rademacher + * + * @extends BuiltinType + */ +class ExplicitStringType extends BuiltinType +{ + public function __construct(private string $explicitType) + { + parent::__construct(TypeIdentifier::STRING); + } + + public function getExplicitType(): string + { + return $this->explicitType; + } +} diff --git a/src/Symfony/Component/TypeInfo/Type/IntRangeType.php b/src/Symfony/Component/TypeInfo/Type/IntRangeType.php new file mode 100644 index 0000000000000..7a0e5989b165c --- /dev/null +++ b/src/Symfony/Component/TypeInfo/Type/IntRangeType.php @@ -0,0 +1,73 @@ + + * + * 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. + * + * @author Martin Rademacher + * + * @extends BuiltinType + */ +class IntRangeType extends BuiltinType +{ + public function __construct( + private int $from = \PHP_INT_MIN, + private int $to = \PHP_INT_MAX, + private bool $zeroIncluded = true, + ) { + parent::__construct(TypeIdentifier::INT); + } + + public function getFrom(): int + { + return $this->from; + } + + public function getTo(): int + { + return $this->to; + } + + public function isZeroIncluded(): bool + { + return $this->zeroIncluded; + } + + public function accepts(mixed $value): bool + { + return \is_int($value) + && $this->from <= $value && $value <= $this->to + && (0 !== $value || $this->zeroIncluded); + } + + public function __toString(): string + { + $min = \PHP_INT_MIN === $this->from ? 'min' : $this->from; + $max = \PHP_INT_MAX === $this->to ? 'max' : $this->to; + + $template = 'int<%s, %s>'; + if (\is_string($min) && \is_string($max)) { + return $this->zeroIncluded ? \sprintf($template, $min, $max) : 'non-zero-int'; + } + + if (\in_array($min, [0, 1], true) && 'max' === $max) { + return 0 === $min ? 'non-negative-int' : 'positive-int'; + } elseif ('min' === $min && \in_array($max, [-1, 0])) { + return 0 === $max ? 'non-positive-int' : 'negative-int'; + } + + return \sprintf($template, $min, $max); + } +} diff --git a/src/Symfony/Component/TypeInfo/TypeFactoryTrait.php b/src/Symfony/Component/TypeInfo/TypeFactoryTrait.php index df47ff7b3ddb1..c05a29e9696bb 100644 --- a/src/Symfony/Component/TypeInfo/TypeFactoryTrait.php +++ b/src/Symfony/Component/TypeInfo/TypeFactoryTrait.php @@ -15,8 +15,10 @@ use Symfony\Component\TypeInfo\Type\BuiltinType; use Symfony\Component\TypeInfo\Type\CollectionType; use Symfony\Component\TypeInfo\Type\EnumType; +use Symfony\Component\TypeInfo\Type\ExplicitStringType; 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; @@ -54,6 +56,11 @@ public static function int(): BuiltinType return self::builtin(TypeIdentifier::INT); } + public static function intRange(int $from, int $to, bool $zeroIncluded = true): IntRangeType + { + return new IntRangeType($from, $to, $zeroIncluded); + } + /** * @return BuiltinType */ @@ -70,6 +77,11 @@ public static function string(): BuiltinType return self::builtin(TypeIdentifier::STRING); } + public static function explicitString(string $explicitType): ExplicitStringType + { + return new ExplicitStringType($explicitType); + } + /** * @return BuiltinType */ diff --git a/src/Symfony/Component/TypeInfo/TypeResolver/StringTypeResolver.php b/src/Symfony/Component/TypeInfo/TypeResolver/StringTypeResolver.php index 5cd0819bd8b76..df349554a51a2 100644 --- a/src/Symfony/Component/TypeInfo/TypeResolver/StringTypeResolver.php +++ b/src/Symfony/Component/TypeInfo/TypeResolver/StringTypeResolver.php @@ -135,9 +135,14 @@ 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(1, \PHP_INT_MAX), + 'negative-int' => Type::intRange(\PHP_INT_MIN, -1), + 'non-positive-int' => Type::intRange(\PHP_INT_MIN, 0), + 'non-negative-int' => Type::intRange(0, \PHP_INT_MAX), + 'non-zero-int' => Type::intRange(\PHP_INT_MIN, \PHP_INT_MAX, false), 'float', 'double' => Type::float(), - 'string', + 'string' => Type::string(), 'class-string', 'trait-string', 'interface-string', @@ -149,7 +154,7 @@ private function getTypeFromNode(TypeNode $node, ?TypeContext $typeContext): Typ 'non-falsy-string', 'truthy-string', 'literal-string', - 'html-escaped-string' => Type::string(), + 'html-escaped-string' => Type::explicitString($node->name), 'resource' => Type::resource(), 'object' => Type::object(), 'callable' => Type::callable(), @@ -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; + $getValueFromNode = 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))); + }; + + $range = array_map(fn (TypeNode $t): int => $getValueFromNode($t), $node->genericTypes); + + return Type::intRange(...$range); } $variableTypes = array_map(fn (TypeNode $t): Type => $this->getTypeFromNode($t, $typeContext), $node->genericTypes); From 49a43b40b67fa7e852a2be0972fb3f3a14f5448d Mon Sep 17 00:00:00 2001 From: DerManoMann Date: Fri, 21 Feb 2025 08:50:27 +1300 Subject: [PATCH 02/10] Update `PropertyInfo` tests --- .../Tests/Extractor/PhpStanExtractorTest.php | 29 +++++++++---------- src/Symfony/Component/TypeInfo/CHANGELOG.md | 2 +- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php index 5563af2a1bf07..2c12e878d831c 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php @@ -946,25 +946,24 @@ public function testPseudoTypes(string $property, ?Type $type) */ public static function pseudoTypesProvider(): iterable { - yield ['classString', Type::string()]; + yield ['classString', Type::explicitString('class-string')]; // BC layer for type-info < 7.2 if (!interface_exists(WrappingTypeInterface::class)) { yield ['classStringGeneric', Type::generic(Type::string(), Type::object(\stdClass::class))]; } else { - yield ['classStringGeneric', Type::string()]; + yield ['classStringGeneric', Type::explicitString('class-string')]; } - yield ['htmlEscapedString', Type::string()]; - yield ['lowercaseString', Type::string()]; - yield ['nonEmptyLowercaseString', Type::string()]; - yield ['nonEmptyString', Type::string()]; - yield ['numericString', Type::string()]; - yield ['traitString', Type::string()]; - yield ['interfaceString', Type::string()]; - yield ['literalString', Type::string()]; - yield ['positiveInt', Type::int()]; - yield ['negativeInt', Type::int()]; + yield ['htmlEscapedString', Type::explicitString('html-escaped-string')]; + yield ['lowercaseString', Type::explicitString('lowercase-string')]; + yield ['nonEmptyLowercaseString', Type::explicitString('non-empty-lowercase-string')]; + yield ['nonEmptyString', Type::explicitString('non-empty-string')]; + yield ['numericString', Type::explicitString('numeric-string')]; + yield ['traitString', Type::explicitString('trait-string')]; + yield ['interfaceString', Type::explicitString('interface-string')]; + yield ['positiveInt', Type::intRange(1, \PHP_INT_MAX)]; + yield ['negativeInt', Type::intRange(\PHP_INT_MIN, -1)]; yield ['nonEmptyArray', Type::array()]; yield ['nonEmptyList', Type::list()]; yield ['scalar', Type::union(Type::int(), Type::float(), Type::string(), Type::bool())]; @@ -1001,9 +1000,9 @@ 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()]; + yield ['a', Type::intRange(0, 100)]; + yield ['b', Type::nullable(Type::intRange(\PHP_INT_MIN, 100))]; + yield ['c', Type::intRange(50, \PHP_INT_MAX)]; } /** diff --git a/src/Symfony/Component/TypeInfo/CHANGELOG.md b/src/Symfony/Component/TypeInfo/CHANGELOG.md index eea6174301ca4..61616883bfc19 100644 --- a/src/Symfony/Component/TypeInfo/CHANGELOG.md +++ b/src/Symfony/Component/TypeInfo/CHANGELOG.md @@ -4,7 +4,7 @@ CHANGELOG 7.3 --- - * Add `IntRangeType` and `ExplicitStringType` classes to capture more specific type detals for `int` and `string` builtin types + * Add `IntRangeType` and `ExplicitStringType` classes to hold specific type details for `int` and `string` builtin types * Add `Type::accepts()` method * Add `TypeFactoryTrait::fromValue()` method * Deprecate constructing a `CollectionType` instance as a list that is not an array From f67554e57e7a613d7d046d9b7d24052a895bc5fa Mon Sep 17 00:00:00 2001 From: DerManoMann Date: Fri, 21 Feb 2025 08:59:46 +1300 Subject: [PATCH 03/10] Make types `final` --- .../TypeInfo/Tests/Type/IntRangeTypeTest.php | 47 ++++++------------- .../TypeResolver/StringTypeResolverTest.php | 24 ++++++---- .../TypeInfo/Type/ExplicitStringType.php | 2 +- .../Component/TypeInfo/Type/IntRangeType.php | 15 ++---- .../Component/TypeInfo/TypeFactoryTrait.php | 4 +- .../TypeResolver/StringTypeResolver.php | 16 +++---- 6 files changed, 44 insertions(+), 64 deletions(-) diff --git a/src/Symfony/Component/TypeInfo/Tests/Type/IntRangeTypeTest.php b/src/Symfony/Component/TypeInfo/Tests/Type/IntRangeTypeTest.php index 1be174a8db5dc..aa1556d127eb7 100644 --- a/src/Symfony/Component/TypeInfo/Tests/Type/IntRangeTypeTest.php +++ b/src/Symfony/Component/TypeInfo/Tests/Type/IntRangeTypeTest.php @@ -12,49 +12,32 @@ namespace Symfony\Component\TypeInfo\Tests\Type; use PHPUnit\Framework\TestCase; -use Symfony\Component\TypeInfo\Type\BuiltinType; +use Symfony\Component\TypeInfo\Type; use Symfony\Component\TypeInfo\Type\IntRangeType; -use Symfony\Component\TypeInfo\TypeIdentifier; class IntRangeTypeTest extends TestCase { public function testToString() { - $this->assertSame('non-negative-int', (string) new IntRangeType(0, \PHP_INT_MAX)); - $this->assertSame('non-positive-int', (string) new IntRangeType(\PHP_INT_MIN, 0)); - $this->assertSame('positive-int', (string) new IntRangeType(1, \PHP_INT_MAX)); - $this->assertSame('negative-int', (string) new IntRangeType(\PHP_INT_MIN, -1)); - $this->assertSame('int<-3, 5>', (string) new IntRangeType(-3, 5)); - $this->assertSame('int', (string) new IntRangeType(\PHP_INT_MIN, \PHP_INT_MAX)); - $this->assertSame('int', (string) new IntRangeType(\PHP_INT_MIN, 5)); - } - - public function testIsIdentifiedBy() - { - $this->assertFalse((new IntRangeType(0, 5))->isIdentifiedBy(TypeIdentifier::ARRAY)); - $this->assertTrue((new BuiltinType(TypeIdentifier::INT))->isIdentifiedBy(TypeIdentifier::INT)); - - $this->assertFalse((new IntRangeType(0, 5))->isIdentifiedBy('array')); - $this->assertTrue((new IntRangeType(0, 5))->isIdentifiedBy('int')); - - $this->assertTrue((new IntRangeType(0, 5))->isIdentifiedBy('string', 'int')); - } - - public function testIsNullable() - { - $this->assertFalse((new IntRangeType(0, 5))->isNullable()); + $this->assertSame('non-negative-int', (string) new IntRangeType(from: 0)); + $this->assertSame('non-positive-int', (string) new IntRangeType(to: 0)); + $this->assertSame('positive-int', (string) new IntRangeType(from: 1)); + $this->assertSame('negative-int', (string) new IntRangeType(to: -1)); + $this->assertSame('int<-3, 5>', (string) new IntRangeType(from: -3, to: 5)); + $this->assertSame('int', (string) new IntRangeType()); + $this->assertSame('int', (string) new IntRangeType(to: 5)); } public function testAccepts() { - $this->assertFalse((new IntRangeType(0, 5))->accepts('string')); - $this->assertFalse((new IntRangeType(0, 5))->accepts([])); + $this->assertFalse((new IntRangeType(from: 0, to: 5))->accepts('string')); + $this->assertFalse((new IntRangeType(from: 0, to: 5))->accepts([])); - $this->assertFalse((new IntRangeType(-1, 5))->accepts(-3)); - $this->assertTrue((new IntRangeType(-1, 5))->accepts(0)); - $this->assertTrue((new IntRangeType(-1, 5))->accepts(2)); - $this->assertFalse((new IntRangeType(-1, 5))->accepts(6)); + $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((new IntRangeType(\PHP_INT_MIN, \PHP_INT_MAX, false))->accepts(0)); + $this->assertFalse(Type::union(Type::intRange(to: -1), Type::intRange(from: 1))->accepts(0)); } } diff --git a/src/Symfony/Component/TypeInfo/Tests/TypeResolver/StringTypeResolverTest.php b/src/Symfony/Component/TypeInfo/Tests/TypeResolver/StringTypeResolverTest.php index eca82e1e1787a..2e222b04a5d23 100644 --- a/src/Symfony/Component/TypeInfo/Tests/TypeResolver/StringTypeResolverTest.php +++ b/src/Symfony/Component/TypeInfo/Tests/TypeResolver/StringTypeResolverTest.php @@ -98,17 +98,9 @@ public static function resolveDataProvider(): iterable yield [Type::false(), 'false']; yield [Type::int(), 'int']; yield [Type::int(), 'integer']; - yield [Type::intRange(1, \PHP_INT_MAX), 'positive-int']; - yield [Type::intRange(\PHP_INT_MIN, -1), 'negative-int']; - yield [Type::intRange(\PHP_INT_MIN, 0), 'non-positive-int']; - yield [Type::intRange(0, \PHP_INT_MAX), 'non-negative-int']; - yield [Type::intRange(\PHP_INT_MIN, \PHP_INT_MAX, false), 'non-zero-int']; yield [Type::float(), 'float']; yield [Type::float(), 'double']; yield [Type::string(), 'string']; - yield [Type::explicitString('class-string'), 'class-string']; - yield [Type::explicitString('literal-string'), 'literal-string']; - yield [Type::explicitString('html-escaped-string'), 'html-escaped-string']; yield [Type::resource(), 'resource']; yield [Type::object(), 'object']; yield [Type::callable(), 'callable']; @@ -137,14 +129,26 @@ 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']; + + // explicit string + yield [Type::explicitString('class-string'), 'class-string']; + yield [Type::explicitString('literal-string'), 'literal-string']; + yield [Type::explicitString('html-escaped-string'), 'html-escaped-string']; + // nullable yield [Type::nullable(Type::int()), '?int']; // generic yield [Type::generic(Type::object(\DateTime::class), Type::string(), Type::bool()), \DateTime::class.'']; yield [Type::generic(Type::object(\DateTime::class), Type::generic(Type::object(\Stringable::class), Type::bool())), \sprintf('%s<%s>', \DateTime::class, \Stringable::class)]; - yield [Type::intRange(0, 100), 'int<0, 100>']; - yield [Type::intRange(\PHP_INT_MIN, \PHP_INT_MAX), 'int']; // union yield [Type::union(Type::int(), Type::string()), 'int|string']; diff --git a/src/Symfony/Component/TypeInfo/Type/ExplicitStringType.php b/src/Symfony/Component/TypeInfo/Type/ExplicitStringType.php index 2f6458362ae7b..318ad8fd7bfda 100644 --- a/src/Symfony/Component/TypeInfo/Type/ExplicitStringType.php +++ b/src/Symfony/Component/TypeInfo/Type/ExplicitStringType.php @@ -20,7 +20,7 @@ * * @extends BuiltinType */ -class ExplicitStringType extends BuiltinType +final class ExplicitStringType extends BuiltinType { public function __construct(private string $explicitType) { diff --git a/src/Symfony/Component/TypeInfo/Type/IntRangeType.php b/src/Symfony/Component/TypeInfo/Type/IntRangeType.php index 7a0e5989b165c..47446f4128d97 100644 --- a/src/Symfony/Component/TypeInfo/Type/IntRangeType.php +++ b/src/Symfony/Component/TypeInfo/Type/IntRangeType.php @@ -20,12 +20,11 @@ * * @extends BuiltinType */ -class IntRangeType extends BuiltinType +final class IntRangeType extends BuiltinType { public function __construct( private int $from = \PHP_INT_MIN, private int $to = \PHP_INT_MAX, - private bool $zeroIncluded = true, ) { parent::__construct(TypeIdentifier::INT); } @@ -40,16 +39,9 @@ public function getTo(): int return $this->to; } - public function isZeroIncluded(): bool - { - return $this->zeroIncluded; - } - public function accepts(mixed $value): bool { - return \is_int($value) - && $this->from <= $value && $value <= $this->to - && (0 !== $value || $this->zeroIncluded); + return parent::accepts($value) && $this->from <= $value && $value <= $this->to; } public function __toString(): string @@ -58,8 +50,9 @@ public function __toString(): string $max = \PHP_INT_MAX === $this->to ? 'max' : $this->to; $template = 'int<%s, %s>'; + if (\is_string($min) && \is_string($max)) { - return $this->zeroIncluded ? \sprintf($template, $min, $max) : 'non-zero-int'; + return \sprintf($template, $min, $max); } if (\in_array($min, [0, 1], true) && 'max' === $max) { diff --git a/src/Symfony/Component/TypeInfo/TypeFactoryTrait.php b/src/Symfony/Component/TypeInfo/TypeFactoryTrait.php index c05a29e9696bb..2b621e437b37a 100644 --- a/src/Symfony/Component/TypeInfo/TypeFactoryTrait.php +++ b/src/Symfony/Component/TypeInfo/TypeFactoryTrait.php @@ -56,9 +56,9 @@ public static function int(): BuiltinType return self::builtin(TypeIdentifier::INT); } - public static function intRange(int $from, int $to, bool $zeroIncluded = true): IntRangeType + public static function intRange(int $from = \PHP_INT_MIN, int $to = \PHP_INT_MAX): IntRangeType { - return new IntRangeType($from, $to, $zeroIncluded); + return new IntRangeType($from, $to); } /** diff --git a/src/Symfony/Component/TypeInfo/TypeResolver/StringTypeResolver.php b/src/Symfony/Component/TypeInfo/TypeResolver/StringTypeResolver.php index df349554a51a2..76e7036d0b681 100644 --- a/src/Symfony/Component/TypeInfo/TypeResolver/StringTypeResolver.php +++ b/src/Symfony/Component/TypeInfo/TypeResolver/StringTypeResolver.php @@ -136,11 +136,11 @@ private function getTypeFromNode(TypeNode $node, ?TypeContext $typeContext): Typ 'true' => Type::true(), 'false' => Type::false(), 'int', 'integer' => Type::int(), - 'positive-int' => Type::intRange(1, \PHP_INT_MAX), - 'negative-int' => Type::intRange(\PHP_INT_MIN, -1), - 'non-positive-int' => Type::intRange(\PHP_INT_MIN, 0), - 'non-negative-int' => Type::intRange(0, \PHP_INT_MAX), - 'non-zero-int' => Type::intRange(\PHP_INT_MIN, \PHP_INT_MAX, false), + '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' => Type::string(), 'class-string', @@ -191,7 +191,7 @@ private function getTypeFromNode(TypeNode $node, ?TypeContext $typeContext): Typ // handle integer ranges if ($type->isIdentifiedBy(TypeIdentifier::INT)) { - $getValueFromNode = function (TypeNode $node) { + $getBoundaryFromNode = function (TypeNode $node) { if ($node instanceof IdentifierTypeNode) { return match ($node->name) { 'min' => \PHP_INT_MIN, @@ -207,9 +207,9 @@ private function getTypeFromNode(TypeNode $node, ?TypeContext $typeContext): Typ throw new \DomainException(\sprintf('Invalid int range expression "%s".', \get_class($node->constExpr))); }; - $range = array_map(fn (TypeNode $t): int => $getValueFromNode($t), $node->genericTypes); + $boundaries = array_map(static fn (TypeNode $t): int => $getBoundaryFromNode($t), $node->genericTypes); - return Type::intRange(...$range); + return Type::intRange($boundaries[0], $boundaries[1]); } $variableTypes = array_map(fn (TypeNode $t): Type => $this->getTypeFromNode($t, $typeContext), $node->genericTypes); From e76cbc1a5680dea9b1a8c421d75743d6f54dfbd2 Mon Sep 17 00:00:00 2001 From: DerManoMann Date: Sat, 22 Feb 2025 10:16:46 +1300 Subject: [PATCH 04/10] Split `ExplicitStringType` off into separate branch --- .../Tests/Extractor/PhpStanExtractorTest.php | 24 +++++++------ src/Symfony/Component/TypeInfo/CHANGELOG.md | 2 +- .../TypeResolver/StringTypeResolverTest.php | 17 +++++++--- .../TypeInfo/Type/ExplicitStringType.php | 34 ------------------- .../Component/TypeInfo/TypeFactoryTrait.php | 5 --- .../TypeResolver/StringTypeResolver.php | 4 +-- 6 files changed, 28 insertions(+), 58 deletions(-) delete mode 100644 src/Symfony/Component/TypeInfo/Type/ExplicitStringType.php diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php index 2c12e878d831c..bde6ce3193b35 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php @@ -946,24 +946,23 @@ public function testPseudoTypes(string $property, ?Type $type) */ public static function pseudoTypesProvider(): iterable { - yield ['classString', Type::explicitString('class-string')]; + yield ['classString', Type::string()]; // BC layer for type-info < 7.2 if (!interface_exists(WrappingTypeInterface::class)) { yield ['classStringGeneric', Type::generic(Type::string(), Type::object(\stdClass::class))]; } else { - yield ['classStringGeneric', Type::explicitString('class-string')]; + yield ['classStringGeneric', Type::string()]; } - yield ['htmlEscapedString', Type::explicitString('html-escaped-string')]; - yield ['lowercaseString', Type::explicitString('lowercase-string')]; - yield ['nonEmptyLowercaseString', Type::explicitString('non-empty-lowercase-string')]; - yield ['nonEmptyString', Type::explicitString('non-empty-string')]; - yield ['numericString', Type::explicitString('numeric-string')]; - yield ['traitString', Type::explicitString('trait-string')]; - yield ['interfaceString', Type::explicitString('interface-string')]; - yield ['positiveInt', Type::intRange(1, \PHP_INT_MAX)]; - yield ['negativeInt', Type::intRange(\PHP_INT_MIN, -1)]; + yield ['htmlEscapedString', Type::string()]; + yield ['lowercaseString', Type::string()]; + yield ['nonEmptyLowercaseString', Type::string()]; + yield ['nonEmptyString', Type::string()]; + yield ['numericString', Type::string()]; + yield ['traitString', Type::string()]; + yield ['interfaceString', Type::string()]; + yield ['literalString', Type::string()]; yield ['nonEmptyArray', Type::array()]; yield ['nonEmptyList', Type::list()]; yield ['scalar', Type::union(Type::int(), Type::float(), Type::string(), Type::bool())]; @@ -971,6 +970,9 @@ public static function pseudoTypesProvider(): iterable yield ['numeric', Type::union(Type::int(), Type::float(), Type::string())]; yield ['arrayKey', Type::union(Type::int(), Type::string())]; yield ['double', Type::float()]; + + yield ['positiveInt', Type::intRange(1)]; + yield ['negativeInt', Type::intRange(\PHP_INT_MIN, -1)]; } public function testDummyNamespace() diff --git a/src/Symfony/Component/TypeInfo/CHANGELOG.md b/src/Symfony/Component/TypeInfo/CHANGELOG.md index 61616883bfc19..0437dad10fb82 100644 --- a/src/Symfony/Component/TypeInfo/CHANGELOG.md +++ b/src/Symfony/Component/TypeInfo/CHANGELOG.md @@ -4,7 +4,7 @@ CHANGELOG 7.3 --- - * Add `IntRangeType` and `ExplicitStringType` classes to hold specific type details for `int` and `string` builtin types + * Add `IntRangeType` class to hold specific type details for `int` builtin types * Add `Type::accepts()` method * Add `TypeFactoryTrait::fromValue()` method * Deprecate constructing a `CollectionType` instance as a list that is not an array diff --git a/src/Symfony/Component/TypeInfo/Tests/TypeResolver/StringTypeResolverTest.php b/src/Symfony/Component/TypeInfo/Tests/TypeResolver/StringTypeResolverTest.php index 2e222b04a5d23..a530fc0161b32 100644 --- a/src/Symfony/Component/TypeInfo/Tests/TypeResolver/StringTypeResolverTest.php +++ b/src/Symfony/Component/TypeInfo/Tests/TypeResolver/StringTypeResolverTest.php @@ -101,6 +101,18 @@ public static function resolveDataProvider(): iterable yield [Type::float(), 'float']; yield [Type::float(), 'double']; yield [Type::string(), 'string']; + yield [Type::string(), 'class-string']; + yield [Type::string(), 'trait-string']; + yield [Type::string(), 'interface-string']; + yield [Type::string(), 'callable-string']; + yield [Type::string(), 'numeric-string']; + yield [Type::string(), 'lowercase-string']; + yield [Type::string(), 'non-empty-lowercase-string']; + yield [Type::string(), 'non-empty-string']; + yield [Type::string(), 'non-falsy-string']; + yield [Type::string(), 'truthy-string']; + yield [Type::string(), 'literal-string']; + yield [Type::string(), 'html-escaped-string']; yield [Type::resource(), 'resource']; yield [Type::object(), 'object']; yield [Type::callable(), 'callable']; @@ -138,11 +150,6 @@ public static function resolveDataProvider(): iterable yield [Type::intRange(0, 100), 'int<0, 100>']; yield [Type::intRange(), 'int']; - // explicit string - yield [Type::explicitString('class-string'), 'class-string']; - yield [Type::explicitString('literal-string'), 'literal-string']; - yield [Type::explicitString('html-escaped-string'), 'html-escaped-string']; - // nullable yield [Type::nullable(Type::int()), '?int']; diff --git a/src/Symfony/Component/TypeInfo/Type/ExplicitStringType.php b/src/Symfony/Component/TypeInfo/Type/ExplicitStringType.php deleted file mode 100644 index 318ad8fd7bfda..0000000000000 --- a/src/Symfony/Component/TypeInfo/Type/ExplicitStringType.php +++ /dev/null @@ -1,34 +0,0 @@ - - * - * 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; - -/** - * Explicit string type. - * - * @author Martin Rademacher - * - * @extends BuiltinType - */ -final class ExplicitStringType extends BuiltinType -{ - public function __construct(private string $explicitType) - { - parent::__construct(TypeIdentifier::STRING); - } - - public function getExplicitType(): string - { - return $this->explicitType; - } -} diff --git a/src/Symfony/Component/TypeInfo/TypeFactoryTrait.php b/src/Symfony/Component/TypeInfo/TypeFactoryTrait.php index 2b621e437b37a..f009606fcb809 100644 --- a/src/Symfony/Component/TypeInfo/TypeFactoryTrait.php +++ b/src/Symfony/Component/TypeInfo/TypeFactoryTrait.php @@ -77,11 +77,6 @@ public static function string(): BuiltinType return self::builtin(TypeIdentifier::STRING); } - public static function explicitString(string $explicitType): ExplicitStringType - { - return new ExplicitStringType($explicitType); - } - /** * @return BuiltinType */ diff --git a/src/Symfony/Component/TypeInfo/TypeResolver/StringTypeResolver.php b/src/Symfony/Component/TypeInfo/TypeResolver/StringTypeResolver.php index 76e7036d0b681..caebd20bd9772 100644 --- a/src/Symfony/Component/TypeInfo/TypeResolver/StringTypeResolver.php +++ b/src/Symfony/Component/TypeInfo/TypeResolver/StringTypeResolver.php @@ -142,7 +142,7 @@ private function getTypeFromNode(TypeNode $node, ?TypeContext $typeContext): Typ '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' => Type::string(), + 'string', 'class-string', 'trait-string', 'interface-string', @@ -154,7 +154,7 @@ private function getTypeFromNode(TypeNode $node, ?TypeContext $typeContext): Typ 'non-falsy-string', 'truthy-string', 'literal-string', - 'html-escaped-string' => Type::explicitString($node->name), + 'html-escaped-string' => Type::string(), 'resource' => Type::resource(), 'object' => Type::object(), 'callable' => Type::callable(), From 79249d937d5f9f25ad9444e9b02939eca6a17695 Mon Sep 17 00:00:00 2001 From: DerManoMann Date: Sat, 22 Feb 2025 16:15:19 +1300 Subject: [PATCH 05/10] CS --- src/Symfony/Component/TypeInfo/TypeFactoryTrait.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Symfony/Component/TypeInfo/TypeFactoryTrait.php b/src/Symfony/Component/TypeInfo/TypeFactoryTrait.php index f009606fcb809..9c8056c71f556 100644 --- a/src/Symfony/Component/TypeInfo/TypeFactoryTrait.php +++ b/src/Symfony/Component/TypeInfo/TypeFactoryTrait.php @@ -15,7 +15,6 @@ use Symfony\Component\TypeInfo\Type\BuiltinType; use Symfony\Component\TypeInfo\Type\CollectionType; use Symfony\Component\TypeInfo\Type\EnumType; -use Symfony\Component\TypeInfo\Type\ExplicitStringType; use Symfony\Component\TypeInfo\Type\GenericType; use Symfony\Component\TypeInfo\Type\IntersectionType; use Symfony\Component\TypeInfo\Type\IntRangeType; From 3f63fb9bcfc9edd99d101baf3115d3db2ee3b372 Mon Sep 17 00:00:00 2001 From: DerManoMann Date: Sat, 22 Feb 2025 16:32:17 +1300 Subject: [PATCH 06/10] Update CHANGELOG.md --- src/Symfony/Component/TypeInfo/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Component/TypeInfo/CHANGELOG.md b/src/Symfony/Component/TypeInfo/CHANGELOG.md index 0437dad10fb82..e8613271eb8c4 100644 --- a/src/Symfony/Component/TypeInfo/CHANGELOG.md +++ b/src/Symfony/Component/TypeInfo/CHANGELOG.md @@ -4,12 +4,12 @@ CHANGELOG 7.3 --- - * Add `IntRangeType` class to hold specific type details for `int` builtin types * Add `Type::accepts()` method * Add `TypeFactoryTrait::fromValue()` method * 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 to hold specific type details for `int` builtin types 7.2 --- From 007cb6d7df599c791eb188ef53d0a6fbbbed6797 Mon Sep 17 00:00:00 2001 From: DerManoMann Date: Sat, 22 Feb 2025 21:19:09 +1300 Subject: [PATCH 07/10] Make `intRange` tests conditional --- .../Tests/Extractor/PhpStanExtractorTest.php | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php index bde6ce3193b35..b4450863acabd 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php @@ -971,8 +971,10 @@ public static function pseudoTypesProvider(): iterable yield ['arrayKey', Type::union(Type::int(), Type::string())]; yield ['double', Type::float()]; - yield ['positiveInt', Type::intRange(1)]; - yield ['negativeInt', Type::intRange(\PHP_INT_MIN, -1)]; + if (method_exists(Type::class, 'intRange')) { + yield ['positiveInt', Type::intRange(1)]; + yield ['negativeInt', Type::intRange(\PHP_INT_MIN, -1)]; + } } public function testDummyNamespace() @@ -1002,9 +1004,15 @@ public function testExtractorIntRangeType(string $property, ?Type $type) */ public static function intRangeTypeProvider(): iterable { - yield ['a', Type::intRange(0, 100)]; - yield ['b', Type::nullable(Type::intRange(\PHP_INT_MIN, 100))]; - yield ['c', Type::intRange(50, \PHP_INT_MAX)]; + 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()]; + } } /** From acee37fbb52452a3e71fce061189e0b6da933abb Mon Sep 17 00:00:00 2001 From: DerManoMann Date: Sun, 23 Feb 2025 14:19:47 +1300 Subject: [PATCH 08/10] Update wording --- src/Symfony/Component/TypeInfo/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Component/TypeInfo/CHANGELOG.md b/src/Symfony/Component/TypeInfo/CHANGELOG.md index e8613271eb8c4..8a0571007e645 100644 --- a/src/Symfony/Component/TypeInfo/CHANGELOG.md +++ b/src/Symfony/Component/TypeInfo/CHANGELOG.md @@ -9,7 +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 to hold specific type details for `int` builtin types + * Add `IntRangeType` class that represents a range of integer values 7.2 --- From 50c55765f2814db5fe63f2ffe741787025b00ae0 Mon Sep 17 00:00:00 2001 From: DerManoMann Date: Sun, 23 Feb 2025 14:20:44 +1300 Subject: [PATCH 09/10] Always serialize to `int` --- .../TypeInfo/Tests/Type/IntRangeTypeTest.php | 8 ++++---- .../Component/TypeInfo/Type/IntRangeType.php | 14 +------------- 2 files changed, 5 insertions(+), 17 deletions(-) diff --git a/src/Symfony/Component/TypeInfo/Tests/Type/IntRangeTypeTest.php b/src/Symfony/Component/TypeInfo/Tests/Type/IntRangeTypeTest.php index aa1556d127eb7..e0ffb23ee7fbc 100644 --- a/src/Symfony/Component/TypeInfo/Tests/Type/IntRangeTypeTest.php +++ b/src/Symfony/Component/TypeInfo/Tests/Type/IntRangeTypeTest.php @@ -19,10 +19,10 @@ class IntRangeTypeTest extends TestCase { public function testToString() { - $this->assertSame('non-negative-int', (string) new IntRangeType(from: 0)); - $this->assertSame('non-positive-int', (string) new IntRangeType(to: 0)); - $this->assertSame('positive-int', (string) new IntRangeType(from: 1)); - $this->assertSame('negative-int', (string) new IntRangeType(to: -1)); + $this->assertSame('int<0, max>', (string) new IntRangeType(from: 0)); + $this->assertSame('int', (string) new IntRangeType(to: 0)); + $this->assertSame('int<1, max>', (string) new IntRangeType(from: 1)); + $this->assertSame('int', (string) new IntRangeType(to: -1)); $this->assertSame('int<-3, 5>', (string) new IntRangeType(from: -3, to: 5)); $this->assertSame('int', (string) new IntRangeType()); $this->assertSame('int', (string) new IntRangeType(to: 5)); diff --git a/src/Symfony/Component/TypeInfo/Type/IntRangeType.php b/src/Symfony/Component/TypeInfo/Type/IntRangeType.php index 47446f4128d97..1761bb5e961bd 100644 --- a/src/Symfony/Component/TypeInfo/Type/IntRangeType.php +++ b/src/Symfony/Component/TypeInfo/Type/IntRangeType.php @@ -49,18 +49,6 @@ public function __toString(): string $min = \PHP_INT_MIN === $this->from ? 'min' : $this->from; $max = \PHP_INT_MAX === $this->to ? 'max' : $this->to; - $template = 'int<%s, %s>'; - - if (\is_string($min) && \is_string($max)) { - return \sprintf($template, $min, $max); - } - - if (\in_array($min, [0, 1], true) && 'max' === $max) { - return 0 === $min ? 'non-negative-int' : 'positive-int'; - } elseif ('min' === $min && \in_array($max, [-1, 0])) { - return 0 === $max ? 'non-positive-int' : 'negative-int'; - } - - return \sprintf($template, $min, $max); + return \sprintf('int<%s, %s>', $min, $max); } } From c6e6eadddfc3804bfbf2cd4447639875655a1237 Mon Sep 17 00:00:00 2001 From: DerManoMann Date: Sun, 23 Feb 2025 14:21:07 +1300 Subject: [PATCH 10/10] Add BC comments and missing `else` case --- .../PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php index b4450863acabd..296f12e1fa9af 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php @@ -971,9 +971,13 @@ public static function pseudoTypesProvider(): iterable 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')) { yield ['positiveInt', Type::intRange(1)]; yield ['negativeInt', Type::intRange(\PHP_INT_MIN, -1)]; + } else { + yield ['positiveInt', Type::int()]; + yield ['negativeInt', Type::int()]; } } @@ -1004,6 +1008,7 @@ public function testExtractorIntRangeType(string $property, ?Type $type) */ public static function intRangeTypeProvider(): iterable { + // 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))];