diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 5a96ce5c15e..9a8b786f71f 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -624,12 +624,6 @@ parameters: count: 1 path: src/Rules/Generics/GenericAncestorsCheck.php - - - rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. - identifier: phpstanApi.instanceofType - count: 1 - path: src/Rules/Generics/TemplateTypeCheck.php - - rawMessage: 'Method PHPStan\Rules\LazyRegistry::getRulesByNodeType() return type with generic interface PHPStan\Rules\Rule does not specify its types: TNodeType' identifier: missingType.generics @@ -1146,12 +1140,42 @@ parameters: count: 3 path: src/Type/Generic/TemplateBooleanType.php + - + rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. + identifier: phpstanApi.instanceofType + count: 3 + path: src/Type/Generic/TemplateCallableType.php + + - + rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. + identifier: phpstanApi.instanceofType + count: 3 + path: src/Type/Generic/TemplateClassStringType.php + + - + rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. + identifier: phpstanApi.instanceofType + count: 3 + path: src/Type/Generic/TemplateClosureType.php + - rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. identifier: phpstanApi.instanceofType count: 3 path: src/Type/Generic/TemplateConstantArrayType.php + - + rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. + identifier: phpstanApi.instanceofType + count: 3 + path: src/Type/Generic/TemplateConstantFloatType.php + + - + rawMessage: 'Method PHPStan\Type\Generic\TemplateConstantFloatType::toPhpDocNode() should return PHPStan\PhpDocParser\Ast\Type\ConstTypeNode but returns PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode.' + identifier: return.type + count: 1 + path: src/Type/Generic/TemplateConstantFloatType.php + - rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. identifier: phpstanApi.instanceofType @@ -1176,6 +1200,12 @@ parameters: count: 3 path: src/Type/Generic/TemplateFloatType.php + - + rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. + identifier: phpstanApi.instanceofType + count: 3 + path: src/Type/Generic/TemplateGenericClassStringType.php + - rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. identifier: phpstanApi.instanceofType @@ -1260,6 +1290,18 @@ parameters: count: 1 path: src/Type/Generic/TemplateTypeFactory.php + - + rawMessage: 'Doing instanceof PHPStan\Type\CallableType is error-prone and deprecated. Use Type::isCallable() and Type::getCallableParametersAcceptors() instead.' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/Generic/TemplateTypeFactory.php + + - + rawMessage: 'Doing instanceof PHPStan\Type\ClassStringType is error-prone and deprecated. Use Type::isClassStringType() instead.' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/Generic/TemplateTypeFactory.php + - rawMessage: 'Doing instanceof PHPStan\Type\Constant\ConstantArrayType is error-prone and deprecated. Use Type::getConstantArrays() instead.' identifier: phpstanApi.instanceofType @@ -1278,6 +1320,12 @@ parameters: count: 1 path: src/Type/Generic/TemplateTypeFactory.php + - + rawMessage: 'Doing instanceof PHPStan\Type\Generic\GenericClassStringType is error-prone and deprecated. Use Type::isClassStringType() and Type::getClassStringObjectType() instead.' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/Generic/TemplateTypeFactory.php + - rawMessage: Doing instanceof PHPStan\Type\Generic\GenericObjectType is error-prone and deprecated. identifier: phpstanApi.instanceofType diff --git a/src/Rules/Generics/TemplateTypeCheck.php b/src/Rules/Generics/TemplateTypeCheck.php index be58cefefed..9f3514c5c18 100644 --- a/src/Rules/Generics/TemplateTypeCheck.php +++ b/src/Rules/Generics/TemplateTypeCheck.php @@ -14,31 +14,12 @@ use PHPStan\Rules\ClassNameUsageLocation; use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\Type\ArrayType; -use PHPStan\Type\BooleanType; -use PHPStan\Type\Constant\ConstantArrayType; -use PHPStan\Type\Constant\ConstantIntegerType; -use PHPStan\Type\Constant\ConstantStringType; -use PHPStan\Type\FloatType; -use PHPStan\Type\Generic\GenericObjectType; -use PHPStan\Type\Generic\TemplateType; +use PHPStan\Type\Generic\TemplateTypeFactory; use PHPStan\Type\Generic\TemplateTypeScope; -use PHPStan\Type\IntegerType; -use PHPStan\Type\IntersectionType; -use PHPStan\Type\IterableType; -use PHPStan\Type\KeyOfType; -use PHPStan\Type\MixedType; -use PHPStan\Type\NullType; -use PHPStan\Type\ObjectShapeType; -use PHPStan\Type\ObjectType; -use PHPStan\Type\ObjectWithoutClassType; -use PHPStan\Type\StringType; use PHPStan\Type\TypeAliasResolver; -use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; use function array_map; use function array_merge; -use function get_class; use function sprintf; #[AutowiredService] @@ -116,28 +97,10 @@ public function check( 'templateTagName' => $templateTagName, ]), $this->checkClassCaseSensitivity)); - $boundTypeClass = get_class($boundType); - if ( - $boundTypeClass !== MixedType::class - && $boundTypeClass !== ConstantArrayType::class - && $boundTypeClass !== ArrayType::class - && $boundTypeClass !== ConstantStringType::class - && $boundTypeClass !== StringType::class - && $boundTypeClass !== ConstantIntegerType::class - && $boundTypeClass !== IntegerType::class - && $boundTypeClass !== FloatType::class - && $boundTypeClass !== BooleanType::class - && $boundTypeClass !== ObjectWithoutClassType::class - && $boundTypeClass !== ObjectType::class - && $boundTypeClass !== ObjectShapeType::class - && $boundTypeClass !== GenericObjectType::class - && $boundTypeClass !== KeyOfType::class - && $boundTypeClass !== IterableType::class - && $boundTypeClass !== NullType::class - && !$boundType instanceof UnionType - && !$boundType instanceof IntersectionType - && !$boundType instanceof TemplateType - ) { + // TemplateTypeFactory silently widens bounds it has no Template* class for + // to `mixed`. Ask it instead of maintaining a second list of supported + // bound types that can drift out of sync with it. + if (TemplateTypeFactory::fromTemplateTag($templateTypeScope, $templateTag)->getBound() !== $boundType) { $messages[] = RuleErrorBuilder::message(sprintf($notSupportedBoundMessage, $templateTagName, $boundType->describe(VerbosityLevel::typeOnly()))) ->identifier('generics.notSupportedBound') ->build(); diff --git a/src/Type/Generic/TemplateCallableType.php b/src/Type/Generic/TemplateCallableType.php new file mode 100644 index 00000000000..c12fd36edd7 --- /dev/null +++ b/src/Type/Generic/TemplateCallableType.php @@ -0,0 +1,47 @@ + */ + use TemplateTypeTrait; + + /** + * @param non-empty-string $name + */ + public function __construct( + TemplateTypeScope $scope, + TemplateTypeStrategy $templateTypeStrategy, + TemplateTypeVariance $templateTypeVariance, + string $name, + CallableType $bound, + ?Type $default, + ) + { + $isCommonCallable = $bound->isCommonCallable(); + parent::__construct( + $isCommonCallable ? null : $bound->getParameters(), + $isCommonCallable ? null : $bound->getReturnType(), + $bound->isVariadic(), + $bound->getTemplateTypeMap(), + $bound->getResolvedTemplateTypeMap(), + $bound->getTemplateTags(), + $bound->isPure(), + $bound->getAsserts(), + ); + + $this->scope = $scope; + $this->strategy = $templateTypeStrategy; + $this->variance = $templateTypeVariance; + $this->name = $name; + $this->bound = $bound; + $this->default = $default; + } + +} diff --git a/src/Type/Generic/TemplateClassStringType.php b/src/Type/Generic/TemplateClassStringType.php new file mode 100644 index 00000000000..01692ddde4d --- /dev/null +++ b/src/Type/Generic/TemplateClassStringType.php @@ -0,0 +1,38 @@ + */ + use TemplateTypeTrait; + use UndecidedComparisonCompoundTypeTrait; + + /** + * @param non-empty-string $name + */ + public function __construct( + TemplateTypeScope $scope, + TemplateTypeStrategy $templateTypeStrategy, + TemplateTypeVariance $templateTypeVariance, + string $name, + ClassStringType $bound, + ?Type $default, + ) + { + parent::__construct(); + $this->scope = $scope; + $this->strategy = $templateTypeStrategy; + $this->variance = $templateTypeVariance; + $this->name = $name; + $this->bound = $bound; + $this->default = $default; + } + +} diff --git a/src/Type/Generic/TemplateClosureType.php b/src/Type/Generic/TemplateClosureType.php new file mode 100644 index 00000000000..e3e1e5b0718 --- /dev/null +++ b/src/Type/Generic/TemplateClosureType.php @@ -0,0 +1,56 @@ + */ + use TemplateTypeTrait; + use UndecidedComparisonCompoundTypeTrait; + + /** + * @param non-empty-string $name + */ + public function __construct( + TemplateTypeScope $scope, + TemplateTypeStrategy $templateTypeStrategy, + TemplateTypeVariance $templateTypeVariance, + string $name, + ClosureType $bound, + ?Type $default, + ) + { + $isCommonCallable = $bound->isCommonCallable(); + parent::__construct( + $isCommonCallable ? null : $bound->getParameters(), + $isCommonCallable ? null : $bound->getReturnType(), + $bound->isVariadic(), + $bound->getTemplateTypeMap(), + $bound->getResolvedTemplateTypeMap(), + $bound->getCallSiteVarianceMap(), + $bound->getTemplateTags(), + $bound->getThrowPoints(), + $bound->getImpurePoints(), + $bound->getInvalidateExpressions(), + $bound->getUsedVariables(), + $bound->acceptsNamedArguments(), + $bound->mustUseReturnValue(), + $bound->getAsserts(), + $bound->isStaticClosure(), + ); + + $this->scope = $scope; + $this->strategy = $templateTypeStrategy; + $this->variance = $templateTypeVariance; + $this->name = $name; + $this->bound = $bound; + $this->default = $default; + } + +} diff --git a/src/Type/Generic/TemplateConstantFloatType.php b/src/Type/Generic/TemplateConstantFloatType.php new file mode 100644 index 00000000000..d132b9fa337 --- /dev/null +++ b/src/Type/Generic/TemplateConstantFloatType.php @@ -0,0 +1,38 @@ + */ + use TemplateTypeTrait; + use UndecidedComparisonCompoundTypeTrait; + + /** + * @param non-empty-string $name + */ + public function __construct( + TemplateTypeScope $scope, + TemplateTypeStrategy $templateTypeStrategy, + TemplateTypeVariance $templateTypeVariance, + string $name, + ConstantFloatType $bound, + ?Type $default, + ) + { + parent::__construct($bound->getValue()); + $this->scope = $scope; + $this->strategy = $templateTypeStrategy; + $this->variance = $templateTypeVariance; + $this->name = $name; + $this->bound = $bound; + $this->default = $default; + } + +} diff --git a/src/Type/Generic/TemplateGenericClassStringType.php b/src/Type/Generic/TemplateGenericClassStringType.php new file mode 100644 index 00000000000..2270f83fef0 --- /dev/null +++ b/src/Type/Generic/TemplateGenericClassStringType.php @@ -0,0 +1,37 @@ + */ + use TemplateTypeTrait; + use UndecidedComparisonCompoundTypeTrait; + + /** + * @param non-empty-string $name + */ + public function __construct( + TemplateTypeScope $scope, + TemplateTypeStrategy $templateTypeStrategy, + TemplateTypeVariance $templateTypeVariance, + string $name, + GenericClassStringType $bound, + ?Type $default, + ) + { + parent::__construct($bound->getGenericType()); + $this->scope = $scope; + $this->strategy = $templateTypeStrategy; + $this->variance = $templateTypeVariance; + $this->name = $name; + $this->bound = $bound; + $this->default = $default; + } + +} diff --git a/src/Type/Generic/TemplateTypeFactory.php b/src/Type/Generic/TemplateTypeFactory.php index 4e0d48ca086..ab3533857e2 100644 --- a/src/Type/Generic/TemplateTypeFactory.php +++ b/src/Type/Generic/TemplateTypeFactory.php @@ -6,7 +6,11 @@ use PHPStan\Type\ArrayType; use PHPStan\Type\BenevolentUnionType; use PHPStan\Type\BooleanType; +use PHPStan\Type\CallableType; +use PHPStan\Type\ClassStringType; +use PHPStan\Type\ClosureType; use PHPStan\Type\Constant\ConstantArrayType; +use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\FloatType; @@ -51,6 +55,14 @@ public static function create(TemplateTypeScope $scope, string $name, ?Type $bou return new TemplateObjectType($scope, $strategy, $variance, $name, $bound, $default); } + if ($bound instanceof ClosureType && ($boundClass === ClosureType::class || $bound instanceof TemplateType)) { + return new TemplateClosureType($scope, $strategy, $variance, $name, $bound, $default); + } + + if ($bound instanceof CallableType && ($boundClass === CallableType::class || $bound instanceof TemplateType)) { + return new TemplateCallableType($scope, $strategy, $variance, $name, $bound, $default); + } + if ($bound instanceof ObjectWithoutClassType && ($boundClass === ObjectWithoutClassType::class || $bound instanceof TemplateType)) { return new TemplateObjectWithoutClassType($scope, $strategy, $variance, $name, $bound, $default); } @@ -67,6 +79,14 @@ public static function create(TemplateTypeScope $scope, string $name, ?Type $bou return new TemplateObjectShapeType($scope, $strategy, $variance, $name, $bound, $default); } + if ($bound instanceof GenericClassStringType && ($boundClass === GenericClassStringType::class || $bound instanceof TemplateType)) { + return new TemplateGenericClassStringType($scope, $strategy, $variance, $name, $bound, $default); + } + + if ($bound instanceof ClassStringType && ($boundClass === ClassStringType::class || $bound instanceof TemplateType)) { + return new TemplateClassStringType($scope, $strategy, $variance, $name, $bound, $default); + } + if ($bound instanceof StringType && ($boundClass === StringType::class || $bound instanceof TemplateType)) { return new TemplateStringType($scope, $strategy, $variance, $name, $bound, $default); } @@ -87,6 +107,10 @@ public static function create(TemplateTypeScope $scope, string $name, ?Type $bou return new TemplateFloatType($scope, $strategy, $variance, $name, $bound, $default); } + if ($bound instanceof ConstantFloatType && ($boundClass === ConstantFloatType::class || $bound instanceof TemplateType)) { + return new TemplateConstantFloatType($scope, $strategy, $variance, $name, $bound, $default); + } + if ($bound instanceof BooleanType && ($boundClass === BooleanType::class || $bound->isTrue()->yes() || $bound->isFalse()->yes() || $bound instanceof TemplateType)) { return new TemplateBooleanType($scope, $strategy, $variance, $name, $bound, $default); } diff --git a/tests/PHPStan/Analyser/nsrt/bug-15273.php b/tests/PHPStan/Analyser/nsrt/bug-15273.php new file mode 100644 index 00000000000..1a278ccb37e --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-15273.php @@ -0,0 +1,45 @@ + $x + 1; + + assertType('static-Closure(int): int', $functions->parameterlessClosure($f)); + assertType('static-Closure(int): int', $functions->parametrizedClosure($f)); + assertType('static-Closure(int): int', $functions->parameterlessCallable($f)); + assertType('static-Closure(int): int', $functions->parametrizedCallable($f)); +} diff --git a/tests/PHPStan/Analyser/nsrt/template-bound-types.php b/tests/PHPStan/Analyser/nsrt/template-bound-types.php new file mode 100644 index 00000000000..cf09ef5c926 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/template-bound-types.php @@ -0,0 +1,55 @@ + + * @param T $class + * @return T + */ +function genericClassStringBound(string $class): string +{ + return $class; +} + +/** + * @template T of int<0, 10> + * @param T $i + * @return T + */ +function integerRangeBound(int $i): int +{ + return $i; +} + +/** + * @template T of 1.5 + * @param T $f + * @return T + */ +function constantFloatBound(float $f): float +{ + return $f; +} + +function test(): void +{ + assertType("'Exception'", classStringBound(Exception::class)); + assertType("'InvalidArgumentException'", genericClassStringBound(InvalidArgumentException::class)); + assertType('5', integerRangeBound(5)); + assertType('1.5', constantFloatBound(1.5)); +} diff --git a/tests/PHPStan/Rules/Generics/FunctionTemplateTypeRuleTest.php b/tests/PHPStan/Rules/Generics/FunctionTemplateTypeRuleTest.php index 84b589f780c..2c3f17115ea 100644 --- a/tests/PHPStan/Rules/Generics/FunctionTemplateTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generics/FunctionTemplateTypeRuleTest.php @@ -80,6 +80,14 @@ public function testRule(): void 'PHPDoc tag @template V for function FunctionTemplateType\requiredAfterOptional() does not have a default type but follows an optional @template U.', 120, ], + [ + 'PHPDoc tag @template T for function FunctionTemplateType\voidBound() with bound type void is not supported.', + 174, + ], + [ + 'PHPDoc tag @template T for function FunctionTemplateType\neverBound() with bound type never is not supported.', + 180, + ], ]); } diff --git a/tests/PHPStan/Rules/Generics/data/function-template.php b/tests/PHPStan/Rules/Generics/data/function-template.php index c938c5dff46..18b7d31c1ad 100644 --- a/tests/PHPStan/Rules/Generics/data/function-template.php +++ b/tests/PHPStan/Rules/Generics/data/function-template.php @@ -121,3 +121,75 @@ function requiredAfterOptional() { } + +/** @template T of callable */ +function callableBound() +{ + +} + +/** @template T of callable(int): string */ +function parametrizedCallableBound() +{ + +} + +/** @template T of \Closure */ +function closureBound() +{ + +} + +/** @template T of \Closure(int): string */ +function parametrizedClosureBound() +{ + +} + +/** @template T of class-string */ +function classStringBound() +{ + +} + +/** @template T of class-string<\Exception> */ +function genericClassStringBound() +{ + +} + +/** @template T of 1.5 */ +function constantFloatBound() +{ + +} + +/** @template T of int<0, 10> */ +function integerRangeBound() +{ + +} + +/** @template T of void */ +function voidBound() +{ + +} + +/** @template T of never */ +function neverBound() +{ + +} + +/** @template T of true */ +function trueBound() +{ + +} + +/** @template T of false */ +function falseBound() +{ + +} diff --git a/tests/PHPStan/Rules/PhpDoc/IncompatiblePhpDocTypeRuleTest.php b/tests/PHPStan/Rules/PhpDoc/IncompatiblePhpDocTypeRuleTest.php index 808c165f1b2..d7fc5529d0b 100644 --- a/tests/PHPStan/Rules/PhpDoc/IncompatiblePhpDocTypeRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/IncompatiblePhpDocTypeRuleTest.php @@ -510,6 +510,11 @@ public function testArrayShapeTemplateKey(): void ]); } + public function testBug15273(): void + { + $this->analyse([__DIR__ . '/data/bug-15273.php'], []); + } + public function testExplainUnresolvable(): void { $this->analyse([__DIR__ . '/data/explain-unresolvable-method-parameter.php'], [ diff --git a/tests/PHPStan/Rules/PhpDoc/data/bug-15273.php b/tests/PHPStan/Rules/PhpDoc/data/bug-15273.php new file mode 100644 index 00000000000..6a5c249cd01 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/bug-15273.php @@ -0,0 +1,45 @@ +