diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php index 5563af2a1bf07..0157fe16ecd8a 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php @@ -946,23 +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::classLikeString('class-string', Type::object(\stdClass::class))]; } - 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 ['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 ['literalString', Type::explicitString('literal-string')]; + yield ['positiveInt', Type::int()]; yield ['negativeInt', Type::int()]; yield ['nonEmptyArray', Type::array()]; diff --git a/src/Symfony/Component/TypeInfo/CHANGELOG.md b/src/Symfony/Component/TypeInfo/CHANGELOG.md index 5eaf445c6b8a0..08df36ca9f6ac 100644 --- a/src/Symfony/Component/TypeInfo/CHANGELOG.md +++ b/src/Symfony/Component/TypeInfo/CHANGELOG.md @@ -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 `ExplicitStringType` and `ClassLikeStringType` classes to hold specific type details for `string` builtin types 7.2 --- diff --git a/src/Symfony/Component/TypeInfo/Tests/Fixtures/DummyInterface.php b/src/Symfony/Component/TypeInfo/Tests/Fixtures/DummyInterface.php new file mode 100644 index 0000000000000..423250d097d94 --- /dev/null +++ b/src/Symfony/Component/TypeInfo/Tests/Fixtures/DummyInterface.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\TypeInfo\Tests\Fixtures; + +interface DummyInterface +{ +} diff --git a/src/Symfony/Component/TypeInfo/Tests/Fixtures/DummyTrait.php b/src/Symfony/Component/TypeInfo/Tests/Fixtures/DummyTrait.php new file mode 100644 index 0000000000000..e1ce382c10c43 --- /dev/null +++ b/src/Symfony/Component/TypeInfo/Tests/Fixtures/DummyTrait.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\TypeInfo\Tests\Fixtures; + +trait DummyTrait +{ +} diff --git a/src/Symfony/Component/TypeInfo/Tests/Type/ExplicitStringTypeTest.php b/src/Symfony/Component/TypeInfo/Tests/Type/ExplicitStringTypeTest.php new file mode 100644 index 0000000000000..70ef77de087c0 --- /dev/null +++ b/src/Symfony/Component/TypeInfo/Tests/Type/ExplicitStringTypeTest.php @@ -0,0 +1,29 @@ + + * + * 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\ExplicitStringType; + +class ExplicitStringTypeTest extends TestCase +{ + public function testToString() + { + $this->assertSame('class-string', (string) new ExplicitStringType('class-string')); + } + + public function testAccepts() + { + $this->assertFalse((new ExplicitStringType('interface-string'))->accepts(false)); + $this->assertTrue((new ExplicitStringType('interface-string'))->accepts('Foo')); + } +} diff --git a/src/Symfony/Component/TypeInfo/Tests/TypeResolver/StringTypeResolverTest.php b/src/Symfony/Component/TypeInfo/Tests/TypeResolver/StringTypeResolverTest.php index bbc1ffc93b738..330911996fea4 100644 --- a/src/Symfony/Component/TypeInfo/Tests/TypeResolver/StringTypeResolverTest.php +++ b/src/Symfony/Component/TypeInfo/Tests/TypeResolver/StringTypeResolverTest.php @@ -19,6 +19,8 @@ use Symfony\Component\TypeInfo\Tests\Fixtures\DummyBackedEnum; use Symfony\Component\TypeInfo\Tests\Fixtures\DummyCollection; use Symfony\Component\TypeInfo\Tests\Fixtures\DummyEnum; +use Symfony\Component\TypeInfo\Tests\Fixtures\DummyInterface; +use Symfony\Component\TypeInfo\Tests\Fixtures\DummyTrait; use Symfony\Component\TypeInfo\Tests\Fixtures\DummyWithTemplates; use Symfony\Component\TypeInfo\Tests\Fixtures\DummyWithTypeAliases; use Symfony\Component\TypeInfo\Type; @@ -106,18 +108,6 @@ 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']; @@ -146,6 +136,24 @@ 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'))]; + // explicit string + yield [Type::explicitString('callable-string'), 'callable-string']; + yield [Type::explicitString('numeric-string'), 'numeric-string']; + yield [Type::explicitString('lowercase-string'), 'lowercase-string']; + yield [Type::explicitString('non-empty-lowercase-string'), 'non-empty-lowercase-string']; + yield [Type::explicitString('non-empty-string'), 'non-empty-string']; + yield [Type::explicitString('non-falsy-string'), 'non-falsy-string']; + yield [Type::explicitString('truthy-string'), 'truthy-string']; + yield [Type::explicitString('literal-string'), 'literal-string']; + yield [Type::explicitString('html-escaped-string'), 'html-escaped-string']; + + yield [Type::explicitString('class-string'), 'class-string']; + yield [Type::classLikeString('class-string', Type::object(Dummy::class)), \sprintf('class-string<%s>', Dummy::class)]; + yield [Type::explicitString('trait-string'), 'trait-string']; + yield [Type::classLikeString('trait-string', Type::object(DummyTrait::class)), \sprintf('trait-string<%s>', DummyTrait::class)]; + yield [Type::explicitString('interface-string'), 'interface-string']; + yield [Type::classLikeString('interface-string', Type::object(DummyInterface::class)), \sprintf('interface-string<%s>', DummyInterface::class)]; + // nullable yield [Type::nullable(Type::int()), '?int']; 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/ClassLikeStringType.php b/src/Symfony/Component/TypeInfo/Type/ClassLikeStringType.php new file mode 100644 index 0000000000000..94bed26501794 --- /dev/null +++ b/src/Symfony/Component/TypeInfo/Type/ClassLikeStringType.php @@ -0,0 +1,39 @@ + + * + * 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 ClassLikeStringType extends ExplicitStringType +{ + public function __construct(string $explicitType, private ObjectType $objectType) + { + parent::__construct($explicitType); + } + + public function getObjectType(): ObjectType + { + return $this->objectType; + } + + public function __toString(): string + { + return \sprintf('%s<%s>', $this->getExplicitType(), $this->objectType); + } +} diff --git a/src/Symfony/Component/TypeInfo/Type/ExplicitStringType.php b/src/Symfony/Component/TypeInfo/Type/ExplicitStringType.php new file mode 100644 index 0000000000000..aa5afa84bf37d --- /dev/null +++ b/src/Symfony/Component/TypeInfo/Type/ExplicitStringType.php @@ -0,0 +1,39 @@ + + * + * 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; + } + + public function __toString(): string + { + return $this->explicitType; + } +} diff --git a/src/Symfony/Component/TypeInfo/TypeFactoryTrait.php b/src/Symfony/Component/TypeInfo/TypeFactoryTrait.php index df47ff7b3ddb1..6dc6aeaefc0ea 100644 --- a/src/Symfony/Component/TypeInfo/TypeFactoryTrait.php +++ b/src/Symfony/Component/TypeInfo/TypeFactoryTrait.php @@ -13,8 +13,10 @@ use Symfony\Component\TypeInfo\Type\BackedEnumType; use Symfony\Component\TypeInfo\Type\BuiltinType; +use Symfony\Component\TypeInfo\Type\ClassLikeStringType; 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\NullableType; @@ -70,6 +72,16 @@ public static function string(): BuiltinType return self::builtin(TypeIdentifier::STRING); } + public static function explicitString(string $explicitType): ExplicitStringType + { + return new ExplicitStringType($explicitType); + } + + public static function classLikeString(string $explicitType, ObjectType $objectType): ClassLikeStringType + { + return new ClassLikeStringType($explicitType, $objectType); + } + /** * @return BuiltinType */ diff --git a/src/Symfony/Component/TypeInfo/TypeResolver/StringTypeResolver.php b/src/Symfony/Component/TypeInfo/TypeResolver/StringTypeResolver.php index 5cd0819bd8b76..16248a1c83f1b 100644 --- a/src/Symfony/Component/TypeInfo/TypeResolver/StringTypeResolver.php +++ b/src/Symfony/Component/TypeInfo/TypeResolver/StringTypeResolver.php @@ -40,6 +40,7 @@ use Symfony\Component\TypeInfo\Type; use Symfony\Component\TypeInfo\Type\BuiltinType; use Symfony\Component\TypeInfo\Type\CollectionType; +use Symfony\Component\TypeInfo\Type\ExplicitStringType; use Symfony\Component\TypeInfo\Type\GenericType; use Symfony\Component\TypeInfo\TypeContext\TypeContext; use Symfony\Component\TypeInfo\TypeIdentifier; @@ -137,7 +138,7 @@ private function getTypeFromNode(TypeNode $node, ?TypeContext $typeContext): Typ 'false' => Type::false(), 'int', 'integer', 'positive-int', 'negative-int', 'non-positive-int', 'non-negative-int', 'non-zero-int' => Type::int(), 'float', 'double' => Type::float(), - 'string', + 'string' => Type::string(), 'class-string', 'trait-string', 'interface-string', @@ -149,7 +150,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(), @@ -215,6 +216,12 @@ private function getTypeFromNode(TypeNode $node, ?TypeContext $typeContext): Typ }; } + if ($type instanceof ExplicitStringType + && \in_array($type->getExplicitType(), ['class-string', 'interface-string', 'trait-string'], true) + && 1 === \count($variableTypes) && $variableTypes[0] instanceof Type\ObjectType) { + return Type::classLikeString($type->getExplicitType(), $variableTypes[0]); + } + if ($type instanceof BuiltinType && TypeIdentifier::ARRAY !== $type->getTypeIdentifier() && TypeIdentifier::ITERABLE !== $type->getTypeIdentifier()) { return $type; }