From fbb340188ee8eab1b01757dbd20d2e4262d98521 Mon Sep 17 00:00:00 2001 From: pratved64 Date: Wed, 17 Jun 2026 20:36:56 +0530 Subject: [PATCH 1/3] add: Useless-Overload-Body rule --- crates/pyrefly_config/src/error_kind.rs | 4 ++++ pyrefly/lib/binding/function.rs | 7 +++++++ pyrefly/lib/test/overload.rs | 22 ++++++++++++++++++++-- 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/crates/pyrefly_config/src/error_kind.rs b/crates/pyrefly_config/src/error_kind.rs index 3dea6f55f3..39bd393091 100644 --- a/crates/pyrefly_config/src/error_kind.rs +++ b/crates/pyrefly_config/src/error_kind.rs @@ -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, @@ -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, } } diff --git a/pyrefly/lib/binding/function.rs b/pyrefly/lib/binding/function.rs index 962ba035bb..5cb407cee0 100644 --- a/pyrefly/lib/binding/function.rs +++ b/pyrefly/lib/binding/function.rs @@ -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 diff --git a/pyrefly/lib/test/overload.rs b/pyrefly/lib/test/overload.rs index 0503519401..f3cc425532 100644 --- a/pyrefly/lib/test/overload.rs +++ b/pyrefly/lib/test/overload.rs @@ -31,6 +31,24 @@ def anywhere(): "#, ); +testcase!( + test_useless_overload_body, + r#" +from typing import overload + +@overload +def foo(x: int) -> int: # E: `@overload` bodies should not contain executable logic + return x + 1 # will never be executed + +@overload +def foo(x: str) -> str: # E: `@overload` bodies should not contain executable logic + return "Oh no, got a string" # will never be executed + +def foo(x: int | str) -> int | str: + raise Exception("unexpected type encountered") + "#, +); + // Regression test for https://github.com/facebook/pyrefly/issues/2867 testcase!( test_urlunparse_prefers_string_overload_for_parse_result, @@ -721,7 +739,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 "#, ); @@ -765,7 +783,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: From 87cd8fe9e725deebdb6ee9d6de131a702f657f5a Mon Sep 17 00:00:00 2001 From: pratved64 Date: Wed, 17 Jun 2026 20:39:17 +0530 Subject: [PATCH 2/3] add: Documentation for Useless-Overload-Body rule --- website/docs/error-kinds.mdx | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/website/docs/error-kinds.mdx b/website/docs/error-kinds.mdx index 8e768107f9..815fe1f2e4 100644 --- a/website/docs/error-kinds.mdx +++ b/website/docs/error-kinds.mdx @@ -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` From 4e97202d719308ecc2ebcc355887cec91ef1263b Mon Sep 17 00:00:00 2001 From: pratved64 Date: Wed, 17 Jun 2026 20:59:43 +0530 Subject: [PATCH 3/3] update: rewrote tests in overload.rs to include all cases --- pyrefly/lib/test/overload.rs | 129 +++++++++++++++++++++++++++++++++-- 1 file changed, 123 insertions(+), 6 deletions(-) diff --git a/pyrefly/lib/test/overload.rs b/pyrefly/lib/test/overload.rs index f3cc425532..a21a89cbd9 100644 --- a/pyrefly/lib/test/overload.rs +++ b/pyrefly/lib/test/overload.rs @@ -36,16 +36,133 @@ testcase!( r#" from typing import overload +# should warn + @overload -def foo(x: int) -> int: # E: `@overload` bodies should not contain executable logic - return x + 1 # will never be executed +def returns_expr(x: int) -> int: # E: `@overload` bodies should not contain executable logic + return x + 1 @overload -def foo(x: str) -> str: # E: `@overload` bodies should not contain executable logic - return "Oh no, got a string" # will never be executed +def returns_expr(x: str) -> str: + ... -def foo(x: int | str) -> int | str: - raise Exception("unexpected type encountered") +@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 "#, );