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
Bug: Incorrect code path analysis for return
in try blocks with catch clause
#17579
Comments
return
in try blocks with catch clausereturn
in try blocks with catch clause
I think #16593 (comment) is a clearer demonstration of the issue. |
I think this is something we should fix. Whether or not it's practical to do in the v9 timeframe is the question. I've just spent a bunch of time digging into code path analysis so I'll take a look. |
I took an initial look at this. At this point, it seems like the problem is in this method: eslint/lib/linter/code-path-analysis/code-path-state.js Lines 1614 to 1635 in 2665552
When we get here, Question: Should the unreachable block even be there at all? (Just leaving these as notes for myself, or in case anyone else wants to take a look.) |
I'm clearly not the one who's supposed to know better, so sorry if my comments sound naive. Just unclear about a couple of things in this graph.
|
|
@mdjermanovic looking for your feedback here. |
For reference, here's the original issue: #7481 |
Unreachable segments continuing to other segments is common in our code path analysis and seems logical to me. For example, in |
The arrows from |
Code path analysis currently creates a connection from the first throwable node (references are considered to be throwable) to the catch clause, and from the end of the try block ( For example:
Now, connections from BlockStatement:exit of the try block to the catch clause always seem logically wrong, because the execution from that point (whether it's reachable or not) never falls into the catch clause. It either goes to finally, if there's However, if we just remove it: then there will be no paths from other throwable nodes ( A solution for logically correct code paths could be to make a connection from each throwable node to the catch clause, as suggested in #7481 (comment), but that could create huge graphs and thus is a performance concern as noted in #7481. Another solution, that doesn't create that many segments, could be to somehow split segments into throwable and non-throwable parts, but that seems very complicated. The graph could look like this: |
I can see your point about Also, keep in mind that these are not 100% relevant to the code path and are just output by the debugging to show where in the traversal we are. It doesn't necessarily mean the segment represents exiting a And your second comment seems like a tangential issue with how |
Segment with
I think it's the same problem. Because code path analysis always generates a connection from the end of the try block to the catch block, the graph in #17579 (comment) suggests that this |
Okay, I'm having trouble keeping all of that in my brain at the moment. Would you be interested in tackling this issue? |
Folks, I'm way too burned out at the moment to even attempt to look at this. @mdjermanovic or @fasttime, do you want to take a look? Otherwise, let's just move this out of v9. |
I can have a look, but no problem to move this out of v9. |
Thanks @fasttime. I'd love to get this done, but also realize that code path analysis is hard and I don't think it should block v9. |
@fasttime are you still working on this? |
Sorry, I've been busy doing other work. I can do some analysis on this on the weekend. |
Okay, no problem. This is one of the last two issues we had planned for the beta, so if you don't think you'll be able to address this soonish, we can move it to v10. |
Per 2024-02-08 Corrected release * |
That should be the beta.1 release. |
Just wanted to give an update on what I've found out so far. It seems to me that all unreachable segments produced by a statement that exits a try block are getting an inconsistent connection to the catch clause. For example, I believe this is another case where code path analysis is producing an incorrect connection: foo:
for (;;) {
try {
bar();
break foo;
}
catch {
baz();
}
} In this example, the Following this model, in this example there should be no connection from "<<unreachable>> BreakStatement:exit" to "CatchClause:enter", because even if So I think that the fix for this bug should consist in disconnecting unreachable segments from segments that enter a catch clause. The unreachable segments should probably still remain in place to indicate the impossible flow of the program from the statement in the try block to what comes after the try-catch statement. |
This seems correct to me. 👍 @mdjermanovic what do you think? |
This is another example that highlights the problem even more: try { }
catch {
foo();
} Here, the code inside the catch clause will never run, because the try block never throws, right? Code path analysis doesn't care and the result is a straight connection from the empty try block into the catch block: It seems unlikely that an empty block can ever throw an exception, but it's a possibility that cannot be completely ruled out. For example, an exception could occur while halting on a breakpoint, or the runtime could throw an exception to report an unexpected error like a stack overflow or out-of-memory condition. At least in theory, it's not impossible for an exception to pop out of nowhere while running code that does nothing. Technically this can happen and it could be entirely unpredictable. But if we accept that an empty block can throw, then so can an empty I think that for almost all practical cases it's safe to assume that certain nodes never throw, and it would be good to teach code path analysis to recognize those nodes and not to branch into the catch clause. This should fix the incorrect connection in try-catch statements for unreachable segments that just exit some nodes, and also for segments whose nodes never throw, like an empty block. |
There is logic that determines whether a node can throw, but it's used only to make the first throwable path: eslint/lib/linter/code-path-analysis/code-path-analyzer.js Lines 640 to 653 in 73a5f06
The last path is always made from the end of the try block. Additional paths are made only for explicit A logically correct solution would probably be to create a path for each throwable node. And never from /* eslint no-useless-assignment: "error" */
function foo() {
// does nothing
}
function bar() {
throw new Error("Boom!");
}
let x;
try {
foo();
x = 1; // false positive, this value will be used in `catch {}` if `bar()` throws.
bar();
x = 2; // false negative, this one is actually never used.
} catch {
console.log(x); // 1
} |
Do you mean to split the try block into as many segments as there are throwable nodes and (contiguous) non-throwable nodes, and create paths only from the throwable segments into the catch block? I guess that should work, although it would make the code graph much larger in some cases. Also, if there is no surrounding try-block, shouldn't the throwable segments have paths to the end of the function or program for consistency?
This looks a different bug than the one in the original post, but I agree that it would be good to fix this case as well while we rework the code path analysis. |
Yes, I think that would be a logically correct code path analysis.
Performance is a concern, indeed. That seems to be the reason why code path analysis in try-catch wasn't implemented this way from the start (#7481 (comment)).
That would be correct for consistency. However, it would be an even bigger performance concern, while not being of much use to the rules, I think, so we could leave code path analysis outside try-catch as is.
I think the |
Per 2024-03-07 TSC meeting, we will follow up on the status of this issue in a few weeks. If we are still not close to done by then, we will contemplate moving to v10. |
This issue has been moved to v10 as agreed in the 2024-03-21 TSC meeting. |
Environment
Node version: v20.6.1
npm version: v9.8.1
Local ESLint version: Not found
Global ESLint version: v8.49.0
Operating System: darwin 22.5.0
What parser are you using?
Default (Espree)
What did you do?
What did you expect to happen?
From #16593 (comment):
I can reproduce this in current versions of ESLint for the above code. For example, when I run this command in a zsh terminal:
I'm getting this Graphviz code in the output:
See the diagram
Notice the edge
s2_3->s2_4
connecting the unreachable segment to the catch clause which seems wrong.What actually happened?
There should be no parent-child connection from the unreachable segment after the return statement to the segment with the catch clause.
Link to Minimal Reproducible Example
https://stackblitz.com/edit/stackblitz-starters-3zwhfs
Participation
Additional comments
I haven't figured out how to fix this yet, so I can't say if the fix would have any side effects.
This bug can be worked around (we are doing this for example in the
no-useless-return
rule), so it's an open question whether this should be tackled in v9.The text was updated successfully, but these errors were encountered: