[ty] Improve diagnostics for bad @overload definitions (#20745)
Some checks are pending
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (windows) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (release) (push) Waiting to run
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / Fuzz for new ty panics (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / check playground (push) Blocked by required conditions
CI / benchmarks instrumented (ruff) (push) Blocked by required conditions
CI / benchmarks instrumented (ty) (push) Blocked by required conditions
CI / benchmarks-walltime (push) Blocked by required conditions
[ty Playground] Release / publish (push) Waiting to run

This commit is contained in:
Alex Waygood 2025-10-07 22:52:57 +01:00 committed by GitHub
parent 1bf4969c96
commit ff386b4797
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 390 additions and 88 deletions

View file

@ -64,6 +64,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) {
registry.register_lint(&INVALID_TYPE_ALIAS_TYPE);
registry.register_lint(&INVALID_METACLASS);
registry.register_lint(&INVALID_OVERLOAD);
registry.register_lint(&USELESS_OVERLOAD_BODY);
registry.register_lint(&INVALID_PARAMETER_DEFAULT);
registry.register_lint(&INVALID_PROTOCOL);
registry.register_lint(&INVALID_NAMED_TUPLE);
@ -958,6 +959,62 @@ declare_lint! {
}
}
declare_lint! {
/// ## What it does
/// Checks for various `@overload`-decorated functions that have non-stub bodies.
///
/// ## Why is this bad?
/// Functions decorated with `@overload` are ignored at runtime; they are overridden
/// by the implementation function that follows the series of overloads. While it is
/// not illegal to provide a body for an `@overload`-decorated function, it may indicate
/// a misunderstanding of how the `@overload` decorator works.
///
/// ## Example
///
/// ```py
/// from typing import overload
///
/// @overload
/// def foo(x: int) -> int:
/// return x + 1 # will never be executed
///
/// @overload
/// def foo(x: str) -> str:
/// return "Oh no, got a string" # will never be executed
///
/// def foo(x: int | str) -> int | str:
/// raise Exception("unexpected type encountered")
/// ```
///
/// Use instead:
///
/// ```py
/// from typing import assert_never, overload
///
/// @overload
/// def foo(x: int) -> int: ...
///
/// @overload
/// def foo(x: str) -> str: ...
///
/// def foo(x: int | str) -> int | str:
/// if isinstance(x, int):
/// return x + 1
/// elif isinstance(x, str):
/// return "Oh no, got a string"
/// else:
/// assert_never(x)
/// ```
///
/// ## References
/// - [Python documentation: `@overload`](https://docs.python.org/3/library/typing.html#typing.overload)
pub(crate) static USELESS_OVERLOAD_BODY = {
summary: "detects `@overload`-decorated functions with non-stub bodies",
status: LintStatus::preview("1.0.0"),
default_level: Level::Warn,
}
}
declare_lint! {
/// ## What it does
/// Checks for default values that can't be

View file

@ -57,7 +57,7 @@ use crate::types::diagnostic::{
INVALID_TYPE_FORM, INVALID_TYPE_GUARD_CALL, INVALID_TYPE_VARIABLE_CONSTRAINTS,
IncompatibleBases, NON_SUBSCRIPTABLE, POSSIBLY_MISSING_IMPLICIT_CALL, POSSIBLY_MISSING_IMPORT,
UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL, UNRESOLVED_IMPORT,
UNRESOLVED_REFERENCE, UNSUPPORTED_OPERATOR, report_bad_dunder_set_call,
UNRESOLVED_REFERENCE, UNSUPPORTED_OPERATOR, USELESS_OVERLOAD_BODY, report_bad_dunder_set_call,
report_cannot_pop_required_field_on_typed_dict, report_implicit_return_type,
report_instance_layout_conflict, report_invalid_assignment,
report_invalid_attribute_assignment, report_invalid_generator_function_return_type,
@ -1005,10 +1005,22 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
.context
.report_lint(&INVALID_OVERLOAD, &function_node.name)
{
builder.into_diagnostic(format_args!(
"Overloaded non-stub function `{}` must have an implementation",
let mut diagnostic = builder.into_diagnostic(format_args!(
"Overloads for function `{}` must be followed by a non-`@overload`-decorated implementation function",
&function_node.name
));
diagnostic.info(format_args!(
"Attempting to call `{}` will raise `TypeError` at runtime",
&function_node.name
));
diagnostic.info(
"Overloaded functions without implementations are only permitted \
in stub files, on protocols, or for abstract methods",
);
diagnostic.info(
"See https://docs.python.org/3/library/typing.html#typing.overload \
for more details",
);
}
}
}
@ -2169,6 +2181,37 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
definition,
&DeclaredAndInferredType::are_the_same_type(inferred_ty),
);
if function_decorators.contains(FunctionDecorators::OVERLOAD) {
for stmt in &function.body {
match stmt {
ast::Stmt::Pass(_) => continue,
ast::Stmt::Expr(ast::StmtExpr { value, .. }) => {
if matches!(
&**value,
ast::Expr::StringLiteral(_) | ast::Expr::EllipsisLiteral(_)
) {
continue;
}
}
_ => {}
}
let Some(builder) = self.context.report_lint(&USELESS_OVERLOAD_BODY, stmt) else {
continue;
};
let mut diagnostic = builder.into_diagnostic(format_args!(
"Useless body for `@overload`-decorated function `{}`",
&function.name
));
diagnostic.set_primary_message("This statement will never be executed");
diagnostic.info(
"`@overload`-decorated functions are solely for type checkers \
and must be overwritten at runtime by a non-`@overload`-decorated implementation",
);
diagnostic.help("Consider replacing this function body with `...` or `pass`");
break;
}
}
}
fn infer_return_type_annotation(