From d05e42cc1d6a413b7540306a3f4f829b91b362f6 Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Wed, 2 Jul 2025 15:31:56 +0900 Subject: [PATCH 1/8] Added Enum stubs to make method purity precise --- .../ReflectionEnumStubFilesExtension.php | 9 +++++++-- stubs/BackedEnum.stub | 14 ++++++++++++++ stubs/UnitEnum.stub | 10 ++++++++++ .../Rules/Pure/PureFunctionRuleTest.php | 6 ++++++ tests/PHPStan/Rules/Pure/data/bug-13201.php | 19 +++++++++++++++++++ 5 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 stubs/BackedEnum.stub create mode 100644 stubs/UnitEnum.stub create mode 100644 tests/PHPStan/Rules/Pure/data/bug-13201.php diff --git a/src/PhpDoc/ReflectionEnumStubFilesExtension.php b/src/PhpDoc/ReflectionEnumStubFilesExtension.php index d48519b735..0854173b94 100644 --- a/src/PhpDoc/ReflectionEnumStubFilesExtension.php +++ b/src/PhpDoc/ReflectionEnumStubFilesExtension.php @@ -19,11 +19,16 @@ public function getFiles(): array return []; } + $files = [ + __DIR__ . '/../../stubs/UnitEnum.stub', + __DIR__ . '/../../stubs/BackedEnum.stub', + ]; + if (!$this->phpVersion->supportsLazyObjects()) { - return [__DIR__ . '/../../stubs/ReflectionEnum.stub']; + return [__DIR__ . '/../../stubs/ReflectionEnum.stub', ...$files]; } - return [__DIR__ . '/../../stubs/ReflectionEnumWithLazyObjects.stub']; + return [__DIR__ . '/../../stubs/ReflectionEnumWithLazyObjects.stub', ...$files]; } } diff --git a/stubs/BackedEnum.stub b/stubs/BackedEnum.stub new file mode 100644 index 0000000000..2450d9e90e --- /dev/null +++ b/stubs/BackedEnum.stub @@ -0,0 +1,14 @@ + + * @pure + */ + public static function cases(): array; +} diff --git a/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php b/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php index 44e326a0d6..66d1cec464 100644 --- a/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php +++ b/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php @@ -174,4 +174,10 @@ public function testBug12224(): void ]); } + #[RequiresPhp('>= 8.1')] + public function testBug13201(): void + { + $this->analyse([__DIR__ . '/data/bug-13201.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Pure/data/bug-13201.php b/tests/PHPStan/Rules/Pure/data/bug-13201.php new file mode 100644 index 0000000000..09ed46ef53 --- /dev/null +++ b/tests/PHPStan/Rules/Pure/data/bug-13201.php @@ -0,0 +1,19 @@ += 8.1 + +namespace PHPStan\Rules\Pure\data\Bug13201; + +enum Foo: string +{ + + case Bar = 'bar'; + case Unknown = 'unknown'; + +} + +/** + * @pure + */ +function createWithFallback(string $type): Foo +{ + return Foo::tryFrom($type) ?? Foo::Unknown; +} From e0afa6c16f96c67b4daccec772c61c9024d4db27 Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Fri, 4 Jul 2025 22:28:04 +0900 Subject: [PATCH 2/8] Add PHPDoc type --- stubs/BackedEnum.stub | 2 ++ 1 file changed, 2 insertions(+) diff --git a/stubs/BackedEnum.stub b/stubs/BackedEnum.stub index 2450d9e90e..680c687e29 100644 --- a/stubs/BackedEnum.stub +++ b/stubs/BackedEnum.stub @@ -3,11 +3,13 @@ interface BackedEnum extends UnitEnum { /** + * @return static * @pure */ public static function from(int|string $value): static; /** + * @return ?static * @pure */ public static function tryFrom(int|string $value): ?static; From f54f78534c117effb54317cfa99d15b29ce0f7ad Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Sat, 5 Jul 2025 19:30:39 +0900 Subject: [PATCH 3/8] Revert "Add PHPDoc type" This reverts commit e0afa6c16f96c67b4daccec772c61c9024d4db27. --- stubs/BackedEnum.stub | 2 -- 1 file changed, 2 deletions(-) diff --git a/stubs/BackedEnum.stub b/stubs/BackedEnum.stub index 680c687e29..2450d9e90e 100644 --- a/stubs/BackedEnum.stub +++ b/stubs/BackedEnum.stub @@ -3,13 +3,11 @@ interface BackedEnum extends UnitEnum { /** - * @return static * @pure */ public static function from(int|string $value): static; /** - * @return ?static * @pure */ public static function tryFrom(int|string $value): ?static; From 6ef434cb824a76191b70f3a113901ba89d2db17a Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Sat, 5 Jul 2025 19:30:43 +0900 Subject: [PATCH 4/8] Revert "Added Enum stubs to make method purity precise" This reverts commit d05e42cc1d6a413b7540306a3f4f829b91b362f6. --- .../ReflectionEnumStubFilesExtension.php | 9 ++------- stubs/BackedEnum.stub | 14 -------------- stubs/UnitEnum.stub | 10 ---------- .../Rules/Pure/PureFunctionRuleTest.php | 6 ------ tests/PHPStan/Rules/Pure/data/bug-13201.php | 19 ------------------- 5 files changed, 2 insertions(+), 56 deletions(-) delete mode 100644 stubs/BackedEnum.stub delete mode 100644 stubs/UnitEnum.stub delete mode 100644 tests/PHPStan/Rules/Pure/data/bug-13201.php diff --git a/src/PhpDoc/ReflectionEnumStubFilesExtension.php b/src/PhpDoc/ReflectionEnumStubFilesExtension.php index 0854173b94..d48519b735 100644 --- a/src/PhpDoc/ReflectionEnumStubFilesExtension.php +++ b/src/PhpDoc/ReflectionEnumStubFilesExtension.php @@ -19,16 +19,11 @@ public function getFiles(): array return []; } - $files = [ - __DIR__ . '/../../stubs/UnitEnum.stub', - __DIR__ . '/../../stubs/BackedEnum.stub', - ]; - if (!$this->phpVersion->supportsLazyObjects()) { - return [__DIR__ . '/../../stubs/ReflectionEnum.stub', ...$files]; + return [__DIR__ . '/../../stubs/ReflectionEnum.stub']; } - return [__DIR__ . '/../../stubs/ReflectionEnumWithLazyObjects.stub', ...$files]; + return [__DIR__ . '/../../stubs/ReflectionEnumWithLazyObjects.stub']; } } diff --git a/stubs/BackedEnum.stub b/stubs/BackedEnum.stub deleted file mode 100644 index 2450d9e90e..0000000000 --- a/stubs/BackedEnum.stub +++ /dev/null @@ -1,14 +0,0 @@ - - * @pure - */ - public static function cases(): array; -} diff --git a/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php b/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php index 66d1cec464..44e326a0d6 100644 --- a/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php +++ b/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php @@ -174,10 +174,4 @@ public function testBug12224(): void ]); } - #[RequiresPhp('>= 8.1')] - public function testBug13201(): void - { - $this->analyse([__DIR__ . '/data/bug-13201.php'], []); - } - } diff --git a/tests/PHPStan/Rules/Pure/data/bug-13201.php b/tests/PHPStan/Rules/Pure/data/bug-13201.php deleted file mode 100644 index 09ed46ef53..0000000000 --- a/tests/PHPStan/Rules/Pure/data/bug-13201.php +++ /dev/null @@ -1,19 +0,0 @@ -= 8.1 - -namespace PHPStan\Rules\Pure\data\Bug13201; - -enum Foo: string -{ - - case Bar = 'bar'; - case Unknown = 'unknown'; - -} - -/** - * @pure - */ -function createWithFallback(string $type): Foo -{ - return Foo::tryFrom($type) ?? Foo::Unknown; -} From 9ae7517ac79be1d72db1e62a1956d76fd497e42f Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Sat, 5 Jul 2025 19:31:36 +0900 Subject: [PATCH 5/8] Removed unnecessary workaround for BackedEnum revert e97439c5987fc7747b68d3617a128fad68102642 --- src/Reflection/SignatureMap/Php8SignatureMapProvider.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Reflection/SignatureMap/Php8SignatureMapProvider.php b/src/Reflection/SignatureMap/Php8SignatureMapProvider.php index c706e7dbbf..61b1553782 100644 --- a/src/Reflection/SignatureMap/Php8SignatureMapProvider.php +++ b/src/Reflection/SignatureMap/Php8SignatureMapProvider.php @@ -64,9 +64,6 @@ public function __construct( public function hasMethodSignature(string $className, string $methodName): bool { $lowerClassName = strtolower($className); - if ($lowerClassName === 'backedenum') { - return false; - } if (!array_key_exists($lowerClassName, $this->map->classes)) { return $this->functionSignatureMapProvider->hasMethodSignature($className, $methodName); } From 113c0cf5adeef4ccbe15c0c5ebccfd488e3fdf48 Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Sat, 5 Jul 2025 19:44:39 +0900 Subject: [PATCH 6/8] Add test --- .../Rules/Pure/PureFunctionRuleTest.php | 6 ++++++ tests/PHPStan/Rules/Pure/data/bug-13201.php | 19 +++++++++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 tests/PHPStan/Rules/Pure/data/bug-13201.php diff --git a/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php b/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php index 44e326a0d6..66d1cec464 100644 --- a/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php +++ b/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php @@ -174,4 +174,10 @@ public function testBug12224(): void ]); } + #[RequiresPhp('>= 8.1')] + public function testBug13201(): void + { + $this->analyse([__DIR__ . '/data/bug-13201.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Pure/data/bug-13201.php b/tests/PHPStan/Rules/Pure/data/bug-13201.php new file mode 100644 index 0000000000..09ed46ef53 --- /dev/null +++ b/tests/PHPStan/Rules/Pure/data/bug-13201.php @@ -0,0 +1,19 @@ += 8.1 + +namespace PHPStan\Rules\Pure\data\Bug13201; + +enum Foo: string +{ + + case Bar = 'bar'; + case Unknown = 'unknown'; + +} + +/** + * @pure + */ +function createWithFallback(string $type): Foo +{ + return Foo::tryFrom($type) ?? Foo::Unknown; +} From 8732aa9ac820d882f0c710c808a17cf1fe08cb77 Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Sun, 6 Jul 2025 07:55:28 +0900 Subject: [PATCH 7/8] Checking for side effects from ancestor metadata --- src/Reflection/Php/PhpClassReflectionExtension.php | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Reflection/Php/PhpClassReflectionExtension.php b/src/Reflection/Php/PhpClassReflectionExtension.php index 4e77208566..424bb6cb30 100644 --- a/src/Reflection/Php/PhpClassReflectionExtension.php +++ b/src/Reflection/Php/PhpClassReflectionExtension.php @@ -839,7 +839,17 @@ public function createUserlandMethodReflection(ClassReflection $fileDeclaringCla } $isInternal = $resolvedPhpDoc->isInternal(); $isFinal = $resolvedPhpDoc->isFinal(); - $isPure = $resolvedPhpDoc->isPure(); + $isPure = null; + foreach ($actualDeclaringClass->getAncestors() as $className => $_) { + if ($this->signatureMapProvider->hasMethodMetadata($className, $methodReflection->getName())) { + $hasSideEffects = $this->signatureMapProvider->getMethodMetadata($className, $methodReflection->getName())['hasSideEffects']; + $isPure = !$hasSideEffects; + + break; + } + } + + $isPure ??= $resolvedPhpDoc->isPure(); $asserts = Assertions::createFromResolvedPhpDocBlock($resolvedPhpDoc); $acceptsNamedArguments = $resolvedPhpDoc->acceptsNamedArguments(); $selfOutType = $resolvedPhpDoc->getSelfOutTag() !== null ? $resolvedPhpDoc->getSelfOutTag()->getType() : null; From 7b67a9b319e29f684a7364f9fbf01d6ab685f6c3 Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Sun, 6 Jul 2025 08:01:56 +0900 Subject: [PATCH 8/8] Fix variable name --- src/Reflection/Php/PhpClassReflectionExtension.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Reflection/Php/PhpClassReflectionExtension.php b/src/Reflection/Php/PhpClassReflectionExtension.php index 424bb6cb30..939e1550d3 100644 --- a/src/Reflection/Php/PhpClassReflectionExtension.php +++ b/src/Reflection/Php/PhpClassReflectionExtension.php @@ -840,7 +840,7 @@ public function createUserlandMethodReflection(ClassReflection $fileDeclaringCla $isInternal = $resolvedPhpDoc->isInternal(); $isFinal = $resolvedPhpDoc->isFinal(); $isPure = null; - foreach ($actualDeclaringClass->getAncestors() as $className => $_) { + foreach ($actualDeclaringClass->getAncestors() as $className => $ancestor) { if ($this->signatureMapProvider->hasMethodMetadata($className, $methodReflection->getName())) { $hasSideEffects = $this->signatureMapProvider->getMethodMetadata($className, $methodReflection->getName())['hasSideEffects']; $isPure = !$hasSideEffects;