diff --git a/src/Rules/Generics/TemplateTypeCheck.php b/src/Rules/Generics/TemplateTypeCheck.php index 91a9927ac1..83b1b8fa35 100644 --- a/src/Rules/Generics/TemplateTypeCheck.php +++ b/src/Rules/Generics/TemplateTypeCheck.php @@ -13,6 +13,8 @@ use PHPStan\Type\ArrayType; use PHPStan\Type\BooleanType; use PHPStan\Type\Constant\ConstantArrayType; +use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\FloatType; use PHPStan\Type\Generic\GenericObjectType; use PHPStan\Type\Generic\TemplateType; @@ -101,7 +103,9 @@ public function check( $boundTypeClass !== MixedType::class && $boundTypeClass !== ConstantArrayType::class && $boundTypeClass !== ArrayType::class + && $boundTypeClass !== ConstantStringType::class && $boundTypeClass !== StringType::class + && $boundTypeClass !== ConstantIntegerType::class && $boundTypeClass !== IntegerType::class && $boundTypeClass !== FloatType::class && $boundTypeClass !== BooleanType::class diff --git a/src/Type/Generic/TemplateConstantIntegerType.php b/src/Type/Generic/TemplateConstantIntegerType.php new file mode 100644 index 0000000000..73d57b3441 --- /dev/null +++ b/src/Type/Generic/TemplateConstantIntegerType.php @@ -0,0 +1,54 @@ + */ + use TemplateTypeTrait; + use UndecidedComparisonCompoundTypeTrait; + + public function __construct( + TemplateTypeScope $scope, + TemplateTypeStrategy $templateTypeStrategy, + TemplateTypeVariance $templateTypeVariance, + string $name, + ConstantIntegerType $bound, + ) + { + parent::__construct($bound->getValue()); + $this->scope = $scope; + $this->strategy = $templateTypeStrategy; + $this->variance = $templateTypeVariance; + $this->name = $name; + $this->bound = $bound; + } + + public function traverse(callable $cb): Type + { + $newBound = $cb($this->getBound()); + if ($this->getBound() !== $newBound && $newBound instanceof ConstantIntegerType) { + return new self( + $this->scope, + $this->strategy, + $this->variance, + $this->name, + $newBound, + ); + } + + return $this; + } + + protected function shouldGeneralizeInferredType(): bool + { + return false; + } + +} diff --git a/src/Type/Generic/TemplateConstantStringType.php b/src/Type/Generic/TemplateConstantStringType.php new file mode 100644 index 0000000000..60be035a93 --- /dev/null +++ b/src/Type/Generic/TemplateConstantStringType.php @@ -0,0 +1,54 @@ + */ + use TemplateTypeTrait; + use UndecidedComparisonCompoundTypeTrait; + + public function __construct( + TemplateTypeScope $scope, + TemplateTypeStrategy $templateTypeStrategy, + TemplateTypeVariance $templateTypeVariance, + string $name, + ConstantStringType $bound, + ) + { + parent::__construct($bound->getValue()); + $this->scope = $scope; + $this->strategy = $templateTypeStrategy; + $this->variance = $templateTypeVariance; + $this->name = $name; + $this->bound = $bound; + } + + public function traverse(callable $cb): Type + { + $newBound = $cb($this->getBound()); + if ($this->getBound() !== $newBound && $newBound instanceof ConstantStringType) { + return new self( + $this->scope, + $this->strategy, + $this->variance, + $this->name, + $newBound, + ); + } + + return $this; + } + + protected function shouldGeneralizeInferredType(): bool + { + return false; + } + +} diff --git a/src/Type/Generic/TemplateTypeFactory.php b/src/Type/Generic/TemplateTypeFactory.php index 50435f904a..add12d4797 100644 --- a/src/Type/Generic/TemplateTypeFactory.php +++ b/src/Type/Generic/TemplateTypeFactory.php @@ -7,6 +7,8 @@ use PHPStan\Type\BenevolentUnionType; use PHPStan\Type\BooleanType; use PHPStan\Type\Constant\ConstantArrayType; +use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\FloatType; use PHPStan\Type\IntegerType; use PHPStan\Type\IntersectionType; @@ -55,10 +57,18 @@ public static function create(TemplateTypeScope $scope, string $name, ?Type $bou return new TemplateStringType($scope, $strategy, $variance, $name, $bound); } + if ($bound instanceof ConstantStringType && ($boundClass === ConstantStringType::class || $bound instanceof TemplateType)) { + return new TemplateConstantStringType($scope, $strategy, $variance, $name, $bound); + } + if ($bound instanceof IntegerType && ($boundClass === IntegerType::class || $bound instanceof TemplateType)) { return new TemplateIntegerType($scope, $strategy, $variance, $name, $bound); } + if ($bound instanceof ConstantIntegerType && ($boundClass === ConstantIntegerType::class || $bound instanceof TemplateType)) { + return new TemplateConstantIntegerType($scope, $strategy, $variance, $name, $bound); + } + if ($bound instanceof FloatType && ($boundClass === FloatType::class || $bound instanceof TemplateType)) { return new TemplateFloatType($scope, $strategy, $variance, $name, $bound); } diff --git a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php index 5da620da03..e59509170b 100644 --- a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php +++ b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php @@ -841,6 +841,12 @@ public function testBug7351(): void $this->assertNoErrors($errors); } + public function testBug7381(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-7381.php'); + $this->assertNoErrors($errors); + } + /** * @param string[]|null $allAnalysedFiles * @return Error[] diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index 16bf724d55..d13b7887bb 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -905,6 +905,7 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/non-empty-string-strcasing-specifying.php'); yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/conditional-complex-templates.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7374.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/template-constant-bound.php'); } /** diff --git a/tests/PHPStan/Analyser/data/bug-7381.php b/tests/PHPStan/Analyser/data/bug-7381.php new file mode 100644 index 0000000000..1f77743761 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-7381.php @@ -0,0 +1,47 @@ + + */ +trait AttributeTrait +{ + /** + * @template K of key-of + * @param K $key + * @return T[K]|null + */ + public function getAttribute(string $key) + { + return $this->getAttributes()[$key] ?? null; + } +} + +/** + * @phpstan-type Attrs array{foo?: string} + */ +class Foo { + /** @use AttributeTrait */ + use AttributeTrait; + + /** @return Attrs */ + public function getAttributes(): array + { + return []; + } +} + +/** + * @phpstan-type Attrs array{foo?: string, bar?: string} + */ +class Bar { + /** @use AttributeTrait */ + use AttributeTrait; + + /** @return Attrs */ + public function getAttributes(): array + { + return []; + } +} diff --git a/tests/PHPStan/Analyser/data/template-constant-bound.php b/tests/PHPStan/Analyser/data/template-constant-bound.php new file mode 100644 index 0000000000..7dcbdf23c7 --- /dev/null +++ b/tests/PHPStan/Analyser/data/template-constant-bound.php @@ -0,0 +1,17 @@ +