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
24 changes: 23 additions & 1 deletion src/Analyser/ExprHandler/MatchHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -468,7 +468,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
}

if (!$hasDefaultCond && !$hasAlwaysTrueCond && $condType->isBoolean()->yes() && $condType->isConstantScalarValue()->yes()) {
if ($this->isScopeConditionallyImpossible($matchScope)) {
if ($this->hasExpressionNarrowedToNever($scope, $matchScope) || $this->isScopeConditionallyImpossible($matchScope)) {
$hasAlwaysTrueCond = true;
$matchScope = $matchScope->addTypeToExpression($expr->cond, new NeverType());
}
Expand Down Expand Up @@ -605,6 +605,28 @@ private function getFilteringExprForMatchArm(Match_ $expr, array $conditions, ar
);
}

/**
* Whether the arms' falsey narrowing left an expression (a variable, a
* property fetch, ...) without any possible value - then no arm being
* matched is impossible.
*/
private function hasExpressionNarrowedToNever(MutatingScope $scopeBeforeArms, MutatingScope $matchScope): bool
{
foreach ($matchScope->expressionTypes as $exprString => $holder) {
if (!$holder->getType() instanceof NeverType) {
continue;
}
$holderBeforeArms = $scopeBeforeArms->expressionTypes[$exprString] ?? null;
if ($holderBeforeArms !== null && $holderBeforeArms->getType() instanceof NeverType) {
continue;
}

return true;
}

return false;
}

private function isScopeConditionallyImpossible(MutatingScope $scope): bool
{
$boolVars = [];
Expand Down
52 changes: 52 additions & 0 deletions tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -601,4 +601,56 @@ public function testBug10583(): void
]);
}

#[RequiresPhp('>= 8.0.0')]
public function testBug11374(): void
{
$this->treatPhpDocTypesAsCertain = true;
$this->analyse([__DIR__ . '/data/bug-11374.php'], [
[
'Call to function is_float() with float will always evaluate to true.',
13,
],
[
'Call to method Bug11374\\HelloWorld::isFloat() with float will always evaluate to true.',
21,
],
[
'Call to method Bug11374\\HelloWorld::isFloat() with float will always evaluate to true.',
29,
],
[
'Call to function is_float() with float will always evaluate to true.',
37,
],
[
'Call to static method Bug11374\\Analogous::isFloatStatic() with float will always evaluate to true.',
75,
],
[
'Call to function isFloatFunction() with float will always evaluate to true.',
83,
],
[
'Call to static method Bug11374\\Analogous::isFloatStatic() with float will always evaluate to true.',
110,
],
[
'Match expression does not handle remaining value: true',
124,
],
[
'Match expression does not handle remaining value: true',
132,
],
[
'Call to static method Bug11374\\MoreAnalogous::isNotFloat() with float will always evaluate to false.',
181,
],
[
'Match expression does not handle remaining value: false',
195,
],
]);
}

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

declare(strict_types = 1);

namespace Bug11374;

class HelloWorld
{
public function test(float|int $value): string
{
return match (true) {
is_int($value) => (string) $value,
is_float($value) => (string) $value,
};
}

public function test2(float|int $value): string
{
return match (true) {
$this->isInt($value) => (string) $value,
$this->isFloat($value) => (string) $value,
};
}

public function test3(float|int $value): string
{
return match (true) {
is_int($value) => (string) $value,
$this->isFloat($value) => (string) $value,
};
}

public function test4(float|int $value): string
{
return match (true) {
$this->isInt($value) => (string) $value,
is_float($value) => (string) $value,
};
}

/**
* @phpstan-assert-if-true float $value
*/
private function isFloat(mixed $value): bool
{
return is_float($value);
}

/**
* @phpstan-assert-if-true int $value
*/
private function isInt(mixed $value): bool
{
return is_int($value);
}
}

class A {}
class B {}

final class Analogous
{

private int|float $prop = 1;

public function setProp(int|float $prop): void
{
$this->prop = $prop;
}

public function staticCall(float|int $value): string
{
return match (true) {
self::isIntStatic($value) => (string) $value,
self::isFloatStatic($value) => (string) $value,
};
}

public function functionCall(float|int $value): string
{
return match (true) {
isIntFunction($value) => (string) $value,
isFloatFunction($value) => (string) $value,
};
}

public function propertyFetch(): string
{
return match (true) {
self::isIntStatic($this->prop) => (string) $this->prop,
self::isFloatStatic($this->prop) => (string) $this->prop,
};
}

/**
* @param array{x: int|float} $a
*/
public function arrayOffset(array $a): string
{
return match (true) {
self::isIntStatic($a['x']) => 'int',
self::isFloatStatic($a['x']) => 'float',
};
}

public function multipleConditions(float|int|string $value): string
{
return match (true) {
self::isIntStatic($value), is_string($value) => 'int or string',
self::isFloatStatic($value) => 'float',
};
}

public function instanceofUnion(A|B $obj): string
{
return match (true) {
$obj instanceof A => 'a',
$obj instanceof B => 'b',
};
}

public function notExhaustiveObject(object $obj): string
{
return match (true) {
$obj instanceof A => 'a',
$obj instanceof B => 'b',
};
}

public function notExhaustiveAssert(float|int|string $value): string
{
return match (true) {
self::isIntStatic($value) => 'int',
self::isFloatStatic($value) => 'float',
};
}

/**
* @phpstan-assert-if-true float $value
*/
private static function isFloatStatic(mixed $value): bool
{
return is_float($value);
}

/**
* @phpstan-assert-if-true int $value
*/
private static function isIntStatic(mixed $value): bool
{
return is_int($value);
}

}

/**
* @phpstan-assert-if-true float $value
*/
function isFloatFunction(mixed $value): bool
{
return is_float($value);
}

/**
* @phpstan-assert-if-true int $value
*/
function isIntFunction(mixed $value): bool
{
return is_int($value);
}

final class MoreAnalogous
{

private int|float $prop = 1;

public function matchFalse(float|int $value): string
{
return match (false) {
self::isNotInt($value) => 'int',
self::isNotFloat($value) => 'float',
};
}

public function assertOnProperty(): string
{
return match (true) {
$this->propIsInt() => 'int',
$this->propIsFloat() => 'float',
};
}

public function notExhaustiveMatchFalse(float|int|string $value): string
{
return match (false) {
self::isNotInt($value) => 'int',
self::isNotFloat($value) => 'float',
};
}

/**
* @phpstan-assert-if-false int $value
*/
private static function isNotInt(mixed $value): bool
{
return !is_int($value);
}

/**
* @phpstan-assert-if-false float $value
*/
private static function isNotFloat(mixed $value): bool
{
return !is_float($value);
}

/**
* @phpstan-assert-if-true int $this->prop
*/
private function propIsInt(): bool
{
return is_int($this->prop);
}

/**
* @phpstan-assert-if-true float $this->prop
*/
private function propIsFloat(): bool
{
return is_float($this->prop);
}

}
Loading
Loading