Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions crates/pyrefly_config/src/error_kind.rs
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,8 @@ pub enum ErrorKind {
UnusedIgnore,
/// A `# type: ignore` comment is unused (no error to suppress on that line)
UnusedTypeIgnore,
/// `@overload` bodies are never executed, so executable body logic is usually dead code.
UselessOverloadBody,
/// The inferred variance of a type variable does not match its declared variance.
/// For example, a type variable used only in covariant positions in a protocol should be declared covariant.
VarianceMismatch,
Expand Down Expand Up @@ -503,6 +505,8 @@ impl ErrorKind {
ErrorKind::UnusedIgnore => Severity::Ignore,
ErrorKind::UnusedTypeIgnore => Severity::Ignore,
ErrorKind::VarianceMismatch => Severity::Warn,
// Overload bodies are runtime-dead, so this should warn rather than fail CI by default.
ErrorKind::UselessOverloadBody => Severity::Warn,
_ => Severity::Error,
}
}
Expand Down
7 changes: 7 additions & 0 deletions pyrefly/lib/binding/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -654,6 +654,13 @@ impl<'a> BindingsBuilder<'a> {
}
_ => None,
};
if decorators.is_overload && !body_is_trivial && placeholder_body_kind.is_none() {
self.error(
func_name.range(),
ErrorKind::UselessOverloadBody,
"`@overload` bodies should not contain executable logic".to_owned(),
);
}
// A `...` body is always interpreted as a stub function.
// Functions with other trivial bodies are interpreted as stubs in some contexts.
let stub_or_impl = if body_is_ellipse
Expand Down
139 changes: 137 additions & 2 deletions pyrefly/lib/test/overload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,141 @@ def anywhere():
"#,
);

testcase!(
test_useless_overload_body,
r#"
from typing import overload

# should warn

@overload
def returns_expr(x: int) -> int: # E: `@overload` bodies should not contain executable logic
return x + 1

@overload
def returns_expr(x: str) -> str:
...

@overload
def raises_other(x: int) -> int: # E: `@overload` bodies should not contain executable logic
raise ValueError("bad")

@overload
def raises_other(x: str) -> str:
...

@overload
def has_assignment(x: int) -> int: # E: `@overload` bodies should not contain executable logic
x = 1
return x

@overload
def has_assignment(x: str) -> str:
...

@overload
def has_multiple_stmts(x: int) -> int: # E: `@overload` bodies should not contain executable logic
print("side effect")
return x

@overload
def has_multiple_stmts(x: str) -> str:
...

def returns_expr(x: int | str) -> int | str:
return x

def raises_other(x: int | str) -> int | str:
return x

def has_assignment(x: int | str) -> int | str:
return x

def has_multiple_stmts(x: int | str) -> int | str:
return x

# should not warn

@overload
def body_pass(x: int) -> int:
pass

@overload
def body_pass(x: str) -> str:
...

def body_pass(x: int | str) -> int | str:
return x

@overload
def body_ellipsis(x: int) -> int:
...

@overload
def body_ellipsis(x: str) -> str:
...

def body_ellipsis(x: int | str) -> int | str:
return x

@overload
def body_docstring_only(x: int) -> int:
"""This is fine."""

@overload
def body_docstring_only(x: str) -> str:
...

def body_docstring_only(x: int | str) -> int | str:
return x

@overload
def body_raise_not_impl(x: int) -> int:
raise NotImplementedError

@overload
def body_raise_not_impl(x: str) -> str:
...

def body_raise_not_impl(x: int | str) -> int | str:
return x

@overload
def body_raise_not_impl_msg(x: int) -> int:
raise NotImplementedError("not done")

@overload
def body_raise_not_impl_msg(x: str) -> str:
...

def body_raise_not_impl_msg(x: int | str) -> int | str:
return x

@overload
def body_return_not_impl(x: int) -> int:
return NotImplemented

@overload
def body_return_not_impl(x: str) -> str:
...

def body_return_not_impl(x: int | str) -> int | str:
return x

@overload
def body_docstring_then_pass(x: int) -> int:
"""docstring stripped first, then pass is trivial."""
pass

@overload
def body_docstring_then_pass(x: str) -> str:
...

def body_docstring_then_pass(x: int | str) -> int | str:
return x
"#,
);

// Regression test for https://github.com/facebook/pyrefly/issues/2867
testcase!(
test_urlunparse_prefers_string_overload_for_parse_result,
Expand Down Expand Up @@ -721,7 +856,7 @@ def f(x: int) -> int: ...
def f(x: str) -> str: ...

@overload
def f(x: int | str) -> int | str: # E: @overload decorator should not be used on function implementation
def f(x: int | str) -> int | str: # E: @overload decorator should not be used on function implementation # E: `@overload` bodies should not contain executable logic
return x
"#,
);
Expand Down Expand Up @@ -765,7 +900,7 @@ from typing import overload, Any
@overload
def foo(a: int) -> int: ...
@overload
def foo(a: str) -> str:
def foo(a: str) -> str: # E: `@overload` bodies should not contain executable logic
"""Docstring"""
return 123 # E: Returned type `Literal[123]` is not assignable to declared return type `str`
def foo(*args, **kwargs) -> Any:
Expand Down
16 changes: 16 additions & 0 deletions website/docs/error-kinds.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1812,6 +1812,22 @@ Default severity: `ignore`

This error is raised when a `# type: ignore` comment is not used to suppress any error, and can be safely removed. This rule is distinct from `unused-ignore` so that projects using multiple type checkers can leave `# type: ignore` comments for other tools (e.g. mypy) without pyrefly flagging them. Enable this rule if your project uses pyrefly exclusively.

## useless-overload-body

Default severity: `warn`

This warning is raised when an `@overload` function contains executable body logic.
Overload bodies are never executed at runtime, so only placeholder bodies like `pass`, `...`,
a docstring-only body, `raise NotImplementedError(...)`, or `return NotImplemented` are useful.

```python
from typing import overload

@overload
def parse(x: int) -> int:
return x + 1 # warning: executable logic in an overload body
```

## variance-mismatch

Default severity: `warn`
Expand Down
Loading