diff --git a/src/Rules/PhpDoc/IncompatiblePhpDocTypeRule.php b/src/Rules/PhpDoc/IncompatiblePhpDocTypeRule.php index 5da471cdb6..e15c2af08c 100644 --- a/src/Rules/PhpDoc/IncompatiblePhpDocTypeRule.php +++ b/src/Rules/PhpDoc/IncompatiblePhpDocTypeRule.php @@ -10,7 +10,7 @@ use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\ArrayType; use PHPStan\Type\FileTypeMapper; -use PHPStan\Type\Generic\TemplateTypeHelper; +use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; @@ -95,7 +95,7 @@ public function processNode(Node $node, Scope $scope): array ) { $phpDocParamType = $phpDocParamType->getItemType(); } - $isParamSuperType = $nativeParamType->isSuperTypeOf(TemplateTypeHelper::resolveToBounds($phpDocParamType)); + $isParamSuperType = $nativeParamType->isSuperTypeOf($phpDocParamType); $escapedParameterName = SprintfHelper::escapeFormatString($parameterName); @@ -128,18 +128,23 @@ public function processNode(Node $node, Scope $scope): array ))->build(); } elseif ($isParamSuperType->maybe()) { - $errors[] = RuleErrorBuilder::message(sprintf( + $errorBuilder = RuleErrorBuilder::message(sprintf( 'PHPDoc tag @param for parameter $%s with type %s is not subtype of native type %s.', $parameterName, $phpDocParamType->describe(VerbosityLevel::typeOnly()), $nativeParamType->describe(VerbosityLevel::typeOnly()) - ))->build(); + )); + if ($phpDocParamType instanceof TemplateType) { + $errorBuilder->tip(sprintf('Write @template %s of %s to fix this.', $phpDocParamType->getName(), $nativeParamType->describe(VerbosityLevel::typeOnly()))); + } + + $errors[] = $errorBuilder->build(); } } } if ($resolvedPhpDoc->getReturnTag() !== null) { - $phpDocReturnType = TemplateTypeHelper::resolveToBounds($resolvedPhpDoc->getReturnTag()->getType()); + $phpDocReturnType = $resolvedPhpDoc->getReturnTag()->getType(); if ( $this->unresolvableTypeHelper->containsUnresolvableType($phpDocReturnType) @@ -163,11 +168,16 @@ public function processNode(Node $node, Scope $scope): array ))->build(); } elseif ($isReturnSuperType->maybe()) { - $errors[] = RuleErrorBuilder::message(sprintf( + $errorBuilder = RuleErrorBuilder::message(sprintf( 'PHPDoc tag @return with type %s is not subtype of native type %s.', $phpDocReturnType->describe(VerbosityLevel::typeOnly()), $nativeReturnType->describe(VerbosityLevel::typeOnly()) - ))->build(); + )); + if ($phpDocReturnType instanceof TemplateType) { + $errorBuilder->tip(sprintf('Write @template %s of %s to fix this.', $phpDocReturnType->getName(), $nativeReturnType->describe(VerbosityLevel::typeOnly()))); + } + + $errors[] = $errorBuilder->build(); } } } diff --git a/src/Rules/PhpDoc/IncompatiblePropertyPhpDocTypeRule.php b/src/Rules/PhpDoc/IncompatiblePropertyPhpDocTypeRule.php index 0f2ab5119d..9755e30d20 100644 --- a/src/Rules/PhpDoc/IncompatiblePropertyPhpDocTypeRule.php +++ b/src/Rules/PhpDoc/IncompatiblePropertyPhpDocTypeRule.php @@ -9,6 +9,7 @@ use PHPStan\Rules\Generics\GenericObjectTypeCheck; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\VerbosityLevel; /** @@ -79,14 +80,20 @@ public function processNode(Node $node, Scope $scope): array ))->build(); } elseif ($isSuperType->maybe()) { - $messages[] = RuleErrorBuilder::message(sprintf( + $errorBuilder = RuleErrorBuilder::message(sprintf( '%s for property %s::$%s with type %s is not subtype of native type %s.', $description, $propertyReflection->getDeclaringClass()->getDisplayName(), $propertyName, $phpDocType->describe(VerbosityLevel::typeOnly()), $nativeType->describe(VerbosityLevel::typeOnly()) - ))->build(); + )); + + if ($phpDocType instanceof TemplateType) { + $errorBuilder->tip(sprintf('Write @template %s of %s to fix this.', $phpDocType->getName(), $nativeType->describe(VerbosityLevel::typeOnly()))); + } + + $messages[] = $errorBuilder->build(); } $className = SprintfHelper::escapeFormatString($propertyReflection->getDeclaringClass()->getDisplayName()); diff --git a/tests/PHPStan/Rules/PhpDoc/IncompatiblePhpDocTypeRuleTest.php b/tests/PHPStan/Rules/PhpDoc/IncompatiblePhpDocTypeRuleTest.php index 163f826ac9..9a73c50d67 100644 --- a/tests/PHPStan/Rules/PhpDoc/IncompatiblePhpDocTypeRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/IncompatiblePhpDocTypeRuleTest.php @@ -70,14 +70,17 @@ public function testRule(): void [ 'PHPDoc tag @param for parameter $a with type T is not subtype of native type int.', 154, + 'Write @template T of int to fix this.', ], [ 'PHPDoc tag @param for parameter $b with type U of DateTimeInterface is not subtype of native type DateTime.', 154, + 'Write @template U of DateTime to fix this.', ], [ - 'PHPDoc tag @return with type DateTimeInterface is not subtype of native type DateTime.', + 'PHPDoc tag @return with type U of DateTimeInterface is not subtype of native type DateTime.', 154, + 'Write @template U of DateTime to fix this.', ], [ 'PHPDoc tag @param for parameter $foo contains generic type InvalidPhpDocDefinitions\Foo but class InvalidPhpDocDefinitions\Foo is not generic.', @@ -143,6 +146,11 @@ public function testRule(): void 'PHPDoc tag @return contains generic type InvalidPhpDocDefinitions\Foo but class InvalidPhpDocDefinitions\Foo is not generic.', 274, ], + [ + 'PHPDoc tag @param for parameter $i with type TFoo is not subtype of native type int.', + 283, + 'Write @template TFoo of int to fix this.', + ], ]); } @@ -165,4 +173,18 @@ public function testBug3753(): void ]); } + public function testTemplateTypeNativeTypeObject(): void + { + if (PHP_VERSION_ID < 70400 && !self::$useStaticReflectionProvider) { + $this->markTestSkipped('Test requires PHP 7.4.'); + } + $this->analyse([__DIR__ . '/data/template-type-native-type-object.php'], [ + [ + 'PHPDoc tag @return with type T is not subtype of native type object.', + 23, + 'Write @template T of object to fix this.', + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/PhpDoc/IncompatiblePropertyPhpDocTypeRuleTest.php b/tests/PHPStan/Rules/PhpDoc/IncompatiblePropertyPhpDocTypeRuleTest.php index 164c03193e..7938a2401c 100644 --- a/tests/PHPStan/Rules/PhpDoc/IncompatiblePropertyPhpDocTypeRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/IncompatiblePropertyPhpDocTypeRuleTest.php @@ -76,6 +76,11 @@ public function testNativeTypes(): void 'PHPDoc tag @var for property IncompatiblePhpDocPropertyNativeType\Foo::$stringOrInt with type int|string is not subtype of native type string.', 21, ], + [ + 'PHPDoc tag @var for property IncompatiblePhpDocPropertyNativeType\Lorem::$string with type T is not subtype of native type string.', + 45, + 'Write @template T of string to fix this.', + ], ]); } diff --git a/tests/PHPStan/Rules/PhpDoc/data/incompatible-property-native-types.php b/tests/PHPStan/Rules/PhpDoc/data/incompatible-property-native-types.php index 93a4a38a91..56c5f7bc60 100644 --- a/tests/PHPStan/Rules/PhpDoc/data/incompatible-property-native-types.php +++ b/tests/PHPStan/Rules/PhpDoc/data/incompatible-property-native-types.php @@ -36,3 +36,12 @@ class Baz private string $stringProp; } + +/** @template T */ +class Lorem +{ + + /** @var T */ + private string $string; + +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/incompatible-types.php b/tests/PHPStan/Rules/PhpDoc/data/incompatible-types.php index 517a3e0ac3..5f7fd45b73 100644 --- a/tests/PHPStan/Rules/PhpDoc/data/incompatible-types.php +++ b/tests/PHPStan/Rules/PhpDoc/data/incompatible-types.php @@ -275,3 +275,12 @@ function genericNestedNonTemplateArgs() { } + +/** + * @template TFoo + * @param TFoo $i + */ +function genericWrongBound(int $i) +{ + +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/template-type-native-type-object.php b/tests/PHPStan/Rules/PhpDoc/data/template-type-native-type-object.php new file mode 100644 index 0000000000..823fb21600 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/template-type-native-type-object.php @@ -0,0 +1,31 @@ += 7.4 + +namespace TemplateTypeNativeTypeObject; + +class HelloWorld +{ + /** + * @var array + */ + private array $instances; + + public function __construct() + { + $this->instances = []; + } + + /** + * @phpstan-template T + * @phpstan-param class-string $className + * + * @phpstan-return T + */ + public function getInstanceByName(string $className, string $name): object + { + $instance = $this->instances["[{$className}]{$name}"]; + + \assert($instance instanceof $className); + + return $instance; + } +}