fix while...else return not recognized as exhaustive #2868#2878
fix while...else return not recognized as exhaustive #2868#2878asukaminato0721 wants to merge 1 commit intofacebook:mainfrom
Conversation
There was a problem hiding this comment.
Pull request overview
This PR fixes pyrefly’s implicit-return exhaustiveness analysis so that while ... else: is treated as a normal-termination branch (like for ... else:), avoiding false positives when the else branch returns.
Changes:
- Update
function_last_expressionsto analyzewhilelooporelseblocks when the loop can terminate normally. - Add a regression test covering
while ... else: returnexhaustiveness (Issue #2868).
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
pyrefly/lib/binding/function.rs |
Adjusts implicit-return control-flow analysis for Stmt::While to account for else termination. |
pyrefly/lib/test/returns.rs |
Adds a regression test ensuring while ... else: return is recognized as exhaustive. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| Stmt::While(x) => { | ||
| // Infinite loops with no breaks cannot fall through | ||
| if sys_info.evaluate_bool(&x.test) != Some(true) { | ||
| return None; | ||
| } | ||
| let mut has_break = false; | ||
| x.body | ||
| .visit(&mut |stmt| loop_body_has_break_statement(stmt, &mut has_break)); | ||
| if has_break { | ||
| return None; | ||
| if sys_info.evaluate_bool(&x.test) == Some(true) { | ||
| // Infinite loops with no breaks cannot fall through. | ||
| if has_break { | ||
| return None; | ||
| } | ||
| } else { | ||
| if has_break || x.orelse.is_empty() { | ||
| return None; | ||
| } | ||
| f(sys_info, &x.orelse, res)?; | ||
| } |
There was a problem hiding this comment.
In Stmt::While, has_break is computed unconditionally and then used to short-circuit the else-clause analysis for all non-while True cases. If the condition is statically false (e.g. while False:), the body is unreachable so any break inside it is unreachable too; treating it as a possible break will incorrectly return None here and can reintroduce a false-positive “missing return” even when the else branch is exhaustive. Consider branching on sys_info.evaluate_bool(&x.test) first (handle Some(false) by ignoring has_break and analyzing orelse if present), or only computing/applying has_break when the body is reachable (test != Some(false)).
|
Diff from mypy_primer, showing the effect of this PR on open source code: mitmproxy (https://github.com/mitmproxy/mitmproxy)
- ERROR test/mitmproxy/proxy/layers/quic/test__stream_layers.py:428:38-42: Function declared to return `bool`, but one or more paths are missing an explicit `return` [bad-return]
setuptools (https://github.com/pypa/setuptools)
- ERROR setuptools/_distutils/command/build_py.py:279:39-73: No matching overload found for function `posixpath.join` called with arguments: (Literal[''] | Unknown | None, Unknown) [no-matching-overload]
werkzeug (https://github.com/pallets/werkzeug)
- ERROR src/werkzeug/test.py:1068:10-22: Function declared to return `TestResponse`, but one or more paths are missing an explicit `return` [bad-return]
prefect (https://github.com/PrefectHQ/prefect)
- ERROR src/integrations/prefect-dbt/prefect_dbt/cloud/jobs.py:381:6-10: Function declared to return `dict[Unknown, Unknown]`, but one or more paths are missing an explicit `return` [bad-return]
|
Primer Diff Classification✅ 4 improvement(s) | 4 project(s) total | -4 errors 4 improvement(s) across mitmproxy, setuptools, werkzeug, prefect.
Detailed analysis✅ Improvement (4)mitmproxy (-1)
setuptools (-1)
werkzeug (-1)
prefect (-1)
Was this helpful? React with 👍 or 👎 Classification by primer-classifier (4 LLM) |
Summary
Fixes #2868
teaching implicit-return analysis to treat while ... else like a normal-termination branch instead of an unconditional fallthrough.
Test Plan
add test