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
8 changes: 4 additions & 4 deletions src/Analyser/ExprHandler/BooleanAndHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -152,10 +152,10 @@ public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $e
$result = $result->setAlwaysOverwriteTypes();
}
return $result->setNewConditionalExpressionHolders($this->conditionalExpressionHolderHelper->mergeConditionalHolders([
$this->conditionalExpressionHolderHelper->processBooleanConditionalTypes($scope, $leftCondTypes, $rightHolderTypes, false, true, $rightScope, $expr->right),
$this->conditionalExpressionHolderHelper->processBooleanConditionalTypes($scope, $rightCondTypes, $leftHolderTypes, false, true, $scope, $expr->left),
$this->conditionalExpressionHolderHelper->processBooleanConditionalTypes($scope, $leftCondTypes, $rightHolderTypes, true, true, $rightScope, $expr->right),
$this->conditionalExpressionHolderHelper->processBooleanConditionalTypes($scope, $rightCondTypes, $leftHolderTypes, true, true, $scope, $expr->left),
$this->conditionalExpressionHolderHelper->processBooleanConditionalTypes($scope, $leftCondTypes, $rightHolderTypes, false, true, $rightScope, $expr->right, $expr->left),
$this->conditionalExpressionHolderHelper->processBooleanConditionalTypes($scope, $rightCondTypes, $leftHolderTypes, false, true, $scope, $expr->left, $expr->right),
$this->conditionalExpressionHolderHelper->processBooleanConditionalTypes($scope, $leftCondTypes, $rightHolderTypes, true, true, $rightScope, $expr->right, $expr->left),
$this->conditionalExpressionHolderHelper->processBooleanConditionalTypes($scope, $rightCondTypes, $leftHolderTypes, true, true, $scope, $expr->left, $expr->right),
]))->setRootExpr($expr);
}

Expand Down
8 changes: 4 additions & 4 deletions src/Analyser/ExprHandler/BooleanOrHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -126,10 +126,10 @@ public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $e
$result = $result->setAlwaysOverwriteTypes();
}
return $result->setNewConditionalExpressionHolders($this->conditionalExpressionHolderHelper->mergeConditionalHolders([
$this->conditionalExpressionHolderHelper->processBooleanConditionalTypes($scope, $leftTypes, $rightTypes, false, false, $rightScope, $expr->right),
$this->conditionalExpressionHolderHelper->processBooleanConditionalTypes($scope, $rightTypes, $leftTypes, false, false, $scope, $expr->left),
$this->conditionalExpressionHolderHelper->processBooleanConditionalTypes($scope, $leftTypes, $rightTypes, true, false, $rightScope, $expr->right),
$this->conditionalExpressionHolderHelper->processBooleanConditionalTypes($scope, $rightTypes, $leftTypes, true, false, $scope, $expr->left),
$this->conditionalExpressionHolderHelper->processBooleanConditionalTypes($scope, $leftTypes, $rightTypes, false, false, $rightScope, $expr->right, $expr->left),
$this->conditionalExpressionHolderHelper->processBooleanConditionalTypes($scope, $rightTypes, $leftTypes, false, false, $scope, $expr->left, $expr->right),
$this->conditionalExpressionHolderHelper->processBooleanConditionalTypes($scope, $leftTypes, $rightTypes, true, false, $rightScope, $expr->right, $expr->left),
$this->conditionalExpressionHolderHelper->processBooleanConditionalTypes($scope, $rightTypes, $leftTypes, true, false, $scope, $expr->left, $expr->right),
]))->setRootExpr($expr);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,8 +141,23 @@ public function mergeConditionalHolders(array $holderLists): array
/**
* @return array<string, ConditionalExpressionHolder[]>
*/
public function processBooleanConditionalTypes(Scope $scope, SpecifiedTypes $conditionSpecifiedTypes, SpecifiedTypes $holderSpecifiedTypes, bool $holdersFromSureTypes, bool $holderSideIsNegated, Scope $rightScope, ?Expr $holderSideExpr = null): array
public function processBooleanConditionalTypes(Scope $scope, SpecifiedTypes $conditionSpecifiedTypes, SpecifiedTypes $holderSpecifiedTypes, bool $holdersFromSureTypes, bool $holderSideIsNegated, Scope $rightScope, ?Expr $holderSideExpr = null, ?Expr $conditionSideExpr = null): array
{
// The condition (antecedent) side asserts a truth value for its
// sub-expression, and the holder guard uses that side's per-expression
// narrowing as if it were equivalent to that truth value. When the
// condition side is a compound boolean whose asserted truth value is a
// disjunction (`$a || $b` asserted true, or `$a && $b` asserted false),
// the narrowing is only a necessary consequence of the truth value, not a
// sufficient one — e.g. `($a && $b) || ($c && $d)` being true narrows a
// shared variable, but that narrowing can hold without the disjunction
// being true. Using such an under-approximating narrowing as a guard fires
// the holder unsoundly, so no holder is built. This mirrors the holder-side
// check below with the opposite polarity.
if ($this->isUnsplittableCompoundSide($conditionSideExpr, !$holderSideIsNegated)) {
return [];
}

// The condition side asserts that its sub-expression evaluates truthy.
// When that sub-expression is itself a compound boolean (e.g. `$a && $b`),
// the narrowings making it true are spread across both the sure and
Expand Down Expand Up @@ -200,7 +215,7 @@ public function processBooleanConditionalTypes(Scope $scope, SpecifiedTypes $con
// Symmetrically, in the `BooleanOr` true context the holder asserts its
// side is true, and a disjunction side (`$a || $b`) is itself a disjunction.
// Such a side is left whole rather than split into over-narrowing holders.
if ($this->isUnsplittableCompoundHolderSide($holderSideExpr, $holderSideIsNegated)) {
if ($this->isUnsplittableCompoundSide($holderSideExpr, $holderSideIsNegated)) {
return [];
}

Expand Down Expand Up @@ -269,22 +284,23 @@ public function processBooleanConditionalTypes(Scope $scope, SpecifiedTypes $con
}

/**
* A holder side whose truth value is asserted as a disjunction cannot be
* decomposed into independent per-expression holders. That happens for a
* conjunction (`&&`) asserted false (negated context) and for a disjunction
* (`||`) asserted true.
* A boolean operand whose asserted truth value is a disjunction cannot be
* decomposed into independent per-expression narrowings. That happens for a
* conjunction (`&&`) asserted false (its negation is a disjunction) and for a
* disjunction (`||`) asserted true. Applies to both the holder (consequent)
* side and the condition (antecedent) side.
*/
private function isUnsplittableCompoundHolderSide(?Expr $holderSideExpr, bool $holderSideIsNegated): bool
private function isUnsplittableCompoundSide(?Expr $sideExpr, bool $isNegated): bool
{
if ($holderSideExpr === null) {
if ($sideExpr === null) {
return false;
}

if ($holderSideIsNegated) {
return $holderSideExpr instanceof BooleanAnd || $holderSideExpr instanceof LogicalAnd;
if ($isNegated) {
return $sideExpr instanceof BooleanAnd || $sideExpr instanceof LogicalAnd;
}

return $holderSideExpr instanceof BooleanOr || $holderSideExpr instanceof LogicalOr;
return $sideExpr instanceof BooleanOr || $sideExpr instanceof LogicalOr;
}

private function isTrackableExpression(Expr $expr): bool
Expand Down
82 changes: 82 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-14908.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?php // lint >= 8.1

declare(strict_types = 1);

namespace Bug14908;

use function PHPStan\Testing\assertType;

enum Grade
{
case One;
case Two;
case Three;
}

enum Kind
{
case K1;
case K2;
case K3;
}

class Flags
{
public bool $flagA = false;
}

function run(Kind $kind, Grade $grade, Flags $flags, bool $extra, bool $cond): void
{
$forced = false;
if (
$grade !== Grade::Three
&& $cond
&& in_array($kind, [Kind::K1, Kind::K2], true)
&& $flags->flagA === true
) {
$forced = true;
}

// Intermediate `if` narrowing ANOTHER value (`$extra === false`) inside a
// disjunction. The disjunction's truth is not captured by narrowing `$grade`
// alone, so the boolean-decomposition holder `$grade ⇒ $forced` must not be
// created — otherwise the narrowings from the first `if` leak into the block
// below.
if (
$forced === false
&& (
($grade === Grade::One && $extra === false)
|| ($cond && $grade !== Grade::Three)
)
) {
throw new \Exception();
}

if ($grade !== Grade::Three) {
assertType('bool', $flags->flagA);
assertType('Bug14908\\Kind', $kind);
assertType('bool', $forced);
}
}

// The narrowing must still fire when the guard genuinely selects the branch:
// `$forced === true` really does imply the first `if` was taken.
function soundNarrowing(Kind $kind, Grade $grade, Flags $flags, bool $cond): void
{
$forced = false;
if (
$grade !== Grade::Three
&& $cond
&& in_array($kind, [Kind::K1, Kind::K2], true)
&& $flags->flagA === true
) {
$forced = true;
}

if ($forced === false) {
throw new \Exception();
}

assertType('true', $flags->flagA);
assertType('Bug14908\\Kind::K1|Bug14908\\Kind::K2', $kind);
}
Original file line number Diff line number Diff line change
Expand Up @@ -1346,6 +1346,13 @@ public function testBug8980(): void
$this->analyse([__DIR__ . '/data/bug-8980.php'], []);
}

#[RequiresPhp('>= 8.1.0')]
public function testBug14908(): void
{
$this->treatPhpDocTypesAsCertain = true;
$this->analyse([__DIR__ . '/data/bug-14908.php'], []);
}

public function testBug6211(): void
{
$this->treatPhpDocTypesAsCertain = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1249,6 +1249,12 @@ public function testBug14878(): void
$this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-14878.php'], []);
}

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

public function testBug14847(): void
{
$this->analyse([__DIR__ . '/data/bug-14847.php'], [
Expand Down
56 changes: 56 additions & 0 deletions tests/PHPStan/Rules/Comparison/data/bug-14908.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php // lint >= 8.1

declare(strict_types = 1);

namespace BugRule14908;

enum Grade
{
case One;
case Two;
case Three;
}

enum Kind
{
case K1;
case K2;
case K3;
}

class Flags
{
public bool $flagA = false;
}

function run(Kind $kind, Grade $grade, Flags $flags, bool $extra, bool $cond): void
{
$forced = false;
if (
$grade !== Grade::Three
&& $cond
&& in_array($kind, [Kind::K1, Kind::K2], true)
&& $flags->flagA === true
) {
$forced = true;
}

if (
$forced === false
&& (
($grade === Grade::One && $extra === false)
|| ($cond && $grade !== Grade::Three)
)
) {
throw new \Exception();
}

if ($grade !== Grade::Three) {
if ($flags->flagA === false) {
throw new \Exception();
}
if (in_array($kind, [Kind::K1, Kind::K2], true)) {
echo 'reachable';
}
}
}
Loading