New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
LastConditionVisitor: condition followed by throw is marked as last #2405
Conversation
You've opened the pull request against the latest branch 1.11.x. If your code is relevant on 1.10.x and you want it to be released sooner, please rebase your pull request and change its target to 1.10.x. |
The problem here is that for this code: if ($color === Color::Red) {
echo "red\n";
} elseif ($color === Color::Blue) { // this is currently reported as error
echo "blue\n";
} else {
throw new LogicException('Unexpected color');
} PHPStan will not report an error even when What I want developers to do to rewrite this to a match expression: echo match ($color) {
Color::Red => 'red',
Color::Blue => 'blue',
}; Which solves all of the problems. I know it's not obvious to go from |
Match expression leads to safety through static analysis. Why is it not always possible and practical? |
Which is still an improvement over current solution. Failing at runtime with exception is preferred over failing at runtime silently. But of course, detecting the issue with PHPStan would be even better. The classic type safe approach that I proposed to support few weeks ago, was not accepted. And would not be compatible with this fix anyway. New idea I have would be to have dedicated "dead code exception", which PHPStan would require to be thrown only from dead code. So this would be fine. if ($color === Color::Red) {
echo "red\n";
} elseif ($color === Color::Blue) {
echo "blue\n";
} else {
// no error, because this is dead code
throw new SpecialDeadCodeOnlyException('Unexpected color');
} But this would PHPStan report as error, because if ($color === Color::Red) {
echo "red\n";
} else {
// error: SpecialDeadCodeOnlyException can be only thrown from dead code
throw new SpecialDeadCodeOnlyException('Unexpected color');
}
Mostly because it does not support statements.
|
Yeah this would be a nice addition to the PHP language, however you can already do this today: match ($color) {
Color::Red => (function () {
echo 'red';
})(),
Color::Blue => (function () {
echo 'blue';
})(),
}; Someone might prefer IIFE and someone might prefer "dead code exception". The easiest way to implement "dead code exception" rule is to pass |
So what I'd like to do:
if ($color === Color::Red) {
echo "red\n";
} elseif ($color === Color::Blue) { // this is currently reported as error
echo "blue\n";
} else {
throw new LogicException('Unexpected color');
} The code in this PR might be useful for this. We could tell the user to rewrite it as
|
That's why both should work. I would be shocked if more people prefer IIFE over "dead code exception". And again it will work very poorly, when you have multiple/inconsistent variable assignments. You have tu use tuples/dto/variable passed by reference. Compare $blueFound = false;
$redFound = false;
foreach ($colors as $color) {
if ($color === Color::Red) {
$redFound = true;
} elseif ($color === Color::Blue) {
$blueFound = true;
} else {
throw new DeadCodeException();
}
} with $blueFound = false;
$redFound = false;
foreach ($colors as $color) {
match ($color) {
Color::Red => (function () use (&$redFound): void {
$redFound = true;
})(),
Color::Blue => (function () use (&$blueFound): void {
$blueFound = true;
})(),
};
}
I thought this was already implemented. Anyway, you iterate over declared variables in scope and if any of them has type
I disagree with that being the right direction. It could be a valid code style choice, but PHPStan is not a code style checker. |
A scope can also be dead without any variables involved, so I'd rather rely on this: phpstan-src/src/Analyser/NodeScopeResolver.php Lines 790 to 794 in 19c838b
But if we did something like this: foreach ($colors as $color) {
if ($color === Color::Red) {
$redFound = true;
} elseif ($color === Color::Blue) {
$blueFound = true;
} else {
/** @phpstan-var-assert never $color */
throw new DeadCodeException();
}
} Then the code would look at the type of
I get it, but the issue is here "how to correctly get rid of this 'always true' error by making code better" so we can tell the user what to do according to our opinion. |
With the introduction of if ($color === Color::Red) {
echo "red\n";
} elseif ($color === Color::Blue) { // this is currently reported as error
echo "blue\n";
} else {
throw new NeverException($color);
}
// ---
final class NeverException extends LogicException
{
/**
* @param never $value
*/
public function __construct(mixed $value)
{
$message = 'Type of value should be never, but got ' . get_debug_type($value);
parent::__construct($message);
}
} This allows PHPStan to report error when enum case is added that the code does not handle. This should address the concern that was blocking the merge. |
What would be great is to differentiate between: if ($color === Color::Red) {
echo "red\n";
} elseif ($color === Color::Blue) {
echo "blue\n";
} else {
throw new NeverException($color); // safe code - no error should be reported with the last elseif
} and: if ($color === Color::Red) {
echo "red\n";
} elseif ($color === Color::Blue) {
echo "blue\n";
} else {
throw new LogicException; // unsafe code - "never" is not enforced - last elseif should still be reported
} WDYT? |
I'd say it would be great, but even current state greatly improves usability. I'd consider this a next step. Can we merge this as-is and possibly focus on this (imo hard) part in separate MR? |
11f1b5c
to
b099ddd
Compare
Yes, agreed :) |
Thank you! |
1 similar comment
Thank you! |
This improves LastConditionVisitor to consider as "last condition" any
elseif
that is followed byelse
with throw statement, or any lastif/elseif
condition that is followed throw statement.This fixes issue, where PHPStan reports error when safety checks are used. Removing the
else
branch leads to less readable and more dangerous code, rewriting to match is not always possible / practical.Removing
else
branch leads to code, that will silently fail when new color is added.Besided enums, we are also encountering this pattern when trying to use sealed classes