Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions src/PhpDoc/TypeNodeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,26 @@ private function resolveGenericTypeNode(GenericTypeNode $typeNode, NameScope $na
return new ErrorType();
} elseif ($mainTypeName === 'value-of') {
if (count($genericTypes) === 1) { // value-of<ValueType>
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();
}

Expand Down
4 changes: 4 additions & 0 deletions tests/PHPStan/Analyser/NodeScopeResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
36 changes: 36 additions & 0 deletions tests/PHPStan/Analyser/data/value-of-enum.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php // lint >= 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<Country>
*/
function us()
{
return Country::US;
}

/**
* @param value-of<Country> $countryName
*/
function hello($countryName)
{
assertType("'The Netherlands'|'United States'", $countryName);
}

function doFoo() {
assertType("'The Netherlands'|'United States'", $this->us());
}
}

16 changes: 16 additions & 0 deletions tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
],
]);
}

Expand Down
40 changes: 40 additions & 0 deletions tests/PHPStan/Rules/Methods/data/call-method-in-enum.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<Country> $countryName
*/
function hello(string $countryName): void
{
// ...
}

/**
* @param array<value-of<Country>, bool> $countryMap
*/
function helloArray(array $countryMap): void {
// ...
}

function doFooArray() {
$this->hello(CountryNo::NL);

// 'abc' does not match value-of<Country>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as expected, we now see errors here.

$this->helloArray(['abc' => true]);
$this->helloArray(['abc' => 123]);

// wrong key type
$this->helloArray([true]);
}
}
20 changes: 20 additions & 0 deletions tests/PHPStan/Rules/PhpDoc/IncompatiblePhpDocTypeRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
],
]);
}

}
47 changes: 47 additions & 0 deletions tests/PHPStan/Rules/PhpDoc/data/value-of-enum.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php // lint >= 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<Country> $countryName
*/
function hello(string $countryName): void
{
// ...
}

/**
* @param value-of<Country> $shouldError
*/
function helloError(int $shouldError): void
{
// ...
}
/**
* @param value-of<CountryNo> $shouldError
*/
function helloError2(string $shouldError): void
{
// ...
}

function doFoo() {
$this->hello(Country::NL);
}
}