From 768bfab11b313811c2feb34a16f50ff8ff3fadd5 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 25 Feb 2021 12:29:55 +0100 Subject: [PATCH] Support int and string as template type bound --- src/Rules/Generics/TemplateTypeCheck.php | 4 + src/Type/Generic/TemplateIntegerType.php | 58 +++++++++ src/Type/Generic/TemplateStringType.php | 58 +++++++++ src/Type/Generic/TemplateTypeFactory.php | 17 ++- src/Type/Generic/TemplateTypeTrait.php | 7 +- .../Generics/TemplateTypeFactoryTest.php | 8 +- .../Generics/ClassTemplateTypeRuleTest.php | 4 +- .../Generics/FunctionTemplateTypeRuleTest.php | 2 +- .../InterfaceTemplateTypeRuleTest.php | 2 +- .../Generics/MethodTemplateTypeRuleTest.php | 2 +- .../Generics/TraitTemplateTypeRuleTest.php | 2 +- .../PHPStan/Rules/Generics/data/bug-3769.php | 37 ++++++ .../Rules/Generics/data/class-template.php | 4 +- .../Rules/Generics/data/function-template.php | 2 +- .../Generics/data/interface-template.php | 2 +- .../Rules/Generics/data/method-template.php | 2 +- .../Rules/Generics/data/trait-template.php | 2 +- .../Type/Generic/GenericObjectTypeTest.php | 6 +- tests/PHPStan/Type/StringTypeTest.php | 112 ++++++++++++++++++ 19 files changed, 307 insertions(+), 24 deletions(-) create mode 100644 src/Type/Generic/TemplateIntegerType.php create mode 100644 src/Type/Generic/TemplateStringType.php diff --git a/src/Rules/Generics/TemplateTypeCheck.php b/src/Rules/Generics/TemplateTypeCheck.php index 702e72faad..d008873305 100644 --- a/src/Rules/Generics/TemplateTypeCheck.php +++ b/src/Rules/Generics/TemplateTypeCheck.php @@ -8,9 +8,11 @@ use PHPStan\Rules\ClassNameNodePair; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\Generic\TemplateTypeScope; +use PHPStan\Type\IntegerType; use PHPStan\Type\MixedType; use PHPStan\Type\ObjectType; use PHPStan\Type\ObjectWithoutClassType; +use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeTraverser; use PHPStan\Type\UnionType; @@ -107,6 +109,8 @@ public function check( $boundClass = get_class($type); if ( $boundClass === MixedType::class + || $boundClass === StringType::class + || $boundClass === IntegerType::class || $boundClass === ObjectWithoutClassType::class || $boundClass === ObjectType::class || $type instanceof UnionType diff --git a/src/Type/Generic/TemplateIntegerType.php b/src/Type/Generic/TemplateIntegerType.php new file mode 100644 index 0000000000..7fcbf37f3d --- /dev/null +++ b/src/Type/Generic/TemplateIntegerType.php @@ -0,0 +1,58 @@ +scope = $scope; + $this->strategy = $templateTypeStrategy; + $this->variance = $templateTypeVariance; + $this->name = $name; + $this->bound = new IntegerType(); + } + + public function toArgument(): TemplateType + { + return new self( + $this->scope, + new TemplateTypeArgumentStrategy(), + $this->variance, + $this->name + ); + } + + protected function shouldGeneralizeInferredType(): bool + { + return false; + } + + /** + * @param mixed[] $properties + * @return Type + */ + public static function __set_state(array $properties): Type + { + return new self( + $properties['scope'], + $properties['strategy'], + $properties['variance'], + $properties['name'] + ); + } + +} diff --git a/src/Type/Generic/TemplateStringType.php b/src/Type/Generic/TemplateStringType.php new file mode 100644 index 0000000000..33591b2bac --- /dev/null +++ b/src/Type/Generic/TemplateStringType.php @@ -0,0 +1,58 @@ +scope = $scope; + $this->strategy = $templateTypeStrategy; + $this->variance = $templateTypeVariance; + $this->name = $name; + $this->bound = new StringType(); + } + + public function toArgument(): TemplateType + { + return new self( + $this->scope, + new TemplateTypeArgumentStrategy(), + $this->variance, + $this->name + ); + } + + protected function shouldGeneralizeInferredType(): bool + { + return false; + } + + /** + * @param mixed[] $properties + * @return Type + */ + public static function __set_state(array $properties): Type + { + return new self( + $properties['scope'], + $properties['strategy'], + $properties['variance'], + $properties['name'] + ); + } + +} diff --git a/src/Type/Generic/TemplateTypeFactory.php b/src/Type/Generic/TemplateTypeFactory.php index c752999c72..b660f522aa 100644 --- a/src/Type/Generic/TemplateTypeFactory.php +++ b/src/Type/Generic/TemplateTypeFactory.php @@ -4,16 +4,19 @@ use PHPStan\PhpDoc\Tag\TemplateTag; use PHPStan\Type\BenevolentUnionType; +use PHPStan\Type\IntegerType; use PHPStan\Type\MixedType; use PHPStan\Type\ObjectType; use PHPStan\Type\ObjectWithoutClassType; +use PHPStan\Type\StringType; use PHPStan\Type\Type; +use PHPStan\Type\TypeWithClassName; use PHPStan\Type\UnionType; final class TemplateTypeFactory { - public static function create(TemplateTypeScope $scope, string $name, ?Type $bound, TemplateTypeVariance $variance): Type + public static function create(TemplateTypeScope $scope, string $name, ?Type $bound, TemplateTypeVariance $variance): TemplateType { $strategy = new TemplateTypeParameterStrategy(); @@ -22,13 +25,21 @@ public static function create(TemplateTypeScope $scope, string $name, ?Type $bou } $boundClass = get_class($bound); - if ($boundClass === ObjectType::class) { + if ($bound instanceof TypeWithClassName && $boundClass === ObjectType::class) { return new TemplateObjectType($scope, $strategy, $variance, $name, $bound->getClassName()); } if ($boundClass === ObjectWithoutClassType::class) { return new TemplateObjectWithoutClassType($scope, $strategy, $variance, $name); } + if ($boundClass === StringType::class) { + return new TemplateStringType($scope, $strategy, $variance, $name); + } + + if ($boundClass === IntegerType::class) { + return new TemplateIntegerType($scope, $strategy, $variance, $name); + } + if ($boundClass === MixedType::class) { return new TemplateMixedType($scope, $strategy, $variance, $name); } @@ -46,7 +57,7 @@ public static function create(TemplateTypeScope $scope, string $name, ?Type $bou return new TemplateMixedType($scope, $strategy, $variance, $name); } - public static function fromTemplateTag(TemplateTypeScope $scope, TemplateTag $tag): Type + public static function fromTemplateTag(TemplateTypeScope $scope, TemplateTag $tag): TemplateType { return self::create($scope, $tag->getName(), $tag->getBound(), $tag->getVariance()); } diff --git a/src/Type/Generic/TemplateTypeTrait.php b/src/Type/Generic/TemplateTypeTrait.php index aa0b6c58a3..cc7488c8fe 100644 --- a/src/Type/Generic/TemplateTypeTrait.php +++ b/src/Type/Generic/TemplateTypeTrait.php @@ -155,7 +155,7 @@ public function inferTemplateTypes(Type $receivedType): TemplateTypeMap if ($this->getBound()->isSuperTypeOf($receivedType)->yes()) { return new TemplateTypeMap([ - $this->name => TemplateTypeHelper::generalizeType($receivedType), + $this->name => $this->shouldGeneralizeInferredType() ? TemplateTypeHelper::generalizeType($receivedType) : $receivedType, ]); } @@ -172,4 +172,9 @@ public function getVariance(): TemplateTypeVariance return $this->variance; } + protected function shouldGeneralizeInferredType(): bool + { + return true; + } + } diff --git a/tests/PHPStan/Generics/TemplateTypeFactoryTest.php b/tests/PHPStan/Generics/TemplateTypeFactoryTest.php index bc1678b168..cea4dd2da3 100644 --- a/tests/PHPStan/Generics/TemplateTypeFactoryTest.php +++ b/tests/PHPStan/Generics/TemplateTypeFactoryTest.php @@ -3,7 +3,6 @@ namespace PHPStan\Generics; use PHPStan\Type\ErrorType; -use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\Generic\TemplateTypeFactory; use PHPStan\Type\Generic\TemplateTypeScope; use PHPStan\Type\Generic\TemplateTypeVariance; @@ -36,7 +35,11 @@ public function dataCreate(): array ], [ new StringType(), - new MixedType(), + new StringType(), + ], + [ + new IntegerType(), + new IntegerType(), ], [ new ErrorType(), @@ -77,7 +80,6 @@ public function testCreate(?Type $bound, Type $expectedBound): void TemplateTypeVariance::createInvariant() ); - $this->assertInstanceOf(TemplateType::class, $templateType); $this->assertTrue( $expectedBound->equals($templateType->getBound()), sprintf('%s -> equals(%s)', $expectedBound->describe(VerbosityLevel::precise()), $templateType->getBound()->describe(VerbosityLevel::precise())) diff --git a/tests/PHPStan/Rules/Generics/ClassTemplateTypeRuleTest.php b/tests/PHPStan/Rules/Generics/ClassTemplateTypeRuleTest.php index a256539b41..bf529a063b 100644 --- a/tests/PHPStan/Rules/Generics/ClassTemplateTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generics/ClassTemplateTypeRuleTest.php @@ -33,7 +33,7 @@ public function testRule(): void 16, ], [ - 'PHPDoc tag @template T for class ClassTemplateType\Baz with bound type int is not supported.', + 'PHPDoc tag @template T for class ClassTemplateType\Baz with bound type float is not supported.', 24, ], [ @@ -53,7 +53,7 @@ public function testRule(): void 50, ], [ - 'PHPDoc tag @template T for anonymous class with bound type int is not supported.', + 'PHPDoc tag @template T for anonymous class with bound type float is not supported.', 55, ], [ diff --git a/tests/PHPStan/Rules/Generics/FunctionTemplateTypeRuleTest.php b/tests/PHPStan/Rules/Generics/FunctionTemplateTypeRuleTest.php index 7ac35058b8..8011881743 100644 --- a/tests/PHPStan/Rules/Generics/FunctionTemplateTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generics/FunctionTemplateTypeRuleTest.php @@ -34,7 +34,7 @@ public function testRule(): void 16, ], [ - 'PHPDoc tag @template T for function FunctionTemplateType\baz() with bound type int is not supported.', + 'PHPDoc tag @template T for function FunctionTemplateType\baz() with bound type float is not supported.', 24, ], [ diff --git a/tests/PHPStan/Rules/Generics/InterfaceTemplateTypeRuleTest.php b/tests/PHPStan/Rules/Generics/InterfaceTemplateTypeRuleTest.php index 7d3f194fab..cf2347028e 100644 --- a/tests/PHPStan/Rules/Generics/InterfaceTemplateTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generics/InterfaceTemplateTypeRuleTest.php @@ -34,7 +34,7 @@ public function testRule(): void 16, ], [ - 'PHPDoc tag @template T for interface InterfaceTemplateType\Baz with bound type int is not supported.', + 'PHPDoc tag @template T for interface InterfaceTemplateType\Baz with bound type float is not supported.', 24, ], [ diff --git a/tests/PHPStan/Rules/Generics/MethodTemplateTypeRuleTest.php b/tests/PHPStan/Rules/Generics/MethodTemplateTypeRuleTest.php index 0bf8add7fc..1df4612e5a 100644 --- a/tests/PHPStan/Rules/Generics/MethodTemplateTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generics/MethodTemplateTypeRuleTest.php @@ -38,7 +38,7 @@ public function testRule(): void 37, ], [ - 'PHPDoc tag @template T for method MethodTemplateType\Baz::doFoo() with bound type int is not supported.', + 'PHPDoc tag @template T for method MethodTemplateType\Baz::doFoo() with bound type float is not supported.', 50, ], [ diff --git a/tests/PHPStan/Rules/Generics/TraitTemplateTypeRuleTest.php b/tests/PHPStan/Rules/Generics/TraitTemplateTypeRuleTest.php index 26b6b61e5c..c6c2eb8707 100644 --- a/tests/PHPStan/Rules/Generics/TraitTemplateTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generics/TraitTemplateTypeRuleTest.php @@ -34,7 +34,7 @@ public function testRule(): void 16, ], [ - 'PHPDoc tag @template T for trait TraitTemplateType\Baz with bound type int is not supported.', + 'PHPDoc tag @template T for trait TraitTemplateType\Baz with bound type float is not supported.', 24, ], [ diff --git a/tests/PHPStan/Rules/Generics/data/bug-3769.php b/tests/PHPStan/Rules/Generics/data/bug-3769.php index 2a249d03e2..f5294f0d55 100644 --- a/tests/PHPStan/Rules/Generics/data/bug-3769.php +++ b/tests/PHPStan/Rules/Generics/data/bug-3769.php @@ -38,3 +38,40 @@ function foo( function fooUnion($foo): void { assertType('T of Exception|stdClass (function Bug3769\fooUnion(), argument)', $foo); } + +/** + * @template T + * @param T $a + * @return T + */ +function mixedBound($a) +{ + return $a; +} + +/** + * @template T of int + * @param T $a + * @return T + */ +function intBound(int $a) +{ + return $a; +} + +/** + * @template T of string + * @param T $a + * @return T + */ +function stringBound(string $a) +{ + return $a; +} + +function (): void { + assertType('int', mixedBound(1)); + assertType('string', mixedBound('str')); + assertType('1', intBound(1)); + assertType('\'str\'', stringBound('str')); +}; diff --git a/tests/PHPStan/Rules/Generics/data/class-template.php b/tests/PHPStan/Rules/Generics/data/class-template.php index b1ac19ec8b..771edc5c00 100644 --- a/tests/PHPStan/Rules/Generics/data/class-template.php +++ b/tests/PHPStan/Rules/Generics/data/class-template.php @@ -19,7 +19,7 @@ class Bar } /** - * @template T of int + * @template T of float */ class Baz { @@ -52,7 +52,7 @@ class Ipsum }; -new /** @template T of int */ class +new /** @template T of float */ class { }; diff --git a/tests/PHPStan/Rules/Generics/data/function-template.php b/tests/PHPStan/Rules/Generics/data/function-template.php index cb2e952b5d..994865f88f 100644 --- a/tests/PHPStan/Rules/Generics/data/function-template.php +++ b/tests/PHPStan/Rules/Generics/data/function-template.php @@ -19,7 +19,7 @@ function bar() } /** - * @template T of int + * @template T of float */ function baz() { diff --git a/tests/PHPStan/Rules/Generics/data/interface-template.php b/tests/PHPStan/Rules/Generics/data/interface-template.php index 30f2c1d8d2..a29f5dc702 100644 --- a/tests/PHPStan/Rules/Generics/data/interface-template.php +++ b/tests/PHPStan/Rules/Generics/data/interface-template.php @@ -19,7 +19,7 @@ interface Bar } /** - * @template T of int + * @template T of float */ interface Baz { diff --git a/tests/PHPStan/Rules/Generics/data/method-template.php b/tests/PHPStan/Rules/Generics/data/method-template.php index f7baec953d..a5fb337100 100644 --- a/tests/PHPStan/Rules/Generics/data/method-template.php +++ b/tests/PHPStan/Rules/Generics/data/method-template.php @@ -45,7 +45,7 @@ class Baz { /** - * @template T of int + * @template T of float */ public function doFoo() { diff --git a/tests/PHPStan/Rules/Generics/data/trait-template.php b/tests/PHPStan/Rules/Generics/data/trait-template.php index bb11c78183..c5f3526edd 100644 --- a/tests/PHPStan/Rules/Generics/data/trait-template.php +++ b/tests/PHPStan/Rules/Generics/data/trait-template.php @@ -19,7 +19,7 @@ trait Bar } /** - * @template T of int + * @template T of float */ trait Baz { diff --git a/tests/PHPStan/Type/Generic/GenericObjectTypeTest.php b/tests/PHPStan/Type/Generic/GenericObjectTypeTest.php index aea9cb9c61..fe412c6971 100644 --- a/tests/PHPStan/Type/Generic/GenericObjectTypeTest.php +++ b/tests/PHPStan/Type/Generic/GenericObjectTypeTest.php @@ -338,16 +338,12 @@ public function testResolveTemplateTypes(Type $received, Type $template, array $ public function dataGetReferencedTypeArguments(): array { $templateType = static function (string $name, ?Type $bound = null): TemplateType { - $templateType = TemplateTypeFactory::create( + return TemplateTypeFactory::create( TemplateTypeScope::createWithFunction('a'), $name, $bound ?? new MixedType(), TemplateTypeVariance::createInvariant() ); - if (!$templateType instanceof TemplateType) { - throw new \PHPStan\ShouldNotHappenException(); - } - return $templateType; }; return [ diff --git a/tests/PHPStan/Type/StringTypeTest.php b/tests/PHPStan/Type/StringTypeTest.php index 727a38ec14..3742c7a046 100644 --- a/tests/PHPStan/Type/StringTypeTest.php +++ b/tests/PHPStan/Type/StringTypeTest.php @@ -7,6 +7,9 @@ use PHPStan\Type\Accessory\HasPropertyType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\Generic\GenericClassStringType; +use PHPStan\Type\Generic\TemplateTypeFactory; +use PHPStan\Type\Generic\TemplateTypeScope; +use PHPStan\Type\Generic\TemplateTypeVariance; use Test\ClassWithToString; class StringTypeTest extends TestCase @@ -20,6 +23,66 @@ public function dataIsSuperTypeOf(): array new GenericClassStringType(new ObjectType(\Exception::class)), TrinaryLogic::createYes(), ], + [ + new StringType(), + TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('foo'), + 'T', + new StringType(), + TemplateTypeVariance::createInvariant() + ), + TrinaryLogic::createYes(), + ], + [ + TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('foo'), + 'T', + new StringType(), + TemplateTypeVariance::createInvariant() + ), + new StringType(), + TrinaryLogic::createMaybe(), + ], + [ + new ClassStringType(), + TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('foo'), + 'T', + new StringType(), + TemplateTypeVariance::createInvariant() + ), + TrinaryLogic::createMaybe(), + ], + [ + TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('foo'), + 'T', + new StringType(), + TemplateTypeVariance::createInvariant() + ), + new ClassStringType(), + TrinaryLogic::createMaybe(), + ], + [ + new GenericClassStringType(new ObjectType(\stdClass::class)), + TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('foo'), + 'T', + new StringType(), + TemplateTypeVariance::createInvariant() + ), + TrinaryLogic::createMaybe(), + ], + [ + TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('foo'), + 'T', + new StringType(), + TemplateTypeVariance::createInvariant() + ), + new GenericClassStringType(new ObjectType(\stdClass::class)), + TrinaryLogic::createMaybe(), + ], ]; } @@ -52,6 +115,55 @@ public function dataAccepts(): iterable new ClassStringType(), TrinaryLogic::createYes(), ]; + + yield [ + new StringType(), + TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('foo'), + 'T', + new StringType(), + TemplateTypeVariance::createInvariant() + ), + TrinaryLogic::createYes(), + ]; + + yield [ + TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('foo'), + 'T', + new StringType(), + TemplateTypeVariance::createInvariant() + ), + new StringType(), + TrinaryLogic::createYes(), + ]; + + yield [ + TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('foo'), + 'T', + new StringType(), + TemplateTypeVariance::createInvariant() + )->toArgument(), + new StringType(), + TrinaryLogic::createNo(), + ]; + + yield [ + TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('foo'), + 'T', + new StringType(), + TemplateTypeVariance::createInvariant() + )->toArgument(), + TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('foo'), + 'T', + new StringType(), + TemplateTypeVariance::createInvariant() + )->toArgument(), + TrinaryLogic::createNo(), + ]; } /**