Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions src/PhpDoc/ResolvedPhpDocBlock.php
Original file line number Diff line number Diff line change
Expand Up @@ -287,8 +287,15 @@ public function merge(ResolvedPhpDocBlock $parent, InheritedPhpDocParameterMappi
$result->paramTags = self::mergeParamTags($this->getParamTags(), $parent, $parameterMapping, $parentClass);
$result->paramOutTags = self::mergeParamOutTags($this->getParamOutTags(), $parent, $parameterMapping, $parentClass);
$result->paramsImmediatelyInvokedCallable = self::mergeParamsImmediatelyInvokedCallable($this->getParamsImmediatelyInvokedCallable(), $parent, $parameterMapping);
$result->paramsPureUnlessCallableIsImpure = self::mergeParamsPureUnlessCallableIsImpure($this->getParamsPureUnlessCallableIsImpure(), $parent, $parameterMapping);
$result->paramsPureUnlessParameterPassed = self::mergeParamsPureUnlessParameterPassed($this->getParamsPureUnlessParameterPassed(), $parent, $parameterMapping);
// @phpstan-pure / @phpstan-impure on the method itself replaces the parent's
// conditional purity, so the @pure-unless-* tags are inherited only without them.
if ($this->isPure() === null) {
$result->paramsPureUnlessCallableIsImpure = self::mergeParamsPureUnlessCallableIsImpure($this->getParamsPureUnlessCallableIsImpure(), $parent, $parameterMapping);
$result->paramsPureUnlessParameterPassed = self::mergeParamsPureUnlessParameterPassed($this->getParamsPureUnlessParameterPassed(), $parent, $parameterMapping);
} else {
$result->paramsPureUnlessCallableIsImpure = $this->getParamsPureUnlessCallableIsImpure();
$result->paramsPureUnlessParameterPassed = $this->getParamsPureUnlessParameterPassed();
}
$result->paramClosureThisTags = self::mergeParamClosureThisTags($this->getParamClosureThisTags(), $parent, $parameterMapping, $parentClass);
$result->returnTag = self::mergeReturnTags($this->getReturnTag(), $declaringClass, $parent, $parameterMapping, $parentClass);
$result->throwsTag = self::mergeThrowsTags($this->getThrowsTag(), $parent, $parameterMapping);
Expand Down
31 changes: 31 additions & 0 deletions tests/PHPStan/Rules/Pure/PureMethodRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -438,4 +438,35 @@ public function testPureUnlessParameterPassed(): void
]);
}

public function testPureUnlessImpureOverride(): void
{
$this->treatPhpDocTypesAsCertain = true;
$this->analyse([__DIR__ . '/data/pure-unless-impure-override.php'], [
[
'Method PureUnlessImpureOverride\ImpureReplacerWithoutSideEffects::replace() is marked as impure but does not have any side effects.',
58,
],
[
'Method PureUnlessImpureOverride\ImpureReplacerWithoutSideEffects::map() is marked as impure but does not have any side effects.',
66,
],
[
'Impure echo in pure method PureUnlessImpureOverride\InheritingReplacer::replace().',
78,
],
[
'Impure echo in pure method PureUnlessImpureOverride\InheritingReplacer::map().',
86,
],
[
'Method PureUnlessImpureOverride\PureReplacer::replace() is marked as pure but parameter $count is passed by reference.',
102,
],
[
'Possibly impure call to a callable in pure method PureUnlessImpureOverride\PureReplacer::map().',
115,
],
]);
}

}
118 changes: 118 additions & 0 deletions tests/PHPStan/Rules/Pure/data/pure-unless-impure-override.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
<?php declare(strict_types = 1);

namespace PureUnlessImpureOverride;

interface Replacer
{

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

/**
* @param callable(string): string $cb
* @pure-unless-callable-is-impure $cb
*/
public function map(callable $cb, string $subject): string;

}

final class ImpureReplacer implements Replacer
{

/**
* @phpstan-impure on the method replaces the inherited
* @pure-unless-parameter-passed, so the body is not checked for purity.
* MethodSignatureRule reports the override instead.
*
* @phpstan-impure
*/
public function replace(string $subject, int &$count = 0): string
{
echo $subject;
$count = 1;

return $subject;
}

/**
* @phpstan-impure
*/
public function map(callable $cb, string $subject): string
{
echo $subject;

return $cb($subject);
}

}

final class ImpureReplacerWithoutSideEffects implements Replacer
{

/**
* @phpstan-impure
*/
public function replace(string $subject, int &$count = 0): string
{
return $subject;
}

/**
* @phpstan-impure
*/
public function map(callable $cb, string $subject): string
{
return $subject;
}

}

final class InheritingReplacer implements Replacer
{

public function replace(string $subject, int &$count = 0): string
{
echo $subject;
$count = 1;

return $subject;
}

public function map(callable $cb, string $subject): string
{
echo $subject;

return $cb($subject);
}

}

final class PureReplacer implements Replacer
{

/**
* @phpstan-pure on the method replaces the inherited
* @pure-unless-parameter-passed, so writing to $count is not exempt.
*
* @phpstan-pure
*/
public function replace(string $subject, int &$count = 0): string
{
return $subject;
}

/**
* @phpstan-pure on the method replaces the inherited
* @pure-unless-callable-is-impure, so invoking $cb is not exempt.
*
* @phpstan-pure
*/
public function map(callable $cb, string $subject): string
{
return $cb($subject);
}

}
Loading