Skip to content
Merged
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
2 changes: 1 addition & 1 deletion src/Analyser/ExprHandler/FuncCallHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
) {
// processed later
} elseif ($parametersAcceptor instanceof CallableParametersAcceptor) {
$callableThrowPoints = array_map(static fn (SimpleThrowPoint $throwPoint) => $throwPoint->isExplicit() ? InternalThrowPoint::createExplicit($scope, $throwPoint->getType(), $expr, $throwPoint->canContainAnyThrowable()) : InternalThrowPoint::createImplicit($scope, $expr), $parametersAcceptor->getThrowPoints());
$callableThrowPoints = array_map(static fn (SimpleThrowPoint $throwPoint) => $throwPoint->isExplicit() ? InternalThrowPoint::createExplicit($scope, $throwPoint->getType(), $expr, $throwPoint->canContainAnyThrowable(), $throwPoint->isFromThrowExpr()) : InternalThrowPoint::createImplicit($scope, $expr), $parametersAcceptor->getThrowPoints());
if (!$this->implicitThrows) {
$callableThrowPoints = array_values(array_filter($callableThrowPoints, static fn (InternalThrowPoint $throwPoint) => $throwPoint->isExplicit()));
}
Expand Down
2 changes: 1 addition & 1 deletion src/Analyser/ExprHandler/Helper/ClosureTypeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -908,7 +908,7 @@ private function assembleClosureType(
);
}

$throwPointsForClosureType = array_map(static fn (ThrowPoint $throwPoint) => $throwPoint->isExplicit() ? SimpleThrowPoint::createExplicit($throwPoint->getType(), $throwPoint->canContainAnyThrowable()) : SimpleThrowPoint::createImplicit(), $throwPoints);
$throwPointsForClosureType = array_map(static fn (ThrowPoint $throwPoint) => $throwPoint->isExplicit() ? SimpleThrowPoint::createExplicit($throwPoint->getType(), $throwPoint->canContainAnyThrowable(), $throwPoint->isFromThrowExpr()) : SimpleThrowPoint::createImplicit(), $throwPoints);
$impurePointsForClosureType = array_map(static fn (ImpurePoint $impurePoint) => new SimpleImpurePoint($impurePoint->getIdentifier(), $impurePoint->getDescription(), $impurePoint->isCertain()), $impurePoints);

if ($writeCache) {
Expand Down
2 changes: 1 addition & 1 deletion src/Analyser/ExprHandler/ThrowHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
expr: $expr,
hasYield: false,
isAlwaysTerminating: true,
throwPoints: array_merge($exprResult->getThrowPoints(), [InternalThrowPoint::createExplicit($scope, $exprResult->getType(), $expr, false)]),
throwPoints: array_merge($exprResult->getThrowPoints(), [InternalThrowPoint::createExplicit($scope, $exprResult->getType(), $expr, false, fromThrowExpr: true)]),
impurePoints: $exprResult->getImpurePoints(),
);
}
Expand Down
21 changes: 16 additions & 5 deletions src/Analyser/InternalThrowPoint.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,15 @@ private function __construct(
private Node $node,
private bool $explicit,
private bool $canContainAnyThrowable,
private bool $fromThrowExpr = false,
)
{
}

public function toPublic(): ThrowPoint
{
if ($this->explicit) {
return ThrowPoint::createExplicit($this->scope, $this->type, $this->node, $this->canContainAnyThrowable);
return ThrowPoint::createExplicit($this->scope, $this->type, $this->node, $this->canContainAnyThrowable, $this->fromThrowExpr);
}

return ThrowPoint::createImplicit($this->scope, $this->node);
Expand All @@ -36,9 +37,9 @@ public function toPublic(): ThrowPoint
/**
* @param Node\Expr|Node\Stmt $node
*/
public static function createExplicit(MutatingScope $scope, Type $type, Node $node, bool $canContainAnyThrowable): self
public static function createExplicit(MutatingScope $scope, Type $type, Node $node, bool $canContainAnyThrowable, bool $fromThrowExpr = false): self
{
return new self($scope, $type, $node, true, $canContainAnyThrowable);
return new self($scope, $type, $node, true, $canContainAnyThrowable, $fromThrowExpr);
}

/**
Expand All @@ -51,7 +52,7 @@ public static function createImplicit(MutatingScope $scope, Node $node): self

public static function createFromPublic(ThrowPoint $throwPoint, MutatingScope $scope): self
{
return new self($scope, $throwPoint->getType(), $throwPoint->getNode(), $throwPoint->isExplicit(), $throwPoint->canContainAnyThrowable());
return new self($scope, $throwPoint->getType(), $throwPoint->getNode(), $throwPoint->isExplicit(), $throwPoint->canContainAnyThrowable(), $throwPoint->isFromThrowExpr());
}

public function getScope(): MutatingScope
Expand Down Expand Up @@ -82,9 +83,19 @@ public function canContainAnyThrowable(): bool
return $this->canContainAnyThrowable;
}

/**
* Whether the throw point comes from a `throw` written in the analysed code,
* as opposed to a throw inferred from what a called function or an operation
* can throw.
*/
public function isFromThrowExpr(): bool
{
return $this->fromThrowExpr;
}

public function subtractCatchType(Type $catchType): self
{
return new self($this->scope, TypeCombinator::remove($this->type, $catchType), $this->node, $this->explicit, $this->canContainAnyThrowable);
return new self($this->scope, TypeCombinator::remove($this->type, $catchType), $this->node, $this->explicit, $this->canContainAnyThrowable, $this->fromThrowExpr);
}

}
6 changes: 3 additions & 3 deletions src/Analyser/NodeScopeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -2135,7 +2135,7 @@ public function processArgs(
// still complete the gathered data - then it keeps re-walking)
$this->container->getByType(ClosureTypeResolver::class)->seedCacheFromClosureWalk($scopeToPass, $arg->value, $closureResult);
if ($this->callCallbackImmediately($parameter, $parameterType, $calleeReflection)) {
$throwPoints = array_merge($throwPoints, array_map(static fn (InternalThrowPoint $throwPoint) => $throwPoint->isExplicit() ? InternalThrowPoint::createExplicit($scope, $throwPoint->getType(), $arg->value, $throwPoint->canContainAnyThrowable()) : InternalThrowPoint::createImplicit($scope, $arg->value), $closureResult->getThrowPoints()));
$throwPoints = array_merge($throwPoints, array_map(static fn (InternalThrowPoint $throwPoint) => $throwPoint->isExplicit() ? InternalThrowPoint::createExplicit($scope, $throwPoint->getType(), $arg->value, $throwPoint->canContainAnyThrowable(), $throwPoint->isFromThrowExpr()) : InternalThrowPoint::createImplicit($scope, $arg->value), $closureResult->getThrowPoints()));
$impurePoints = array_merge($impurePoints, $closureResult->getImpurePoints());
}

Expand Down Expand Up @@ -2227,7 +2227,7 @@ public function processArgs(
$this->container->getByType(ClosureTypeResolver::class)->seedCacheFromArrowFunctionWalk($scopeToPass, $arg->value, $processArrowFunctionResult);
$arrowFunctionResult = $processArrowFunctionResult->getExpressionResult();
if ($this->callCallbackImmediately($parameter, $parameterType, $calleeReflection)) {
$throwPoints = array_merge($throwPoints, array_map(static fn (InternalThrowPoint $throwPoint) => $throwPoint->isExplicit() ? InternalThrowPoint::createExplicit($scope, $throwPoint->getType(), $arg->value, $throwPoint->canContainAnyThrowable()) : InternalThrowPoint::createImplicit($scope, $arg->value), $arrowFunctionResult->getThrowPoints()));
$throwPoints = array_merge($throwPoints, array_map(static fn (InternalThrowPoint $throwPoint) => $throwPoint->isExplicit() ? InternalThrowPoint::createExplicit($scope, $throwPoint->getType(), $arg->value, $throwPoint->canContainAnyThrowable(), $throwPoint->isFromThrowExpr()) : InternalThrowPoint::createImplicit($scope, $arg->value), $arrowFunctionResult->getThrowPoints()));
$impurePoints = array_merge($impurePoints, $arrowFunctionResult->getImpurePoints());
}
if ($this->shouldInvalidateCallbackExpressions($parameter)) {
Expand Down Expand Up @@ -2264,7 +2264,7 @@ public function processArgs(
$deferredInvalidateExpressions[] = [$acceptors[0]->getInvalidateExpressions(), $acceptors[0]->getUsedVariables()];
}
if ($this->callCallbackImmediately($parameter, $parameterType, $calleeReflection)) {
$callableThrowPoints = array_map(static fn (SimpleThrowPoint $throwPoint) => $throwPoint->isExplicit() ? InternalThrowPoint::createExplicit($scope, $throwPoint->getType(), $arg->value, $throwPoint->canContainAnyThrowable()) : InternalThrowPoint::createImplicit($scope, $arg->value), $acceptors[0]->getThrowPoints());
$callableThrowPoints = array_map(static fn (SimpleThrowPoint $throwPoint) => $throwPoint->isExplicit() ? InternalThrowPoint::createExplicit($scope, $throwPoint->getType(), $arg->value, $throwPoint->canContainAnyThrowable(), $throwPoint->isFromThrowExpr()) : InternalThrowPoint::createImplicit($scope, $arg->value), $acceptors[0]->getThrowPoints());
if (!$this->implicitThrows) {
$callableThrowPoints = array_values(array_filter($callableThrowPoints, static fn (InternalThrowPoint $throwPoint) => $throwPoint->isExplicit()));
}
Expand Down
6 changes: 4 additions & 2 deletions src/Analyser/StmtHandler/ExpressionHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use PHPStan\Analyser\ExpressionResultStorage;
use PHPStan\Analyser\InternalStatementExitPoint;
use PHPStan\Analyser\InternalStatementResult;
use PHPStan\Analyser\InternalThrowPoint;
use PHPStan\Analyser\MutatingScope;
use PHPStan\Analyser\NodeScopeResolver;
use PHPStan\Analyser\Scope;
Expand Down Expand Up @@ -77,9 +78,10 @@ public function processStmt(
$nodeScopeResolver->callNodeCallback($nodeCallback, $stmt, $entryScope, $storage);
// Errors signal programmer mistakes (ValueError, TypeError, DivisionByZeroError...),
// nobody calls an otherwise pure expression just to have them thrown, so they
// do not make the expression statement meaningful.
// do not make the expression statement meaningful. A `throw` written in the
// statement is a different story - throwing is the whole point of it.
$errorType = new ObjectType(Error::class);
$throwPoints = array_filter($result->getThrowPoints(), static fn ($throwPoint) => $throwPoint->isExplicit() && !$errorType->isSuperTypeOf($throwPoint->getType())->yes());
$throwPoints = array_filter($result->getThrowPoints(), static fn (InternalThrowPoint $throwPoint) => $throwPoint->isExplicit() && ($throwPoint->isFromThrowExpr() || !$errorType->isSuperTypeOf($throwPoint->getType())->yes()));
if (
count($result->getImpurePoints()) === 0
&& count($throwPoints) === 0
Expand Down
17 changes: 14 additions & 3 deletions src/Analyser/ThrowPoint.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,17 @@ private function __construct(
private Node $node,
private bool $explicit,
private bool $canContainAnyThrowable,
private bool $fromThrowExpr = false,
)
{
}

/**
* @param Node\Expr|Node\Stmt $node
*/
public static function createExplicit(Scope $scope, Type $type, Node $node, bool $canContainAnyThrowable): self
public static function createExplicit(Scope $scope, Type $type, Node $node, bool $canContainAnyThrowable, bool $fromThrowExpr = false): self
{
return new self($scope, $type, $node, true, $canContainAnyThrowable);
return new self($scope, $type, $node, true, $canContainAnyThrowable, $fromThrowExpr);
}

/**
Expand Down Expand Up @@ -71,9 +72,19 @@ public function canContainAnyThrowable(): bool
return $this->canContainAnyThrowable;
}

/**
* Whether the throw point comes from a `throw` written in the analysed code,
* as opposed to a throw inferred from what a called function or an operation
* can throw.
*/
public function isFromThrowExpr(): bool
{
return $this->fromThrowExpr;
}

public function subtractCatchType(Type $catchType): self
{
return new self($this->scope, TypeCombinator::remove($this->type, $catchType), $this->node, $this->explicit, $this->canContainAnyThrowable);
return new self($this->scope, TypeCombinator::remove($this->type, $catchType), $this->node, $this->explicit, $this->canContainAnyThrowable, $this->fromThrowExpr);
}

}
14 changes: 12 additions & 2 deletions src/Reflection/Callables/SimpleThrowPoint.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,14 @@ private function __construct(
private Type $type,
private bool $explicit,
private bool $canContainAnyThrowable,
private bool $fromThrowExpr = false,
)
{
}

public static function createExplicit(Type $type, bool $canContainAnyThrowable): self
public static function createExplicit(Type $type, bool $canContainAnyThrowable, bool $fromThrowExpr = false): self
{
return new self($type, true, $canContainAnyThrowable);
return new self($type, true, $canContainAnyThrowable, $fromThrowExpr);
}

public static function createImplicit(): self
Expand All @@ -53,4 +54,13 @@ public function canContainAnyThrowable(): bool
return $this->canContainAnyThrowable;
}

/**
* Whether the throw point comes from a `throw` written in the callable's body,
* as opposed to a throw only declared in PHPDoc.
*/
public function isFromThrowExpr(): bool
{
return $this->fromThrowExpr;
}

}
6 changes: 6 additions & 0 deletions tests/PHPStan/Rules/DeadCode/NoopRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,12 @@ public function testErrorThrows(): void
]);
}

#[RequiresPhp('>= 8.0.0')]
public function testBug15258(): void
{
$this->analyse([__DIR__ . '/data/bug-15258.php'], []);
}

public function testNullsafe(): void
{
$this->analyse([__DIR__ . '/data/nullsafe-property-fetch-noop.php'], [
Expand Down
18 changes: 18 additions & 0 deletions tests/PHPStan/Rules/DeadCode/data/bug-15258.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php // lint >= 8.0

declare(strict_types = 1);

namespace Bug15258;

/**
* Given a string of the shape 'foo-bar', extract the prefix 'foo-'
* @phpstan-pure
* @param non-empty-string $x
* @return non-empty-string
*/
function extract_prefix(string $x): string {
$pos = strpos($x, '-');
if ($pos === false)
throw new \ValueError('cannot extract a prefix from a string that has no prefix');
return substr($x, 0, $pos + 1);
}
57 changes: 57 additions & 0 deletions tests/PHPStan/Rules/DeadCode/data/noop-error-throws.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,60 @@ function (int $a, int $b, string $s) {
};
new \DateTimeImmutable($s);
};

function () {
throw new \ValueError('x');
};

function (\Error $e) {
throw $e;
};

function (?int $n) {
$n ?? throw new \ValueError('x');
};

function (bool $c) {
$c ? throw new \ValueError('x') : null;
};

function (?int $n) {
$n ?: throw new \ValueError('x');
};

function (bool $c) {
$c && throw new \ValueError('x');
};

function (bool $c) {
$c || throw new \ValueError('x');
};

function (int $a) {
match (true) {
$a > 0 => throw new \ValueError('x'),
default => null,
};
};

function () {
(function (): void {
throw new \ValueError('x');
})();
};

function () {
$fn = function (): void {
throw new \ValueError('x');
};
$fn();
};

function () {
new class {
public function __construct()
{
throw new \ValueError('x');
}
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,17 @@ public function testErrorThrows(): void
]);
}

#[RequiresPhp('>= 8.0.0')]
public function testThrowExprInCallbackArgument(): void
{
$this->analyse([__DIR__ . '/data/function-call-statement-no-side-effects-throw-expr.php'], [
[
'Call to function array_map() on a separate line has no effect.',
21,
],
]);
}

public function testBug4455(): void
{
require_once __DIR__ . '/data/bug-4455.php';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php // lint >= 8.0

namespace FunctionCallStatementNoSideEffectsThrowExpr;

class Foo
{

/**
* @param list<int> $a
*/
public function doFoo(array $a): void
{
array_map(static fn (int $i) => throw new \ValueError('x'), $a);
array_map(static function (int $i): int {
throw new \ValueError('x');
}, $a);
$callback = static function (int $i): int {
throw new \ValueError('x');
};
array_map($callback, $a);
array_map(static fn (int $i) => $i * 2, $a);
}

}
Loading