From 724c8bad2e458852f177df26cb863ad62aa0c41d Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 8 Jun 2021 12:56:55 +0200 Subject: [PATCH] Bleeding edge - intersect array key type with int|string --- conf/config.neon | 2 + src/PhpDoc/TypeNodeResolver.php | 15 +++- .../Rules/Functions/ReturnTypeRuleTest.php | 7 +- .../PHPStan/Rules/Functions/data/bug-2568.php | 20 +++++ .../Rules/Methods/data/infer-array-key.php | 76 +++++++++++++++++++ .../PhpDoc/IncompatiblePhpDocTypeRuleTest.php | 4 + tests/PHPStan/Type/TypeCombinatorTest.php | 41 ++++++++++ 7 files changed, 162 insertions(+), 3 deletions(-) diff --git a/conf/config.neon b/conf/config.neon index 5a64dc9f22..f9251d5885 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -404,6 +404,8 @@ services: - class: PHPStan\PhpDoc\TypeNodeResolver + arguments: + deepInspectTypes: %featureToggles.deepInspectTypes% - class: PHPStan\PhpDoc\TypeNodeResolverExtensionRegistryProvider diff --git a/src/PhpDoc/TypeNodeResolver.php b/src/PhpDoc/TypeNodeResolver.php index 158a0da949..918e4a8197 100644 --- a/src/PhpDoc/TypeNodeResolver.php +++ b/src/PhpDoc/TypeNodeResolver.php @@ -73,13 +73,17 @@ class TypeNodeResolver private Container $container; + private bool $deepInspectTypes; + public function __construct( TypeNodeResolverExtensionRegistryProvider $extensionRegistryProvider, - Container $container + Container $container, + bool $deepInspectTypes = false ) { $this->extensionRegistryProvider = $extensionRegistryProvider; $this->container = $container; + $this->deepInspectTypes = $deepInspectTypes; } /** @api */ @@ -417,7 +421,14 @@ private function resolveGenericTypeNode(GenericTypeNode $typeNode, NameScope $na if (count($genericTypes) === 1) { // array $arrayType = new ArrayType(new MixedType(true), $genericTypes[0]); } elseif (count($genericTypes) === 2) { // array - $arrayType = new ArrayType($genericTypes[0], $genericTypes[1]); + $keyType = $genericTypes[0]; + if ($this->deepInspectTypes) { + $keyType = TypeCombinator::intersect($keyType, new UnionType([ + new IntegerType(), + new StringType(), + ])); + } + $arrayType = new ArrayType($keyType, $genericTypes[1]); } else { return new ErrorType(); } diff --git a/tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php b/tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php index 3567dd7190..9425f462b6 100644 --- a/tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php @@ -86,7 +86,12 @@ public function testIsGenerator(): void public function testBug2568(): void { require_once __DIR__ . '/data/bug-2568.php'; - $this->analyse([__DIR__ . '/data/bug-2568.php'], []); + $this->analyse([__DIR__ . '/data/bug-2568.php'], [ + [ + 'Function Bug2568\my_array_keys() should return array but returns array.', + 12, + ], + ]); } public function testBug2723(): void diff --git a/tests/PHPStan/Rules/Functions/data/bug-2568.php b/tests/PHPStan/Rules/Functions/data/bug-2568.php index 16f4ff0745..f86417ab64 100644 --- a/tests/PHPStan/Rules/Functions/data/bug-2568.php +++ b/tests/PHPStan/Rules/Functions/data/bug-2568.php @@ -11,3 +11,23 @@ function my_array_keys($arr) { return array_keys($arr); } + +/** + * @template T of array-key + * + * @param array $arr + * @return array + */ +function my_array_keys2($arr) { + return array_keys($arr); +} + +/** + * @template T of int|string + * + * @param array $arr + * @return array + */ +function my_array_keys3($arr) { + return array_keys($arr); +} diff --git a/tests/PHPStan/Rules/Methods/data/infer-array-key.php b/tests/PHPStan/Rules/Methods/data/infer-array-key.php index d43e6ed0f9..7107f700c2 100644 --- a/tests/PHPStan/Rules/Methods/data/infer-array-key.php +++ b/tests/PHPStan/Rules/Methods/data/infer-array-key.php @@ -22,3 +22,79 @@ public function getIterator() } } + +/** + * @implements \IteratorAggregate + */ +class Bar implements \IteratorAggregate +{ + + /** @var array */ + private $items; + + public function getIterator() + { + $it = new \ArrayIterator($this->items); + assertType('int', $it->key()); + + return $it; + } + +} + +/** + * @implements \IteratorAggregate + */ +class Baz implements \IteratorAggregate +{ + + /** @var array */ + private $items; + + public function getIterator() + { + $it = new \ArrayIterator($this->items); + assertType('string', $it->key()); + + return $it; + } + +} + +/** + * @implements \IteratorAggregate + */ +class Lorem implements \IteratorAggregate +{ + + /** @var array<\stdClass> */ + private $items; + + public function getIterator() + { + $it = new \ArrayIterator($this->items); + assertType('(int|string)', $it->key()); + + return $it; + } + +} + +/** + * @implements \IteratorAggregate + */ +class Ipsum implements \IteratorAggregate +{ + + /** @var array */ + private $items; + + public function getIterator() + { + $it = new \ArrayIterator($this->items); + assertType('int|string', $it->key()); + + return $it; + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/IncompatiblePhpDocTypeRuleTest.php b/tests/PHPStan/Rules/PhpDoc/IncompatiblePhpDocTypeRuleTest.php index bbe710b6a9..cfdf2131fc 100644 --- a/tests/PHPStan/Rules/PhpDoc/IncompatiblePhpDocTypeRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/IncompatiblePhpDocTypeRuleTest.php @@ -162,6 +162,10 @@ public function testBug3753(): void 'PHPDoc tag @param for parameter $foo contains unresolvable type.', 20, ], + [ + 'PHPDoc tag @param for parameter $bars contains unresolvable type.', + 28, + ], ]); } diff --git a/tests/PHPStan/Type/TypeCombinatorTest.php b/tests/PHPStan/Type/TypeCombinatorTest.php index 0694e0458d..b17212fb15 100644 --- a/tests/PHPStan/Type/TypeCombinatorTest.php +++ b/tests/PHPStan/Type/TypeCombinatorTest.php @@ -15,12 +15,14 @@ use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\Generic\GenericClassStringType; use PHPStan\Type\Generic\GenericObjectType; +use PHPStan\Type\Generic\TemplateBenevolentUnionType; use PHPStan\Type\Generic\TemplateObjectType; use PHPStan\Type\Generic\TemplateObjectWithoutClassType; use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\Generic\TemplateTypeFactory; use PHPStan\Type\Generic\TemplateTypeScope; use PHPStan\Type\Generic\TemplateTypeVariance; +use PHPStan\Type\Generic\TemplateUnionType; class TypeCombinatorTest extends \PHPStan\Testing\TestCase { @@ -2945,6 +2947,45 @@ public function dataIntersect(): array NeverType::class, '*NEVER*', ], + [ + [ + TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('my_array_keys'), + 'T', + new BenevolentUnionType([new IntegerType(), new StringType()]), + TemplateTypeVariance::createInvariant(), + ), + new UnionType([new IntegerType(), new StringType()]), + ], + TemplateBenevolentUnionType::class, + 'T of (int|string) (function my_array_keys(), parameter)', + ], + [ + [ + TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('my_array_keys'), + 'T', + new BenevolentUnionType([new IntegerType(), new StringType()]), + TemplateTypeVariance::createInvariant(), + ), + new BenevolentUnionType([new IntegerType(), new StringType()]), + ], + TemplateBenevolentUnionType::class, + 'T of (int|string) (function my_array_keys(), parameter)', + ], + [ + [ + TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('my_array_keys'), + 'T', + new UnionType([new IntegerType(), new StringType()]), + TemplateTypeVariance::createInvariant(), + ), + new UnionType([new IntegerType(), new StringType()]), + ], + UnionType::class, + 'T of int|string (function my_array_keys(), parameter)', + ], ]; }