diff --git a/src/PhpDoc/TypeNodeResolver.php b/src/PhpDoc/TypeNodeResolver.php index be7f04e1e4..3c832876b5 100644 --- a/src/PhpDoc/TypeNodeResolver.php +++ b/src/PhpDoc/TypeNodeResolver.php @@ -525,6 +525,26 @@ private function resolveGenericTypeNode(GenericTypeNode $typeNode, NameScope $na return new ErrorType(); } elseif ($mainTypeName === 'value-of') { if (count($genericTypes) === 1) { // value-of + if ($genericTypes[0] instanceof TypeWithClassName) { + if ($this->getReflectionProvider()->hasClass($genericTypes[0]->getClassName())) { + $classReflection = $this->getReflectionProvider()->getClass($genericTypes[0]->getClassName()); + + if ($classReflection->isBackedEnum()) { + $cases = []; + foreach ($classReflection->getEnumCases() as $enumCaseReflection) { + $backingType = $enumCaseReflection->getBackingValueType(); + if ($backingType === null) { + continue; + } + + $cases[] = $backingType; + } + + return TypeCombinator::union(...$cases); + } + } + } + return $genericTypes[0]->getIterableValueType(); } diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index 27e9bd2f01..e0ec6c27d9 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -806,6 +806,10 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6251.php'); } + if (PHP_VERSION_ID >= 80100) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/value-of-enum.php'); + } + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6584.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6439.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6748.php'); diff --git a/tests/PHPStan/Analyser/data/value-of-enum.php b/tests/PHPStan/Analyser/data/value-of-enum.php new file mode 100644 index 0000000000..34ee4447f6 --- /dev/null +++ b/tests/PHPStan/Analyser/data/value-of-enum.php @@ -0,0 +1,36 @@ += 8.1 + +declare(strict_types=1); + +namespace ValueOfEnum; + +use function PHPStan\Testing\assertType; + +enum Country: string +{ + case NL = 'The Netherlands'; + case US = 'United States'; +} + +class Foo { + /** + * @return value-of + */ + function us() + { + return Country::US; + } + + /** + * @param value-of $countryName + */ + function hello($countryName) + { + assertType("'The Netherlands'|'United States'", $countryName); + } + + function doFoo() { + assertType("'The Netherlands'|'United States'", $this->us()); + } +} + diff --git a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php index e973f044e9..68d33c7cfc 100644 --- a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php @@ -2287,6 +2287,22 @@ public function testEnums(): void 'Call to an undefined method CallMethodInEnum\Bar::doNonexistent().', 22, ], + [ + 'Parameter #1 $countryName of method CallMethodInEnum\FooCall::hello() expects \'The Netherlands\'|\'United States\', CallMethodInEnum\CountryNo::NL given.', + 63, + ], + [ + 'Parameter #1 $countryMap of method CallMethodInEnum\FooCall::helloArray() expects array<\'The Netherlands\'|\'United States\', bool>, array{abc: true} given.', + 66, + ], + [ + 'Parameter #1 $countryMap of method CallMethodInEnum\FooCall::helloArray() expects array<\'The Netherlands\'|\'United States\', bool>, array{abc: 123} given.', + 67, + ], + [ + 'Parameter #1 $countryMap of method CallMethodInEnum\FooCall::helloArray() expects array<\'The Netherlands\'|\'United States\', bool>, array{true} given.', + 70, + ], ]); } diff --git a/tests/PHPStan/Rules/Methods/data/call-method-in-enum.php b/tests/PHPStan/Rules/Methods/data/call-method-in-enum.php index c7a2393a67..986fc854cb 100644 --- a/tests/PHPStan/Rules/Methods/data/call-method-in-enum.php +++ b/tests/PHPStan/Rules/Methods/data/call-method-in-enum.php @@ -30,3 +30,43 @@ enum Bar use FooTrait; } + +enum Country: string +{ + case NL = 'The Netherlands'; + case US = 'United States'; +} + +enum CountryNo: int +{ + case NL = 1; + case US = 2; +} + +enum FooCall { + /** + * @param value-of $countryName + */ + function hello(string $countryName): void + { + // ... + } + + /** + * @param array, bool> $countryMap + */ + function helloArray(array $countryMap): void { + // ... + } + + function doFooArray() { + $this->hello(CountryNo::NL); + + // 'abc' does not match value-of + $this->helloArray(['abc' => true]); + $this->helloArray(['abc' => 123]); + + // wrong key type + $this->helloArray([true]); + } +} diff --git a/tests/PHPStan/Rules/PhpDoc/IncompatiblePhpDocTypeRuleTest.php b/tests/PHPStan/Rules/PhpDoc/IncompatiblePhpDocTypeRuleTest.php index d7cf8d5470..2783431de3 100644 --- a/tests/PHPStan/Rules/PhpDoc/IncompatiblePhpDocTypeRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/IncompatiblePhpDocTypeRuleTest.php @@ -204,4 +204,24 @@ public function testEnums(): void ]); } + public function testValueOfEnum(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('This test needs PHP 8.1'); + } + + require __DIR__ . '/data/value-of-enum.php'; + + $this->analyse([__DIR__ . '/data/value-of-enum.php'], [ + [ + 'PHPDoc tag @param for parameter $shouldError with type string is incompatible with native type int.', + 31, + ], + [ + 'PHPDoc tag @param for parameter $shouldError with type int is incompatible with native type string.', + 38, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/PhpDoc/data/value-of-enum.php b/tests/PHPStan/Rules/PhpDoc/data/value-of-enum.php new file mode 100644 index 0000000000..aa467170c7 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/value-of-enum.php @@ -0,0 +1,47 @@ += 8.1 + +declare(strict_types=1); + +namespace ValueOfEnum; + +enum Country: string +{ + case NL = 'The Netherlands'; + case US = 'United States'; +} + +enum CountryNo: int +{ + case NL = 1; + case US = 2; +} + +class Foo { + /** + * @param value-of $countryName + */ + function hello(string $countryName): void + { + // ... + } + + /** + * @param value-of $shouldError + */ + function helloError(int $shouldError): void + { + // ... + } + /** + * @param value-of $shouldError + */ + function helloError2(string $shouldError): void + { + // ... + } + + function doFoo() { + $this->hello(Country::NL); + } +} +