Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
8 changes: 8 additions & 0 deletions src/Type/ValueOfType.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use PHPStan\Type\Generic\TemplateType;
use PHPStan\Type\Generic\TemplateTypeVariance;
use PHPStan\Type\Traits\LateResolvableTypeTrait;
use PHPStan\Type\Traits\NonGeneralizableTypeTrait;
Expand Down Expand Up @@ -50,6 +51,13 @@ public function isResolvable(): bool
protected function getResult(): Type
{
if ($this->type->isEnum()->yes()) {
if (
$this->type instanceof TemplateType
&& $this->type->getBound()->equals(new ObjectType('BackedEnum'))
Copy link
Contributor

Choose a reason for hiding this comment

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

should this be the other way arround?

Suggested change
&& $this->type->getBound()->equals(new ObjectType('BackedEnum'))
&& new ObjectType('BackedEnum')->isSuperTypeOf($this->type->getBound())->yes()?

Copy link
Contributor

@staabm staabm Nov 11, 2025

Choose a reason for hiding this comment

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

just had a look into the code-base. it seems we have spots where a Bound is compared using equals and sometimes it uses isSuperTypeOf.

I am not sure whether thats on purpose or an oversight

Copy link
Contributor Author

Choose a reason for hiding this comment

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

isSuperTypeOf is better here indeed, in case someone use

interface SuperBackedEnum extends \BackedEnum

and a template of SuperBackedEnum.

test added

) {
return new UnionType([new IntegerType(), new StringType()]);
}

$valueTypes = [];
foreach ($this->type->getEnumCases() as $enumCase) {
$valueType = $enumCase->getBackingValueType();
Expand Down
46 changes: 46 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-13282.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php // lint >= 8.1

namespace Bug13283;

use function PHPStan\Testing\assertType;

enum Test: string
{
case NAME = 'name';
case VALUE = 'value';
}

/**
* @template T of \BackedEnum
* @param class-string<T> $enum
* @phpstan-assert null|value-of<T> $value
*/
function assertValue(mixed $value, string $enum): void
{
if (null === $value) {
return;
}

if (! is_int($value) && ! is_string($value)) {
throw new \Exception();
}

if (null === $enum::tryFrom($value)) {
throw new \Exception();
}
}

function getFromRequest(): mixed
{
return 'name';
}

$v = getFromRequest();

assertType('mixed', $v);

assertValue($v, Test::class);

assertType("'name'|'value'|null", $v);

$a = null !== $v ? Test::from($v) : null;
Original file line number Diff line number Diff line change
Expand Up @@ -1034,6 +1034,12 @@ public function testBug13208(): void
$this->analyse([__DIR__ . '/data/bug-13208.php'], []);
}

#[RequiresPhp('>= 8.1')]
public function testBug13282(): void
{
$this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-13282.php'], []);
}

public function testBug11609(): void
{
$this->analyse([__DIR__ . '/data/bug-11609.php'], [
Expand Down
8 changes: 8 additions & 0 deletions tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,14 @@ public function testBug12274(): void
]);
}

#[RequiresPhp('>= 8.1')]
public function testBug13638(): void
{
$this->checkNullables = true;
$this->checkExplicitMixed = true;
$this->analyse([__DIR__ . '/data/bug-13638.php'], []);
}

public function testBug13484(): void
{
$this->checkExplicitMixed = true;
Expand Down
15 changes: 15 additions & 0 deletions tests/PHPStan/Rules/Functions/data/bug-13638.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php // lint >= 8.1

namespace Bug13638;

use BackedEnum;

/**
* @template T of BackedEnum
* @param ?value-of<T> $a
* @return ($a is null ? list<?value-of<T>> : list<value-of<T>>)
*/
function test1(int | string | null $a): array
{
return [$a];
}
10 changes: 10 additions & 0 deletions tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3666,6 +3666,16 @@ public function testBug3396(): void
$this->analyse([__DIR__ . '/data/bug-3396.php'], []);
}

#[RequiresPhp('>= 8.1')]
public function testBug12219(): void
{
$this->checkThisOnly = false;
$this->checkNullables = true;
$this->checkUnionTypes = true;
$this->checkExplicitMixed = true;
$this->analyse([__DIR__ . '/data/bug-12219.php'], []);
}

public function testBug13511(): void
{
$this->checkThisOnly = false;
Expand Down
26 changes: 26 additions & 0 deletions tests/PHPStan/Rules/Methods/data/bug-12219.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php // lint >= 8.1

namespace Bug12219;

class Bug
{
/**
* @template T of \BackedEnum
* @param class-string<T> $class
* @param value-of<T> $value
*/
public function foo(string $class, mixed $value): void
{
}

/**
* @template Q of \BackedEnum
* @param class-string<Q> $class
* @param value-of<Q> $value
*/
public function bar(string $class, mixed $value): void
{
// Parameter #2 $value of static method Bug::foo() contains unresolvable type.
$this->foo($class, $value);
}
}
Loading