Skip to content

[ty] report unreachable code as unnecessary hint diagnostics#24580

Open
denyszhak wants to merge 8 commits intoastral-sh:mainfrom
denyszhak:feat/unreachable-code-hints
Open

[ty] report unreachable code as unnecessary hint diagnostics#24580
denyszhak wants to merge 8 commits intoastral-sh:mainfrom
denyszhak:feat/unreachable-code-hints

Conversation

@denyszhak
Copy link
Copy Markdown
Contributor

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::UNNECESSARY hints.

Test Plan

Unit tests coverage, e2e test coverage and manual testing

@astral-sh-bot astral-sh-bot bot added the ty Multi-file analysis & type inference label Apr 13, 2026
@denyszhak
Copy link
Copy Markdown
Contributor Author

denyszhak commented Apr 13, 2026

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.

@denyszhak denyszhak changed the title Feat/unreachable code hints [ty] report unreachable code as unnecessary hint diagnostics Apr 13, 2026
@AlexWaygood AlexWaygood added the server Related to the LSP server label Apr 13, 2026
@denyszhak denyszhak force-pushed the feat/unreachable-code-hints branch from 2588537 to 3013807 Compare April 13, 2026 21:56
@astral-sh-bot
Copy link
Copy Markdown

astral-sh-bot bot commented Apr 13, 2026

Typing conformance results

No changes detected ✅

Current numbers
The 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.

@astral-sh-bot
Copy link
Copy Markdown

astral-sh-bot bot commented Apr 13, 2026

Memory usage report

Memory usage unchanged ✅

@astral-sh-bot
Copy link
Copy Markdown

astral-sh-bot bot commented Apr 13, 2026

ecosystem-analyzer results

No diagnostic changes detected ✅

Full report with detailed diff (timing results)

@denyszhak denyszhak force-pushed the feat/unreachable-code-hints branch from 3013807 to 23e75da Compare April 13, 2026 22:59
@denyszhak
Copy link
Copy Markdown
Contributor Author

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.

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.

Copy link
Copy Markdown
Member

@MichaReiser MichaReiser left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread crates/ty_python_semantic/src/types/ide_support/unreachable_code.rs
Comment thread crates/ty_server/src/server/api/requests/workspace_diagnostic.rs Outdated
Comment thread crates/ty_server/src/server/api/diagnostics.rs
Comment thread crates/ty_python_semantic/src/types/ide_support/unreachable_code.rs Outdated
Comment thread crates/ty_python_semantic/src/types/ide_support/unreachable_code.rs Outdated
Comment thread crates/ty_python_semantic/src/types/ide_support/unreachable_code.rs Outdated
Comment thread crates/ty_python_semantic/src/types/ide_support/unreachable_code.rs Outdated
Comment thread crates/ty_python_semantic/src/types/ide_support/unreachable_code.rs Outdated
Comment thread crates/ty_python_semantic/src/types/ide_support/unreachable_code.rs Outdated
"Code is unreachable".to_owned()
}
Self::UnreachableCode(UnreachableKind::CurrentAnalysis) => {
"Code is unreachable under the current analysis".to_owned()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'd go with Code is unreachable for this project’s configured settings

Copy link
Copy Markdown
Contributor Author

@denyszhak denyszhak Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I need to first think about David's suggestion below so that this statement will be true in reality.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

denyszhak and others added 2 commits April 15, 2026 16:09
Co-authored-by: Micha Reiser <micha@reiser.io>
…de.rs

Co-authored-by: Micha Reiser <micha@reiser.io>
Comment on lines +38 to +42
kind: if constraint == ScopedReachabilityConstraintId::ALWAYS_FALSE {
UnreachableKind::Unconditional
} else {
UnreachableKind::CurrentAnalysis
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown
Contributor Author

@denyszhak denyszhak Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is actually the correct way for UX, I will update this

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread crates/ty_python_semantic/src/types/ide_support/unreachable_code.rs Outdated
Comment on lines +541 to +548
let snippets = collect_unreachable_entries(&db, "/src/main.py", &source);
assert_eq!(
snippets,
vec![(
"if False:\n x = lambda: 1".to_owned(),
UnreachableKind::CurrentAnalysis
)]
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Contributor Author

@denyszhak denyszhak Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@AlexWaygood AlexWaygood removed their request for review April 15, 2026 15:33
@denyszhak denyszhak force-pushed the feat/unreachable-code-hints branch from de32cae to e5ee50a Compare April 15, 2026 17:26
@denyszhak denyszhak force-pushed the feat/unreachable-code-hints branch from e5ee50a to e53946c Compare April 15, 2026 17:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

server Related to the LSP server ty Multi-file analysis & type inference

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Gray out unreachable code

6 participants