Skip to content

Commit 9084de4

Browse files
fix
1 parent b291170 commit 9084de4

7 files changed

Lines changed: 120 additions & 19 deletions

File tree

‎crates/pyrefly_config/src/error_kind.rs‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,8 @@ pub enum ErrorKind {
242242
/// An inconsistency between a function parameter's type in an overload signature and its
243243
/// default value in the implementation.
244244
InconsistentOverloadDefault,
245+
/// A function declares a non-None, non-Never return type but can only loop forever.
246+
InfiniteLoop,
245247
/// Internal Pyrefly error.
246248
InternalError,
247249
/// An `@abstractmethod` is defined in a class that is not abstract.
@@ -616,6 +618,7 @@ impl ErrorKind {
616618
ErrorKind::ImplicitReexport => Severity::Ignore,
617619
ErrorKind::ImplicitlyDefinedAttribute => Severity::Ignore,
618620
ErrorKind::IncompatibleComparison => Severity::Ignore,
621+
ErrorKind::InfiniteLoop => Severity::Warn,
619622
ErrorKind::InvalidAbstractMethod => Severity::Ignore,
620623
ErrorKind::InvalidCast => Severity::Ignore,
621624
ErrorKind::InvalidDecorator => Severity::Warn,

‎pyrefly/lib/alt/solve.rs‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2894,6 +2894,7 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> {
28942894
is_async,
28952895
is_generator,
28962896
has_explicit_return,
2897+
has_infinite_loop,
28972898
} => {
28982899
let annotation = self.get_idx(*annotation).annotation.get_type().clone();
28992900
let implicit_return = self.get_idx(*implicit_return);
@@ -2903,6 +2904,7 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> {
29032904
*is_async,
29042905
*is_generator,
29052906
*has_explicit_return,
2907+
*has_infinite_loop,
29062908
range,
29072909
errors,
29082910
);
@@ -6524,9 +6526,27 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> {
65246526
is_async: bool,
65256527
is_generator: bool,
65266528
has_explicit_returns: bool,
6529+
has_infinite_loop: bool,
65276530
range: TextRange,
65286531
errors: &ErrorCollector,
65296532
) {
6533+
if has_infinite_loop
6534+
&& !has_explicit_returns
6535+
&& !is_generator
6536+
&& !annotation.is_never()
6537+
&& !annotation.is_none()
6538+
&& implicit_return.ty().is_never()
6539+
{
6540+
self.error(
6541+
errors,
6542+
range,
6543+
ErrorKind::InfiniteLoop,
6544+
format!(
6545+
"Function declared to return `{}` but can never return",
6546+
self.for_display(annotation.clone())
6547+
),
6548+
);
6549+
}
65306550
if is_async && is_generator {
65316551
let hints = self.decompose_hint(HintRef::soft(annotation), |hint| {
65326552
self.decompose_async_generator(hint)

‎pyrefly/lib/binding/binding.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1253,6 +1253,7 @@ pub enum BindingExpect {
12531253
is_async: bool,
12541254
is_generator: bool,
12551255
has_explicit_return: bool,
1256+
has_infinite_loop: bool,
12561257
},
12571258
/// Expression used in a boolean context (`bool()`, `if`, or `while`)
12581259
Bool(Expr),

‎pyrefly/lib/binding/function.rs‎

Lines changed: 45 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -484,8 +484,10 @@ impl<'a> BindingsBuilder<'a> {
484484
/// This function must not be called unless the function body statements will be bound;
485485
/// it relies on that binding to ensure we don't have a dangling `Idx<Key>` (which could lead
486486
/// to a panic).
487-
fn implicit_return(&mut self, body: &[Stmt], func_name: &Identifier) -> Idx<Key> {
488-
let last_exprs = function_last_expressions(body, self.sys_info).map(|x| {
487+
fn implicit_return(&mut self, body: &[Stmt], func_name: &Identifier) -> (Idx<Key>, bool) {
488+
let last_exprs = function_last_expressions(body, self.sys_info);
489+
let has_infinite_loop = last_exprs.as_ref().is_some_and(|x| x.has_infinite_loop);
490+
let last_exprs = last_exprs.map(|x| {
489491
x.exprs
490492
.into_map(|(last, x)| {
491493
(
@@ -495,9 +497,12 @@ impl<'a> BindingsBuilder<'a> {
495497
})
496498
.into_boxed_slice()
497499
});
498-
self.insert_binding(
499-
Key::ReturnImplicit(ShortIdentifier::new(func_name)),
500-
Binding::ReturnImplicit(ReturnImplicit { last_exprs }),
500+
(
501+
self.insert_binding(
502+
Key::ReturnImplicit(ShortIdentifier::new(func_name)),
503+
Binding::ReturnImplicit(ReturnImplicit { last_exprs }),
504+
),
505+
has_infinite_loop,
501506
)
502507
}
503508

@@ -509,7 +514,7 @@ impl<'a> BindingsBuilder<'a> {
509514
is_async: bool,
510515
yields_and_returns: YieldsAndReturns,
511516
return_ann_with_range: Option<(TextRange, Idx<KeyAnnotation>)>,
512-
implicit_return: Option<Idx<Key>>,
517+
implicit_return: Option<(Idx<Key>, bool)>,
513518
should_infer_return_type: bool,
514519
is_stub: bool,
515520
) {
@@ -521,6 +526,10 @@ impl<'a> BindingsBuilder<'a> {
521526
} else {
522527
None
523528
};
529+
let has_reachable_return = yields_and_returns
530+
.returns
531+
.iter()
532+
.any(|(_, _, is_unreachable)| !is_unreachable);
524533

525534
// Collect the keys of explicit returns.
526535
let return_keys = yields_and_returns
@@ -570,15 +579,20 @@ impl<'a> BindingsBuilder<'a> {
570579

571580
let return_type_binding = {
572581
let kind = match (return_ann_with_range, implicit_return, is_stub) {
573-
(Some((range, annotation)), Some(implicit_return), false) => {
582+
(
583+
Some((range, annotation)),
584+
Some((implicit_return, has_infinite_loop)),
585+
false,
586+
) => {
574587
self.insert_binding(
575588
KeyExpect::ValidateImplicitReturn(range),
576589
BindingExpect::ValidateImplicitReturn {
577590
annotation,
578591
implicit_return,
579592
is_async,
580593
is_generator,
581-
has_explicit_return: !return_keys.is_empty(),
594+
has_explicit_return: has_reachable_return,
595+
has_infinite_loop,
582596
},
583597
);
584598
ReturnTypeKind::ShouldTrustAnnotation {
@@ -597,7 +611,7 @@ impl<'a> BindingsBuilder<'a> {
597611
is_generator,
598612
}
599613
}
600-
(None, Some(implicit_return), _) if should_infer_return_type => {
614+
(None, Some((implicit_return, _)), _) if should_infer_return_type => {
601615
// We don't have an explicit return annotation, but we want to infer it.
602616
ReturnTypeKind::ShouldInferType {
603617
returns: return_keys,
@@ -985,6 +999,8 @@ pub(crate) struct LastExpressions<'a> {
985999
/// settling an assertion. The answer then holds for that configuration only, which suits
9861000
/// inferring a return type but not reporting code as dead.
9871001
pub decided_by_environment: bool,
1002+
/// Set when an unconditional loop is one of the terminal paths.
1003+
pub has_infinite_loop: bool,
9881004
}
9891005

9901006
/// Given the body of a function, what are the potential expressions that
@@ -1002,6 +1018,7 @@ pub(crate) fn function_last_expressions<'a>(
10021018
x: &'a [Stmt],
10031019
res: &mut Vec<(LastStmt, &'a Expr)>,
10041020
environment: &mut bool,
1021+
has_infinite_loop: &mut bool,
10051022
) -> Option<()> {
10061023
fn loop_body_has_break_statement(statement: &Stmt, has_break: &mut bool) {
10071024
match statement {
@@ -1025,7 +1042,7 @@ pub(crate) fn function_last_expressions<'a>(
10251042
for y in &x.items {
10261043
res.push((LastStmt::With(kind), &y.context_expr));
10271044
}
1028-
f(sys_info, &x.body, res, environment)?;
1045+
f(sys_info, &x.body, res, environment, has_infinite_loop)?;
10291046
}
10301047
Stmt::While(x) => {
10311048
let test_value = sys_info.evaluate_bool(&x.test);
@@ -1038,10 +1055,11 @@ pub(crate) fn function_last_expressions<'a>(
10381055
}
10391056
if test_value == Some(true) && !has_break {
10401057
// Infinite loop with no break never falls through.
1058+
*has_infinite_loop = true;
10411059
} else if has_break || x.orelse.is_empty() {
10421060
return None;
10431061
} else {
1044-
f(sys_info, &x.orelse, res, environment)?;
1062+
f(sys_info, &x.orelse, res, environment, has_infinite_loop)?;
10451063
}
10461064
}
10471065
Stmt::For(x) => {
@@ -1051,7 +1069,7 @@ pub(crate) fn function_last_expressions<'a>(
10511069
if has_break || x.orelse.is_empty() {
10521070
return None;
10531071
}
1054-
f(sys_info, &x.orelse, res, environment)?;
1072+
f(sys_info, &x.orelse, res, environment, has_infinite_loop)?;
10551073
}
10561074
Stmt::If(x) => {
10571075
*environment |= iter::once(Some(&*x.test))
@@ -1062,7 +1080,7 @@ pub(crate) fn function_last_expressions<'a>(
10621080
for (test, body) in sys_info.pruned_if_branches(x) {
10631081
any_branch_processed = true;
10641082
last_test = test;
1065-
f(sys_info, body, res, environment)?;
1083+
f(sys_info, body, res, environment, has_infinite_loop)?;
10661084
}
10671085
if !any_branch_processed {
10681086
// All branches were pruned, so the code falls through
@@ -1087,17 +1105,17 @@ pub(crate) fn function_last_expressions<'a>(
10871105
.iter()
10881106
.any(|stmt| matches!(stmt, Stmt::Return(_)))
10891107
{
1090-
f(sys_info, &x.finalbody, res, environment)?;
1108+
f(sys_info, &x.finalbody, res, environment, has_infinite_loop)?;
10911109
} else {
10921110
if x.orelse.is_empty() {
1093-
f(sys_info, &x.body, res, environment)?;
1111+
f(sys_info, &x.body, res, environment, has_infinite_loop)?;
10941112
} else {
1095-
f(sys_info, &x.orelse, res, environment)?;
1113+
f(sys_info, &x.orelse, res, environment, has_infinite_loop)?;
10961114
}
10971115
for handler in &x.handlers {
10981116
match handler {
10991117
ExceptHandler::ExceptHandler(x) => {
1100-
f(sys_info, &x.body, res, environment)?
1118+
f(sys_info, &x.body, res, environment, has_infinite_loop)?
11011119
}
11021120
}
11031121
}
@@ -1107,7 +1125,7 @@ pub(crate) fn function_last_expressions<'a>(
11071125
Stmt::Match(x) => {
11081126
let mut syntactically_exhaustive = false;
11091127
for case in x.cases.iter() {
1110-
f(sys_info, &case.body, res, environment)?;
1128+
f(sys_info, &case.body, res, environment, has_infinite_loop)?;
11111129
// Must match the binding step's exhaustiveness judgment in
11121130
// `stmt_match`; otherwise the `Key::Exhaustive(Match, ...)` promised
11131131
// below is never inserted and solve time panics.
@@ -1134,10 +1152,18 @@ pub(crate) fn function_last_expressions<'a>(
11341152

11351153
let mut res = Vec::new();
11361154
let mut environment = false;
1137-
f(sys_info, x, &mut res, &mut environment)?;
1155+
let mut has_infinite_loop = false;
1156+
f(
1157+
sys_info,
1158+
x,
1159+
&mut res,
1160+
&mut environment,
1161+
&mut has_infinite_loop,
1162+
)?;
11381163
Some(LastExpressions {
11391164
exprs: res,
11401165
decided_by_environment: environment,
1166+
has_infinite_loop,
11411167
})
11421168
}
11431169

‎pyrefly/lib/test/returns.rs‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,42 @@ def f() -> int:
282282
"#,
283283
);
284284

285+
// Regression test for https://github.com/facebook/pyrefly/issues/1132
286+
testcase!(
287+
test_return_infinite_loop_without_return,
288+
r#"
289+
from typing import Never, TypeAlias
290+
291+
NeverAlias: TypeAlias = Never
292+
293+
def f() -> int: # E: Function declared to return `int` but can never return
294+
while True:
295+
pass
296+
297+
def returns_never() -> Never:
298+
while True:
299+
pass
300+
301+
def returns_never_alias() -> NeverAlias:
302+
while True:
303+
pass
304+
305+
def event_loop() -> None:
306+
while True:
307+
pass
308+
309+
def unreachable_return() -> int: # E: Function declared to return `int` but can never return
310+
while True:
311+
continue
312+
return 1 # E: This code is unreachable
313+
314+
def falls_through(b: bool) -> int: # E: Function declared to return `int` but is missing an explicit `return`
315+
if b:
316+
while True:
317+
pass
318+
"#,
319+
);
320+
285321
testcase!(
286322
test_return_while_false_break_else,
287323
r#"

‎scripts/error_presets.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
"inconsistent-inheritance": ["legacy", "default", "strict", "all"],
6161
"inconsistent-overload": ["legacy", "default", "strict", "all"],
6262
"inconsistent-overload-default": ["legacy", "default", "strict", "all"],
63+
"infinite-loop": ["legacy", "default", "strict", "all"],
6364
"internal-error": ["legacy", "default", "strict", "all"],
6465
"invalid-abstract-method": ["all"],
6566
"invalid-annotation": ["basic", "legacy", "default", "strict", "all"],

‎website/docs/error-kinds.mdx‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -976,6 +976,20 @@ def f(x: bool = False) -> int | None:
976976
return 0 if x else None
977977
```
978978

979+
## infinite-loop
980+
981+
Default severity: `warn`
982+
983+
This warning is emitted when a function declares a return type other than
984+
`None`, `Never`, or `NoReturn`, but its body has no reachable return and ends
985+
in an unconditional loop.
986+
987+
```python
988+
def f() -> int: # Function declared to return `int` but can never return [infinite-loop]
989+
while True:
990+
pass
991+
```
992+
979993
## internal-error
980994

981995
Ideally you'll never see this one. If you do, please consider [filing a bug](https://github.com/facebook/pyrefly/issues).

0 commit comments

Comments
 (0)