Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
21 changes: 20 additions & 1 deletion src/Analyser/MutatingScope.php
Original file line number Diff line number Diff line change
Expand Up @@ -3818,7 +3818,7 @@ public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self
$scope->getNamespace(),
$scope->expressionTypes,
$scope->nativeExpressionTypes,
array_merge($specifiedTypes->getNewConditionalExpressionHolders(), $scope->conditionalExpressions),
$this->mergeConditionalExpressions($specifiedTypes->getNewConditionalExpressionHolders(), $scope->conditionalExpressions),
$scope->inClosureBindScopeClasses,
$scope->anonymousFunctionReflection,
$scope->inFirstLevelStatement,
Expand Down Expand Up @@ -4007,6 +4007,25 @@ private function intersectConditionalExpressions(array $otherConditionalExpressi
return $newConditionalExpressions;
}

/**
* @param array<string, ConditionalExpressionHolder[]> $newConditionalExpressions
* @param array<string, ConditionalExpressionHolder[]> $existingConditionalExpressions
* @return array<string, ConditionalExpressionHolder[]>
*/
private function mergeConditionalExpressions(array $newConditionalExpressions, array $existingConditionalExpressions): array
{
$result = $existingConditionalExpressions;
foreach ($newConditionalExpressions as $exprString => $holders) {
if (!array_key_exists($exprString, $result)) {
$result[$exprString] = $holders;
} else {
$result[$exprString] = array_merge($result[$exprString], $holders);
}
}

return $result;
}

/**
* @param array<string, ConditionalExpressionHolder[]> $conditionalExpressions
* @param array<string, ExpressionTypeHolder> $ourExpressionTypes
Expand Down
58 changes: 58 additions & 0 deletions src/Analyser/NodeScopeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -4364,6 +4364,13 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context, $sto
$matchScope = $matchScope->filterByFalseyValue($filteringExpr);
}

if (!$hasDefaultCond && !$hasAlwaysTrueCond && $condType->isBoolean()->yes() && $condType->isConstantScalarValue()->yes()) {
if ($this->isScopeConditionallyImpossible($matchScope)) {
$hasAlwaysTrueCond = true;
$matchScope = $matchScope->addTypeToExpression($expr->cond, new NeverType());
}
}

$isExhaustive = $hasDefaultCond || $hasAlwaysTrueCond;
if (!$isExhaustive) {
$remainingType = $matchScope->getType($expr->cond);
Expand Down Expand Up @@ -7598,6 +7605,57 @@ private function getFilteringExprForMatchArm(Expr\Match_ $expr, array $condition
);
}

/**
* Checks if a scope's conditional expressions form a contradiction,
* meaning no combination of variable values is possible.
* Used for match(true) exhaustiveness detection.
*/
private function isScopeConditionallyImpossible(MutatingScope $scope): bool
{
$boolVars = [];
foreach ($scope->getDefinedVariables() as $varName) {
$varType = $scope->getVariableType($varName);
if ($varType->isBoolean()->yes() && !$varType->isConstantScalarValue()->yes()) {
$boolVars[] = $varName;
}
}

if ($boolVars === []) {
return false;
}

// Check if any boolean variable's both truth values lead to contradictions
foreach ($boolVars as $varName) {
$varExpr = new Variable($varName);
$truthyScope = $scope->filterByTruthyValue($varExpr);
$falseyScope = $scope->filterByFalseyValue($varExpr);

$truthyContradiction = $this->scopeHasNeverVariable($truthyScope, $boolVars);
$falseyContradiction = $this->scopeHasNeverVariable($falseyScope, $boolVars);

if ($truthyContradiction && $falseyContradiction) {
return true;
}
Comment thread
ondrejmirtes marked this conversation as resolved.
}

return false;
}

/**
* @param string[] $varNames
*/
private function scopeHasNeverVariable(MutatingScope $scope, array $varNames): bool
{
foreach ($varNames as $varName) {
$type = $scope->getVariableType($varName);
if ($type instanceof NeverType) {
return true;
}
}

return false;
}

private function inferForLoopExpressions(For_ $stmt, Expr $lastCondExpr, MutatingScope $bodyScope): MutatingScope
{
// infer $items[$i] type from for ($i = 0; $i < count($items); $i++) {...}
Expand Down
10 changes: 10 additions & 0 deletions src/Analyser/SpecifiedTypes.php
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,16 @@ public function unionWith(SpecifiedTypes $other): self
$result = $result->setAlwaysOverwriteTypes();
}

$conditionalExpressionHolders = $this->newConditionalExpressionHolders;
foreach ($other->newConditionalExpressionHolders as $exprString => $holders) {
if (!array_key_exists($exprString, $conditionalExpressionHolders)) {
$conditionalExpressionHolders[$exprString] = $holders;
} else {
$conditionalExpressionHolders[$exprString] = array_merge($conditionalExpressionHolders[$exprString], $holders);
}
}
$result->newConditionalExpressionHolders = $conditionalExpressionHolders;

return $result->setRootExpr($rootExpr);
}

Expand Down
18 changes: 14 additions & 4 deletions src/Analyser/TypeSpecifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -645,6 +645,16 @@ public function specifyTypesInCondition(
$rightTypes = $this->specifyTypesInCondition($rightScope, $expr->right, $context)->setRootExpr($expr);
$types = $context->true() ? $leftTypes->unionWith($rightTypes) : $leftTypes->normalize($scope)->intersectWith($rightTypes->normalize($rightScope));
if ($context->false()) {
$leftTypesForHolders = $leftTypes;
$rightTypesForHolders = $rightTypes;
if ($context->truthy()) {
if ($leftTypesForHolders->getSureTypes() === [] && $leftTypesForHolders->getSureNotTypes() === []) {
$leftTypesForHolders = $this->specifyTypesInCondition($scope, $expr->left, TypeSpecifierContext::createFalsey())->setRootExpr($expr);
}
if ($rightTypesForHolders->getSureTypes() === [] && $rightTypesForHolders->getSureNotTypes() === []) {
$rightTypesForHolders = $this->specifyTypesInCondition($rightScope, $expr->right, TypeSpecifierContext::createFalsey())->setRootExpr($expr);
}
}
$result = new SpecifiedTypes(
$types->getSureTypes(),
$types->getSureNotTypes(),
Expand All @@ -653,10 +663,10 @@ public function specifyTypesInCondition(
$result = $result->setAlwaysOverwriteTypes();
}
return $result->setNewConditionalExpressionHolders(array_merge(
$this->processBooleanNotSureConditionalTypes($scope, $leftTypes, $rightTypes),
$this->processBooleanNotSureConditionalTypes($scope, $rightTypes, $leftTypes),
$this->processBooleanSureConditionalTypes($scope, $leftTypes, $rightTypes),
$this->processBooleanSureConditionalTypes($scope, $rightTypes, $leftTypes),
$this->processBooleanNotSureConditionalTypes($scope, $leftTypesForHolders, $rightTypesForHolders),
$this->processBooleanNotSureConditionalTypes($scope, $rightTypesForHolders, $leftTypesForHolders),
$this->processBooleanSureConditionalTypes($scope, $leftTypesForHolders, $rightTypesForHolders),
$this->processBooleanSureConditionalTypes($scope, $rightTypesForHolders, $leftTypesForHolders),
))->setRootExpr($expr);
}

Expand Down
5 changes: 5 additions & 0 deletions tests/PHPStan/Rules/Classes/ImpossibleInstanceOfRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,11 @@ public function testBug3632(): void

$tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting <fg=cyan>treatPhpDocTypesAsCertain: false</> in your <fg=cyan>%configurationFile%</>.';
$this->analyse([__DIR__ . '/data/bug-3632.php'], [
[
'Instanceof between null and Bug3632\NiceClass will always evaluate to false.',
32,
$tipText,
],
[
'Instanceof between Bug3632\NiceClass and Bug3632\NiceClass will always evaluate to true.',
36,
Expand Down
11 changes: 11 additions & 0 deletions tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -405,4 +405,15 @@ public function testBug13048(): void
$this->analyse([__DIR__ . '/data/bug-13048.php'], []);
}

#[RequiresPhp('>= 8.0')]
public function testBug13303(): void
{
$this->analyse([__DIR__ . '/data/bug-13303.php'], [
[
'Match expression does not handle remaining value: true',
34,
],
]);
}

}
39 changes: 39 additions & 0 deletions tests/PHPStan/Rules/Comparison/data/bug-13303.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php // lint >= 8.0

namespace Bug13303;

function a(bool $b, bool $c): int {
return match(true) {
$b && $c => 1,
!$b && !$c => 2,
!$b && $c => 3,
$b && !$c => 4,
};
}

function b(bool $b, bool $c): int {
return match(true) {
$b && $c,
!$b && !$c => 1,
!$b && $c,
$b && !$c => 2,
};
}

function c(bool $b, bool $c): int {
return match(true) {
$b === true && $c === true => 1,
$b === false && $c === false => 2,
$b === false && $c === true => 3,
$b === true && $c === false => 4,
};
}

function d(bool $b, bool $c): int {
// Not exhaustive - should still report error
return match(true) {
$b && $c => 1,
!$b && !$c => 2,
!$b && $c => 3,
};
}
Loading