Skip to content

Commit 73df4eb

Browse files
committed
Do not inherit @pure-unless-* tags into a method marked @phpstan-pure or @phpstan-impure
1 parent c85b24b commit 73df4eb

4 files changed

Lines changed: 53 additions & 11 deletions

File tree

‎src/PhpDoc/ResolvedPhpDocBlock.php‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -287,8 +287,15 @@ public function merge(ResolvedPhpDocBlock $parent, InheritedPhpDocParameterMappi
287287
$result->paramTags = self::mergeParamTags($this->getParamTags(), $parent, $parameterMapping, $parentClass);
288288
$result->paramOutTags = self::mergeParamOutTags($this->getParamOutTags(), $parent, $parameterMapping, $parentClass);
289289
$result->paramsImmediatelyInvokedCallable = self::mergeParamsImmediatelyInvokedCallable($this->getParamsImmediatelyInvokedCallable(), $parent, $parameterMapping);
290-
$result->paramsPureUnlessCallableIsImpure = self::mergeParamsPureUnlessCallableIsImpure($this->getParamsPureUnlessCallableIsImpure(), $parent, $parameterMapping);
291-
$result->paramsPureUnlessParameterPassed = self::mergeParamsPureUnlessParameterPassed($this->getParamsPureUnlessParameterPassed(), $parent, $parameterMapping);
290+
// @phpstan-pure / @phpstan-impure on the method itself replaces the parent's
291+
// conditional purity, so the @pure-unless-* tags are inherited only without them.
292+
if ($this->isPure() === null) {
293+
$result->paramsPureUnlessCallableIsImpure = self::mergeParamsPureUnlessCallableIsImpure($this->getParamsPureUnlessCallableIsImpure(), $parent, $parameterMapping);
294+
$result->paramsPureUnlessParameterPassed = self::mergeParamsPureUnlessParameterPassed($this->getParamsPureUnlessParameterPassed(), $parent, $parameterMapping);
295+
} else {
296+
$result->paramsPureUnlessCallableIsImpure = $this->getParamsPureUnlessCallableIsImpure();
297+
$result->paramsPureUnlessParameterPassed = $this->getParamsPureUnlessParameterPassed();
298+
}
292299
$result->paramClosureThisTags = self::mergeParamClosureThisTags($this->getParamClosureThisTags(), $parent, $parameterMapping, $parentClass);
293300
$result->returnTag = self::mergeReturnTags($this->getReturnTag(), $declaringClass, $parent, $parameterMapping, $parentClass);
294301
$result->throwsTag = self::mergeThrowsTags($this->getThrowsTag(), $parent, $parameterMapping);

‎src/Rules/Pure/FunctionPurityCheck.php‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -154,14 +154,13 @@ public function check(
154154
}
155155

156156
$errors = array_merge($errors, $this->reportImpurePoints($impurePoints, $pureUnlessCallableParamNames, $functionDescription));
157-
} elseif (!$isPure->no() && ($pureUnlessCallableParamNames !== [] || $pureUnlessParameterPassedParamNames !== [])) {
157+
} elseif ($pureUnlessCallableParamNames !== [] || $pureUnlessParameterPassedParamNames !== []) {
158158
// A function declared @pure-unless-callable-is-impure is pure except
159159
// for the flagged callables, so its body is checked for purity while
160160
// the flagged callables' own invocations are exempt. The same holds for
161161
// @pure-unless-parameter-passed: writing through the flagged by-ref
162162
// parameter is not an impure point, so the rest of the body still has
163-
// to be pure for the conditional verdict to hold. A method marked
164-
// @phpstan-impure that inherits either tag makes no such promise.
163+
// to be pure for the conditional verdict to hold.
165164
$errors = array_merge($errors, $this->reportImpurePoints($impurePoints, $pureUnlessCallableParamNames, $functionDescription));
166165
} elseif ($isPure->no()) {
167166
if (

‎tests/PHPStan/Rules/Pure/PureMethodRuleTest.php‎

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -444,19 +444,27 @@ public function testPureUnlessImpureOverride(): void
444444
$this->analyse([__DIR__ . '/data/pure-unless-impure-override.php'], [
445445
[
446446
'Method PureUnlessImpureOverride\ImpureReplacerWithoutSideEffects::replace() is marked as impure but does not have any side effects.',
447-
57,
447+
58,
448448
],
449449
[
450450
'Method PureUnlessImpureOverride\ImpureReplacerWithoutSideEffects::map() is marked as impure but does not have any side effects.',
451-
65,
451+
66,
452452
],
453453
[
454454
'Impure echo in pure method PureUnlessImpureOverride\InheritingReplacer::replace().',
455-
77,
455+
78,
456456
],
457457
[
458458
'Impure echo in pure method PureUnlessImpureOverride\InheritingReplacer::map().',
459-
85,
459+
86,
460+
],
461+
[
462+
'Method PureUnlessImpureOverride\PureReplacer::replace() is marked as pure but parameter $count is passed by reference.',
463+
102,
464+
],
465+
[
466+
'Possibly impure call to a callable in pure method PureUnlessImpureOverride\PureReplacer::map().',
467+
115,
460468
],
461469
]);
462470
}

‎tests/PHPStan/Rules/Pure/data/pure-unless-impure-override.php‎

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,9 @@ final class ImpureReplacer implements Replacer
2323
{
2424

2525
/**
26-
* The method is marked impure, so its body is not checked for purity
27-
* although it inherits @pure-unless-parameter-passed.
26+
* @phpstan-impure on the method replaces the inherited
27+
* @pure-unless-parameter-passed, so the body is not checked for purity.
28+
* MethodSignatureRule reports the override instead.
2829
*
2930
* @phpstan-impure
3031
*/
@@ -88,3 +89,30 @@ public function map(callable $cb, string $subject): string
8889
}
8990

9091
}
92+
93+
final class PureReplacer implements Replacer
94+
{
95+
96+
/**
97+
* @phpstan-pure on the method replaces the inherited
98+
* @pure-unless-parameter-passed, so writing to $count is not exempt.
99+
*
100+
* @phpstan-pure
101+
*/
102+
public function replace(string $subject, int &$count = 0): string
103+
{
104+
return $subject;
105+
}
106+
107+
/**
108+
* @phpstan-pure on the method replaces the inherited
109+
* @pure-unless-callable-is-impure, so invoking $cb is not exempt.
110+
*
111+
* @phpstan-pure
112+
*/
113+
public function map(callable $cb, string $subject): string
114+
{
115+
return $cb($subject);
116+
}
117+
118+
}

0 commit comments

Comments
 (0)