From c6e8ce5ae7a65956bebeb88b6f28deafe51c535b Mon Sep 17 00:00:00 2001 From: Richard van Velzen Date: Tue, 14 Jun 2022 11:56:10 +0200 Subject: [PATCH 01/12] Implement template default types --- src/Analyser/MutatingScope.php | 5 +++ src/PhpDoc/PhpDocNodeResolver.php | 5 ++- src/PhpDoc/Tag/TemplateTag.php | 7 +++- src/Reflection/ClassReflection.php | 4 +- src/Rules/FunctionCallParametersCheck.php | 2 +- src/Type/Generic/TemplateArrayType.php | 3 ++ .../Generic/TemplateBenevolentUnionType.php | 3 ++ src/Type/Generic/TemplateBooleanType.php | 3 ++ .../Generic/TemplateConstantArrayType.php | 3 ++ .../Generic/TemplateConstantIntegerType.php | 3 ++ .../Generic/TemplateConstantStringType.php | 3 ++ src/Type/Generic/TemplateFloatType.php | 3 ++ .../Generic/TemplateGenericObjectType.php | 3 ++ src/Type/Generic/TemplateIntegerType.php | 3 ++ src/Type/Generic/TemplateIntersectionType.php | 3 ++ src/Type/Generic/TemplateKeyOfType.php | 3 ++ src/Type/Generic/TemplateMixedType.php | 3 ++ src/Type/Generic/TemplateObjectType.php | 3 ++ .../TemplateObjectWithoutClassType.php | 3 ++ src/Type/Generic/TemplateStrictMixedType.php | 2 + src/Type/Generic/TemplateStringType.php | 3 ++ src/Type/Generic/TemplateType.php | 2 + src/Type/Generic/TemplateTypeFactory.php | 42 +++++++++---------- src/Type/Generic/TemplateTypeHelper.php | 2 +- src/Type/Generic/TemplateTypeTrait.php | 21 +++++++++- src/Type/Generic/TemplateUnionType.php | 3 ++ src/Type/TypeCombinator.php | 1 + .../Analyser/NodeScopeResolverTest.php | 2 + tests/PHPStan/Analyser/data/bug-4801.php | 25 +++++++++++ .../Rules/Methods/CallMethodsRuleTest.php | 9 ++++ tests/PHPStan/Rules/Methods/data/bug-4801.php | 22 ++++++++++ 31 files changed, 170 insertions(+), 29 deletions(-) create mode 100644 tests/PHPStan/Analyser/data/bug-4801.php create mode 100644 tests/PHPStan/Rules/Methods/data/bug-4801.php diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index d8cc9faf06..4d95e7ab2f 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -5606,6 +5606,11 @@ private function exactInstantiation(New_ $node, string $className): ?Type $list[] = $templateType; continue; } + $default = $tag->getDefault(); + if ($default !== null) { + $list[] = $default; + continue; + } $bound = $tag->getBound(); if ($bound instanceof MixedType && $bound->isExplicitMixed()) { $bound = new MixedType(false); diff --git a/src/PhpDoc/PhpDocNodeResolver.php b/src/PhpDoc/PhpDocNodeResolver.php index 402f8c7dd1..090b392341 100644 --- a/src/PhpDoc/PhpDocNodeResolver.php +++ b/src/PhpDoc/PhpDocNodeResolver.php @@ -327,9 +327,12 @@ public function resolveTemplateTags(PhpDocNode $phpDocNode, NameScope $nameScope } } + $nameScopeWithoutCurrent = $nameScope->unsetTemplateType($valueNode->name); + $resolved[$valueNode->name] = new TemplateTag( $valueNode->name, - $valueNode->bound !== null ? $this->typeNodeResolver->resolve($valueNode->bound, $nameScope->unsetTemplateType($valueNode->name)) : new MixedType(true), + $valueNode->bound !== null ? $this->typeNodeResolver->resolve($valueNode->bound, $nameScopeWithoutCurrent) : new MixedType(true), + $valueNode->default !== null ? $this->typeNodeResolver->resolve($valueNode->default, $nameScopeWithoutCurrent) : null, $variance, ); $resolvedPrefix[$valueNode->name] = $prefix; diff --git a/src/PhpDoc/Tag/TemplateTag.php b/src/PhpDoc/Tag/TemplateTag.php index a14fa2c6ab..4ae755597f 100644 --- a/src/PhpDoc/Tag/TemplateTag.php +++ b/src/PhpDoc/Tag/TemplateTag.php @@ -15,7 +15,7 @@ class TemplateTag /** * @param non-empty-string $name */ - public function __construct(private string $name, private Type $bound, private TemplateTypeVariance $variance) + public function __construct(private string $name, private Type $bound, private ?Type $default, private TemplateTypeVariance $variance) { } @@ -32,6 +32,11 @@ public function getBound(): Type return $this->bound; } + public function getDefault(): ?Type + { + return $this->default; + } + public function getVariance(): TemplateTypeVariance { return $this->variance; diff --git a/src/Reflection/ClassReflection.php b/src/Reflection/ClassReflection.php index d980be7864..0254204a63 100644 --- a/src/Reflection/ClassReflection.php +++ b/src/Reflection/ClassReflection.php @@ -1442,7 +1442,7 @@ public function typeMapFromList(array $types): TemplateTypeMap $map = []; $i = 0; foreach ($resolvedPhpDoc->getTemplateTags() as $tag) { - $map[$tag->getName()] = $types[$i] ?? $tag->getBound(); + $map[$tag->getName()] = $types[$i] ?? $tag->getDefault() ?? $tag->getBound(); $i++; } @@ -1479,7 +1479,7 @@ public function typeMapToList(TemplateTypeMap $typeMap): array $list = []; foreach ($resolvedPhpDoc->getTemplateTags() as $tag) { - $list[] = $typeMap->getType($tag->getName()) ?? $tag->getBound(); + $list[] = $typeMap->getType($tag->getName()) ?? $tag->getDefault() ?? $tag->getBound(); } return $list; diff --git a/src/Rules/FunctionCallParametersCheck.php b/src/Rules/FunctionCallParametersCheck.php index 6a1d2f16b1..8cdded6e9e 100644 --- a/src/Rules/FunctionCallParametersCheck.php +++ b/src/Rules/FunctionCallParametersCheck.php @@ -432,7 +432,7 @@ static function (Type $type, callable $traverse) use (&$returnTemplateTypes): Ty $type = $type->resolve(); } - if ($type instanceof TemplateType) { + if ($type instanceof TemplateType && $type->getDefault() === null) { $returnTemplateTypes[$type->getName()] = true; return $type; } diff --git a/src/Type/Generic/TemplateArrayType.php b/src/Type/Generic/TemplateArrayType.php index bb0192d02b..e6658632cd 100644 --- a/src/Type/Generic/TemplateArrayType.php +++ b/src/Type/Generic/TemplateArrayType.php @@ -4,6 +4,7 @@ use PHPStan\Type\ArrayType; use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; +use PHPStan\Type\Type; /** @api */ final class TemplateArrayType extends ArrayType implements TemplateType @@ -22,6 +23,7 @@ public function __construct( TemplateTypeVariance $templateTypeVariance, string $name, ArrayType $bound, + ?Type $default, ) { parent::__construct($bound->getKeyType(), $bound->getItemType()); @@ -30,6 +32,7 @@ public function __construct( $this->variance = $templateTypeVariance; $this->name = $name; $this->bound = $bound; + $this->default = $default; } protected function shouldGeneralizeInferredType(): bool diff --git a/src/Type/Generic/TemplateBenevolentUnionType.php b/src/Type/Generic/TemplateBenevolentUnionType.php index 3107542f29..cc630fd0dd 100644 --- a/src/Type/Generic/TemplateBenevolentUnionType.php +++ b/src/Type/Generic/TemplateBenevolentUnionType.php @@ -21,6 +21,7 @@ public function __construct( TemplateTypeVariance $templateTypeVariance, string $name, BenevolentUnionType $bound, + ?Type $default, ) { parent::__construct($bound->getTypes()); @@ -30,6 +31,7 @@ public function __construct( $this->variance = $templateTypeVariance; $this->name = $name; $this->bound = $bound; + $this->default = $default; } /** @param Type[] $types */ @@ -41,6 +43,7 @@ public function withTypes(array $types): self $this->variance, $this->name, new BenevolentUnionType($types), + $this->default, ); } diff --git a/src/Type/Generic/TemplateBooleanType.php b/src/Type/Generic/TemplateBooleanType.php index 66ce5db4ec..27fc50f21b 100644 --- a/src/Type/Generic/TemplateBooleanType.php +++ b/src/Type/Generic/TemplateBooleanType.php @@ -4,6 +4,7 @@ use PHPStan\Type\BooleanType; use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; +use PHPStan\Type\Type; /** @api */ final class TemplateBooleanType extends BooleanType implements TemplateType @@ -22,6 +23,7 @@ public function __construct( TemplateTypeVariance $templateTypeVariance, string $name, BooleanType $bound, + ?Type $default, ) { parent::__construct(); @@ -30,6 +32,7 @@ public function __construct( $this->variance = $templateTypeVariance; $this->name = $name; $this->bound = $bound; + $this->default = $default; } protected function shouldGeneralizeInferredType(): bool diff --git a/src/Type/Generic/TemplateConstantArrayType.php b/src/Type/Generic/TemplateConstantArrayType.php index b291dab577..53ea994935 100644 --- a/src/Type/Generic/TemplateConstantArrayType.php +++ b/src/Type/Generic/TemplateConstantArrayType.php @@ -4,6 +4,7 @@ use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; +use PHPStan\Type\Type; /** @api */ final class TemplateConstantArrayType extends ConstantArrayType implements TemplateType @@ -22,6 +23,7 @@ public function __construct( TemplateTypeVariance $templateTypeVariance, string $name, ConstantArrayType $bound, + ?Type $default, ) { parent::__construct($bound->getKeyTypes(), $bound->getValueTypes(), $bound->getNextAutoIndexes(), $bound->getOptionalKeys(), $bound->isList()); @@ -30,6 +32,7 @@ public function __construct( $this->variance = $templateTypeVariance; $this->name = $name; $this->bound = $bound; + $this->default = $default; } protected function shouldGeneralizeInferredType(): bool diff --git a/src/Type/Generic/TemplateConstantIntegerType.php b/src/Type/Generic/TemplateConstantIntegerType.php index e411af4edc..a4bc35b848 100644 --- a/src/Type/Generic/TemplateConstantIntegerType.php +++ b/src/Type/Generic/TemplateConstantIntegerType.php @@ -4,6 +4,7 @@ use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; +use PHPStan\Type\Type; /** @api */ final class TemplateConstantIntegerType extends ConstantIntegerType implements TemplateType @@ -22,6 +23,7 @@ public function __construct( TemplateTypeVariance $templateTypeVariance, string $name, ConstantIntegerType $bound, + ?Type $default, ) { parent::__construct($bound->getValue()); @@ -30,6 +32,7 @@ public function __construct( $this->variance = $templateTypeVariance; $this->name = $name; $this->bound = $bound; + $this->default = $default; } protected function shouldGeneralizeInferredType(): bool diff --git a/src/Type/Generic/TemplateConstantStringType.php b/src/Type/Generic/TemplateConstantStringType.php index bcb3b0cf94..f4d3b8dbbb 100644 --- a/src/Type/Generic/TemplateConstantStringType.php +++ b/src/Type/Generic/TemplateConstantStringType.php @@ -4,6 +4,7 @@ use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; +use PHPStan\Type\Type; /** @api */ final class TemplateConstantStringType extends ConstantStringType implements TemplateType @@ -22,6 +23,7 @@ public function __construct( TemplateTypeVariance $templateTypeVariance, string $name, ConstantStringType $bound, + ?Type $default, ) { parent::__construct($bound->getValue()); @@ -30,6 +32,7 @@ public function __construct( $this->variance = $templateTypeVariance; $this->name = $name; $this->bound = $bound; + $this->default = $default; } protected function shouldGeneralizeInferredType(): bool diff --git a/src/Type/Generic/TemplateFloatType.php b/src/Type/Generic/TemplateFloatType.php index 32332bb3ef..b3df6ccd2e 100644 --- a/src/Type/Generic/TemplateFloatType.php +++ b/src/Type/Generic/TemplateFloatType.php @@ -4,6 +4,7 @@ use PHPStan\Type\FloatType; use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; +use PHPStan\Type\Type; /** @api */ final class TemplateFloatType extends FloatType implements TemplateType @@ -22,6 +23,7 @@ public function __construct( TemplateTypeVariance $templateTypeVariance, string $name, FloatType $bound, + ?Type $default, ) { parent::__construct(); @@ -30,6 +32,7 @@ public function __construct( $this->variance = $templateTypeVariance; $this->name = $name; $this->bound = $bound; + $this->default = $default; } protected function shouldGeneralizeInferredType(): bool diff --git a/src/Type/Generic/TemplateGenericObjectType.php b/src/Type/Generic/TemplateGenericObjectType.php index 3810841ec9..0c58b3b41e 100644 --- a/src/Type/Generic/TemplateGenericObjectType.php +++ b/src/Type/Generic/TemplateGenericObjectType.php @@ -22,6 +22,7 @@ public function __construct( TemplateTypeVariance $templateTypeVariance, string $name, GenericObjectType $bound, + ?Type $default, ) { parent::__construct($bound->getClassName(), $bound->getTypes(), null, null, $bound->getVariances()); @@ -31,6 +32,7 @@ public function __construct( $this->variance = $templateTypeVariance; $this->name = $name; $this->bound = $bound; + $this->default = $default; } protected function recreate(string $className, array $types, ?Type $subtractedType, array $variances = []): GenericObjectType @@ -41,6 +43,7 @@ protected function recreate(string $className, array $types, ?Type $subtractedTy $this->variance, $this->name, $this->getBound(), + $this->default, ); } diff --git a/src/Type/Generic/TemplateIntegerType.php b/src/Type/Generic/TemplateIntegerType.php index 64c631980d..b4057fa327 100644 --- a/src/Type/Generic/TemplateIntegerType.php +++ b/src/Type/Generic/TemplateIntegerType.php @@ -4,6 +4,7 @@ use PHPStan\Type\IntegerType; use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; +use PHPStan\Type\Type; /** @api */ final class TemplateIntegerType extends IntegerType implements TemplateType @@ -22,6 +23,7 @@ public function __construct( TemplateTypeVariance $templateTypeVariance, string $name, IntegerType $bound, + ?Type $default, ) { parent::__construct(); @@ -30,6 +32,7 @@ public function __construct( $this->variance = $templateTypeVariance; $this->name = $name; $this->bound = $bound; + $this->default = $default; } protected function shouldGeneralizeInferredType(): bool diff --git a/src/Type/Generic/TemplateIntersectionType.php b/src/Type/Generic/TemplateIntersectionType.php index 87f1ca18a7..7576541dbc 100644 --- a/src/Type/Generic/TemplateIntersectionType.php +++ b/src/Type/Generic/TemplateIntersectionType.php @@ -3,6 +3,7 @@ namespace PHPStan\Type\Generic; use PHPStan\Type\IntersectionType; +use PHPStan\Type\Type; /** @api */ final class TemplateIntersectionType extends IntersectionType implements TemplateType @@ -20,6 +21,7 @@ public function __construct( TemplateTypeVariance $templateTypeVariance, string $name, IntersectionType $bound, + ?Type $default, ) { parent::__construct($bound->getTypes()); @@ -29,6 +31,7 @@ public function __construct( $this->variance = $templateTypeVariance; $this->name = $name; $this->bound = $bound; + $this->default = $default; } } diff --git a/src/Type/Generic/TemplateKeyOfType.php b/src/Type/Generic/TemplateKeyOfType.php index 7312ea2ef4..d8522eb503 100644 --- a/src/Type/Generic/TemplateKeyOfType.php +++ b/src/Type/Generic/TemplateKeyOfType.php @@ -23,6 +23,7 @@ public function __construct( TemplateTypeVariance $templateTypeVariance, string $name, KeyOfType $bound, + ?Type $default, ) { parent::__construct($bound->getType()); @@ -31,6 +32,7 @@ public function __construct( $this->variance = $templateTypeVariance; $this->name = $name; $this->bound = $bound; + $this->default = $default; } protected function getResult(): Type @@ -43,6 +45,7 @@ protected function getResult(): Type $result, $this->getVariance(), $this->getStrategy(), + $this->getDefault(), ); } diff --git a/src/Type/Generic/TemplateMixedType.php b/src/Type/Generic/TemplateMixedType.php index 132cb20d6b..c06b082d52 100644 --- a/src/Type/Generic/TemplateMixedType.php +++ b/src/Type/Generic/TemplateMixedType.php @@ -24,6 +24,7 @@ public function __construct( TemplateTypeVariance $templateTypeVariance, string $name, MixedType $bound, + ?Type $default, ) { parent::__construct(true); @@ -33,6 +34,7 @@ public function __construct( $this->variance = $templateTypeVariance; $this->name = $name; $this->bound = $bound; + $this->default = $default; } public function isSuperTypeOfMixed(MixedType $type): TrinaryLogic @@ -62,6 +64,7 @@ public function toStrictMixedType(): TemplateStrictMixedType $this->variance, $this->name, new StrictMixedType(), + $this->default, ); } diff --git a/src/Type/Generic/TemplateObjectType.php b/src/Type/Generic/TemplateObjectType.php index a67aa723dd..220414ca14 100644 --- a/src/Type/Generic/TemplateObjectType.php +++ b/src/Type/Generic/TemplateObjectType.php @@ -4,6 +4,7 @@ use PHPStan\Type\ObjectType; use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; +use PHPStan\Type\Type; /** @api */ final class TemplateObjectType extends ObjectType implements TemplateType @@ -22,6 +23,7 @@ public function __construct( TemplateTypeVariance $templateTypeVariance, string $name, ObjectType $bound, + ?Type $default, ) { parent::__construct($bound->getClassName()); @@ -31,6 +33,7 @@ public function __construct( $this->variance = $templateTypeVariance; $this->name = $name; $this->bound = $bound; + $this->default = $default; } } diff --git a/src/Type/Generic/TemplateObjectWithoutClassType.php b/src/Type/Generic/TemplateObjectWithoutClassType.php index 3d3cb9e8ca..7d6aebc6f9 100644 --- a/src/Type/Generic/TemplateObjectWithoutClassType.php +++ b/src/Type/Generic/TemplateObjectWithoutClassType.php @@ -4,6 +4,7 @@ use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; +use PHPStan\Type\Type; /** @api */ class TemplateObjectWithoutClassType extends ObjectWithoutClassType implements TemplateType @@ -22,6 +23,7 @@ public function __construct( TemplateTypeVariance $templateTypeVariance, string $name, ObjectWithoutClassType $bound, + ?Type $default, ) { parent::__construct(); @@ -31,6 +33,7 @@ public function __construct( $this->variance = $templateTypeVariance; $this->name = $name; $this->bound = $bound; + $this->default = $default; } } diff --git a/src/Type/Generic/TemplateStrictMixedType.php b/src/Type/Generic/TemplateStrictMixedType.php index 071475e215..fa204aade2 100644 --- a/src/Type/Generic/TemplateStrictMixedType.php +++ b/src/Type/Generic/TemplateStrictMixedType.php @@ -24,6 +24,7 @@ public function __construct( TemplateTypeVariance $templateTypeVariance, string $name, StrictMixedType $bound, + ?Type $default, ) { $this->scope = $scope; @@ -31,6 +32,7 @@ public function __construct( $this->variance = $templateTypeVariance; $this->name = $name; $this->bound = $bound; + $this->default = $default; } public function isSuperTypeOfMixed(MixedType $type): TrinaryLogic diff --git a/src/Type/Generic/TemplateStringType.php b/src/Type/Generic/TemplateStringType.php index 084612c641..1ae72a3384 100644 --- a/src/Type/Generic/TemplateStringType.php +++ b/src/Type/Generic/TemplateStringType.php @@ -4,6 +4,7 @@ use PHPStan\Type\StringType; use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; +use PHPStan\Type\Type; /** @api */ final class TemplateStringType extends StringType implements TemplateType @@ -22,6 +23,7 @@ public function __construct( TemplateTypeVariance $templateTypeVariance, string $name, StringType $bound, + ?Type $default, ) { parent::__construct(); @@ -30,6 +32,7 @@ public function __construct( $this->variance = $templateTypeVariance; $this->name = $name; $this->bound = $bound; + $this->default = $default; } protected function shouldGeneralizeInferredType(): bool diff --git a/src/Type/Generic/TemplateType.php b/src/Type/Generic/TemplateType.php index 7661078ca1..8374c6c0ac 100644 --- a/src/Type/Generic/TemplateType.php +++ b/src/Type/Generic/TemplateType.php @@ -18,6 +18,8 @@ public function getScope(): TemplateTypeScope; public function getBound(): Type; + public function getDefault(): ?Type; + public function toArgument(): TemplateType; public function isArgument(): bool; diff --git a/src/Type/Generic/TemplateTypeFactory.php b/src/Type/Generic/TemplateTypeFactory.php index c29a175d2c..8f56cc6cbb 100644 --- a/src/Type/Generic/TemplateTypeFactory.php +++ b/src/Type/Generic/TemplateTypeFactory.php @@ -28,91 +28,91 @@ final class TemplateTypeFactory /** * @param non-empty-string $name */ - public static function create(TemplateTypeScope $scope, string $name, ?Type $bound, TemplateTypeVariance $variance, ?TemplateTypeStrategy $strategy = null): TemplateType + public static function create(TemplateTypeScope $scope, string $name, ?Type $bound, TemplateTypeVariance $variance, ?TemplateTypeStrategy $strategy = null, ?Type $default = null): TemplateType { $strategy ??= new TemplateTypeParameterStrategy(); if ($bound === null) { - return new TemplateMixedType($scope, $strategy, $variance, $name, new MixedType(true)); + return new TemplateMixedType($scope, $strategy, $variance, $name, new MixedType(true), $default); } $boundClass = get_class($bound); if ($bound instanceof ObjectType && ($boundClass === ObjectType::class || $bound instanceof TemplateType)) { - return new TemplateObjectType($scope, $strategy, $variance, $name, $bound); + return new TemplateObjectType($scope, $strategy, $variance, $name, $bound, $default); } if ($bound instanceof GenericObjectType && ($boundClass === GenericObjectType::class || $bound instanceof TemplateType)) { - return new TemplateGenericObjectType($scope, $strategy, $variance, $name, $bound); + return new TemplateGenericObjectType($scope, $strategy, $variance, $name, $bound, $default); } if ($bound instanceof ObjectWithoutClassType && ($boundClass === ObjectWithoutClassType::class || $bound instanceof TemplateType)) { - return new TemplateObjectWithoutClassType($scope, $strategy, $variance, $name, $bound); + return new TemplateObjectWithoutClassType($scope, $strategy, $variance, $name, $bound, $default); } if ($bound instanceof ArrayType && ($boundClass === ArrayType::class || $bound instanceof TemplateType)) { - return new TemplateArrayType($scope, $strategy, $variance, $name, $bound); + return new TemplateArrayType($scope, $strategy, $variance, $name, $bound, $default); } if ($bound instanceof ConstantArrayType && ($boundClass === ConstantArrayType::class || $bound instanceof TemplateType)) { - return new TemplateConstantArrayType($scope, $strategy, $variance, $name, $bound); + return new TemplateConstantArrayType($scope, $strategy, $variance, $name, $bound, $default); } if ($bound instanceof ObjectShapeType && ($boundClass === ObjectShapeType::class || $bound instanceof TemplateType)) { - return new TemplateObjectShapeType($scope, $strategy, $variance, $name, $bound); + return new TemplateObjectShapeType($scope, $strategy, $variance, $name, $bound, $default); } if ($bound instanceof StringType && ($boundClass === StringType::class || $bound instanceof TemplateType)) { - return new TemplateStringType($scope, $strategy, $variance, $name, $bound); + return new TemplateStringType($scope, $strategy, $variance, $name, $bound, $default); } if ($bound instanceof ConstantStringType && ($boundClass === ConstantStringType::class || $bound instanceof TemplateType)) { - return new TemplateConstantStringType($scope, $strategy, $variance, $name, $bound); + return new TemplateConstantStringType($scope, $strategy, $variance, $name, $bound, $default); } if ($bound instanceof IntegerType && ($boundClass === IntegerType::class || $bound instanceof TemplateType)) { - return new TemplateIntegerType($scope, $strategy, $variance, $name, $bound); + return new TemplateIntegerType($scope, $strategy, $variance, $name, $bound, $default); } if ($bound instanceof ConstantIntegerType && ($boundClass === ConstantIntegerType::class || $bound instanceof TemplateType)) { - return new TemplateConstantIntegerType($scope, $strategy, $variance, $name, $bound); + return new TemplateConstantIntegerType($scope, $strategy, $variance, $name, $bound, $default); } if ($bound instanceof FloatType && ($boundClass === FloatType::class || $bound instanceof TemplateType)) { - return new TemplateFloatType($scope, $strategy, $variance, $name, $bound); + return new TemplateFloatType($scope, $strategy, $variance, $name, $bound, $default); } if ($bound instanceof BooleanType && ($boundClass === BooleanType::class || $bound instanceof TemplateType)) { - return new TemplateBooleanType($scope, $strategy, $variance, $name, $bound); + return new TemplateBooleanType($scope, $strategy, $variance, $name, $bound, $default); } if ($bound instanceof MixedType && ($boundClass === MixedType::class || $bound instanceof TemplateType)) { - return new TemplateMixedType($scope, $strategy, $variance, $name, $bound); + return new TemplateMixedType($scope, $strategy, $variance, $name, $bound, $default); } if ($bound instanceof UnionType) { if ($boundClass === UnionType::class || $bound instanceof TemplateUnionType) { - return new TemplateUnionType($scope, $strategy, $variance, $name, $bound); + return new TemplateUnionType($scope, $strategy, $variance, $name, $bound, $default); } if ($bound instanceof BenevolentUnionType) { - return new TemplateBenevolentUnionType($scope, $strategy, $variance, $name, $bound); + return new TemplateBenevolentUnionType($scope, $strategy, $variance, $name, $bound, $default); } } if ($bound instanceof IntersectionType) { - return new TemplateIntersectionType($scope, $strategy, $variance, $name, $bound); + return new TemplateIntersectionType($scope, $strategy, $variance, $name, $bound, $default); } if ($bound instanceof KeyOfType && ($boundClass === KeyOfType::class || $bound instanceof TemplateType)) { - return new TemplateKeyOfType($scope, $strategy, $variance, $name, $bound); + return new TemplateKeyOfType($scope, $strategy, $variance, $name, $bound, $default); } - return new TemplateMixedType($scope, $strategy, $variance, $name, new MixedType(true)); + return new TemplateMixedType($scope, $strategy, $variance, $name, new MixedType(true), $default); } public static function fromTemplateTag(TemplateTypeScope $scope, TemplateTag $tag): TemplateType { - return self::create($scope, $tag->getName(), $tag->getBound(), $tag->getVariance()); + return self::create($scope, $tag->getName(), $tag->getBound(), $tag->getVariance(), null, $tag->getDefault()); } } diff --git a/src/Type/Generic/TemplateTypeHelper.php b/src/Type/Generic/TemplateTypeHelper.php index a38d656556..58460efaee 100644 --- a/src/Type/Generic/TemplateTypeHelper.php +++ b/src/Type/Generic/TemplateTypeHelper.php @@ -45,7 +45,7 @@ public static function resolveTemplateTypes( } if ($newType instanceof ErrorType && !$keepErrorTypes) { - return $traverse($type->getBound()); + return $traverse($type->getDefault() ?? $type->getBound()); } $callSiteVariance = $callSiteVariances->getVariance($type->getName()); diff --git a/src/Type/Generic/TemplateTypeTrait.php b/src/Type/Generic/TemplateTypeTrait.php index b6cca8b290..133420ccca 100644 --- a/src/Type/Generic/TemplateTypeTrait.php +++ b/src/Type/Generic/TemplateTypeTrait.php @@ -38,6 +38,8 @@ trait TemplateTypeTrait /** @var TBound */ private Type $bound; + private ?Type $default; + /** @return non-empty-string */ public function getName(): string { @@ -55,6 +57,11 @@ public function getBound(): Type return $this->bound; } + public function getDefault(): ?Type + { + return $this->default; + } + public function describe(VerbosityLevel $level): string { $basicDescription = function () use ($level): string { @@ -64,10 +71,12 @@ public function describe(VerbosityLevel $level): string } else { $boundDescription = sprintf(' of %s', $this->bound->describe($level)); } + $defaultDescription = $this->default !== null ? sprintf(' = %s', $this->default->describe($level)) : ''; return sprintf( - '%s%s', + '%s%s%s', $this->name, $boundDescription, + $defaultDescription, ); }; @@ -91,6 +100,7 @@ public function toArgument(): TemplateType $this->variance, $this->name, TemplateTypeHelper::toArgument($this->getBound()), + $this->default !== null ? TemplateTypeHelper::toArgument($this->default) : null, ); } @@ -113,6 +123,7 @@ public function subtract(Type $typeToRemove): Type $removedBound, $this->getVariance(), $this->getStrategy(), + $this->getDefault(), ); } @@ -129,6 +140,7 @@ public function getTypeWithoutSubtractedType(): Type $bound->getTypeWithoutSubtractedType(), $this->getVariance(), $this->getStrategy(), + $this->getDefault(), ); } @@ -145,6 +157,7 @@ public function changeSubtractedType(?Type $subtractedType): Type $bound->changeSubtractedType($subtractedType), $this->getVariance(), $this->getStrategy(), + $this->getDefault(), ); } @@ -317,7 +330,9 @@ protected function shouldGeneralizeInferredType(): bool public function traverse(callable $cb): Type { $bound = $cb($this->getBound()); - if ($this->getBound() === $bound) { + $default = $this->getDefault() !== null ? $cb($this->getDefault()) : null; + + if ($this->getBound() === $bound && $this->getDefault() === $default) { return $this; } @@ -347,6 +362,7 @@ public function traverseSimultaneously(Type $right, callable $cb): Type $bound, $this->getVariance(), $this->getStrategy(), + $default, ); } @@ -382,6 +398,7 @@ public static function __set_state(array $properties): Type $properties['variance'], $properties['name'], $properties['bound'], + $properties['default'] ?? null, ); } diff --git a/src/Type/Generic/TemplateUnionType.php b/src/Type/Generic/TemplateUnionType.php index 997fb21238..cc196a07f4 100644 --- a/src/Type/Generic/TemplateUnionType.php +++ b/src/Type/Generic/TemplateUnionType.php @@ -2,6 +2,7 @@ namespace PHPStan\Type\Generic; +use PHPStan\Type\Type; use PHPStan\Type\UnionType; /** @api */ @@ -20,6 +21,7 @@ public function __construct( TemplateTypeVariance $templateTypeVariance, string $name, UnionType $bound, + ?Type $default, ) { parent::__construct($bound->getTypes()); @@ -29,6 +31,7 @@ public function __construct( $this->variance = $templateTypeVariance; $this->name = $name; $this->bound = $bound; + $this->default = $default; } } diff --git a/src/Type/TypeCombinator.php b/src/Type/TypeCombinator.php index 1db0ce23a2..252dfb1f8d 100644 --- a/src/Type/TypeCombinator.php +++ b/src/Type/TypeCombinator.php @@ -1015,6 +1015,7 @@ public static function intersect(Type ...$types): Type $union, $type->getVariance(), $type->getStrategy(), + $type->getDefault(), ); } diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index e0998e5252..e9a3d36c51 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -206,6 +206,8 @@ private static function findTestFiles(): iterable yield __DIR__ . '/../Rules/Classes/data/bug-11591-method-tag.php'; yield __DIR__ . '/../Rules/Classes/data/bug-11591-property-tag.php'; yield __DIR__ . '/../Rules/Classes/data/mixin-trait-use.php'; + + yield __DIR__ . '/data/bug-4801.php'; } /** diff --git a/tests/PHPStan/Analyser/data/bug-4801.php b/tests/PHPStan/Analyser/data/bug-4801.php new file mode 100644 index 0000000000..ea8eb66604 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-4801.php @@ -0,0 +1,25 @@ + + */ + public function work(callable|null $a): I; +} + +/** + * @param I $i + */ +function x(I $i) { + assertType('Bug4801\\I', $i->work(null)); + assertType('Bug4801\\I', $i->work(fn(string $a) => (int) $a)); +} diff --git a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php index 93b5b9303e..7a46d849d3 100644 --- a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php @@ -3386,4 +3386,13 @@ public function testBug10159(): void $this->analyse([__DIR__ . '/data/bug-10159.php'], []); } + public function testBug4801(): void + { + $this->checkThisOnly = false; + $this->checkNullables = false; + $this->checkUnionTypes = false; + $this->checkExplicitMixed = false; + $this->analyse([__DIR__ . '/data/bug-4801.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Methods/data/bug-4801.php b/tests/PHPStan/Rules/Methods/data/bug-4801.php new file mode 100644 index 0000000000..fce55a8db5 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-4801.php @@ -0,0 +1,22 @@ + + */ + public function work(callable|null $a): I; +} + +/** + * @param I $i + */ +function x(I $i) { + $i->work(null); +} From cba95b2045817eefd8693a76bd1143cc390c277d Mon Sep 17 00:00:00 2001 From: Richard van Velzen Date: Fri, 14 Oct 2022 10:04:43 +0200 Subject: [PATCH 02/12] Cover more situations and add tests --- src/PhpDoc/TypeNodeResolver.php | 10 +++ src/Type/Generic/TemplateTypeMap.php | 6 +- .../Analyser/NodeScopeResolverTest.php | 1 + .../Analyser/data/template-default.php | 82 +++++++++++++++++++ 4 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 tests/PHPStan/Analyser/data/template-default.php diff --git a/src/PhpDoc/TypeNodeResolver.php b/src/PhpDoc/TypeNodeResolver.php index 06098d7938..c1f6dba34f 100644 --- a/src/PhpDoc/TypeNodeResolver.php +++ b/src/PhpDoc/TypeNodeResolver.php @@ -106,6 +106,7 @@ use Traversable; use function array_key_exists; use function array_map; +use function array_values; use function count; use function explode; use function get_class; @@ -792,6 +793,15 @@ static function (string $variance): TemplateTypeVariance { $classReflection = $this->getReflectionProvider()->getClass($mainTypeClassName); if ($classReflection->isGeneric()) { + $templateTypes = array_values($classReflection->getTemplateTypeMap()->getTypes()); + for ($i = count($genericTypes), $templateTypesCount = count($templateTypes); $i < $templateTypesCount; $i++) { + $templateType = $templateTypes[$i]; + if (!$templateType instanceof TemplateType || $templateType->getDefault() === null) { + continue; + } + $genericTypes[] = $templateType->getDefault(); + } + if (in_array($mainTypeClassName, [ Traversable::class, IteratorAggregate::class, diff --git a/src/Type/Generic/TemplateTypeMap.php b/src/Type/Generic/TemplateTypeMap.php index 00fbc34a1d..30cd0e52c0 100644 --- a/src/Type/Generic/TemplateTypeMap.php +++ b/src/Type/Generic/TemplateTypeMap.php @@ -5,6 +5,7 @@ use PHPStan\Type\NeverType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use PHPStan\Type\TypeTraverser; use PHPStan\Type\TypeUtils; use function array_key_exists; use function count; @@ -211,7 +212,10 @@ public function resolveToBounds(): self if ($this->resolvedToBounds !== null) { return $this->resolvedToBounds; } - return $this->resolvedToBounds = $this->map(static fn (string $name, Type $type): Type => TemplateTypeHelper::resolveToBounds($type)); + return $this->resolvedToBounds = $this->map(static fn (string $name, Type $type): Type => TypeTraverser::map( + $type, + static fn (Type $type, callable $traverse): Type => $type instanceof TemplateType ? $traverse($type->getDefault() ?? $type->getBound()) : $traverse($type), + )); } /** diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index e9a3d36c51..d671eb2ccb 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -208,6 +208,7 @@ private static function findTestFiles(): iterable yield __DIR__ . '/../Rules/Classes/data/mixin-trait-use.php'; yield __DIR__ . '/data/bug-4801.php'; + yield __DIR__ . '/data/template-default.php'; } /** diff --git a/tests/PHPStan/Analyser/data/template-default.php b/tests/PHPStan/Analyser/data/template-default.php new file mode 100644 index 0000000000..235a72d2a9 --- /dev/null +++ b/tests/PHPStan/Analyser/data/template-default.php @@ -0,0 +1,82 @@ + $one + * @param Test $two + * @param Test $three + */ +function foo(Test $one, Test $two, Test $three) +{ + assertType('TemplateDefault\\Test', $one); + assertType('TemplateDefault\\Test', $two); + assertType('TemplateDefault\\Test', $three); +} + + +/** + * @template S = false + * @template T = false + */ +class Builder +{ + /** + * @phpstan-self-out self + */ + public function one(): void + { + } + + /** + * @phpstan-self-out self + */ + public function two(): void + { + } + + /** + * @return ($this is self ? void : never) + */ + public function execute(): void + { + } +} + +function () { + $qb = new Builder(); + assertType('TemplateDefault\\Builder', $qb); + $qb->one(); + assertType('TemplateDefault\\Builder', $qb); + $qb->two(); + assertType('TemplateDefault\\Builder', $qb); + assertType('void', $qb->execute()); +}; + +function () { + $qb = new Builder(); + assertType('TemplateDefault\\Builder', $qb); + $qb->two(); + assertType('TemplateDefault\\Builder', $qb); + $qb->one(); + assertType('TemplateDefault\\Builder', $qb); + assertType('void', $qb->execute()); +}; + +function () { + $qb = new Builder(); + assertType('TemplateDefault\\Builder', $qb); + $qb->one(); + assertType('TemplateDefault\\Builder', $qb); + assertType('*NEVER*', $qb->execute()); +}; From e0396cf4d3177642c1a52a400ede17dad21de1bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Pudil?= Date: Fri, 20 Sep 2024 14:22:53 +0200 Subject: [PATCH 03/12] bring implementation up to date --- src/Analyser/MutatingScope.php | 1 + src/Dependency/DependencyResolver.php | 11 +++++++++++ src/PhpDoc/PhpDocNodeResolver.php | 3 +++ src/PhpDoc/TypeNodeResolver.php | 3 +++ src/Type/Generic/TemplateObjectShapeType.php | 3 +++ src/Type/Generic/TemplateTypeTrait.php | 12 ++++++++++-- src/Type/TypeCombinator.php | 1 + src/Type/TypeUtils.php | 1 + tests/PHPStan/Analyser/data/template-default.php | 6 +++--- 9 files changed, 36 insertions(+), 5 deletions(-) diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 4d95e7ab2f..5d6636683f 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -2539,6 +2539,7 @@ private function createFirstClassCallable( $templateTags[$templateType->getName()] = new TemplateTag( $templateType->getName(), $templateType->getBound(), + $templateType->getDefault(), $templateType->getVariance(), ); } diff --git a/src/Dependency/DependencyResolver.php b/src/Dependency/DependencyResolver.php index f9bfcf314b..231db1ba7d 100644 --- a/src/Dependency/DependencyResolver.php +++ b/src/Dependency/DependencyResolver.php @@ -531,6 +531,17 @@ private function addClassToDependencies(string $className, array &$dependenciesR } $dependenciesReflections[] = $this->reflectionProvider->getClass($referencedClass); } + + $default = $templateTag->getDefault(); + if ($default === null) { + continue; + } + foreach ($default->getReferencedClasses() as $referencedClass) { + if (!$this->reflectionProvider->hasClass($referencedClass)) { + continue; + } + $dependenciesReflections[] = $this->reflectionProvider->getClass($referencedClass); + } } foreach ($classReflection->getPropertyTags() as $propertyTag) { diff --git a/src/PhpDoc/PhpDocNodeResolver.php b/src/PhpDoc/PhpDocNodeResolver.php index 090b392341..02fed04bcc 100644 --- a/src/PhpDoc/PhpDocNodeResolver.php +++ b/src/PhpDoc/PhpDocNodeResolver.php @@ -176,6 +176,9 @@ public function resolveMethodTags(PhpDocNode $phpDocNode, NameScope $nameScope): $templateType->bound !== null ? $this->typeNodeResolver->resolve($templateType->bound, $nameScope) : new MixedType(), + $templateType->default !== null + ? $this->typeNodeResolver->resolve($templateType->default, $nameScope) + : null, TemplateTypeVariance::createInvariant(), ); } diff --git a/src/PhpDoc/TypeNodeResolver.php b/src/PhpDoc/TypeNodeResolver.php index c1f6dba34f..6e483222d7 100644 --- a/src/PhpDoc/TypeNodeResolver.php +++ b/src/PhpDoc/TypeNodeResolver.php @@ -920,6 +920,9 @@ private function resolveCallableTypeNode(CallableTypeNode $typeNode, NameScope $ $templateType->bound !== null ? $this->resolve($templateType->bound, $nameScope) : new MixedType(), + $templateType->default !== null + ? $this->resolve($templateType->default, $nameScope) + : null, TemplateTypeVariance::createInvariant(), ); } diff --git a/src/Type/Generic/TemplateObjectShapeType.php b/src/Type/Generic/TemplateObjectShapeType.php index 5b1f187c6d..270af37931 100644 --- a/src/Type/Generic/TemplateObjectShapeType.php +++ b/src/Type/Generic/TemplateObjectShapeType.php @@ -4,6 +4,7 @@ use PHPStan\Type\ObjectShapeType; use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; +use PHPStan\Type\Type; /** @api */ final class TemplateObjectShapeType extends ObjectShapeType implements TemplateType @@ -22,6 +23,7 @@ public function __construct( TemplateTypeVariance $templateTypeVariance, string $name, ObjectShapeType $bound, + ?Type $default, ) { parent::__construct($bound->getProperties(), $bound->getOptionalProperties()); @@ -30,6 +32,7 @@ public function __construct( $this->variance = $templateTypeVariance; $this->name = $name; $this->bound = $bound; + $this->default = $default; } protected function shouldGeneralizeInferredType(): bool diff --git a/src/Type/Generic/TemplateTypeTrait.php b/src/Type/Generic/TemplateTypeTrait.php index 133420ccca..9a0fa73a59 100644 --- a/src/Type/Generic/TemplateTypeTrait.php +++ b/src/Type/Generic/TemplateTypeTrait.php @@ -176,7 +176,11 @@ public function equals(Type $type): bool return $type instanceof self && $type->scope->equals($this->scope) && $type->name === $this->name - && $this->bound->equals($type->bound); + && $this->bound->equals($type->bound) + && ( + ($this->default === null && $type->default === null) + || ($this->default !== null && $type->default !== null && $this->default->equals($type->default)) + ); } public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic @@ -342,6 +346,7 @@ public function traverse(callable $cb): Type $bound, $this->getVariance(), $this->getStrategy(), + $default, ); } @@ -352,7 +357,9 @@ public function traverseSimultaneously(Type $right, callable $cb): Type } $bound = $cb($this->getBound(), $right->getBound()); - if ($this->getBound() === $bound) { + $default = $this->getDefault() !== null && $right->getDefault() !== null ? $cb($this->getDefault(), $right->getDefault()) : null; + + if ($this->getBound() === $bound && $this->getDefault() === $default) { return $this; } @@ -379,6 +386,7 @@ public function tryRemove(Type $typeToRemove): ?Type $bound, $this->getVariance(), $this->getStrategy(), + $this->getDefault(), ); } diff --git a/src/Type/TypeCombinator.php b/src/Type/TypeCombinator.php index 252dfb1f8d..d27fd6fbdb 100644 --- a/src/Type/TypeCombinator.php +++ b/src/Type/TypeCombinator.php @@ -750,6 +750,7 @@ private static function processArrayTypes(array $arrayTypes): array $templateArray->getVariance(), $templateArray->getName(), $arrayType, + $templateArray->getDefault(), ); } diff --git a/src/Type/TypeUtils.php b/src/Type/TypeUtils.php index 8ae601b832..9998d64422 100644 --- a/src/Type/TypeUtils.php +++ b/src/Type/TypeUtils.php @@ -292,6 +292,7 @@ public static function toStrictUnion(Type $type): Type $type->getVariance(), $type->getName(), static::toStrictUnion($type->getBound()), + $type->getDefault(), ); } diff --git a/tests/PHPStan/Analyser/data/template-default.php b/tests/PHPStan/Analyser/data/template-default.php index 235a72d2a9..8b839e55cf 100644 --- a/tests/PHPStan/Analyser/data/template-default.php +++ b/tests/PHPStan/Analyser/data/template-default.php @@ -60,7 +60,7 @@ function () { assertType('TemplateDefault\\Builder', $qb); $qb->two(); assertType('TemplateDefault\\Builder', $qb); - assertType('void', $qb->execute()); + assertType('null', $qb->execute()); }; function () { @@ -70,7 +70,7 @@ function () { assertType('TemplateDefault\\Builder', $qb); $qb->one(); assertType('TemplateDefault\\Builder', $qb); - assertType('void', $qb->execute()); + assertType('null', $qb->execute()); }; function () { @@ -78,5 +78,5 @@ function () { assertType('TemplateDefault\\Builder', $qb); $qb->one(); assertType('TemplateDefault\\Builder', $qb); - assertType('*NEVER*', $qb->execute()); + assertType('never', $qb->execute()); }; From 932b3b4956bc178ac0000cc3f3fa3abf5f363f66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Pudil?= Date: Fri, 20 Sep 2024 14:24:02 +0200 Subject: [PATCH 04/12] check default type validity --- src/Rules/FunctionCallParametersCheck.php | 2 +- src/Rules/Generics/ClassTemplateTypeRule.php | 2 + .../Generics/FunctionTemplateTypeRule.php | 2 + .../Generics/InterfaceTemplateTypeRule.php | 2 + .../Generics/MethodTagTemplateTypeCheck.php | 2 + src/Rules/Generics/MethodTemplateTypeRule.php | 2 + src/Rules/Generics/TemplateTypeCheck.php | 42 +++++++++++++++++++ src/Rules/Generics/TraitTemplateTypeRule.php | 2 + .../PhpDoc/GenericCallableRuleHelper.php | 2 + .../Generics/ClassTemplateTypeRuleTest.php | 8 ++++ .../Generics/FunctionTemplateTypeRuleTest.php | 8 ++++ .../InterfaceTemplateTypeRuleTest.php | 8 ++++ .../Generics/MethodTemplateTypeRuleTest.php | 8 ++++ .../Generics/TraitTemplateTypeRuleTest.php | 8 ++++ .../Rules/Generics/data/class-template.php | 16 +++++++ .../Rules/Generics/data/function-template.php | 16 +++++++ .../Generics/data/interface-template.php | 16 +++++++ .../Rules/Generics/data/method-template.php | 21 ++++++++++ .../Rules/Generics/data/trait-template.php | 16 +++++++ 19 files changed, 182 insertions(+), 1 deletion(-) diff --git a/src/Rules/FunctionCallParametersCheck.php b/src/Rules/FunctionCallParametersCheck.php index 8cdded6e9e..40fb657bcc 100644 --- a/src/Rules/FunctionCallParametersCheck.php +++ b/src/Rules/FunctionCallParametersCheck.php @@ -444,7 +444,7 @@ static function (Type $type, callable $traverse) use (&$returnTemplateTypes): Ty $parameterTemplateTypes = []; foreach ($originalParametersAcceptor->getParameters() as $parameter) { TypeTraverser::map($parameter->getType(), static function (Type $type, callable $traverse) use (&$parameterTemplateTypes): Type { - if ($type instanceof TemplateType) { + if ($type instanceof TemplateType && $type->getDefault() === null) { $parameterTemplateTypes[$type->getName()] = true; return $type; } diff --git a/src/Rules/Generics/ClassTemplateTypeRule.php b/src/Rules/Generics/ClassTemplateTypeRule.php index 6c21c3a33d..4f8c3956f6 100644 --- a/src/Rules/Generics/ClassTemplateTypeRule.php +++ b/src/Rules/Generics/ClassTemplateTypeRule.php @@ -49,6 +49,8 @@ public function processNode(Node $node, Scope $scope): array sprintf('PHPDoc tag @template for %s cannot have existing type alias %%s as its name.', $displayName), sprintf('PHPDoc tag @template %%s for %s has invalid bound type %%s.', $displayName), sprintf('PHPDoc tag @template %%s for %s with bound type %%s is not supported.', $displayName), + sprintf('PHPDoc tag @template %%s for %s has invalid default type %%s.', $displayName), + sprintf('Default type %%s in PHPDoc tag @template %%s for %s is not subtype of bound type %%s.', $displayName), ); } diff --git a/src/Rules/Generics/FunctionTemplateTypeRule.php b/src/Rules/Generics/FunctionTemplateTypeRule.php index 2fe0ab6bfb..faef00cc61 100644 --- a/src/Rules/Generics/FunctionTemplateTypeRule.php +++ b/src/Rules/Generics/FunctionTemplateTypeRule.php @@ -60,6 +60,8 @@ public function processNode(Node $node, Scope $scope): array sprintf('PHPDoc tag @template for function %s() cannot have existing type alias %%s as its name.', $escapedFunctionName), sprintf('PHPDoc tag @template %%s for function %s() has invalid bound type %%s.', $escapedFunctionName), sprintf('PHPDoc tag @template %%s for function %s() with bound type %%s is not supported.', $escapedFunctionName), + sprintf('PHPDoc tag @template %%s for function %s() has invalid default type %%s.', $escapedFunctionName), + sprintf('Default type %%s in PHPDoc tag @template %%s for function %s() is not subtype of bound type %%s.', $escapedFunctionName), ); } diff --git a/src/Rules/Generics/InterfaceTemplateTypeRule.php b/src/Rules/Generics/InterfaceTemplateTypeRule.php index 30be451eae..9dae49a11f 100644 --- a/src/Rules/Generics/InterfaceTemplateTypeRule.php +++ b/src/Rules/Generics/InterfaceTemplateTypeRule.php @@ -46,6 +46,8 @@ public function processNode(Node $node, Scope $scope): array sprintf('PHPDoc tag @template for interface %s cannot have existing type alias %%s as its name.', $escapadInterfaceName), sprintf('PHPDoc tag @template %%s for interface %s has invalid bound type %%s.', $escapadInterfaceName), sprintf('PHPDoc tag @template %%s for interface %s with bound type %%s is not supported.', $escapadInterfaceName), + sprintf('PHPDoc tag @template %%s for interface %s has invalid default type %%s.', $escapadInterfaceName), + sprintf('Default type %%s in PHPDoc tag @template %%s for interface %s is not subtype of bound type %%s.', $escapadInterfaceName), ); } diff --git a/src/Rules/Generics/MethodTagTemplateTypeCheck.php b/src/Rules/Generics/MethodTagTemplateTypeCheck.php index b0b6441c92..0abdd7b4a7 100644 --- a/src/Rules/Generics/MethodTagTemplateTypeCheck.php +++ b/src/Rules/Generics/MethodTagTemplateTypeCheck.php @@ -61,6 +61,8 @@ public function check( sprintf('PHPDoc tag @method template for method %s::%s() cannot have existing type alias %%s as its name.', $escapedClassName, $escapedMethodName), sprintf('PHPDoc tag @method template %%s for method %s::%s() has invalid bound type %%s.', $escapedClassName, $escapedMethodName), sprintf('PHPDoc tag @method template %%s for method %s::%s() with bound type %%s is not supported.', $escapedClassName, $escapedMethodName), + sprintf('PHPDoc tag @method template %%s for method %s::%s() has invalid default type %%s', $escapedClassName, $escapedMethodName), + sprintf('Default type %%s in PHPDoc tag @method template %%s for method %s::%s() is not subtype of bound type %%s', $escapedClassName, $escapedMethodName), )); foreach (array_keys($methodTemplateTags) as $name) { diff --git a/src/Rules/Generics/MethodTemplateTypeRule.php b/src/Rules/Generics/MethodTemplateTypeRule.php index fa9a6ecb06..50373c974b 100644 --- a/src/Rules/Generics/MethodTemplateTypeRule.php +++ b/src/Rules/Generics/MethodTemplateTypeRule.php @@ -66,6 +66,8 @@ public function processNode(Node $node, Scope $scope): array sprintf('PHPDoc tag @template for method %s::%s() cannot have existing type alias %%s as its name.', $escapedClassName, $escapedMethodName), sprintf('PHPDoc tag @template %%s for method %s::%s() has invalid bound type %%s.', $escapedClassName, $escapedMethodName), sprintf('PHPDoc tag @template %%s for method %s::%s() with bound type %%s is not supported.', $escapedClassName, $escapedMethodName), + sprintf('PHPDoc tag @template %%s for method %s::%s() has invalid default type %%s.', $escapedClassName, $escapedMethodName), + sprintf('Default type %%s in PHPDoc tag @template %%s for method %s::%s() is not subtype of bound type %%s.', $escapedClassName, $escapedMethodName), ); $classTemplateTypes = $classReflection->getTemplateTypeMap()->getTypes(); diff --git a/src/Rules/Generics/TemplateTypeCheck.php b/src/Rules/Generics/TemplateTypeCheck.php index 5eb8d8bdf4..87f372893d 100644 --- a/src/Rules/Generics/TemplateTypeCheck.php +++ b/src/Rules/Generics/TemplateTypeCheck.php @@ -62,6 +62,8 @@ public function check( string $sameTemplateTypeNameAsTypeMessage, string $invalidBoundTypeMessage, string $notSupportedBoundMessage, + string $invalidDefaultTypeMessage, + string $defaultNotSubtypeOfBoundMessage, ): array { $messages = []; @@ -141,6 +143,46 @@ public function check( foreach ($genericObjectErrors as $genericObjectError) { $messages[] = $genericObjectError; } + + $defaultType = $templateTag->getDefault(); + if ($defaultType === null) { + continue; + } + + foreach ($defaultType->getReferencedClasses() as $referencedClass) { + if ( + $this->reflectionProvider->hasClass($referencedClass) + && !$this->reflectionProvider->getClass($referencedClass)->isTrait() + ) { + continue; + } + + $messages[] = RuleErrorBuilder::message(sprintf($invalidDefaultTypeMessage, $templateTagName, $referencedClass)) + ->identifier('generics.invalidTemplateDefault') + ->build(); + } + + $classNameNodePairs = array_map(static fn (string $referencedClass): ClassNameNodePair => new ClassNameNodePair($referencedClass, $node), $defaultType->getReferencedClasses()); + $messages = array_merge($messages, $this->classCheck->checkClassNames($classNameNodePairs, $this->checkClassCaseSensitivity)); + + $genericDefaultErrors = $this->genericObjectTypeCheck->check( + $defaultType, + sprintf('PHPDoc tag @template %s default contains generic type %%s but class %%s is not generic.', $escapedTemplateTagName), + sprintf('PHPDoc tag @template %s default has type %%s which does not specify all template types of class %%s: %%s', $escapedTemplateTagName), + sprintf('PHPDoc tag @template %s default has type %%s which specifies %%d template types, but class %%s supports only %%d: %%s', $escapedTemplateTagName), + sprintf('Type %%s in generic type %%s in PHPDoc tag @template %s default is not subtype of template type %%s of class %%s.', $escapedTemplateTagName), + sprintf('Call-site variance of %%s in generic type %%s in PHPDoc tag @template %s default is in conflict with %%s template type %%s of %%s %%s.', $escapedTemplateTagName), + sprintf('Call-site variance of %%s in generic type %%s in PHPDoc tag @template %s default is redundant, template type %%s of %%s %%s has the same variance.', $escapedTemplateTagName), + ); + foreach ($genericDefaultErrors as $genericDefaultError) { + $messages[] = $genericDefaultError; + } + + if ($boundType->accepts($defaultType, $scope->isDeclareStrictTypes())->no()) { + $messages[] = RuleErrorBuilder::message(sprintf($defaultNotSubtypeOfBoundMessage, $defaultType->describe(VerbosityLevel::typeOnly()), $templateTagName, $boundType->describe(VerbosityLevel::typeOnly()))) + ->identifier('generics.templateDefaultOutOfBounds') + ->build(); + } } return $messages; diff --git a/src/Rules/Generics/TraitTemplateTypeRule.php b/src/Rules/Generics/TraitTemplateTypeRule.php index b08f12f32d..8ed1330457 100644 --- a/src/Rules/Generics/TraitTemplateTypeRule.php +++ b/src/Rules/Generics/TraitTemplateTypeRule.php @@ -60,6 +60,8 @@ public function processNode(Node $node, Scope $scope): array sprintf('PHPDoc tag @template for trait %s cannot have existing type alias %%s as its name.', $escapedTraitName), sprintf('PHPDoc tag @template %%s for trait %s has invalid bound type %%s.', $escapedTraitName), sprintf('PHPDoc tag @template %%s for trait %s with bound type %%s is not supported.', $escapedTraitName), + sprintf('PHPDoc tag @template %%s for trait %s has invalid default type %%s.', $escapedTraitName), + sprintf('Default type %%s in PHPDoc tag @template %%s for trait %s is not subtype of bound type %%s.', $escapedTraitName), ); } diff --git a/src/Rules/PhpDoc/GenericCallableRuleHelper.php b/src/Rules/PhpDoc/GenericCallableRuleHelper.php index 3e849cd1dc..1f06305ca5 100644 --- a/src/Rules/PhpDoc/GenericCallableRuleHelper.php +++ b/src/Rules/PhpDoc/GenericCallableRuleHelper.php @@ -60,6 +60,8 @@ public function check( sprintf('PHPDoc tag %s template of %s cannot have existing type alias %%s as its name.', $location, $typeDescription), sprintf('PHPDoc tag %s template %%s of %s has invalid bound type %%s.', $location, $typeDescription), sprintf('PHPDoc tag %s template %%s of %s with bound type %%s is not supported.', $location, $typeDescription), + sprintf('PHPDoc tag %s template %%s of %s has invalid default type %%s.', $location, $typeDescription), + sprintf('Default type %%s in PHPDoc tag %s template %%s of %s is not subtype of bound type %%s.', $location, $typeDescription), ); $templateTags = $type->getTemplateTags(); diff --git a/tests/PHPStan/Rules/Generics/ClassTemplateTypeRuleTest.php b/tests/PHPStan/Rules/Generics/ClassTemplateTypeRuleTest.php index 788be4cbd5..195685b899 100644 --- a/tests/PHPStan/Rules/Generics/ClassTemplateTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generics/ClassTemplateTypeRuleTest.php @@ -86,6 +86,14 @@ public function testRule(): void 'Call-site variance of contravariant int in generic type ClassTemplateType\Consecteur in PHPDoc tag @template W is in conflict with covariant template type T of class ClassTemplateType\Consecteur.', 113, ], + [ + 'PHPDoc tag @template T for class ClassTemplateType\Elit has invalid default type ClassTemplateType\Zazzzu.', + 121, + ], + [ + 'Default type bool in PHPDoc tag @template T for class ClassTemplateType\Venenatis is not subtype of bound type object.', + 129, + ], ]); } diff --git a/tests/PHPStan/Rules/Generics/FunctionTemplateTypeRuleTest.php b/tests/PHPStan/Rules/Generics/FunctionTemplateTypeRuleTest.php index 35df206865..51648c74b9 100644 --- a/tests/PHPStan/Rules/Generics/FunctionTemplateTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generics/FunctionTemplateTypeRuleTest.php @@ -67,6 +67,14 @@ public function testRule(): void 'Call-site variance of contravariant int in generic type FunctionTemplateType\GenericCovariant in PHPDoc tag @template W is in conflict with covariant template type T of class FunctionTemplateType\GenericCovariant.', 94, ], + [ + 'PHPDoc tag @template T for function FunctionTemplateType\invalidDefault() has invalid default type FunctionTemplateType\Zazzzu.', + 102, + ], + [ + 'Default type bool in PHPDoc tag @template T for function FunctionTemplateType\outOfBoundsDefault() is not subtype of bound type object.', + 110, + ], ]); } diff --git a/tests/PHPStan/Rules/Generics/InterfaceTemplateTypeRuleTest.php b/tests/PHPStan/Rules/Generics/InterfaceTemplateTypeRuleTest.php index 0623a7d1c2..a8d32f577c 100644 --- a/tests/PHPStan/Rules/Generics/InterfaceTemplateTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generics/InterfaceTemplateTypeRuleTest.php @@ -65,6 +65,14 @@ public function testRule(): void 'Call-site variance of contravariant int in generic type InterfaceTemplateType\Covariant in PHPDoc tag @template W is in conflict with covariant template type T of interface InterfaceTemplateType\Covariant.', 74, ], + [ + 'PHPDoc tag @template T for interface InterfaceTemplateType\InvalidDefault has invalid default type InterfaceTemplateType\Zazzzu.', + 82, + ], + [ + 'Default type bool in PHPDoc tag @template T for interface InterfaceTemplateType\OutOfBoundsDefault is not subtype of bound type object.', + 90, + ], ]); } diff --git a/tests/PHPStan/Rules/Generics/MethodTemplateTypeRuleTest.php b/tests/PHPStan/Rules/Generics/MethodTemplateTypeRuleTest.php index a845993344..d6dcf3729e 100644 --- a/tests/PHPStan/Rules/Generics/MethodTemplateTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generics/MethodTemplateTypeRuleTest.php @@ -73,6 +73,14 @@ public function testRule(): void 'Call-site variance of contravariant int in generic type MethodTemplateType\Dolor in PHPDoc tag @template W is in conflict with covariant template type T of class MethodTemplateType\Dolor.', 109, ], + [ + 'PHPDoc tag @template T for method MethodTemplateType\InvalidDefault::invalid() has invalid default type MethodTemplateType\Zazzzu.', + 122, + ], + [ + 'Default type bool in PHPDoc tag @template T for method MethodTemplateType\InvalidDefault::outOfBounds() is not subtype of bound type object.', + 130, + ], ]); } diff --git a/tests/PHPStan/Rules/Generics/TraitTemplateTypeRuleTest.php b/tests/PHPStan/Rules/Generics/TraitTemplateTypeRuleTest.php index 99ad839123..114880b9a7 100644 --- a/tests/PHPStan/Rules/Generics/TraitTemplateTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generics/TraitTemplateTypeRuleTest.php @@ -69,6 +69,14 @@ public function testRule(): void 'Call-site variance of contravariant int in generic type TraitTemplateType\Dolor in PHPDoc tag @template W is in conflict with covariant template type T of class TraitTemplateType\Dolor.', 64, ], + [ + 'PHPDoc tag @template T for trait TraitTemplateType\Adipiscing has invalid default type TraitTemplateType\Zazzzu.', + 72, + ], + [ + 'Default type bool in PHPDoc tag @template T for trait TraitTemplateType\Elit is not subtype of bound type object.', + 80, + ], ]); } diff --git a/tests/PHPStan/Rules/Generics/data/class-template.php b/tests/PHPStan/Rules/Generics/data/class-template.php index a76c2eeab0..598e87ec63 100644 --- a/tests/PHPStan/Rules/Generics/data/class-template.php +++ b/tests/PHPStan/Rules/Generics/data/class-template.php @@ -114,3 +114,19 @@ class Adipiscing { } + +/** + * @template T = Zazzzu + */ +class Elit +{ + +} + +/** + * @template T of object = bool + */ +class Venenatis +{ + +} diff --git a/tests/PHPStan/Rules/Generics/data/function-template.php b/tests/PHPStan/Rules/Generics/data/function-template.php index 8b9de2cdaf..288436e5f7 100644 --- a/tests/PHPStan/Rules/Generics/data/function-template.php +++ b/tests/PHPStan/Rules/Generics/data/function-template.php @@ -95,3 +95,19 @@ function typeProjections() { } + +/** + * @template T = Zazzzu + */ +function invalidDefault() +{ + +} + +/** + * @template T of object = bool + */ +function outOfBoundsDefault() +{ + +} diff --git a/tests/PHPStan/Rules/Generics/data/interface-template.php b/tests/PHPStan/Rules/Generics/data/interface-template.php index 8ae819c99b..c18e27855b 100644 --- a/tests/PHPStan/Rules/Generics/data/interface-template.php +++ b/tests/PHPStan/Rules/Generics/data/interface-template.php @@ -75,3 +75,19 @@ interface TypeProjections { } + +/** + * @template T = Zazzzu + */ +interface InvalidDefault +{ + +} + +/** + * @template T of object = bool + */ +interface OutOfBoundsDefault +{ + +} diff --git a/tests/PHPStan/Rules/Generics/data/method-template.php b/tests/PHPStan/Rules/Generics/data/method-template.php index 0fc67c1b24..8774a6116a 100644 --- a/tests/PHPStan/Rules/Generics/data/method-template.php +++ b/tests/PHPStan/Rules/Generics/data/method-template.php @@ -112,3 +112,24 @@ public function doSit() } } + +class InvalidDefault +{ + + /** + * @template T = Zazzzu + */ + public function invalid() + { + + } + + /** + * @template T of object = bool + */ + public function outOfBounds() + { + + } + +} diff --git a/tests/PHPStan/Rules/Generics/data/trait-template.php b/tests/PHPStan/Rules/Generics/data/trait-template.php index 7c9e179295..f179d4b224 100644 --- a/tests/PHPStan/Rules/Generics/data/trait-template.php +++ b/tests/PHPStan/Rules/Generics/data/trait-template.php @@ -65,3 +65,19 @@ trait Sit { } + +/** + * @template T = Zazzzu + */ +trait Adipiscing +{ + +} + +/** + * @template T of object = bool + */ +trait Elit +{ + +} From a53db1f2401cba5240d12d2cea16c75d1f53bf15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Pudil?= Date: Fri, 20 Sep 2024 15:11:14 +0200 Subject: [PATCH 05/12] update invalid default type detection --- src/Rules/Generics/TemplateTypeCheck.php | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/Rules/Generics/TemplateTypeCheck.php b/src/Rules/Generics/TemplateTypeCheck.php index 87f372893d..1ecb035dc2 100644 --- a/src/Rules/Generics/TemplateTypeCheck.php +++ b/src/Rules/Generics/TemplateTypeCheck.php @@ -150,16 +150,23 @@ public function check( } foreach ($defaultType->getReferencedClasses() as $referencedClass) { - if ( - $this->reflectionProvider->hasClass($referencedClass) - && !$this->reflectionProvider->getClass($referencedClass)->isTrait() - ) { + if (!$this->reflectionProvider->hasClass($referencedClass)) { + $messages[] = RuleErrorBuilder::message(sprintf( + $invalidDefaultTypeMessage, + $templateTagName, + $referencedClass, + ))->identifier('class.notFound')->build(); + continue; + } + if (!$this->reflectionProvider->getClass($referencedClass)->isTrait()) { continue; } - $messages[] = RuleErrorBuilder::message(sprintf($invalidDefaultTypeMessage, $templateTagName, $referencedClass)) - ->identifier('generics.invalidTemplateDefault') - ->build(); + $messages[] = RuleErrorBuilder::message(sprintf( + $invalidDefaultTypeMessage, + $templateTagName, + $referencedClass, + ))->identifier('generics.traitBound')->build(); } $classNameNodePairs = array_map(static fn (string $referencedClass): ClassNameNodePair => new ClassNameNodePair($referencedClass, $node), $defaultType->getReferencedClasses()); From 971db708d19c28f6542a26c7bfe899e09c1566a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Pudil?= Date: Fri, 20 Sep 2024 15:11:38 +0200 Subject: [PATCH 06/12] deduplicate test fixtures --- .../Analyser/NodeScopeResolverTest.php | 2 +- tests/PHPStan/Analyser/data/bug-4801.php | 25 ------------------- tests/PHPStan/Rules/Methods/data/bug-4801.php | 5 +++- 3 files changed, 5 insertions(+), 27 deletions(-) delete mode 100644 tests/PHPStan/Analyser/data/bug-4801.php diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index d671eb2ccb..1e3afb4e70 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -207,7 +207,7 @@ private static function findTestFiles(): iterable yield __DIR__ . '/../Rules/Classes/data/bug-11591-property-tag.php'; yield __DIR__ . '/../Rules/Classes/data/mixin-trait-use.php'; - yield __DIR__ . '/data/bug-4801.php'; + yield __DIR__ . '/../Rules/Methods/data/bug-4801.php'; yield __DIR__ . '/data/template-default.php'; } diff --git a/tests/PHPStan/Analyser/data/bug-4801.php b/tests/PHPStan/Analyser/data/bug-4801.php deleted file mode 100644 index ea8eb66604..0000000000 --- a/tests/PHPStan/Analyser/data/bug-4801.php +++ /dev/null @@ -1,25 +0,0 @@ - - */ - public function work(callable|null $a): I; -} - -/** - * @param I $i - */ -function x(I $i) { - assertType('Bug4801\\I', $i->work(null)); - assertType('Bug4801\\I', $i->work(fn(string $a) => (int) $a)); -} diff --git a/tests/PHPStan/Rules/Methods/data/bug-4801.php b/tests/PHPStan/Rules/Methods/data/bug-4801.php index fce55a8db5..ea8eb66604 100644 --- a/tests/PHPStan/Rules/Methods/data/bug-4801.php +++ b/tests/PHPStan/Rules/Methods/data/bug-4801.php @@ -2,6 +2,8 @@ namespace Bug4801; +use function PHPStan\Testing\assertType; + /** * @template T */ @@ -18,5 +20,6 @@ public function work(callable|null $a): I; * @param I $i */ function x(I $i) { - $i->work(null); + assertType('Bug4801\\I', $i->work(null)); + assertType('Bug4801\\I', $i->work(fn(string $a) => (int) $a)); } From dd4df25c0505a0897a9671f800c3a7c968bffc50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Pudil?= Date: Fri, 20 Sep 2024 15:14:21 +0200 Subject: [PATCH 07/12] fix cs --- src/Rules/Generics/TemplateTypeCheck.php | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Rules/Generics/TemplateTypeCheck.php b/src/Rules/Generics/TemplateTypeCheck.php index 1ecb035dc2..88846da94a 100644 --- a/src/Rules/Generics/TemplateTypeCheck.php +++ b/src/Rules/Generics/TemplateTypeCheck.php @@ -185,11 +185,13 @@ public function check( $messages[] = $genericDefaultError; } - if ($boundType->accepts($defaultType, $scope->isDeclareStrictTypes())->no()) { - $messages[] = RuleErrorBuilder::message(sprintf($defaultNotSubtypeOfBoundMessage, $defaultType->describe(VerbosityLevel::typeOnly()), $templateTagName, $boundType->describe(VerbosityLevel::typeOnly()))) - ->identifier('generics.templateDefaultOutOfBounds') - ->build(); + if (!$boundType->accepts($defaultType, $scope->isDeclareStrictTypes())->no()) { + continue; } + + $messages[] = RuleErrorBuilder::message(sprintf($defaultNotSubtypeOfBoundMessage, $defaultType->describe(VerbosityLevel::typeOnly()), $templateTagName, $boundType->describe(VerbosityLevel::typeOnly()))) + ->identifier('generics.templateDefaultOutOfBounds') + ->build(); } return $messages; From 1ba4beb8caffcae8dceaa05d7ceff419ac63bd98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Pudil?= Date: Fri, 20 Sep 2024 15:55:01 +0200 Subject: [PATCH 08/12] fix test for php 7.4 --- tests/PHPStan/Rules/Methods/data/bug-4801.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/PHPStan/Rules/Methods/data/bug-4801.php b/tests/PHPStan/Rules/Methods/data/bug-4801.php index ea8eb66604..a09d113367 100644 --- a/tests/PHPStan/Rules/Methods/data/bug-4801.php +++ b/tests/PHPStan/Rules/Methods/data/bug-4801.php @@ -13,7 +13,7 @@ interface I { * @param (callable(T): TResult)|null $a * @return I */ - public function work(callable|null $a): I; + public function work(?callable $a): I; } /** From 0ee1c3a6767bf8a3a6f0db13aa9a1fc2143dc758 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Pudil?= Date: Thu, 26 Sep 2024 18:27:56 +0200 Subject: [PATCH 09/12] add test for a default template type above a method --- .../Analyser/data/template-default.php | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/PHPStan/Analyser/data/template-default.php b/tests/PHPStan/Analyser/data/template-default.php index 8b839e55cf..eed40cc13b 100644 --- a/tests/PHPStan/Analyser/data/template-default.php +++ b/tests/PHPStan/Analyser/data/template-default.php @@ -53,6 +53,22 @@ public function execute(): void } } +class FormData {} +class Form +{ + /** + * @template Data of object = \stdClass + * @param Data|null $values + * @return Data + */ + public function mapValues(object|null $values = null): object + { + $values ??= new \stdClass; + // ... map into $values ... + return $values; + } +} + function () { $qb = new Builder(); assertType('TemplateDefault\\Builder', $qb); @@ -80,3 +96,10 @@ function () { assertType('TemplateDefault\\Builder', $qb); assertType('never', $qb->execute()); }; + +function () { + $form = new Form(); + + assertType('TemplateDefault\\FormData', $form->mapValues(new FormData)); + assertType('stdClass', $form->mapValues()); +}; From 86c307bcfd96274b32e08c20ea83ef097b75d4e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Pudil?= Date: Thu, 26 Sep 2024 19:23:57 +0200 Subject: [PATCH 10/12] report required template type after optional --- src/Rules/Generics/ClassTemplateTypeRule.php | 1 + src/Rules/Generics/FunctionTemplateTypeRule.php | 1 + src/Rules/Generics/InterfaceTemplateTypeRule.php | 1 + src/Rules/Generics/MethodTagTemplateTypeCheck.php | 1 + src/Rules/Generics/MethodTemplateTypeRule.php | 1 + src/Rules/Generics/TemplateTypeCheck.php | 12 ++++++++++++ src/Rules/Generics/TraitTemplateTypeRule.php | 1 + src/Rules/PhpDoc/GenericCallableRuleHelper.php | 1 + .../Rules/Generics/ClassTemplateTypeRuleTest.php | 4 ++++ .../Rules/Generics/FunctionTemplateTypeRuleTest.php | 4 ++++ .../Rules/Generics/InterfaceTemplateTypeRuleTest.php | 4 ++++ .../Rules/Generics/MethodTemplateTypeRuleTest.php | 4 ++++ .../Rules/Generics/TraitTemplateTypeRuleTest.php | 4 ++++ tests/PHPStan/Rules/Generics/data/class-template.php | 10 ++++++++++ .../Rules/Generics/data/function-template.php | 10 ++++++++++ .../Rules/Generics/data/interface-template.php | 10 ++++++++++ .../PHPStan/Rules/Generics/data/method-template.php | 10 ++++++++++ tests/PHPStan/Rules/Generics/data/trait-template.php | 10 ++++++++++ 18 files changed, 89 insertions(+) diff --git a/src/Rules/Generics/ClassTemplateTypeRule.php b/src/Rules/Generics/ClassTemplateTypeRule.php index 4f8c3956f6..f574d76460 100644 --- a/src/Rules/Generics/ClassTemplateTypeRule.php +++ b/src/Rules/Generics/ClassTemplateTypeRule.php @@ -51,6 +51,7 @@ public function processNode(Node $node, Scope $scope): array sprintf('PHPDoc tag @template %%s for %s with bound type %%s is not supported.', $displayName), sprintf('PHPDoc tag @template %%s for %s has invalid default type %%s.', $displayName), sprintf('Default type %%s in PHPDoc tag @template %%s for %s is not subtype of bound type %%s.', $displayName), + sprintf('PHPDoc tag @template %%s for %s does not have a default type but follows an optional @template %%s.', $displayName), ); } diff --git a/src/Rules/Generics/FunctionTemplateTypeRule.php b/src/Rules/Generics/FunctionTemplateTypeRule.php index faef00cc61..d4b56da5f2 100644 --- a/src/Rules/Generics/FunctionTemplateTypeRule.php +++ b/src/Rules/Generics/FunctionTemplateTypeRule.php @@ -62,6 +62,7 @@ public function processNode(Node $node, Scope $scope): array sprintf('PHPDoc tag @template %%s for function %s() with bound type %%s is not supported.', $escapedFunctionName), sprintf('PHPDoc tag @template %%s for function %s() has invalid default type %%s.', $escapedFunctionName), sprintf('Default type %%s in PHPDoc tag @template %%s for function %s() is not subtype of bound type %%s.', $escapedFunctionName), + sprintf('PHPDoc tag @template %%s for function %s() does not have a default type but follows an optional @template %%s.', $escapedFunctionName), ); } diff --git a/src/Rules/Generics/InterfaceTemplateTypeRule.php b/src/Rules/Generics/InterfaceTemplateTypeRule.php index 9dae49a11f..53adafb43a 100644 --- a/src/Rules/Generics/InterfaceTemplateTypeRule.php +++ b/src/Rules/Generics/InterfaceTemplateTypeRule.php @@ -48,6 +48,7 @@ public function processNode(Node $node, Scope $scope): array sprintf('PHPDoc tag @template %%s for interface %s with bound type %%s is not supported.', $escapadInterfaceName), sprintf('PHPDoc tag @template %%s for interface %s has invalid default type %%s.', $escapadInterfaceName), sprintf('Default type %%s in PHPDoc tag @template %%s for interface %s is not subtype of bound type %%s.', $escapadInterfaceName), + sprintf('PHPDoc tag @template %%s for interface %s does not have a default type but follows an optional @template %%s.', $escapadInterfaceName), ); } diff --git a/src/Rules/Generics/MethodTagTemplateTypeCheck.php b/src/Rules/Generics/MethodTagTemplateTypeCheck.php index 0abdd7b4a7..afa672f983 100644 --- a/src/Rules/Generics/MethodTagTemplateTypeCheck.php +++ b/src/Rules/Generics/MethodTagTemplateTypeCheck.php @@ -63,6 +63,7 @@ public function check( sprintf('PHPDoc tag @method template %%s for method %s::%s() with bound type %%s is not supported.', $escapedClassName, $escapedMethodName), sprintf('PHPDoc tag @method template %%s for method %s::%s() has invalid default type %%s', $escapedClassName, $escapedMethodName), sprintf('Default type %%s in PHPDoc tag @method template %%s for method %s::%s() is not subtype of bound type %%s', $escapedClassName, $escapedMethodName), + sprintf('PHPDoc tag @template %%s for method %s::%s() does not have a default type but follows an optional @template %%s.', $escapedClassName, $escapedMethodName), )); foreach (array_keys($methodTemplateTags) as $name) { diff --git a/src/Rules/Generics/MethodTemplateTypeRule.php b/src/Rules/Generics/MethodTemplateTypeRule.php index 50373c974b..65653f833f 100644 --- a/src/Rules/Generics/MethodTemplateTypeRule.php +++ b/src/Rules/Generics/MethodTemplateTypeRule.php @@ -68,6 +68,7 @@ public function processNode(Node $node, Scope $scope): array sprintf('PHPDoc tag @template %%s for method %s::%s() with bound type %%s is not supported.', $escapedClassName, $escapedMethodName), sprintf('PHPDoc tag @template %%s for method %s::%s() has invalid default type %%s.', $escapedClassName, $escapedMethodName), sprintf('Default type %%s in PHPDoc tag @template %%s for method %s::%s() is not subtype of bound type %%s.', $escapedClassName, $escapedMethodName), + sprintf('PHPDoc tag @template %%s for method %s::%s() does not have a default type but follows an optional @template %%s.', $escapedClassName, $escapedMethodName), ); $classTemplateTypes = $classReflection->getTemplateTypeMap()->getTypes(); diff --git a/src/Rules/Generics/TemplateTypeCheck.php b/src/Rules/Generics/TemplateTypeCheck.php index 88846da94a..dda84c8629 100644 --- a/src/Rules/Generics/TemplateTypeCheck.php +++ b/src/Rules/Generics/TemplateTypeCheck.php @@ -64,9 +64,11 @@ public function check( string $notSupportedBoundMessage, string $invalidDefaultTypeMessage, string $defaultNotSubtypeOfBoundMessage, + string $requiredTypeAfterOptionalMessage, ): array { $messages = []; + $templateTagWithDefaultType = null; foreach ($templateTags as $templateTag) { $templateTagName = $scope->resolveName(new Node\Name($templateTag->getName())); if ($this->reflectionProvider->hasClass($templateTagName)) { @@ -146,9 +148,19 @@ public function check( $defaultType = $templateTag->getDefault(); if ($defaultType === null) { + if ($templateTagWithDefaultType !== null) { + $messages[] = RuleErrorBuilder::message(sprintf( + $requiredTypeAfterOptionalMessage, + $templateTagName, + $templateTagWithDefaultType, + ))->identifier('generics.requiredTypeAfterOptional')->build(); + } + continue; } + $templateTagWithDefaultType = $templateTagName; + foreach ($defaultType->getReferencedClasses() as $referencedClass) { if (!$this->reflectionProvider->hasClass($referencedClass)) { $messages[] = RuleErrorBuilder::message(sprintf( diff --git a/src/Rules/Generics/TraitTemplateTypeRule.php b/src/Rules/Generics/TraitTemplateTypeRule.php index 8ed1330457..27ce74e298 100644 --- a/src/Rules/Generics/TraitTemplateTypeRule.php +++ b/src/Rules/Generics/TraitTemplateTypeRule.php @@ -62,6 +62,7 @@ public function processNode(Node $node, Scope $scope): array sprintf('PHPDoc tag @template %%s for trait %s with bound type %%s is not supported.', $escapedTraitName), sprintf('PHPDoc tag @template %%s for trait %s has invalid default type %%s.', $escapedTraitName), sprintf('Default type %%s in PHPDoc tag @template %%s for trait %s is not subtype of bound type %%s.', $escapedTraitName), + sprintf('PHPDoc tag @template %%s for trait %s does not have a default type but follows an optional @template %%s.', $escapedTraitName), ); } diff --git a/src/Rules/PhpDoc/GenericCallableRuleHelper.php b/src/Rules/PhpDoc/GenericCallableRuleHelper.php index 1f06305ca5..c32491fe42 100644 --- a/src/Rules/PhpDoc/GenericCallableRuleHelper.php +++ b/src/Rules/PhpDoc/GenericCallableRuleHelper.php @@ -62,6 +62,7 @@ public function check( sprintf('PHPDoc tag %s template %%s of %s with bound type %%s is not supported.', $location, $typeDescription), sprintf('PHPDoc tag %s template %%s of %s has invalid default type %%s.', $location, $typeDescription), sprintf('Default type %%s in PHPDoc tag %s template %%s of %s is not subtype of bound type %%s.', $location, $typeDescription), + sprintf('PHPDoc tag %s template %%s of %s does not have a default type but follows an optional template %%s.', $location, $typeDescription), ); $templateTags = $type->getTemplateTags(); diff --git a/tests/PHPStan/Rules/Generics/ClassTemplateTypeRuleTest.php b/tests/PHPStan/Rules/Generics/ClassTemplateTypeRuleTest.php index 195685b899..0538311ee5 100644 --- a/tests/PHPStan/Rules/Generics/ClassTemplateTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generics/ClassTemplateTypeRuleTest.php @@ -94,6 +94,10 @@ public function testRule(): void 'Default type bool in PHPDoc tag @template T for class ClassTemplateType\Venenatis is not subtype of bound type object.', 129, ], + [ + 'PHPDoc tag @template V for class ClassTemplateType\Mauris does not have a default type but follows an optional @template U.', + 139, + ], ]); } diff --git a/tests/PHPStan/Rules/Generics/FunctionTemplateTypeRuleTest.php b/tests/PHPStan/Rules/Generics/FunctionTemplateTypeRuleTest.php index 51648c74b9..f9d15da674 100644 --- a/tests/PHPStan/Rules/Generics/FunctionTemplateTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generics/FunctionTemplateTypeRuleTest.php @@ -75,6 +75,10 @@ public function testRule(): void 'Default type bool in PHPDoc tag @template T for function FunctionTemplateType\outOfBoundsDefault() is not subtype of bound type object.', 110, ], + [ + 'PHPDoc tag @template V for function FunctionTemplateType\requiredAfterOptional() does not have a default type but follows an optional @template U.', + 120, + ], ]); } diff --git a/tests/PHPStan/Rules/Generics/InterfaceTemplateTypeRuleTest.php b/tests/PHPStan/Rules/Generics/InterfaceTemplateTypeRuleTest.php index a8d32f577c..b997498703 100644 --- a/tests/PHPStan/Rules/Generics/InterfaceTemplateTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generics/InterfaceTemplateTypeRuleTest.php @@ -73,6 +73,10 @@ public function testRule(): void 'Default type bool in PHPDoc tag @template T for interface InterfaceTemplateType\OutOfBoundsDefault is not subtype of bound type object.', 90, ], + [ + 'PHPDoc tag @template V for interface InterfaceTemplateType\RequiredAfterOptional does not have a default type but follows an optional @template U.', + 100, + ], ]); } diff --git a/tests/PHPStan/Rules/Generics/MethodTemplateTypeRuleTest.php b/tests/PHPStan/Rules/Generics/MethodTemplateTypeRuleTest.php index d6dcf3729e..9276fec7c1 100644 --- a/tests/PHPStan/Rules/Generics/MethodTemplateTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generics/MethodTemplateTypeRuleTest.php @@ -81,6 +81,10 @@ public function testRule(): void 'Default type bool in PHPDoc tag @template T for method MethodTemplateType\InvalidDefault::outOfBounds() is not subtype of bound type object.', 130, ], + [ + 'PHPDoc tag @template V for method MethodTemplateType\InvalidDefault::requiredAfterOptional() does not have a default type but follows an optional @template U.', + 140, + ], ]); } diff --git a/tests/PHPStan/Rules/Generics/TraitTemplateTypeRuleTest.php b/tests/PHPStan/Rules/Generics/TraitTemplateTypeRuleTest.php index 114880b9a7..84351d9ea9 100644 --- a/tests/PHPStan/Rules/Generics/TraitTemplateTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generics/TraitTemplateTypeRuleTest.php @@ -77,6 +77,10 @@ public function testRule(): void 'Default type bool in PHPDoc tag @template T for trait TraitTemplateType\Elit is not subtype of bound type object.', 80, ], + [ + 'PHPDoc tag @template V for trait TraitTemplateType\Consecteur does not have a default type but follows an optional @template U.', + 90, + ], ]); } diff --git a/tests/PHPStan/Rules/Generics/data/class-template.php b/tests/PHPStan/Rules/Generics/data/class-template.php index 598e87ec63..06400bd536 100644 --- a/tests/PHPStan/Rules/Generics/data/class-template.php +++ b/tests/PHPStan/Rules/Generics/data/class-template.php @@ -130,3 +130,13 @@ class Venenatis { } + +/** + * @template T + * @template U = string + * @template V + */ +class Mauris +{ + +} diff --git a/tests/PHPStan/Rules/Generics/data/function-template.php b/tests/PHPStan/Rules/Generics/data/function-template.php index 288436e5f7..8a1ff456f9 100644 --- a/tests/PHPStan/Rules/Generics/data/function-template.php +++ b/tests/PHPStan/Rules/Generics/data/function-template.php @@ -111,3 +111,13 @@ function outOfBoundsDefault() { } + +/** + * @template T + * @template U = string + * @template V + */ +function requiredAfterOptional() +{ + +} diff --git a/tests/PHPStan/Rules/Generics/data/interface-template.php b/tests/PHPStan/Rules/Generics/data/interface-template.php index c18e27855b..7f0da436e7 100644 --- a/tests/PHPStan/Rules/Generics/data/interface-template.php +++ b/tests/PHPStan/Rules/Generics/data/interface-template.php @@ -91,3 +91,13 @@ interface OutOfBoundsDefault { } + +/** + * @template T + * @template U = string + * @template V + */ +interface RequiredAfterOptional +{ + +} diff --git a/tests/PHPStan/Rules/Generics/data/method-template.php b/tests/PHPStan/Rules/Generics/data/method-template.php index 8774a6116a..edf5d62201 100644 --- a/tests/PHPStan/Rules/Generics/data/method-template.php +++ b/tests/PHPStan/Rules/Generics/data/method-template.php @@ -132,4 +132,14 @@ public function outOfBounds() } + /** + * @template T + * @template U = string + * @template V + */ + public function requiredAfterOptional() + { + + } + } diff --git a/tests/PHPStan/Rules/Generics/data/trait-template.php b/tests/PHPStan/Rules/Generics/data/trait-template.php index f179d4b224..39126a88f2 100644 --- a/tests/PHPStan/Rules/Generics/data/trait-template.php +++ b/tests/PHPStan/Rules/Generics/data/trait-template.php @@ -81,3 +81,13 @@ trait Elit { } + +/** + * @template T + * @template U = string + * @template V + */ +trait Consecteur +{ + +} From 0b15a013b11aadcbc69cc2d317abe7c43968d593 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Pudil?= Date: Mon, 7 Oct 2024 12:56:57 +0200 Subject: [PATCH 11/12] report missing template types with regard to optional templates --- src/Rules/Classes/LocalTypeAliasesCheck.php | 3 +- src/Rules/Classes/MethodTagCheck.php | 3 +- src/Rules/Classes/MixinCheck.php | 3 +- src/Rules/Classes/PropertyTagCheck.php | 3 +- .../MissingClassConstantTypehintRule.php | 3 +- .../MissingFunctionParameterTypehintRule.php | 3 +- .../MissingFunctionReturnTypehintRule.php | 3 +- src/Rules/Generics/GenericAncestorsCheck.php | 16 +++++++++- src/Rules/Generics/GenericObjectTypeCheck.php | 19 ++++++++--- .../MissingMethodParameterTypehintRule.php | 3 +- .../MissingMethodReturnTypehintRule.php | 3 +- .../Methods/MissingMethodSelfOutTypeRule.php | 3 +- src/Rules/MissingTypehintCheck.php | 20 ++++++++++-- src/Rules/PhpDoc/AssertRuleHelper.php | 3 +- .../PhpDoc/InvalidPhpDocVarTagTypeRule.php | 3 +- .../MissingPropertyTypehintRule.php | 3 +- .../Generics/data/class-ancestors-extends.php | 13 ++++++++ .../data/class-ancestors-implements.php | 15 +++++++++ .../Rules/Generics/data/enum-ancestors.php | 13 ++++++++ .../Rules/Generics/data/used-traits.php | 14 ++++++++ ...MissingMethodParameterTypehintRuleTest.php | 4 +++ .../MissingMethodReturnTypehintRuleTest.php | 4 +++ .../missing-method-parameter-typehint.php | 32 +++++++++++++++++++ .../data/missing-method-return-typehint.php | 32 +++++++++++++++++++ .../InvalidPhpDocVarTagTypeRuleTest.php | 6 +++- .../data/invalid-phpdoc-definitions.php | 17 ++++++++++ .../PhpDoc/data/invalid-var-tag-type.php | 6 ++++ .../MissingPropertyTypehintRuleTest.php | 4 +++ .../data/missing-property-typehint.php | 28 ++++++++++++++++ 29 files changed, 248 insertions(+), 34 deletions(-) diff --git a/src/Rules/Classes/LocalTypeAliasesCheck.php b/src/Rules/Classes/LocalTypeAliasesCheck.php index 5347681a90..e2d463ab9c 100644 --- a/src/Rules/Classes/LocalTypeAliasesCheck.php +++ b/src/Rules/Classes/LocalTypeAliasesCheck.php @@ -24,7 +24,6 @@ use PHPStan\Type\VerbosityLevel; use function array_key_exists; use function array_merge; -use function implode; use function in_array; use function sprintf; @@ -211,7 +210,7 @@ public function checkInTraitDefinitionContext(ClassReflection $reflection): arra $reflection->getDisplayName(), $aliasName, $name, - implode(', ', $genericTypeNames), + $genericTypeNames, )) ->identifier('missingType.generics') ->build(); diff --git a/src/Rules/Classes/MethodTagCheck.php b/src/Rules/Classes/MethodTagCheck.php index b37d373772..5730ea3a9b 100644 --- a/src/Rules/Classes/MethodTagCheck.php +++ b/src/Rules/Classes/MethodTagCheck.php @@ -16,7 +16,6 @@ use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; use function array_merge; -use function implode; use function sprintf; final class MethodTagCheck @@ -174,7 +173,7 @@ private function checkMethodTypeInTraitDefinitionContext(ClassReflection $classR $methodName, $description, $innerName, - implode(', ', $genericTypeNames), + $genericTypeNames, )) ->identifier('missingType.generics') ->build(); diff --git a/src/Rules/Classes/MixinCheck.php b/src/Rules/Classes/MixinCheck.php index a17ef3d200..3ce9535164 100644 --- a/src/Rules/Classes/MixinCheck.php +++ b/src/Rules/Classes/MixinCheck.php @@ -14,7 +14,6 @@ use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\VerbosityLevel; use function array_merge; -use function implode; use function sprintf; final class MixinCheck @@ -90,7 +89,7 @@ public function checkInTraitDefinitionContext(ClassReflection $classReflection): $errors[] = RuleErrorBuilder::message(sprintf( 'PHPDoc tag @mixin contains generic %s but does not specify its types: %s', $innerName, - implode(', ', $genericTypeNames), + $genericTypeNames, )) ->identifier('missingType.generics') ->build(); diff --git a/src/Rules/Classes/PropertyTagCheck.php b/src/Rules/Classes/PropertyTagCheck.php index e05c4c676b..c3e9fda73f 100644 --- a/src/Rules/Classes/PropertyTagCheck.php +++ b/src/Rules/Classes/PropertyTagCheck.php @@ -18,7 +18,6 @@ use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; use function array_merge; -use function implode; use function sprintf; final class PropertyTagCheck @@ -155,7 +154,7 @@ private function checkPropertyTypeInTraitDefinitionContext(ClassReflection $clas $classReflection->getDisplayName(), $propertyName, $innerName, - implode(', ', $genericTypeNames), + $genericTypeNames, )) ->identifier('missingType.generics') ->build(); diff --git a/src/Rules/Constants/MissingClassConstantTypehintRule.php b/src/Rules/Constants/MissingClassConstantTypehintRule.php index 8a9aee1355..e0cbaa844c 100644 --- a/src/Rules/Constants/MissingClassConstantTypehintRule.php +++ b/src/Rules/Constants/MissingClassConstantTypehintRule.php @@ -12,7 +12,6 @@ use PHPStan\ShouldNotHappenException; use PHPStan\Type\VerbosityLevel; use function array_merge; -use function implode; use function sprintf; /** @@ -76,7 +75,7 @@ private function processSingleConstant(ClassReflection $classReflection, string $constantReflection->getDeclaringClass()->getDisplayName(), $constantName, $name, - implode(', ', $genericTypeNames), + $genericTypeNames, )) ->identifier('missingType.generics') ->build(); diff --git a/src/Rules/Functions/MissingFunctionParameterTypehintRule.php b/src/Rules/Functions/MissingFunctionParameterTypehintRule.php index a7519de7a5..c988c70c08 100644 --- a/src/Rules/Functions/MissingFunctionParameterTypehintRule.php +++ b/src/Rules/Functions/MissingFunctionParameterTypehintRule.php @@ -13,7 +13,6 @@ use PHPStan\Type\MixedType; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; -use function implode; use function sprintf; /** @@ -100,7 +99,7 @@ private function checkFunctionParameter(FunctionReflection $functionReflection, $functionReflection->getName(), $parameterMessage, $name, - implode(', ', $genericTypeNames), + $genericTypeNames, )) ->identifier('missingType.generics') ->build(); diff --git a/src/Rules/Functions/MissingFunctionReturnTypehintRule.php b/src/Rules/Functions/MissingFunctionReturnTypehintRule.php index d49e7f9aa9..648636973e 100644 --- a/src/Rules/Functions/MissingFunctionReturnTypehintRule.php +++ b/src/Rules/Functions/MissingFunctionReturnTypehintRule.php @@ -10,7 +10,6 @@ use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\MixedType; use PHPStan\Type\VerbosityLevel; -use function implode; use function sprintf; /** @@ -58,7 +57,7 @@ public function processNode(Node $node, Scope $scope): array 'Function %s() return type with generic %s does not specify its types: %s', $functionReflection->getName(), $name, - implode(', ', $genericTypeNames), + $genericTypeNames, )) ->identifier('missingType.generics') ->build(); diff --git a/src/Rules/Generics/GenericAncestorsCheck.php b/src/Rules/Generics/GenericAncestorsCheck.php index ef9ce469b5..c2eaef580e 100644 --- a/src/Rules/Generics/GenericAncestorsCheck.php +++ b/src/Rules/Generics/GenericAncestorsCheck.php @@ -9,11 +9,13 @@ use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\Generic\GenericObjectType; +use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\Generic\TemplateTypeVariance; use PHPStan\Type\Generic\TypeProjectionHelper; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; use function array_fill_keys; +use function array_filter; use function array_keys; use function array_map; use function array_merge; @@ -173,10 +175,22 @@ public function check( continue; } + $templateTypes = $unusedNameClassReflection->getTemplateTypeMap()->getTypes(); + $templateTypesCount = count($templateTypes); + $requiredTemplateTypesCount = count(array_filter($templateTypes, static fn (Type $type) => $type instanceof TemplateType && $type->getDefault() === null)); + if ($requiredTemplateTypesCount === 0) { + continue; + } + + $templateTypesList = implode(', ', array_keys($templateTypes)); + if ($requiredTemplateTypesCount !== $templateTypesCount) { + $templateTypesList .= sprintf(' (%d-%d required)', $requiredTemplateTypesCount, $templateTypesCount); + } + $messages[] = RuleErrorBuilder::message(sprintf( $genericClassInNonGenericObjectType, $unusedName, - implode(', ', array_keys($unusedNameClassReflection->getTemplateTypeMap()->getTypes())), + $templateTypesList, )) ->identifier('missingType.generics') ->build(); diff --git a/src/Rules/Generics/GenericObjectTypeCheck.php b/src/Rules/Generics/GenericObjectTypeCheck.php index 46901218ef..3f437a0b91 100644 --- a/src/Rules/Generics/GenericObjectTypeCheck.php +++ b/src/Rules/Generics/GenericObjectTypeCheck.php @@ -14,6 +14,7 @@ use PHPStan\Type\Type; use PHPStan\Type\TypeTraverser; use PHPStan\Type\VerbosityLevel; +use function array_filter; use function array_keys; use function array_values; use function count; @@ -59,15 +60,26 @@ public function check( $genericTypeVariances = $genericType->getVariances(); $templateTypesCount = count($templateTypes); $genericTypeTypesCount = count($genericTypeTypes); - if ($templateTypesCount > $genericTypeTypesCount) { + $requiredTemplateTypesCount = count(array_filter($templateTypes, static fn (Type $type) => $type instanceof TemplateType && $type->getDefault() === null)); + if ($requiredTemplateTypesCount > $genericTypeTypesCount) { + $templateTypesList = implode(', ', array_keys($classReflection->getTemplateTypeMap()->getTypes())); + if ($requiredTemplateTypesCount !== $templateTypesCount) { + $templateTypesList .= sprintf(' (%d-%d required).', $requiredTemplateTypesCount, $templateTypesCount); + } + $messages[] = RuleErrorBuilder::message(sprintf( $notEnoughTypesMessage, $genericType->describe(VerbosityLevel::typeOnly()), $classLikeDescription, $classReflection->getDisplayName(false), - implode(', ', array_keys($classReflection->getTemplateTypeMap()->getTypes())), + $templateTypesList, ))->identifier('generics.lessTypes')->build(); } elseif ($templateTypesCount < $genericTypeTypesCount) { + $templateTypesList = implode(', ', array_keys($classReflection->getTemplateTypeMap()->getTypes())); + if ($requiredTemplateTypesCount !== $templateTypesCount) { + $templateTypesList .= sprintf(' (%d-%d required)', $requiredTemplateTypesCount, $templateTypesCount); + } + $messages[] = RuleErrorBuilder::message(sprintf( $extraTypesMessage, $genericType->describe(VerbosityLevel::typeOnly()), @@ -75,11 +87,10 @@ public function check( $classLikeDescription, $classReflection->getDisplayName(false), $templateTypesCount, - implode(', ', array_keys($classReflection->getTemplateTypeMap()->getTypes())), + $templateTypesList, ))->identifier('generics.moreTypes')->build(); } - $templateTypesCount = count($templateTypes); for ($i = 0; $i < $templateTypesCount; $i++) { if (!isset($genericTypeTypes[$i])) { continue; diff --git a/src/Rules/Methods/MissingMethodParameterTypehintRule.php b/src/Rules/Methods/MissingMethodParameterTypehintRule.php index 32d64a68ad..8d5edd7890 100644 --- a/src/Rules/Methods/MissingMethodParameterTypehintRule.php +++ b/src/Rules/Methods/MissingMethodParameterTypehintRule.php @@ -13,7 +13,6 @@ use PHPStan\Type\MixedType; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; -use function implode; use function sprintf; /** @@ -103,7 +102,7 @@ private function checkMethodParameter(MethodReflection $methodReflection, string $methodReflection->getName(), $parameterMessage, $name, - implode(', ', $genericTypeNames), + $genericTypeNames, )) ->identifier('missingType.generics') ->build(); diff --git a/src/Rules/Methods/MissingMethodReturnTypehintRule.php b/src/Rules/Methods/MissingMethodReturnTypehintRule.php index e48ed2d785..2b3c563f2d 100644 --- a/src/Rules/Methods/MissingMethodReturnTypehintRule.php +++ b/src/Rules/Methods/MissingMethodReturnTypehintRule.php @@ -10,7 +10,6 @@ use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\MixedType; use PHPStan\Type\VerbosityLevel; -use function implode; use function sprintf; /** @@ -70,7 +69,7 @@ public function processNode(Node $node, Scope $scope): array $methodReflection->getDeclaringClass()->getDisplayName(), $methodReflection->getName(), $name, - implode(', ', $genericTypeNames), + $genericTypeNames, )) ->identifier('missingType.generics') ->build(); diff --git a/src/Rules/Methods/MissingMethodSelfOutTypeRule.php b/src/Rules/Methods/MissingMethodSelfOutTypeRule.php index 4b602b5fa1..e4e023ce21 100644 --- a/src/Rules/Methods/MissingMethodSelfOutTypeRule.php +++ b/src/Rules/Methods/MissingMethodSelfOutTypeRule.php @@ -9,7 +9,6 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\VerbosityLevel; -use function implode; use function sprintf; /** @@ -63,7 +62,7 @@ public function processNode(Node $node, Scope $scope): array $methodReflection->getName(), $phpDocTagMessage, $name, - implode(', ', $genericTypeNames), + $genericTypeNames, )) ->identifier('missingType.generics') ->build(); diff --git a/src/Rules/MissingTypehintCheck.php b/src/Rules/MissingTypehintCheck.php index 75dd681fb1..3467703921 100644 --- a/src/Rules/MissingTypehintCheck.php +++ b/src/Rules/MissingTypehintCheck.php @@ -21,8 +21,11 @@ use PHPStan\Type\Type; use PHPStan\Type\TypeTraverser; use Traversable; +use function array_filter; use function array_keys; use function array_merge; +use function count; +use function implode; use function in_array; use function sprintf; use function strtolower; @@ -100,7 +103,7 @@ public function getIterableTypesWithMissingValueTypehint(Type $type): array } /** - * @return array + * @return array */ public function getNonGenericObjectTypesWithGenericClass(Type $type): array { @@ -140,9 +143,22 @@ public function getNonGenericObjectTypesWithGenericClass(Type $type): array if (!$resolvedType instanceof ObjectType) { throw new ShouldNotHappenException(); } + + $templateTypes = $classReflection->getTemplateTypeMap()->getTypes(); + $templateTypesCount = count($templateTypes); + $requiredTemplateTypesCount = count(array_filter($templateTypes, static fn (Type $type) => $type instanceof TemplateType && $type->getDefault() === null)); + if ($requiredTemplateTypesCount === 0) { + return $type; + } + + $templateTypesList = implode(', ', array_keys($templateTypes)); + if ($requiredTemplateTypesCount !== $templateTypesCount) { + $templateTypesList .= sprintf(' (%d-%d required)', $requiredTemplateTypesCount, $templateTypesCount); + } + $objectTypes[] = [ sprintf('%s %s', strtolower($classReflection->getClassTypeDescription()), $classReflection->getDisplayName(false)), - array_keys($classReflection->getTemplateTypeMap()->getTypes()), + $templateTypesList, ]; return $type; } diff --git a/src/Rules/PhpDoc/AssertRuleHelper.php b/src/Rules/PhpDoc/AssertRuleHelper.php index 073d131922..0dec8f4d24 100644 --- a/src/Rules/PhpDoc/AssertRuleHelper.php +++ b/src/Rules/PhpDoc/AssertRuleHelper.php @@ -23,7 +23,6 @@ use PHPStan\Type\VerbosityLevel; use function array_key_exists; use function array_merge; -use function implode; use function sprintf; use function substr; @@ -198,7 +197,7 @@ public function check( $tagName, $assertedExprString, $innerName, - implode(', ', $genericTypeNames), + $genericTypeNames, )) ->identifier('missingType.generics') ->build(); diff --git a/src/Rules/PhpDoc/InvalidPhpDocVarTagTypeRule.php b/src/Rules/PhpDoc/InvalidPhpDocVarTagTypeRule.php index 4a227ffd07..53d4c4e6a6 100644 --- a/src/Rules/PhpDoc/InvalidPhpDocVarTagTypeRule.php +++ b/src/Rules/PhpDoc/InvalidPhpDocVarTagTypeRule.php @@ -16,7 +16,6 @@ use PHPStan\Type\VerbosityLevel; use function array_map; use function array_merge; -use function implode; use function is_string; use function sprintf; @@ -116,7 +115,7 @@ public function processNode(Node $node, Scope $scope): array '%s contains generic %s but does not specify its types: %s', $identifier, $innerName, - implode(', ', $genericTypeNames), + $genericTypeNames, )) ->identifier('missingType.generics') ->build(); diff --git a/src/Rules/Properties/MissingPropertyTypehintRule.php b/src/Rules/Properties/MissingPropertyTypehintRule.php index c429a30fdd..84c8a20325 100644 --- a/src/Rules/Properties/MissingPropertyTypehintRule.php +++ b/src/Rules/Properties/MissingPropertyTypehintRule.php @@ -10,7 +10,6 @@ use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\MixedType; use PHPStan\Type\VerbosityLevel; -use function implode; use function sprintf; /** @@ -68,7 +67,7 @@ public function processNode(Node $node, Scope $scope): array $propertyReflection->getDeclaringClass()->getDisplayName(), $node->getName(), $name, - implode(', ', $genericTypeNames), + $genericTypeNames, )) ->identifier('missingType.generics') ->build(); diff --git a/tests/PHPStan/Rules/Generics/data/class-ancestors-extends.php b/tests/PHPStan/Rules/Generics/data/class-ancestors-extends.php index e66591168e..c04a5665a4 100644 --- a/tests/PHPStan/Rules/Generics/data/class-ancestors-extends.php +++ b/tests/PHPStan/Rules/Generics/data/class-ancestors-extends.php @@ -260,3 +260,16 @@ class TraitInExtends extends FooGeneric { } + +/** + * @template T = string + */ +class FooGenericDefault +{ + +} + +class FooGenericExtendsDefault extends FooGenericDefault +{ + +} diff --git a/tests/PHPStan/Rules/Generics/data/class-ancestors-implements.php b/tests/PHPStan/Rules/Generics/data/class-ancestors-implements.php index 7a016c36ce..abbc514279 100644 --- a/tests/PHPStan/Rules/Generics/data/class-ancestors-implements.php +++ b/tests/PHPStan/Rules/Generics/data/class-ancestors-implements.php @@ -242,3 +242,18 @@ class FooCollection implements AbstractFooCollection class FooTypeProjection implements FooGeneric { } + +/** + * @template T = string + */ +interface FooGenericDefault +{ +} + +interface FooGenericExtendsDefault extends FooGenericDefault +{ +} + +class FooGenericImplementsDefault implements FooGenericDefault +{ +} diff --git a/tests/PHPStan/Rules/Generics/data/enum-ancestors.php b/tests/PHPStan/Rules/Generics/data/enum-ancestors.php index e5d7848498..1cda1bcbcd 100644 --- a/tests/PHPStan/Rules/Generics/data/enum-ancestors.php +++ b/tests/PHPStan/Rules/Generics/data/enum-ancestors.php @@ -94,3 +94,16 @@ enum TypeProjection implements Generic { } + +/** + * @template T = string + */ +interface GenericDefault +{ + +} + +enum Foo9 implements GenericDefault +{ + +} diff --git a/tests/PHPStan/Rules/Generics/data/used-traits.php b/tests/PHPStan/Rules/Generics/data/used-traits.php index f01c5e9dfb..f34fb5ffb9 100644 --- a/tests/PHPStan/Rules/Generics/data/used-traits.php +++ b/tests/PHPStan/Rules/Generics/data/used-traits.php @@ -69,3 +69,17 @@ class Dolor use GenericTrait; } + +/** + * @template T = string + */ +trait GenericDefault +{ +} + +class Sit +{ + + use GenericDefault; + +} diff --git a/tests/PHPStan/Rules/Methods/MissingMethodParameterTypehintRuleTest.php b/tests/PHPStan/Rules/Methods/MissingMethodParameterTypehintRuleTest.php index fecb8a1045..89e29bb606 100644 --- a/tests/PHPStan/Rules/Methods/MissingMethodParameterTypehintRuleTest.php +++ b/tests/PHPStan/Rules/Methods/MissingMethodParameterTypehintRuleTest.php @@ -82,6 +82,10 @@ public function testRule(): void 'Method MissingMethodParameterTypehint\MissingPureClosureSignatureType::doFoo() has parameter $cb with no signature specified for Closure.', 238, ], + [ + 'Method MissingMethodParameterTypehint\Baz::acceptsGenericWithSomeDefaults() has parameter $c with generic class MissingMethodParameterTypehint\GenericClassWithSomeDefaults but does not specify its types: T, U (1-2 required)', + 270, + ], ]; $this->analyse([__DIR__ . '/data/missing-method-parameter-typehint.php'], $errors); diff --git a/tests/PHPStan/Rules/Methods/MissingMethodReturnTypehintRuleTest.php b/tests/PHPStan/Rules/Methods/MissingMethodReturnTypehintRuleTest.php index 0762900901..49fc0b97d1 100644 --- a/tests/PHPStan/Rules/Methods/MissingMethodReturnTypehintRuleTest.php +++ b/tests/PHPStan/Rules/Methods/MissingMethodReturnTypehintRuleTest.php @@ -53,6 +53,10 @@ public function testRule(): void 'Method MissingMethodReturnTypehint\CallableSignature::doFoo() return type has no signature specified for callable.', 99, ], + [ + 'Method MissingMethodReturnTypehint\Baz::returnsGenericWithSomeDefaults() return type with generic class MissingMethodReturnTypehint\GenericClassWithSomeDefaults does not specify its types: T, U (1-2 required)', + 142, + ], ]); } diff --git a/tests/PHPStan/Rules/Methods/data/missing-method-parameter-typehint.php b/tests/PHPStan/Rules/Methods/data/missing-method-parameter-typehint.php index d5a333491b..27fa039ef4 100644 --- a/tests/PHPStan/Rules/Methods/data/missing-method-parameter-typehint.php +++ b/tests/PHPStan/Rules/Methods/data/missing-method-parameter-typehint.php @@ -241,3 +241,35 @@ function doFoo(\Closure $cb): void } } + +/** + * @template T = string + */ +class GenericClassWithDefault +{ + +} + +/** + * @template T + * @template U = string + */ +class GenericClassWithSomeDefaults +{ + +} + +class Baz +{ + + public function acceptsGenericWithDefault(GenericClassWithDefault $i) + { + + } + + public function acceptsGenericWithSomeDefaults(GenericClassWithSomeDefaults $c) + { + + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/missing-method-return-typehint.php b/tests/PHPStan/Rules/Methods/data/missing-method-return-typehint.php index 5b708cad89..480373825a 100644 --- a/tests/PHPStan/Rules/Methods/data/missing-method-return-typehint.php +++ b/tests/PHPStan/Rules/Methods/data/missing-method-return-typehint.php @@ -113,3 +113,35 @@ public function doFoo(): \Traversable } } + +/** + * @template T = string + */ +class GenericClassWithDefault +{ + +} + +/** + * @template T + * @template U = string + */ +class GenericClassWithSomeDefaults +{ + +} + +class Baz +{ + + public function returnsGenericWithDefault(): GenericClassWithDefault + { + + } + + public function returnsGenericWithSomeDefaults(): GenericClassWithSomeDefaults + { + + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/InvalidPhpDocVarTagTypeRuleTest.php b/tests/PHPStan/Rules/PhpDoc/InvalidPhpDocVarTagTypeRuleTest.php index 9d4dc0bb3e..c2916f9864 100644 --- a/tests/PHPStan/Rules/PhpDoc/InvalidPhpDocVarTagTypeRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/InvalidPhpDocVarTagTypeRuleTest.php @@ -107,8 +107,12 @@ public function testRule(): void 73, ], [ - 'PHPDoc tag @var for variable $foo contains unknown class InvalidVarTagType\Blabla.', + 'PHPDoc tag @var for variable $test contains generic class InvalidPhpDocDefinitions\FooGenericWithSomeDefaults but does not specify its types: T, U (1-2 required)', 79, + ], + [ + 'PHPDoc tag @var for variable $foo contains unknown class InvalidVarTagType\Blabla.', + 85, 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], ]); diff --git a/tests/PHPStan/Rules/PhpDoc/data/invalid-phpdoc-definitions.php b/tests/PHPStan/Rules/PhpDoc/data/invalid-phpdoc-definitions.php index f52cae0d04..74ad37a779 100644 --- a/tests/PHPStan/Rules/PhpDoc/data/invalid-phpdoc-definitions.php +++ b/tests/PHPStan/Rules/PhpDoc/data/invalid-phpdoc-definitions.php @@ -23,3 +23,20 @@ class FooCovariantGeneric { } + +/** + * @template T = string + */ +class FooGenericWithDefault +{ + +} + +/** + * @template T + * @template U = string + */ +class FooGenericWithSomeDefaults +{ + +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/invalid-var-tag-type.php b/tests/PHPStan/Rules/PhpDoc/data/invalid-var-tag-type.php index fd9688445d..cd996b4a21 100644 --- a/tests/PHPStan/Rules/PhpDoc/data/invalid-var-tag-type.php +++ b/tests/PHPStan/Rules/PhpDoc/data/invalid-var-tag-type.php @@ -71,6 +71,12 @@ public function doFoo() /** @var \InvalidPhpDocDefinitions\FooCovariantGeneric $test */ $test = doFoo(); + + /** @var \InvalidPhpDocDefinitions\FooGenericWithDefault $test */ + $test = doFoo(); + + /** @var \InvalidPhpDocDefinitions\FooGenericWithSomeDefaults $test */ + $test = doFoo(); } public function doBar($foo) diff --git a/tests/PHPStan/Rules/Properties/MissingPropertyTypehintRuleTest.php b/tests/PHPStan/Rules/Properties/MissingPropertyTypehintRuleTest.php index bd92fe752e..324946835a 100644 --- a/tests/PHPStan/Rules/Properties/MissingPropertyTypehintRuleTest.php +++ b/tests/PHPStan/Rules/Properties/MissingPropertyTypehintRuleTest.php @@ -54,6 +54,10 @@ public function testRule(): void 106, MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP, ], + [ + 'Property MissingPropertyTypehint\Baz::$bar with generic class MissingPropertyTypehint\GenericClassWithSomeDefaults does not specify its types: T, U (1-2 required)', + 134, + ], ]); } diff --git a/tests/PHPStan/Rules/Properties/data/missing-property-typehint.php b/tests/PHPStan/Rules/Properties/data/missing-property-typehint.php index 952016e860..374397cd0a 100644 --- a/tests/PHPStan/Rules/Properties/data/missing-property-typehint.php +++ b/tests/PHPStan/Rules/Properties/data/missing-property-typehint.php @@ -106,3 +106,31 @@ class NestedArrayInProperty public $args; } + +/** + * @template T = string + */ +class GenericClassWithDefault +{ + +} + +/** + * @template T + * @template U = string + */ +class GenericClassWithSomeDefaults +{ + +} + +class Baz +{ + + /** @var \MissingPropertyTypehint\GenericClassWithDefault */ + private $foo; + + /** @var \MissingPropertyTypehint\GenericClassWithSomeDefaults */ + private $bar; + +} From 58e9c410877ba395c7068bb5110ef7cdf662b2ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Pudil?= Date: Mon, 7 Oct 2024 12:58:18 +0200 Subject: [PATCH 12/12] add test for a default template in generic ancestors --- .../Analyser/data/template-default.php | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/PHPStan/Analyser/data/template-default.php b/tests/PHPStan/Analyser/data/template-default.php index eed40cc13b..979fbc3636 100644 --- a/tests/PHPStan/Analyser/data/template-default.php +++ b/tests/PHPStan/Analyser/data/template-default.php @@ -103,3 +103,34 @@ function () { assertType('TemplateDefault\\FormData', $form->mapValues(new FormData)); assertType('stdClass', $form->mapValues()); }; + +/** + * @template T + * @template U = string + */ +interface Foo +{ + /** + * @return U + */ + public function get(): mixed; +} + +/** + * @extends Foo + */ +interface Bar extends Foo +{ +} + +/** + * @extends Foo + */ +interface Baz extends Foo +{ +} + +function (Bar $bar, Baz $baz) { + assertType('string', $bar->get()); + assertType('bool', $baz->get()); +};