Skip to content

Report a docblock that combines a purity tag with a @pure-unless-* tag - #6670

Open
zonuexe wants to merge 1 commit into
phpstan:2.3.xfrom
zonuexe:feature/conflicting-purity-tags
Open

zonuexe wants to merge 1 commit into
phpstan:2.3.xfrom
zonuexe:feature/conflicting-purity-tags

Conversation

@zonuexe

@zonuexe zonuexe commented Oct 4, 2026

Copy link
Copy Markdown
Contributor

Follow-up to #6669, which leaves this case out. A docblock that marks a function or method @phpstan-pure or @phpstan-impure and also makes its purity conditional with @pure-unless-callable-is-impure or @pure-unless-parameter-passed contradicts itself, and PHPStan follows a different tag depending on where it looks:

/**
 * @param callable(string): string $cb
 * @phpstan-impure
 * @pure-unless-callable-is-impure $cb
 */
function map(callable $cb, string $subject): string
{
	echo $subject; // Impure echo in pure function map().

	return $cb($subject);
}

With @phpstan-impure, callers see an impure function, since SimpleImpurePoint::createFromVariant() skips the conditional verdict once isPure() is no, while FunctionPurityCheck checks the body against the conditional tag and reports the echo.

With @phpstan-pure, callers see an unconditionally pure function. For @pure-unless-parameter-passed, PHPStan reports the by-ref parameter as pureFunction.parameterByRef. For @pure-unless-callable-is-impure, the body check exempts the $cb() call, so neither the body check nor the callers notice an impure callback, and no existing error covers that case.

Changes

  • The new ConflictingPurityTagsRule resolves the docblock of a function or method with FileTypeMapper and reports each @pure-unless-* parameter when the same docblock has a purity tag: Function map() is marked as impure, which conflicts with @pure-unless-callable-is-impure for parameter $cb. (pureFunction.conflictingPurityTags / pureMethod.conflictingPurityTags). Aliases such as @psalm-pure, @impure and the @phpstan-pure-unless-* variants count.
  • It reads the docblock as written, so tags inherited from a parent, interface or stub and the class-level @phpstan-all-methods-pure / -impure don't count. It covers interface, abstract, trait, enum and anonymous-class methods.
  • It skips property hooks, where PHPStan reads @phpstan-pure but ignores @pure-unless-*, and closures, where neither tag applies. Like the other bleeding-edge rules, it doesn't validate stub files.
  • For a docblock with both @phpstan-pure and @phpstan-impure, the message says "marked as pure", since ResolvedPhpDocBlock::isPure() prefers @phpstan-pure.
  • It doesn't change how PHPStan treats such a function or method; it reports the docblock. Where pureFunction.parameterByRef or pureFunction.redundantUnlessCallable fire for the same docblock, both errors appear.
  • It can report existing code, so I registered it on level 2 behind the reportConflictingPurityTags bleeding-edge toggle, next to PureFunctionRule and PureMethodRule. The config lines touch the same spot as Check the purity declarations of methods without a body #6668, so whichever merges second needs a rebase.

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.

1 participant