diff --git a/resources/functionMetadata.php b/resources/functionMetadata.php index 4ebbb532d99..c67cfdbfe64 100644 --- a/resources/functionMetadata.php +++ b/resources/functionMetadata.php @@ -1758,4 +1758,4 @@ 'zlib_encode' => ['hasSideEffects' => false], 'zlib_get_coding_type' => ['hasSideEffects' => false], -]; +]; \ No newline at end of file diff --git a/src/Analyser/ExprHandler/AssignHandler.php b/src/Analyser/ExprHandler/AssignHandler.php index 3b6f921f3d5..d31aaf3a6af 100644 --- a/src/Analyser/ExprHandler/AssignHandler.php +++ b/src/Analyser/ExprHandler/AssignHandler.php @@ -34,6 +34,7 @@ use PHPStan\Analyser\SpecifiedTypes; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; +use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\Expr\ExistingArrayDimFetch; use PHPStan\Node\Expr\GetOffsetValueTypeExpr; @@ -84,6 +85,8 @@ final class AssignHandler implements ExprHandler public function __construct( private TypeSpecifier $typeSpecifier, private PhpVersion $phpVersion, + #[AutowiredParameter] + private bool $rememberPossiblyImpureFunctionValues, ) { } @@ -240,6 +243,7 @@ public function processAssignVar( } $assignedExpr = $this->unwrapAssign($assignedExpr); $type = $scopeBeforeAssignEval->getType($assignedExpr); + $isImpure = count($impurePoints) > 0; $conditionalExpressions = []; if ($assignedExpr instanceof Ternary) { @@ -259,23 +263,25 @@ public function processAssignVar( $truthyType->isSuperTypeOf($falseyType)->no() && $falseyType->isSuperTypeOf($truthyType)->no() ) { - $conditionalExpressions = $this->processSureTypesForConditionalExpressionsAfterAssign($condScope, $var->name, $conditionalExpressions, $truthySpecifiedTypes, $truthyType); - $conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign($condScope, $var->name, $conditionalExpressions, $truthySpecifiedTypes, $truthyType); - $conditionalExpressions = $this->processSureTypesForConditionalExpressionsAfterAssign($condScope, $var->name, $conditionalExpressions, $falseySpecifiedTypes, $falseyType); - $conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign($condScope, $var->name, $conditionalExpressions, $falseySpecifiedTypes, $falseyType); + $conditionalExpressions = $this->processSureTypesForConditionalExpressionsAfterAssign($condScope, $var->name, $conditionalExpressions, $truthySpecifiedTypes, $truthyType, $isImpure); + $conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign($condScope, $var->name, $conditionalExpressions, $truthySpecifiedTypes, $truthyType, $isImpure); + $conditionalExpressions = $this->processSureTypesForConditionalExpressionsAfterAssign($condScope, $var->name, $conditionalExpressions, $falseySpecifiedTypes, $falseyType, $isImpure); + $conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign($condScope, $var->name, $conditionalExpressions, $falseySpecifiedTypes, $falseyType, $isImpure); } } $truthyType = TypeCombinator::removeFalsey($type); - if ($truthyType !== $type) { + if ( + $truthyType !== $type + ) { $truthySpecifiedTypes = $this->typeSpecifier->specifyTypesInCondition($scope, $assignedExpr, TypeSpecifierContext::createTruthy()); - $conditionalExpressions = $this->processSureTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $truthySpecifiedTypes, $truthyType); - $conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $truthySpecifiedTypes, $truthyType); + $conditionalExpressions = $this->processSureTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $truthySpecifiedTypes, $truthyType, $isImpure); + $conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $truthySpecifiedTypes, $truthyType, $isImpure); $falseyType = TypeCombinator::intersect($type, StaticTypeFactory::falsey()); $falseySpecifiedTypes = $this->typeSpecifier->specifyTypesInCondition($scope, $assignedExpr, TypeSpecifierContext::createFalsey()); - $conditionalExpressions = $this->processSureTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $falseySpecifiedTypes, $falseyType); - $conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $falseySpecifiedTypes, $falseyType); + $conditionalExpressions = $this->processSureTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $falseySpecifiedTypes, $falseyType, $isImpure); + $conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $falseySpecifiedTypes, $falseyType, $isImpure); } foreach ([null, false, 0, 0.0, '', '0', []] as $falseyScalar) { @@ -304,13 +310,13 @@ public function processAssignVar( $notIdenticalConditionExpr = new Expr\BinaryOp\NotIdentical($assignedExpr, $astNode); $notIdenticalSpecifiedTypes = $this->typeSpecifier->specifyTypesInCondition($scope, $notIdenticalConditionExpr, TypeSpecifierContext::createTrue()); - $conditionalExpressions = $this->processSureTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $notIdenticalSpecifiedTypes, $withoutFalseyType); - $conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $notIdenticalSpecifiedTypes, $withoutFalseyType); + $conditionalExpressions = $this->processSureTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $notIdenticalSpecifiedTypes, $withoutFalseyType, $isImpure); + $conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $notIdenticalSpecifiedTypes, $withoutFalseyType, $isImpure); $identicalConditionExpr = new Expr\BinaryOp\Identical($assignedExpr, $astNode); $identicalSpecifiedTypes = $this->typeSpecifier->specifyTypesInCondition($scope, $identicalConditionExpr, TypeSpecifierContext::createTrue()); - $conditionalExpressions = $this->processSureTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $identicalSpecifiedTypes, $falseyType); - $conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $identicalSpecifiedTypes, $falseyType); + $conditionalExpressions = $this->processSureTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $identicalSpecifiedTypes, $falseyType, $isImpure); + $conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $identicalSpecifiedTypes, $falseyType, $isImpure); } $nodeScopeResolver->callNodeCallback($nodeCallback, new VariableAssignNode($var, $assignedExpr), $scopeBeforeAssignEval, $storage); @@ -850,7 +856,14 @@ private function unwrapAssign(Expr $expr): Expr * @param array $conditionalExpressions * @return array */ - private function processSureTypesForConditionalExpressionsAfterAssign(Scope $scope, string $variableName, array $conditionalExpressions, SpecifiedTypes $specifiedTypes, Type $variableType): array + private function processSureTypesForConditionalExpressionsAfterAssign( + Scope $scope, + string $variableName, + array $conditionalExpressions, + SpecifiedTypes $specifiedTypes, + Type $variableType, + bool $isImpure, + ): array { foreach ($specifiedTypes->getSureTypes() as $exprString => [$expr, $exprType]) { if ($expr instanceof Variable) { @@ -861,10 +874,23 @@ private function processSureTypesForConditionalExpressionsAfterAssign(Scope $sco if ($expr->name === $variableName) { continue; } + } elseif ( + ( + $expr instanceof FuncCall + || $expr instanceof MethodCall + || $expr instanceof Expr\StaticCall + ) + ) { + if (!$this->rememberPossiblyImpureFunctionValues) { + continue; + } + + if ($isImpure) { + continue; + } } elseif ( !$expr instanceof PropertyFetch && !$expr instanceof ArrayDimFetch - && !$expr instanceof FuncCall ) { continue; } @@ -889,7 +915,14 @@ private function processSureTypesForConditionalExpressionsAfterAssign(Scope $sco * @param array $conditionalExpressions * @return array */ - private function processSureNotTypesForConditionalExpressionsAfterAssign(Scope $scope, string $variableName, array $conditionalExpressions, SpecifiedTypes $specifiedTypes, Type $variableType): array + private function processSureNotTypesForConditionalExpressionsAfterAssign( + Scope $scope, + string $variableName, + array $conditionalExpressions, + SpecifiedTypes $specifiedTypes, + Type $variableType, + bool $isImpure, + ): array { foreach ($specifiedTypes->getSureNotTypes() as $exprString => [$expr, $exprType]) { if ($expr instanceof Variable) { @@ -900,10 +933,23 @@ private function processSureNotTypesForConditionalExpressionsAfterAssign(Scope $ if ($expr->name === $variableName) { continue; } + } elseif ( + ( + $expr instanceof FuncCall + || $expr instanceof MethodCall + || $expr instanceof Expr\StaticCall + ) + ) { + if (!$this->rememberPossiblyImpureFunctionValues) { + continue; + } + + if ($isImpure) { + continue; + } } elseif ( !$expr instanceof PropertyFetch && !$expr instanceof ArrayDimFetch - && !$expr instanceof FuncCall ) { continue; } diff --git a/tests/PHPStan/Analyser/DoNotRememberPossiblyImpureFunctionValuesTest.php b/tests/PHPStan/Analyser/DoNotRememberPossiblyImpureFunctionValuesTest.php index 75a9797ee38..64fb6736c52 100644 --- a/tests/PHPStan/Analyser/DoNotRememberPossiblyImpureFunctionValuesTest.php +++ b/tests/PHPStan/Analyser/DoNotRememberPossiblyImpureFunctionValuesTest.php @@ -11,6 +11,7 @@ class DoNotRememberPossiblyImpureFunctionValuesTest extends TypeInferenceTestCas public static function dataAsserts(): iterable { yield from self::gatherAssertTypes(__DIR__ . '/data/do-not-remember-possibly-impure-function-values.php'); + yield from self::gatherAssertTypes(__DIR__ . '/data/bug-9455-not-remembered-possibly-impure.php'); } /** diff --git a/tests/PHPStan/Analyser/data/bug-9455-not-remembered-possibly-impure.php b/tests/PHPStan/Analyser/data/bug-9455-not-remembered-possibly-impure.php new file mode 100644 index 00000000000..5ab742224eb --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-9455-not-remembered-possibly-impure.php @@ -0,0 +1,104 @@ += 8.0 + +declare(strict_types = 1); + +namespace Bug9455DoNotRememberedPossiblyImpureFunctionValues; + +use function PHPStan\Testing\assertType; + +class A { + public function __construct(private int $id){} + + public function getId(): int { + return $this->id; + } +} + +class B { + public function __construct(private int $id, private ?A $a = null){} + + public function getId(): int { + return $this->id; + } + + /** + * @phpstan-pure + */ + public function getPure(): ?A { + return $this->a; + } +} + +class HelloWorld +{ + public function testFails(): void + { + $a = new A(1); + $b = new B(1, $a); + + $hasA = $b->getPure() !== null; + + if($hasA) { + assertType('Bug9455DoNotRememberedPossiblyImpureFunctionValues\A|null', $b->getPure()); + } + } + + public function testSucceeds(): void + { + $a = new A(1); + $b = new B(1, $a); + + if($b->getPure() !== null) { + assertType('Bug9455DoNotRememberedPossiblyImpureFunctionValues\A', $b->getPure()); + } + } +} + +class C { + /** + * @phpstan-impure + */ + public function getImpure(): ?A { + return rand(0, 1) ? new A(1) : null; + } +} + +class ImpureTest +{ + public function testImpureMethodNotNarrowed(): void + { + $c = new C(); + + $hasA = $c->getImpure() !== null; + + if($hasA) { + assertType('Bug9455DoNotRememberedPossiblyImpureFunctionValues\A|null', $c->getImpure()); + } + } + + public function testImpureMethodInline(): void + { + $c = new C(); + + if($c->getImpure() !== null) { + assertType('Bug9455DoNotRememberedPossiblyImpureFunctionValues\A|null', $c->getImpure()); + } + } +} + +class D { + public function getUnknownPurity(): ?A { + return rand(0, 1) ? new A(1) : null; + } +} + +function doUnknownPurity(): void +{ + $d = new D(); + + $hasA = $d->getUnknownPurity() !== null; + + if($hasA) { + assertType('Bug9455DoNotRememberedPossiblyImpureFunctionValues\A|null', $d->getUnknownPurity()); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-3190.php b/tests/PHPStan/Analyser/nsrt/bug-3190.php index 7348ca36234..bc2c5331889 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-3190.php +++ b/tests/PHPStan/Analyser/nsrt/bug-3190.php @@ -6,6 +6,7 @@ interface Server { + /** @pure */ public function isDedicated(): bool; public function getSize(): int; diff --git a/tests/PHPStan/Analyser/nsrt/bug-5207.php b/tests/PHPStan/Analyser/nsrt/bug-5207.php new file mode 100644 index 00000000000..629d1571477 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-5207.php @@ -0,0 +1,83 @@ += 8.0 + +declare(strict_types = 1); + +namespace Bug5207; + +use function PHPStan\Testing\assertType; + +abstract class HelloWorld { + /** @phpstan-pure */ + abstract public function getChild(): ?HelloWorld; + + public function sayHello(): void { + $foo = null !== $this->getChild(); + if ($foo) { + assertType('Bug5207\HelloWorld', $this->getChild()); + } + } + + public function sayHelloInline(): void { + if (null !== $this->getChild()) { + assertType('Bug5207\HelloWorld', $this->getChild()); + } + } +} + +abstract class StaticWorld { + /** @phpstan-pure */ + abstract public static function getChild(): ?StaticWorld; + + public static function sayHello(): void { + $foo = null !== static::getChild(); + if ($foo) { + assertType('Bug5207\StaticWorld', static::getChild()); + } + } + + public static function sayHelloInline(): void { + if (null !== static::getChild()) { + assertType('Bug5207\StaticWorld', static::getChild()); + } + } +} + +abstract class ImpureStaticWorld { + /** + * @phpstan-impure + */ + abstract public static function getChild(): ?ImpureStaticWorld; + + public static function sayHello(): void { + $foo = null !== static::getChild(); + if ($foo) { + assertType('Bug5207\ImpureStaticWorld|null', static::getChild()); + } + } + + public static function sayHelloInline(): void { + if (null !== static::getChild()) { + assertType('Bug5207\ImpureStaticWorld|null', static::getChild()); + } + } +} + +abstract class ImpureWorld { + /** + * @phpstan-impure + */ + abstract public function getChild(): ?ImpureWorld; + + public function sayHello(): void { + $foo = null !== $this->getChild(); + if ($foo) { + assertType('Bug5207\ImpureWorld|null', $this->getChild()); + } + } + + public function sayHelloInline(): void { + if (null !== $this->getChild()) { + assertType('Bug5207\ImpureWorld|null', $this->getChild()); + } + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-9455.php b/tests/PHPStan/Analyser/nsrt/bug-9455.php new file mode 100644 index 00000000000..7f6c22c6cf7 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-9455.php @@ -0,0 +1,104 @@ += 8.0 + +declare(strict_types = 1); + +namespace Bug9455; + +use function PHPStan\Testing\assertType; + +class A { + public function __construct(private int $id){} + + public function getId(): int { + return $this->id; + } +} + +class B { + public function __construct(private int $id, private ?A $a = null){} + + public function getId(): int { + return $this->id; + } + + /** + * @phpstan-pure + */ + public function getPure(): ?A { + return $this->a; + } +} + +class HelloWorld +{ + public function testFails(): void + { + $a = new A(1); + $b = new B(1, $a); + + $hasA = $b->getPure() !== null; + + if($hasA) { + assertType('Bug9455\A', $b->getPure()); + } + } + + public function testSucceeds(): void + { + $a = new A(1); + $b = new B(1, $a); + + if($b->getPure() !== null) { + assertType('Bug9455\A', $b->getPure()); + } + } +} + +class C { + /** + * @phpstan-impure + */ + public function getImpure(): ?A { + return rand(0, 1) ? new A(1) : null; + } +} + +class ImpureTest +{ + public function testImpureMethodNotNarrowed(): void + { + $c = new C(); + + $hasA = $c->getImpure() !== null; + + if($hasA) { + assertType('Bug9455\A|null', $c->getImpure()); + } + } + + public function testImpureMethodInline(): void + { + $c = new C(); + + if($c->getImpure() !== null) { + assertType('Bug9455\A|null', $c->getImpure()); + } + } +} + +class D { + public function getUnknownPurity(): ?A { + return rand(0, 1) ? new A(1) : null; + } +} + +function doUnknownPurity(): void +{ + $d = new D(); + + $hasA = $d->getUnknownPurity() !== null; + + if($hasA) { + assertType('Bug9455\A|null', $d->getUnknownPurity()); + } +}