Skip to content

Commit

Permalink
Support constant string/int as template bound
Browse files Browse the repository at this point in the history
  • Loading branch information
rvanvelzen authored and ondrejmirtes committed Jun 7, 2022
1 parent 23e95a9 commit 6fe8a46
Show file tree
Hide file tree
Showing 9 changed files with 209 additions and 0 deletions.
4 changes: 4 additions & 0 deletions src/Rules/Generics/TemplateTypeCheck.php
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
54 changes: 54 additions & 0 deletions src/Type/Generic/TemplateConstantIntegerType.php
@@ -0,0 +1,54 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type\Generic;

use PHPStan\Type\Constant\ConstantIntegerType;
use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait;
use PHPStan\Type\Type;

/** @api */
final class TemplateConstantIntegerType extends ConstantIntegerType implements TemplateType
{

/** @use TemplateTypeTrait<ConstantIntegerType> */
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;
}

}
54 changes: 54 additions & 0 deletions src/Type/Generic/TemplateConstantStringType.php
@@ -0,0 +1,54 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type\Generic;

use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait;
use PHPStan\Type\Type;

/** @api */
final class TemplateConstantStringType extends ConstantStringType implements TemplateType
{

/** @use TemplateTypeTrait<ConstantStringType> */
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;
}

}
10 changes: 10 additions & 0 deletions src/Type/Generic/TemplateTypeFactory.php
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
Expand Down
6 changes: 6 additions & 0 deletions tests/PHPStan/Analyser/AnalyserIntegrationTest.php
Expand Up @@ -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[]
Expand Down
1 change: 1 addition & 0 deletions tests/PHPStan/Analyser/NodeScopeResolverTest.php
Expand Up @@ -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');
}

/**
Expand Down
47 changes: 47 additions & 0 deletions tests/PHPStan/Analyser/data/bug-7381.php
@@ -0,0 +1,47 @@
<?php declare(strict_types = 1);

namespace Bug7381;

/**
* @template T of array<string, mixed>
*/
trait AttributeTrait
{
/**
* @template K of key-of<T>
* @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<Attrs> */
use AttributeTrait;

/** @return Attrs */
public function getAttributes(): array
{
return [];
}
}

/**
* @phpstan-type Attrs array{foo?: string, bar?: string}
*/
class Bar {
/** @use AttributeTrait<Attrs> */
use AttributeTrait;

/** @return Attrs */
public function getAttributes(): array
{
return [];
}
}
17 changes: 17 additions & 0 deletions tests/PHPStan/Analyser/data/template-constant-bound.php
@@ -0,0 +1,17 @@
<?php declare(strict_types = 1);

namespace TemplateConstantBound;

use function PHPStan\Testing\assertType;

/**
* @template T1 of 'foo'
* @template T2 of 5
* @param T1 $foo
* @param T2 $bar
*/
function foo(string $foo, int $bar): void
{
assertType("T1 of 'foo' (function TemplateConstantBound\\foo(), argument)", $foo);
assertType('T2 of 5 (function TemplateConstantBound\foo(), argument)', $bar);
}
16 changes: 16 additions & 0 deletions tests/PHPStan/Rules/Generics/data/class-template.php
Expand Up @@ -79,3 +79,19 @@ class Dolor
{

};

/**
* @template T of 'string'
*/
class Sit
{

}

/**
* @template T of 5
*/
class Amet
{

}

0 comments on commit 6fe8a46

Please sign in to comment.