[ty] report unreachable code as unnecessary hint diagnostics#24580
[ty] report unreachable code as unnecessary hint diagnostics#24580denyszhak wants to merge 8 commits intoastral-sh:mainfrom
Conversation
|
It's a draft for now with only two types of unreachable code but I need to think slightly more to map it to more precise types: structural, static, maybe type related before sending for review. |
2588537 to
3013807
Compare
Typing conformance resultsNo changes detected ✅Current numbersThe percentage of diagnostics emitted that were expected errors held steady at 87.94%. The percentage of expected errors that received a diagnostic held steady at 83.36%. The number of fully passing files held steady at 79/133. |
Memory usage reportMemory usage unchanged ✅ |
|
3013807 to
23e75da
Compare
I ended up keeping only two categories for now. Splitting further into structural/static/type-analysis-style categories is possible in principle, but with the current reachability plumbing that would require extra classification logic at this layer mainly to refine the user-facing message. The current implementation still covers the full set of unreachable cases, what’s intentionally deferred is finer-grained messaging. I didn’t want to add more categorization logic unless we can align it with ty’s actual reachability model rather than approximate pyright’s labels. |
MichaReiser
left a comment
There was a problem hiding this comment.
This is very cool. Thank you for working on it.
I only have a few suggestions around wording, documentation, and test structure. This is otherwise good to go.
| "Code is unreachable".to_owned() | ||
| } | ||
| Self::UnreachableCode(UnreachableKind::CurrentAnalysis) => { | ||
| "Code is unreachable under the current analysis".to_owned() |
There was a problem hiding this comment.
I think I'd go with Code is unreachable for this project’s configured settings
There was a problem hiding this comment.
Yes this is better, I didn't have a good name but didn't like the one I came up with and I knew you would have a good suggestion, thanks!
There was a problem hiding this comment.
I think I need to first think about David's suggestion below so that this statement will be true in reality.
There was a problem hiding this comment.
Based on the discussion below, mentioning project settings for the CurrentAnalysis bucket would be misleading, since some of those cases are not settings-driven. For settings cases, it would be better to add a separate Configuration category for the patterns ty can reliably distinguish today. I can take a look at that either in this PR or as a follow up after analyzing what those patterns are.
Co-authored-by: Micha Reiser <micha@reiser.io>
…de.rs Co-authored-by: Micha Reiser <micha@reiser.io>
| kind: if constraint == ScopedReachabilityConstraintId::ALWAYS_FALSE { | ||
| UnreachableKind::Unconditional | ||
| } else { | ||
| UnreachableKind::CurrentAnalysis | ||
| }, |
There was a problem hiding this comment.
Only certain simple patterns like if False have a constraint of ALWAYS_FALSE. There are more complex patterns like if 1 + 1 == 3 or if isinstance("test", str) which need type inference for evaluation. It looks like we are categorizing these as CurrentAnalysis here.
I have not reviewed the rest of the code, so maybe this is not problematic. But it might be slightly confusing for users?
(I was always expecting that we would have to do the opposite to achieve this categorization: to match on a restricted set of patterns like if sys.version_info >= (3, 10) which we would know depend on the current user settings; and then categorize everything else as Unconditional)
There was a problem hiding this comment.
What you say is to flip this thing to have like two types of unreachable: Configuration and Unconditional and we have let's say is_configuration_expr and everything that evaluate as config goes into this bucket and everything else is Unconditional?
This sounds good actually, I was slightly confused in what separate would be best.
There was a problem hiding this comment.
I think this is actually the correct way for UX, I will update this
There was a problem hiding this comment.
Yes. It would be good to understand which of these failure cases is more problematic:
- an "unconditionally unreachable" statement that is falsely being labeled as "unreachable due to config settings"
- a statement that is "unreachable due to config settings" that is falsely being labeled as "unconditionally unreachable"
We should probably answer that before we make any changes here.
There was a problem hiding this comment.
Second one sounds worse for me. Labeling config-dependent code as unconditionally unreachable would be a stronger claim than we can justify, since that code may still be reachable on another Python version or platform. The other direction is still a loss of precision, but less misleading. So I think the safer approach is to keep the configuration bucket narrow and only use for explicitly recognized config patterns, and make the fallback bucket the regular generic unreachable case rather than “unconditional”.
wdyt?
There was a problem hiding this comment.
I agree with this. The thing we will surface to users is the claim that some code is (always) unreachable (implied: you may as well delete it.) False positives in this identification are worse than false negatives.
There was a problem hiding this comment.
I think the current version is in a good state to merge as-is. It already covers the unreachable ranges correctly. If we were to refine the split, I think the safer shape is Configuration for explicitly recognized patterns, Unconditional for ALWAYS_FALSE, and CurrentAnalysis as the fallback (maybe improve the wording for this one now).
I could work on that either in this PR or in a follow-up, after analyzing which patterns ty can classify reliably today.
| let snippets = collect_unreachable_entries(&db, "/src/main.py", &source); | ||
| assert_eq!( | ||
| snippets, | ||
| vec![( | ||
| "if False:\n x = lambda: 1".to_owned(), | ||
| UnreachableKind::CurrentAnalysis | ||
| )] | ||
| ); |
There was a problem hiding this comment.
This is interesting. Should this have a TODO comment maybe that this could be even better, in principle? The if False: is of kind CurrentAnalysis, but the x = lambda: 1 could be of the (stronger) unreachable kind Unconditional, right?
There was a problem hiding this comment.
Yeah, good point! x = lambda: 1 is Unconditional on its own, but the merged span also includes if False: and that line is only dead under the current analysis. So CurrentAnalysis looks as the correct kind for the whole merged span, not a limitation. Happy to add a comment to the test clarifying this if useful.
There was a problem hiding this comment.
As for reporting two separate ranges - I'm not sure that would be better UX either, since you will end up with two overlapping dim regions for the same code.
de32cae to
e5ee50a
Compare
e5ee50a to
e53946c
Compare
Closes astral-sh/ty#784
Summary
Implements dimming for unreachable code in LSP-capable editors. Unreachable ranges are collected from the semantic index and emitted as
DiagnosticTag::UNNECESSARYhints.Test Plan
Unit tests coverage, e2e test coverage and manual testing