Skip to content

Do not check the body of an impure method against an inherited @pure-unless-* tag - #6669

Open
zonuexe wants to merge 1 commit into
phpstan:2.3.xfrom
zonuexe:feature/impure-override-skips-conditional-body-check
Open

zonuexe wants to merge 1 commit into
phpstan:2.3.xfrom
zonuexe:feature/impure-override-skips-conditional-body-check

Conversation

@zonuexe

@zonuexe zonuexe commented Oct 3, 2026

Copy link
Copy Markdown
Contributor

Found while working on #6667. FunctionPurityCheck checked the body of a method for purity whenever it had a @pure-unless-callable-is-impure or @pure-unless-parameter-passed tag, even when the method itself is marked @phpstan-impure and only inherits the tag:

interface Replacer
{
	/**
	 * @param-out int $count
	 * @pure-unless-parameter-passed $count
	 */
	public function replace(string $subject, int &$count = 0): string;
}

final class ImpureReplacer implements Replacer
{
	/** @phpstan-impure */
	public function replace(string $subject, int &$count = 0): string
	{
		echo $subject; // Impure echo in pure method ImpureReplacer::replace().
		$count = 1;

		return $subject;
	}
}

The method declares itself impure, so PHPStan shouldn't call it a pure method or report its side effects. With bleeding edge, MethodSignatureRule reports the override itself as method.impureOverridePureUnlessParameterPassed or method.impureOverridePureUnlessCallable.

Changes

  • FunctionPurityCheck::check() checks the body against the conditional purity only when the method is not marked impure. A method marked @phpstan-impure goes through the existing impure branch, so one without any side effects gets impureMethod.pure like any other.
  • The test covers both tags, an impure override with and without side effects, and an override without a purity tag, whose body PHPStan still checks.

@VincentLanglet VincentLanglet left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I don't think your example is valid because if I call

Replacer::replace() I'll expect that the call is pure, which is invalid with your implementation.

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.

2 participants