Skip to content

Commit

Permalink
ObjectType - fix enum property with subtracted type
Browse files Browse the repository at this point in the history
  • Loading branch information
ondrejmirtes committed Feb 25, 2023
1 parent 53c643d commit eb00fd2
Show file tree
Hide file tree
Showing 5 changed files with 141 additions and 2 deletions.
24 changes: 24 additions & 0 deletions src/Type/ObjectType.php
Expand Up @@ -28,6 +28,7 @@
use PHPStan\Reflection\TrivialParametersAcceptor;
use PHPStan\Reflection\Type\CalledOnTypeUnresolvedMethodPrototypeReflection;
use PHPStan\Reflection\Type\CalledOnTypeUnresolvedPropertyPrototypeReflection;
use PHPStan\Reflection\Type\UnionTypePropertyReflection;
use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection;
use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection;
use PHPStan\ShouldNotHappenException;
Expand Down Expand Up @@ -154,6 +155,29 @@ public function hasProperty(string $propertyName): TrinaryLogic

public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): PropertyReflection
{
$classReflection = $this->getClassReflection();
if ($classReflection !== null) {
if ($classReflection->isEnum()) {
if (
$propertyName === 'name'
|| ($propertyName === 'value' && $classReflection->isBackedEnum())
) {
$properties = [];
foreach ($this->getEnumCases() as $enumCase) {
$properties[] = $enumCase->getProperty($propertyName, $scope);
}

if (count($properties) > 0) {
if (count($properties) === 1) {
return $properties[0];
}

return new UnionTypePropertyReflection($properties);
}
}
}
}

return $this->getUnresolvedPropertyPrototype($propertyName, $scope)->getTransformedProperty();
}

Expand Down
23 changes: 23 additions & 0 deletions src/Type/StaticType.php
Expand Up @@ -12,6 +12,7 @@
use PHPStan\Reflection\ReflectionProviderStaticAccessor;
use PHPStan\Reflection\Type\CallbackUnresolvedMethodPrototypeReflection;
use PHPStan\Reflection\Type\CallbackUnresolvedPropertyPrototypeReflection;
use PHPStan\Reflection\Type\UnionTypePropertyReflection;
use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection;
use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection;
use PHPStan\TrinaryLogic;
Expand All @@ -20,6 +21,7 @@
use PHPStan\Type\Traits\NonGeneralizableTypeTrait;
use PHPStan\Type\Traits\NonGenericTypeTrait;
use PHPStan\Type\Traits\UndecidedComparisonTypeTrait;
use function count;
use function get_class;
use function sprintf;

Expand Down Expand Up @@ -218,6 +220,27 @@ public function hasProperty(string $propertyName): TrinaryLogic

public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): PropertyReflection
{
$classReflection = $this->getClassReflection();
if ($classReflection->isEnum()) {
if (
$propertyName === 'name'
|| ($propertyName === 'value' && $classReflection->isBackedEnum())
) {
$properties = [];
foreach ($this->getEnumCases() as $enumCase) {
$properties[] = $enumCase->getProperty($propertyName, $scope);
}

if (count($properties) > 0) {
if (count($properties) === 1) {
return $properties[0];
}

return new UnionTypePropertyReflection($properties);
}
}
}

return $this->getUnresolvedPropertyPrototype($propertyName, $scope)->getTransformedProperty();
}

Expand Down
4 changes: 4 additions & 0 deletions tests/PHPStan/Analyser/NodeScopeResolverTest.php
Expand Up @@ -1207,6 +1207,10 @@ public function dataFileAsserts(): iterable
yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Constants/data/bug-8957.php');
}

if (PHP_VERSION_ID >= 80100) {
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8486.php');
}

yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8956.php');
}

Expand Down
88 changes: 88 additions & 0 deletions tests/PHPStan/Analyser/data/bug-8486.php
@@ -0,0 +1,88 @@
<?php // lint >= 8.1

namespace Bug8486;

use function PHPStan\Testing\assertType;

enum Operator: string
{
case Foo = 'foo';
case Bar = 'bar';
case None = '';

public function explode(): void
{
$character = match ($this) {
self::None => 'baz',
default => $this->value,
};

assertType("'bar'|'baz'|'foo'", $character);
}

public function typeInference(): void
{
match ($this) {
self::None => 'baz',
default => assertType('$this(Bug8486\Operator~Bug8486\Operator::None)', $this),
};
}

public function typeInference2(): void
{
if ($this === self::None) {
return;
}

assertType("'Bar'|'Foo'", $this->name);
assertType("'bar'|'foo'", $this->value);
}
}

class Foo
{

public function doFoo(Operator $operator)
{
$character = match ($operator) {
Operator::None => 'baz',
default => $operator->value,
};

assertType("'bar'|'baz'|'foo'", $character);
}

public function typeInference(Operator $operator): void
{
match ($operator) {
Operator::None => 'baz',
default => assertType('Bug8486\Operator~Bug8486\Operator::None', $operator),
};
}

public function typeInference2(Operator $operator): void
{
if ($operator === Operator::None) {
return;
}

assertType("'Bar'|'Foo'", $operator->name);
assertType("'bar'|'foo'", $operator->value);
}

public function typeInference3(Operator $operator): void
{
if ($operator === Operator::None) {
return;
}

if ($operator === Operator::Foo) {
return;
}

assertType("Bug8486\Operator::Bar", $operator);
assertType("'Bar'", $operator->name);
assertType("'bar'", $operator->value);
}

}
Expand Up @@ -84,7 +84,7 @@ public function testRule(): void
'Readonly property ReadonlyPropertyAssign\ListAssign::$foo is assigned outside of the constructor.',
127,
],
[
/*[
'Readonly property ReadonlyPropertyAssign\FooEnum::$name is assigned outside of the constructor.',
140,
],
Expand All @@ -99,7 +99,7 @@ public function testRule(): void
[
'Readonly property ReadonlyPropertyAssign\FooEnum::$value is assigned outside of its declaring class.',
152,
],
],*/
[
'Readonly property ReadonlyPropertyAssign\Foo::$baz is assigned outside of its declaring class.',
162,
Expand Down

0 comments on commit eb00fd2

Please sign in to comment.