-
Notifications
You must be signed in to change notification settings - Fork 287
fix Narrowing inside try is preserved into finally, ignoring type outside of try #2845 #2849
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -756,7 +756,8 @@ impl FlowStyle { | |||||||
| MergeStyle::Loop | ||||||||
| | MergeStyle::LoopDefinitelyRuns | ||||||||
| | MergeStyle::Exclusive | ||||||||
| | MergeStyle::Inclusive => FlowStyle::PossiblyUninitialized, | ||||||||
| | MergeStyle::Inclusive | ||||||||
| | MergeStyle::Finally => FlowStyle::PossiblyUninitialized, | ||||||||
| } | ||||||||
| } | ||||||||
| } | ||||||||
|
|
@@ -2945,6 +2946,10 @@ enum MergeStyle { | |||||||
| /// Distinct from [Branching] because we have to be more lax about | ||||||||
| /// uninitialized locals (see `FlowStyle::merge` for details). | ||||||||
| BoolOp, | ||||||||
| /// This is the merge immediately before analyzing a `finally` block. | ||||||||
| /// Terminating branches still contribute because `finally` executes before | ||||||||
| /// their control-flow effect is observed outside the statement. | ||||||||
| Finally, | ||||||||
| } | ||||||||
|
|
||||||||
| impl MergeStyle { | ||||||||
|
|
@@ -3297,17 +3302,20 @@ impl<'a> BindingsBuilder<'a> { | |||||||
| return; | ||||||||
| } | ||||||||
|
|
||||||||
| let use_all_branches = matches!(merge_style, MergeStyle::Finally); | ||||||||
| // We normally only merge the live branches (where control flow is not | ||||||||
| // known to terminate), but if nothing is live we still need to fill in | ||||||||
| // the Phi keys and potentially analyze downstream code, so in that case | ||||||||
| // we'll use the terminated branches. | ||||||||
| let (terminated_branches, live_branches): (Vec<_>, Vec<_>) = | ||||||||
| branches.into_iter().partition(|flow| flow.has_terminated); | ||||||||
| let has_terminated = live_branches.is_empty() && !merge_style.is_loop(); | ||||||||
| let flows = if has_terminated { | ||||||||
| terminated_branches | ||||||||
| let n_live_branches = branches.iter().filter(|flow| !flow.has_terminated).count(); | ||||||||
| let has_terminated = n_live_branches == 0 && !merge_style.is_loop(); | ||||||||
| let flows = if use_all_branches || has_terminated { | ||||||||
| branches | ||||||||
| } else { | ||||||||
| live_branches | ||||||||
| branches | ||||||||
| .into_iter() | ||||||||
| .filter(|flow| !flow.has_terminated) | ||||||||
| .collect() | ||||||||
| }; | ||||||||
| // Determine reachability of the merged flow. | ||||||||
| // For Loop style with empty flows (all branches terminated), the loop body might | ||||||||
|
|
@@ -3319,6 +3327,11 @@ impl<'a> BindingsBuilder<'a> { | |||||||
| MergeStyle::Loop => base.is_definitely_unreachable, | ||||||||
|
||||||||
| MergeStyle::Loop => base.is_definitely_unreachable, | |
| MergeStyle::Loop => base.is_definitely_unreachable, | |
| MergeStyle::Finally => false, |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -2036,6 +2036,33 @@ def main(resolve: bool) -> None: | |||||||||||||||||||||||||||||||||||||||||||||||
| "#, | ||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // Issue #2845: `finally` must see both the successful `try` state and terminating `except` state. | ||||||||||||||||||||||||||||||||||||||||||||||||
| testcase!( | ||||||||||||||||||||||||||||||||||||||||||||||||
| test_try_finally_preserves_pre_try_possibility_from_terminating_except, | ||||||||||||||||||||||||||||||||||||||||||||||||
| r#" | ||||||||||||||||||||||||||||||||||||||||||||||||
| from typing import assert_type | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| def something_that_might_throw() -> None: | ||||||||||||||||||||||||||||||||||||||||||||||||
| raise Exception() | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| class Thing: | ||||||||||||||||||||||||||||||||||||||||||||||||
| def something(self) -> None: | ||||||||||||||||||||||||||||||||||||||||||||||||
| pass | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| def blah() -> None: | ||||||||||||||||||||||||||||||||||||||||||||||||
| x = None | ||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||
| something_that_might_throw() | ||||||||||||||||||||||||||||||||||||||||||||||||
| if x is None: | ||||||||||||||||||||||||||||||||||||||||||||||||
| x = Thing() | ||||||||||||||||||||||||||||||||||||||||||||||||
| except Exception: | ||||||||||||||||||||||||||||||||||||||||||||||||
| raise | ||||||||||||||||||||||||||||||||||||||||||||||||
| finally: | ||||||||||||||||||||||||||||||||||||||||||||||||
| assert_type(x, Thing | None) | ||||||||||||||||||||||||||||||||||||||||||||||||
| x.something() # E: Object of class `NoneType` has no attribute `something` | ||||||||||||||||||||||||||||||||||||||||||||||||
| "#, | ||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // Issue #2845: `finally` must also handle single-branch try/finally where the try terminates. | |
| testcase!( | |
| test_try_finally_preserves_pre_try_possibility_from_terminating_try_no_except, | |
| r#" | |
| from typing import assert_type | |
| class Thing: | |
| def something(self) -> None: | |
| pass | |
| def blah() -> None: | |
| x = None | |
| try: | |
| if x is None: | |
| x = Thing() | |
| return | |
| finally: | |
| assert_type(x, Thing | None) | |
| x.something() # E: Object of class `NoneType` has no attribute `something` | |
| "#, | |
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
merge_flowshort-circuits whenbranches.len() == 1, which meansMergeStyle::Finallynever gets a chance to apply its special handling intry/finallyconstructs with noexcepthandlers. If thetrybranch terminates (e.g.return/raise), this makes thefinallybody appear unreachable and can produce incorrect “unreachable return” diagnostics insidefinally. Consider disabling the single-branch short-circuit forMergeStyle::Finally, or otherwise ensuring thefinallybody is analyzed as reachable even when the pre-finally flow has terminated.