From b944c2c53cf347f200266336503d099f0ab74658 Mon Sep 17 00:00:00 2001 From: Sam Mousa Date: Fri, 24 Oct 2025 10:03:56 +0200 Subject: [PATCH 1/4] feat: isEnum class-string narrowing --- stubs/ReflectionClass.stub | 7 +++++++ .../Analyser/data/enum-reflection-php81.php | 14 ++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/stubs/ReflectionClass.stub b/stubs/ReflectionClass.stub index 06f8e08a2e..9e026d809c 100644 --- a/stubs/ReflectionClass.stub +++ b/stubs/ReflectionClass.stub @@ -48,4 +48,11 @@ class ReflectionClass public function getAttributes(?string $name = null, int $flags = 0) { } + + /** + * @phpstan-assert-if-true class-string $this->getName() + */ + public function isEnum(): bool + { + } } diff --git a/tests/PHPStan/Analyser/data/enum-reflection-php81.php b/tests/PHPStan/Analyser/data/enum-reflection-php81.php index 502de6eebd..89b1168732 100644 --- a/tests/PHPStan/Analyser/data/enum-reflection-php81.php +++ b/tests/PHPStan/Analyser/data/enum-reflection-php81.php @@ -2,6 +2,7 @@ namespace EnumReflection81; +use ReflectionClass; use ReflectionEnum; use ReflectionEnumBackedCase; use ReflectionEnumUnitCase; @@ -21,3 +22,16 @@ function testNarrowGetBackingTypeAfterIsBacked() { assertType('ReflectionType', $r->getBackingType()); } } + +function testNarrowClassAfterIsEnum() { + /** + * @var class-string + */ + $classString = Foo::class; + $r = new ReflectionClass($classString); + assertType('class-string', $classString); + if ($r->isEnum()) { + assertType('class-string<\UnitEnum>', $classString); + } +} + From 1f6cc7116e041602bc93144fdc33d2369b2e85c6 Mon Sep 17 00:00:00 2001 From: Sam Mousa Date: Fri, 24 Oct 2025 12:31:02 +0200 Subject: [PATCH 2/4] chore: move test to separate file --- stubs/ReflectionClass.stub | 4 +++- .../Analyser/NodeScopeResolverTest.php | 1 + .../Analyser/data/enum-reflection-php81.php | 16 ---------------- .../Analyser/data/reflectionclass-isEnum.php | 19 +++++++++++++++++++ 4 files changed, 23 insertions(+), 17 deletions(-) create mode 100644 tests/PHPStan/Analyser/data/reflectionclass-isEnum.php diff --git a/stubs/ReflectionClass.stub b/stubs/ReflectionClass.stub index 9e026d809c..8ba2984be5 100644 --- a/stubs/ReflectionClass.stub +++ b/stubs/ReflectionClass.stub @@ -50,7 +50,9 @@ class ReflectionClass } /** - * @phpstan-assert-if-true class-string $this->getName() + * @phpstan-assert-if-true class-string<\UnitEnum> $this->name + * @phpstan-assert-if-true ReflectionClass<\UnitEnum> $this + * @phpstan-assert-if-false !class-string<\UnitEnum> $this->name */ public function isEnum(): bool { diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index a90728cb30..06d96763ab 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -238,6 +238,7 @@ private static function findTestFiles(): iterable yield __DIR__ . '/../Rules/Methods/data/bug-4801.php'; yield __DIR__ . '/../Rules/Arrays/data/narrow-superglobal.php'; yield __DIR__ . '/../Rules/Methods/data/bug-12927.php'; + yield __DIR__ . '/data/reflectionclass-isEnum.php'; } /** diff --git a/tests/PHPStan/Analyser/data/enum-reflection-php81.php b/tests/PHPStan/Analyser/data/enum-reflection-php81.php index 89b1168732..57e6f67ee7 100644 --- a/tests/PHPStan/Analyser/data/enum-reflection-php81.php +++ b/tests/PHPStan/Analyser/data/enum-reflection-php81.php @@ -2,10 +2,7 @@ namespace EnumReflection81; -use ReflectionClass; use ReflectionEnum; -use ReflectionEnumBackedCase; -use ReflectionEnumUnitCase; use function PHPStan\Testing\assertType; enum Foo: int @@ -22,16 +19,3 @@ function testNarrowGetBackingTypeAfterIsBacked() { assertType('ReflectionType', $r->getBackingType()); } } - -function testNarrowClassAfterIsEnum() { - /** - * @var class-string - */ - $classString = Foo::class; - $r = new ReflectionClass($classString); - assertType('class-string', $classString); - if ($r->isEnum()) { - assertType('class-string<\UnitEnum>', $classString); - } -} - diff --git a/tests/PHPStan/Analyser/data/reflectionclass-isEnum.php b/tests/PHPStan/Analyser/data/reflectionclass-isEnum.php new file mode 100644 index 0000000000..feeab9b78d --- /dev/null +++ b/tests/PHPStan/Analyser/data/reflectionclass-isEnum.php @@ -0,0 +1,19 @@ += 8.1 + +namespace ReflectionClassIsEnum; + +use ReflectionClass; +use function PHPStan\Testing\assertType; + +/** + * @param class-string $class + */ +function testNarrowClassAfterIsEnum(string $class): void { + $r = new ReflectionClass($class); + assertType('class-string', $r->name); + if ($r->isEnum()) { + assertType('ReflectionClass<\UnitEnum>', $r); + assertType('class-string<\UnitEnum>', $r->name); + } +} + From 95ad39c19513e3986bc24661f567edc36963fcf3 Mon Sep 17 00:00:00 2001 From: Sam Mousa Date: Mon, 27 Oct 2025 12:32:37 +0100 Subject: [PATCH 3/4] feat: type narrowing for ReflectionClass:: --- stubs/ReflectionClass.stub | 18 +++++----- stubs/ReflectionClassWithLazyObjects.stub | 9 +++++ .../Analyser/NodeScopeResolverTest.php | 1 - .../Analyser/data/reflectionclass-isEnum.php | 19 ---------- .../Analyser/nsrt/reflectionclass-isEnum.php | 36 +++++++++++++++++++ 5 files changed, 55 insertions(+), 28 deletions(-) delete mode 100644 tests/PHPStan/Analyser/data/reflectionclass-isEnum.php create mode 100644 tests/PHPStan/Analyser/nsrt/reflectionclass-isEnum.php diff --git a/stubs/ReflectionClass.stub b/stubs/ReflectionClass.stub index 8ba2984be5..39f65a4e82 100644 --- a/stubs/ReflectionClass.stub +++ b/stubs/ReflectionClass.stub @@ -49,12 +49,14 @@ class ReflectionClass { } - /** - * @phpstan-assert-if-true class-string<\UnitEnum> $this->name - * @phpstan-assert-if-true ReflectionClass<\UnitEnum> $this - * @phpstan-assert-if-false !class-string<\UnitEnum> $this->name - */ - public function isEnum(): bool - { - } + /** + * @phpstan-assert-if-true class-string<\UnitEnum> $this->name + * @phpstan-assert-if-true class-string<\UnitEnum> $this->getName() + * @phpstan-assert-if-true ReflectionClass $this + * @return (T is \UnitEnum ? true: false) + */ + public function isEnum(): bool + { + } + } diff --git a/stubs/ReflectionClassWithLazyObjects.stub b/stubs/ReflectionClassWithLazyObjects.stub index 1a53ae8750..2c09505532 100644 --- a/stubs/ReflectionClassWithLazyObjects.stub +++ b/stubs/ReflectionClassWithLazyObjects.stub @@ -110,4 +110,13 @@ class ReflectionClass public function isUninitializedLazyObject(object $object): bool { } + + /** + * @phpstan-assert-if-true class-string<\UnitEnum> $this->name + * @phpstan-assert-if-true class-string<\UnitEnum> $this->getName() + * @return (T is \UnitEnum ? true: false) + */ + public function isEnum(): bool + { + } } diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index 06d96763ab..a90728cb30 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -238,7 +238,6 @@ private static function findTestFiles(): iterable yield __DIR__ . '/../Rules/Methods/data/bug-4801.php'; yield __DIR__ . '/../Rules/Arrays/data/narrow-superglobal.php'; yield __DIR__ . '/../Rules/Methods/data/bug-12927.php'; - yield __DIR__ . '/data/reflectionclass-isEnum.php'; } /** diff --git a/tests/PHPStan/Analyser/data/reflectionclass-isEnum.php b/tests/PHPStan/Analyser/data/reflectionclass-isEnum.php deleted file mode 100644 index feeab9b78d..0000000000 --- a/tests/PHPStan/Analyser/data/reflectionclass-isEnum.php +++ /dev/null @@ -1,19 +0,0 @@ -= 8.1 - -namespace ReflectionClassIsEnum; - -use ReflectionClass; -use function PHPStan\Testing\assertType; - -/** - * @param class-string $class - */ -function testNarrowClassAfterIsEnum(string $class): void { - $r = new ReflectionClass($class); - assertType('class-string', $r->name); - if ($r->isEnum()) { - assertType('ReflectionClass<\UnitEnum>', $r); - assertType('class-string<\UnitEnum>', $r->name); - } -} - diff --git a/tests/PHPStan/Analyser/nsrt/reflectionclass-isEnum.php b/tests/PHPStan/Analyser/nsrt/reflectionclass-isEnum.php new file mode 100644 index 0000000000..7279eaf2ef --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/reflectionclass-isEnum.php @@ -0,0 +1,36 @@ += 8.1 + +namespace ReflectionClassIsEnum; + +use ReflectionClass; +use function PHPStan\Testing\assertType; + +/** + * @param class-string $class + */ +function testNarrowClassAfterIsEnum(string $class): void { + $r = new ReflectionClass($class); + if ($r->isEnum()) { + assertType('class-string', $r->name); + assertType('class-string', $r->getName()); + + + // Todo: + //assertType('ReflectionClass', $r); + + } + + + +} + +function testTemplateAssertions(): void { + $enumR = new ReflectionClass(Foo::class); + assertType('ReflectionClass', $enumR); + assertType('true', $enumR->isEnum()); +} + + +enum Foo { + case Bar; +} From e636966fd792c5d7a5ee8e2f9a46ad2d75355cae Mon Sep 17 00:00:00 2001 From: Sam Mousa Date: Mon, 27 Oct 2025 14:35:32 +0100 Subject: [PATCH 4/4] chore: sync annotations between stubs --- stubs/ReflectionClassWithLazyObjects.stub | 1 + tests/PHPStan/Analyser/nsrt/reflectionclass-isEnum.php | 2 ++ 2 files changed, 3 insertions(+) diff --git a/stubs/ReflectionClassWithLazyObjects.stub b/stubs/ReflectionClassWithLazyObjects.stub index 2c09505532..373e3deaaa 100644 --- a/stubs/ReflectionClassWithLazyObjects.stub +++ b/stubs/ReflectionClassWithLazyObjects.stub @@ -114,6 +114,7 @@ class ReflectionClass /** * @phpstan-assert-if-true class-string<\UnitEnum> $this->name * @phpstan-assert-if-true class-string<\UnitEnum> $this->getName() + * @phpstan-assert-if-true ReflectionClass $this * @return (T is \UnitEnum ? true: false) */ public function isEnum(): bool diff --git a/tests/PHPStan/Analyser/nsrt/reflectionclass-isEnum.php b/tests/PHPStan/Analyser/nsrt/reflectionclass-isEnum.php index 7279eaf2ef..2937e75789 100644 --- a/tests/PHPStan/Analyser/nsrt/reflectionclass-isEnum.php +++ b/tests/PHPStan/Analyser/nsrt/reflectionclass-isEnum.php @@ -14,6 +14,8 @@ function testNarrowClassAfterIsEnum(string $class): void { assertType('class-string', $r->name); assertType('class-string', $r->getName()); + assertType('UnitEnum', $r->newInstance()); + // Todo: //assertType('ReflectionClass', $r);