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
275 changes: 275 additions & 0 deletions src/Analyser/ExprSideEffectsHelper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,275 @@
<?php declare(strict_types = 1);

namespace PHPStan\Analyser;

use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\ArrayDimFetch;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\PropertyFetch;
use PhpParser\Node\Expr\StaticCall;
use PhpParser\Node\Expr\StaticPropertyFetch;
use PhpParser\Node\Name;
use PHPStan\DependencyInjection\AutowiredParameter;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Reflection\ReflectionProvider;

#[AutowiredService]
final class ExprSideEffectsHelper
{

public function __construct(
private ReflectionProvider $reflectionProvider,
#[AutowiredParameter]
private bool $rememberPossiblyImpureFunctionValues,
)
{
}

public function rememberFuncCall(FuncCall $expr, Scope $scope): bool
{
if ($expr->name instanceof Name) {
if (!$this->reflectionProvider->hasFunction($expr->name, $scope)) {
return false;
}

$functionReflection = $this->reflectionProvider->getFunction($expr->name, $scope);
$hasSideEffects = $functionReflection->hasSideEffects();
if ($hasSideEffects->yes()) {

Check warning on line 39 in src/Analyser/ExprSideEffectsHelper.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.3, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ $functionReflection = $this->reflectionProvider->getFunction($expr->name, $scope); $hasSideEffects = $functionReflection->hasSideEffects(); - if ($hasSideEffects->yes()) { + if (!$hasSideEffects->no()) { return false; }

Check warning on line 39 in src/Analyser/ExprSideEffectsHelper.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ $functionReflection = $this->reflectionProvider->getFunction($expr->name, $scope); $hasSideEffects = $functionReflection->hasSideEffects(); - if ($hasSideEffects->yes()) { + if (!$hasSideEffects->no()) { return false; }
return false;
}

if (!$this->rememberPossiblyImpureFunctionValues && !$hasSideEffects->no()) {
return false;
}
} else {
$nameType = $scope->getType($expr->name);
if ($nameType->isCallable()->yes()) {

Check warning on line 48 in src/Analyser/ExprSideEffectsHelper.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.3, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ } } else { $nameType = $scope->getType($expr->name); - if ($nameType->isCallable()->yes()) { + if (!$nameType->isCallable()->no()) { $isPure = null; foreach ($nameType->getCallableParametersAcceptors($scope) as $variant) { $variantIsPure = $variant->isPure();

Check warning on line 48 in src/Analyser/ExprSideEffectsHelper.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ } } else { $nameType = $scope->getType($expr->name); - if ($nameType->isCallable()->yes()) { + if (!$nameType->isCallable()->no()) { $isPure = null; foreach ($nameType->getCallableParametersAcceptors($scope) as $variant) { $variantIsPure = $variant->isPure();
$isPure = null;
foreach ($nameType->getCallableParametersAcceptors($scope) as $variant) {
$variantIsPure = $variant->isPure();
$isPure = $isPure === null ? $variantIsPure : $isPure->and($variantIsPure);
}

if ($isPure !== null) {
if ($isPure->no()) {

Check warning on line 56 in src/Analyser/ExprSideEffectsHelper.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.3, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ } if ($isPure !== null) { - if ($isPure->no()) { + if (!$isPure->yes()) { return false; }

Check warning on line 56 in src/Analyser/ExprSideEffectsHelper.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ } if ($isPure !== null) { - if ($isPure->no()) { + if (!$isPure->yes()) { return false; }
return false;
}

if (!$this->rememberPossiblyImpureFunctionValues && !$isPure->yes()) {
return false;
}
}
}
}

return !$this->callLikeArgsHaveSideEffects($expr, $scope);
}

public function rememberMethodCall(MethodCall $expr, Scope $scope): bool
{
if (!$expr->name instanceof Node\Identifier) {
return false;
}

$methodName = $expr->name->toString();
$calledOnType = $scope->getType($expr->var);
$methodReflection = $scope->getMethodReflection($calledOnType, $methodName);

if (
$methodReflection === null
|| $methodReflection->hasSideEffects()->yes()

Check warning on line 82 in src/Analyser/ExprSideEffectsHelper.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.3, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ if ( $methodReflection === null - || $methodReflection->hasSideEffects()->yes() + || !$methodReflection->hasSideEffects()->no() || (!$this->rememberPossiblyImpureFunctionValues && !$methodReflection->hasSideEffects()->no()) || $this->expressionHasSideEffects($expr->var, $scope) || $this->callLikeArgsHaveSideEffects($expr, $scope)

Check warning on line 82 in src/Analyser/ExprSideEffectsHelper.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ if ( $methodReflection === null - || $methodReflection->hasSideEffects()->yes() + || !$methodReflection->hasSideEffects()->no() || (!$this->rememberPossiblyImpureFunctionValues && !$methodReflection->hasSideEffects()->no()) || $this->expressionHasSideEffects($expr->var, $scope) || $this->callLikeArgsHaveSideEffects($expr, $scope)
|| (!$this->rememberPossiblyImpureFunctionValues && !$methodReflection->hasSideEffects()->no())
|| $this->expressionHasSideEffects($expr->var, $scope)
|| $this->callLikeArgsHaveSideEffects($expr, $scope)
) {
return false;
}

return true;
}

public function rememberStaticCall(StaticCall $expr, Scope $scope): bool
{
if (!$expr->name instanceof Node\Identifier) {
return false;
}

$methodName = $expr->name->toString();
if ($expr->class instanceof Name) {
$calledOnType = $scope->resolveTypeByName($expr->class);
} else {
$calledOnType = $scope->getType($expr->class);
}

$methodReflection = $scope->getMethodReflection($calledOnType, $methodName);

if (
$methodReflection === null
|| $methodReflection->hasSideEffects()->yes()

Check warning on line 110 in src/Analyser/ExprSideEffectsHelper.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.3, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ if ( $methodReflection === null - || $methodReflection->hasSideEffects()->yes() + || !$methodReflection->hasSideEffects()->no() || (!$this->rememberPossiblyImpureFunctionValues && !$methodReflection->hasSideEffects()->no()) || ($expr->class instanceof Expr && $this->expressionHasSideEffects($expr->class, $scope)) || $this->callLikeArgsHaveSideEffects($expr, $scope)

Check warning on line 110 in src/Analyser/ExprSideEffectsHelper.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ if ( $methodReflection === null - || $methodReflection->hasSideEffects()->yes() + || !$methodReflection->hasSideEffects()->no() || (!$this->rememberPossiblyImpureFunctionValues && !$methodReflection->hasSideEffects()->no()) || ($expr->class instanceof Expr && $this->expressionHasSideEffects($expr->class, $scope)) || $this->callLikeArgsHaveSideEffects($expr, $scope)
|| (!$this->rememberPossiblyImpureFunctionValues && !$methodReflection->hasSideEffects()->no())
|| ($expr->class instanceof Expr && $this->expressionHasSideEffects($expr->class, $scope))
|| $this->callLikeArgsHaveSideEffects($expr, $scope)
) {
return false;
}

return true;
}

public function subExpressionsHaveSideEffects(Expr $expr, Scope $scope): bool
{
if (
$expr instanceof MethodCall
|| $expr instanceof Expr\NullsafeMethodCall
|| $expr instanceof PropertyFetch
|| $expr instanceof Expr\NullsafePropertyFetch
|| $expr instanceof ArrayDimFetch
) {
if ($this->expressionHasSideEffects($expr->var, $scope)) {
return true;
}
} elseif (
$expr instanceof StaticCall
|| $expr instanceof StaticPropertyFetch
) {
if ($expr->class instanceof Expr && $this->expressionHasSideEffects($expr->class, $scope)) {
return true;
}
}

if ($expr instanceof Expr\CallLike && $this->callLikeArgsHaveSideEffects($expr, $scope)) {
return true;
}

return false;
}

private function callLikeArgsHaveSideEffects(Expr\CallLike $expr, Scope $scope): bool
{
if ($expr->isFirstClassCallable()) {
return false;
}

foreach ($expr->getArgs() as $arg) {
if ($this->expressionHasSideEffects($arg->value, $scope)) {
return true;
}
}

return false;
}

private function expressionHasSideEffects(Expr $expr, Scope $scope): bool
{
if ($expr instanceof Expr\New_) {
return true;
}

if ($expr instanceof FuncCall) {
if ($expr->isFirstClassCallable()) {
return false;
}
if (!($expr->name instanceof Name)) {
return true;
}

if (!$this->reflectionProvider->hasFunction($expr->name, $scope)) {
return true;
}
$functionReflection = $this->reflectionProvider->getFunction($expr->name, $scope);
$hasSideEffects = $functionReflection->hasSideEffects();
if ($hasSideEffects->yes()) {

Check warning on line 183 in src/Analyser/ExprSideEffectsHelper.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.3, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ } $functionReflection = $this->reflectionProvider->getFunction($expr->name, $scope); $hasSideEffects = $functionReflection->hasSideEffects(); - if ($hasSideEffects->yes()) { + if (!$hasSideEffects->no()) { return true; } if (!$this->rememberPossiblyImpureFunctionValues && !$hasSideEffects->no()) {

Check warning on line 183 in src/Analyser/ExprSideEffectsHelper.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ } $functionReflection = $this->reflectionProvider->getFunction($expr->name, $scope); $hasSideEffects = $functionReflection->hasSideEffects(); - if ($hasSideEffects->yes()) { + if (!$hasSideEffects->no()) { return true; } if (!$this->rememberPossiblyImpureFunctionValues && !$hasSideEffects->no()) {
return true;
}
if (!$this->rememberPossiblyImpureFunctionValues && !$hasSideEffects->no()) {
return true;
}
foreach ($expr->getArgs() as $arg) {
if ($this->expressionHasSideEffects($arg->value, $scope)) {
return true;
}
}
return false;
}

if ($expr instanceof MethodCall || $expr instanceof Expr\NullsafeMethodCall) {
if ($expr->isFirstClassCallable()) {
return $this->expressionHasSideEffects($expr->var, $scope);
}
if (!($expr->name instanceof Node\Identifier)) {
return true;
}

$calledOnType = $scope->getType($expr->var);
$methodReflection = $scope->getMethodReflection($calledOnType, $expr->name->toString());
if (
$methodReflection === null
|| $methodReflection->hasSideEffects()->yes()

Check warning on line 209 in src/Analyser/ExprSideEffectsHelper.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.3, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ $methodReflection = $scope->getMethodReflection($calledOnType, $expr->name->toString()); if ( $methodReflection === null - || $methodReflection->hasSideEffects()->yes() + || !$methodReflection->hasSideEffects()->no() || (!$this->rememberPossiblyImpureFunctionValues && !$methodReflection->hasSideEffects()->no()) ) { return true;

Check warning on line 209 in src/Analyser/ExprSideEffectsHelper.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ $methodReflection = $scope->getMethodReflection($calledOnType, $expr->name->toString()); if ( $methodReflection === null - || $methodReflection->hasSideEffects()->yes() + || !$methodReflection->hasSideEffects()->no() || (!$this->rememberPossiblyImpureFunctionValues && !$methodReflection->hasSideEffects()->no()) ) { return true;
|| (!$this->rememberPossiblyImpureFunctionValues && !$methodReflection->hasSideEffects()->no())
) {
return true;
}
foreach ($expr->getArgs() as $arg) {
if ($this->expressionHasSideEffects($arg->value, $scope)) {
return true;
}
}
return $this->expressionHasSideEffects($expr->var, $scope);
}

if ($expr instanceof StaticCall) {
if ($expr->isFirstClassCallable()) {
if ($expr->class instanceof Expr) {
return $this->expressionHasSideEffects($expr->class, $scope);
}
return false;
}
if (!($expr->name instanceof Node\Identifier)) {
return true;
}

if ($expr->class instanceof Name) {
$calledOnType = $scope->resolveTypeByName($expr->class);
} else {
$calledOnType = $scope->getType($expr->class);
}
$methodReflection = $scope->getMethodReflection($calledOnType, $expr->name->toString());
if (
$methodReflection === null
|| $methodReflection->hasSideEffects()->yes()

Check warning on line 241 in src/Analyser/ExprSideEffectsHelper.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.3, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ $methodReflection = $scope->getMethodReflection($calledOnType, $expr->name->toString()); if ( $methodReflection === null - || $methodReflection->hasSideEffects()->yes() + || !$methodReflection->hasSideEffects()->no() || (!$this->rememberPossiblyImpureFunctionValues && !$methodReflection->hasSideEffects()->no()) ) { return true;

Check warning on line 241 in src/Analyser/ExprSideEffectsHelper.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ $methodReflection = $scope->getMethodReflection($calledOnType, $expr->name->toString()); if ( $methodReflection === null - || $methodReflection->hasSideEffects()->yes() + || !$methodReflection->hasSideEffects()->no() || (!$this->rememberPossiblyImpureFunctionValues && !$methodReflection->hasSideEffects()->no()) ) { return true;
|| (!$this->rememberPossiblyImpureFunctionValues && !$methodReflection->hasSideEffects()->no())
) {
return true;
}
foreach ($expr->getArgs() as $arg) {
if ($this->expressionHasSideEffects($arg->value, $scope)) {
return true;
}
}
if ($expr->class instanceof Expr) {
return $this->expressionHasSideEffects($expr->class, $scope);
}
return false;
}

if ($expr instanceof PropertyFetch || $expr instanceof Expr\NullsafePropertyFetch) {
return $this->expressionHasSideEffects($expr->var, $scope);
}

if ($expr instanceof ArrayDimFetch) {
return $this->expressionHasSideEffects($expr->var, $scope);
}

if ($expr instanceof StaticPropertyFetch) {
if ($expr->class instanceof Expr) {
return $this->expressionHasSideEffects($expr->class, $scope);
}
return false;
}

return false;
}

}
90 changes: 18 additions & 72 deletions src/Analyser/TypeSpecifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ public function __construct(
private array $functionTypeSpecifyingExtensions,
private array $methodTypeSpecifyingExtensions,
private array $staticMethodTypeSpecifyingExtensions,
private bool $rememberPossiblyImpureFunctionValues,
private ExprSideEffectsHelper $exprSideEffectsHelper,
)
{
}
Expand Down Expand Up @@ -2319,94 +2319,40 @@ private function createForExpr(
}
}

if (
$expr instanceof FuncCall
&& $expr->name instanceof Name
) {
$has = $this->reflectionProvider->hasFunction($expr->name, $scope);
if (!$has) {
// backwards compatibility with previous behaviour
return new SpecifiedTypes([], []);
}

$functionReflection = $this->reflectionProvider->getFunction($expr->name, $scope);
$hasSideEffects = $functionReflection->hasSideEffects();
if ($hasSideEffects->yes()) {
return new SpecifiedTypes([], []);
}

if (!$this->rememberPossiblyImpureFunctionValues && !$hasSideEffects->no()) {
return new SpecifiedTypes([], []);
}
}

if (
$expr instanceof FuncCall
&& !$expr->name instanceof Name
) {
$nameType = $scope->getType($expr->name);
if ($nameType->isCallable()->yes()) {
$isPure = null;
foreach ($nameType->getCallableParametersAcceptors($scope) as $variant) {
$variantIsPure = $variant->isPure();
$isPure = $isPure === null ? $variantIsPure : $isPure->and($variantIsPure);
}

if ($isPure !== null) {
if ($isPure->no()) {
return new SpecifiedTypes([], []);
}

if (!$this->rememberPossiblyImpureFunctionValues && !$isPure->yes()) {
return new SpecifiedTypes([], []);
}
}
}
if ($expr instanceof FuncCall && !$this->exprSideEffectsHelper->rememberFuncCall($expr, $scope)) {
return new SpecifiedTypes([], []);
}

if (
$expr instanceof MethodCall
&& $expr->name instanceof Node\Identifier
&& !$this->exprSideEffectsHelper->rememberMethodCall($expr, $scope)
) {
$methodName = $expr->name->toString();
$calledOnType = $scope->getType($expr->var);
$methodReflection = $scope->getMethodReflection($calledOnType, $methodName);
if (
$methodReflection === null
|| $methodReflection->hasSideEffects()->yes()
|| (!$this->rememberPossiblyImpureFunctionValues && !$methodReflection->hasSideEffects()->no())
) {
if (isset($containsNull) && !$containsNull) {
return $this->createNullsafeTypes($originalExpr, $scope, $context, $type);
}

return new SpecifiedTypes([], []);
if (isset($containsNull) && !$containsNull) {
return $this->createNullsafeTypes($originalExpr, $scope, $context, $type);
}

return new SpecifiedTypes([], []);
}

if (
$expr instanceof StaticCall
&& $expr->name instanceof Node\Identifier
&& !$this->exprSideEffectsHelper->rememberStaticCall($expr, $scope)
) {
$methodName = $expr->name->toString();
if ($expr->class instanceof Name) {
$calledOnType = $scope->resolveTypeByName($expr->class);
} else {
$calledOnType = $scope->getType($expr->class);
if (isset($containsNull) && !$containsNull) {
return $this->createNullsafeTypes($originalExpr, $scope, $context, $type);
}

$methodReflection = $scope->getMethodReflection($calledOnType, $methodName);
if (
$methodReflection === null
|| $methodReflection->hasSideEffects()->yes()
|| (!$this->rememberPossiblyImpureFunctionValues && !$methodReflection->hasSideEffects()->no())
) {
if (isset($containsNull) && !$containsNull) {
return $this->createNullsafeTypes($originalExpr, $scope, $context, $type);
}
return new SpecifiedTypes([], []);
}

return new SpecifiedTypes([], []);
if ($this->exprSideEffectsHelper->subExpressionsHaveSideEffects($expr, $scope)) {
if (isset($containsNull) && !$containsNull) {
return $this->createNullsafeTypes($originalExpr, $scope, $context, $type);
}

return new SpecifiedTypes([], []);
}

$sureTypes = [];
Expand Down
Loading
Loading