Skip to content

Correctly retrieve hasSideEffects metadata for inherited methods #4079

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Jul 6, 2025

Conversation

zonuexe
Copy link
Contributor

@zonuexe zonuexe commented Jul 2, 2025

@zonuexe zonuexe force-pushed the add-enum-stub branch 2 times, most recently from 98a9c9b to 17b4ef2 Compare July 2, 2025 06:39
@zonuexe zonuexe marked this pull request as draft July 2, 2025 06:47
public static function from(int|string $value): static;

/**
* @return ?static
Copy link
Contributor Author

Choose a reason for hiding this comment

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

While debugging with @nsfisis, he pointed out that this DocBlock causes the return value to be a mixed type.

Although it is redundant, in the scope of this PR it makes sense to duplicate the type declarations in the PHPDoc.

@zonuexe zonuexe marked this pull request as ready for review July 4, 2025 14:23
@phpstan-bot
Copy link
Collaborator

This pull request has been marked as ready for review.

Copy link
Member

@ondrejmirtes ondrejmirtes left a comment

Choose a reason for hiding this comment

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

Can you please debug why these lines are not sufficient?

'BackedEnum::from' => ['hasSideEffects' => false],
'BackedEnum::tryFrom' => ['hasSideEffects' => false],
- they should be.

@zonuexe
Copy link
Contributor Author

zonuexe commented Jul 5, 2025

I discovered that the functionMetadataMap wasn't being accessed in this process, so in this PR I only fixed the metadata.

I agree that this should be the case, so I'll investigate the underlying issue.

@zonuexe zonuexe marked this pull request as draft July 5, 2025 09:55
@ondrejmirtes
Copy link
Member

The code path that creates these methods should use the metadata too.

There's a chance it will fix more than just enums.

@zonuexe zonuexe changed the title Added Enum stubs to make method purity precise Fix purity of BackedEnum methods Jul 5, 2025
@zonuexe
Copy link
Contributor Author

zonuexe commented Jul 5, 2025

Here are some notes I've taken while looking at the code:

PHPStan\Reflection\ExtendedMethodReflection::isPure() is the authoritative source.

I checked it by injecting the following code into PHPStan\PhpDoc\PhpDocBlock::resolveMethodPhpDocBlockFromClass():

modified   src/PhpDoc/PhpDocBlock.php
@@ -343,6 +343,8 @@ final class PhpDocBlock
 			if ($parentReflection->isPrivate()) {
 				return null;
 			}
+			var_dump(["{$classReflection->getName()}::{$name}" => $parentReflection->isPure()->describe()]);
+			$isPure = $parentReflection->isPure()->maybe() ? null : $parentReflection->isPure()->yes();
 
 			$classReflection = $parentReflection->getDeclaringClass();
 			$traitReflection = null;

ExtendedMethodReflection is degenerated by $classReflection = $parentReflection->getDeclaringClass() before being passed to the PhpDocBlock, so there's no way to get it out of here.

To bring out $isPure from here would require breaking backwards compatibility and extending PhpDocBlock or MethodReflection. But, PhpDocBlock::isPure() is unnatural and should not be extended.

Again, we need to pass an appropriate $isPure directly to PHPStan\Reflection\Php\PhpMethodReflection from PHPStan\Reflection\Php\PhpClassReflectionExtension::createUserlandMethodReflection().

Here's the problem: PhpDocBlock::resolveMethodPhpDocBlockFromClass() calls ClassReflection::getMethod() to get reflection of the parent class, which relies on PhpClassReflectionExtension contained in MethodsClassReflectionExtension[] $methodsClassReflectionExtensions.

Therefore, injecting the following code will result in an infinite loop due to recursive calls.

modified   src/Reflection/Php/PhpClassReflectionExtension.php
@@ -839,7 +839,16 @@ final class PhpClassReflectionExtension
 		}
 		$isInternal = $resolvedPhpDoc->isInternal();
 		$isFinal = $resolvedPhpDoc->isFinal();
-		$isPure = $resolvedPhpDoc->isPure();
+		$isPure = null;
+		if ($actualDeclaringClass->hasNativeMethod($methodReflection->getName())) {
+			var_dump("{$actualDeclaringClass->getName()}::{$methodReflection->getName()}");
+			$parentReflection = $actualDeclaringClass->getNativeMethod($methodReflection->getName());
+			if (!$parentReflection->isPrivate()) {
+				$isPure = $parentReflection->isPure()->maybe() ? null : $parentReflection->isPure()->yes();
+			}
+		}
+
+		$isPure ??= $resolvedPhpDoc->isPure();
 		$asserts = Assertions::createFromResolvedPhpDocBlock($resolvedPhpDoc);
 		$acceptsNamedArguments = $resolvedPhpDoc->acceptsNamedArguments();
 		$selfOutType = $resolvedPhpDoc->getSelfOutTag() !== null ? $resolvedPhpDoc->getSelfOutTag()->getType() : null;

We can get the metadata directly as follows, but first we need to identify the parent class name:

			if ($this->signatureMapProvider->hasMethodMetadata($declaringClassName, $methodReflection->getName())) {
				$hasSideEffects = TrinaryLogic::createFromBoolean($this->signatureMapProvider->getMethodMetadata($declaringClassName, $methodReflection->getName())['hasSideEffects']);
			} else {
				$hasSideEffects = TrinaryLogic::createMaybe();
			}

Checking for side effects from ancestor metadata.

@zonuexe zonuexe changed the title Fix purity of BackedEnum methods Fix inherited method for getting hasSideEffects metadata Jul 5, 2025
@zonuexe zonuexe changed the title Fix inherited method for getting hasSideEffects metadata Fix purity of BackedEnum methods Jul 5, 2025
@zonuexe zonuexe changed the title Fix purity of BackedEnum methods Correctly retrieve hasSideEffects metadata for inherited methods Jul 5, 2025
@zonuexe zonuexe marked this pull request as ready for review July 5, 2025 23:11
@phpstan-bot
Copy link
Collaborator

This pull request has been marked as ready for review.

@ondrejmirtes ondrejmirtes merged commit 7a6a575 into phpstan:2.1.x Jul 6, 2025
417 of 420 checks passed
@ondrejmirtes
Copy link
Member

Perfect, thank you!

@zonuexe zonuexe deleted the add-enum-stub branch July 6, 2025 12:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Enums functions are reported as possibly impure
3 participants