From ec10b26aaf1951fad5fae6318d4635c5d303b263 Mon Sep 17 00:00:00 2001 From: Emily Yao Date: Sun, 17 May 2026 13:21:21 -0400 Subject: [PATCH 1/5] Add quick fix to remove redundant cast --- .../resources/mdtest/directives/cast.md | 8 ++++++++ ...ast_warni\342\200\246_(75ac240a2d1f7108).snap" | 15 ++++++++++++++- crates/ty_python_semantic/src/types/function.rs | 8 ++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/crates/ty_python_semantic/resources/mdtest/directives/cast.md b/crates/ty_python_semantic/resources/mdtest/directives/cast.md index f88e39a08cebb5..ec604cc755a19b 100644 --- a/crates/ty_python_semantic/resources/mdtest/directives/cast.md +++ b/crates/ty_python_semantic/resources/mdtest/directives/cast.md @@ -114,6 +114,14 @@ warning[redundant-cast]: Value is already of type `int` 5 | cast(int, secrets.randbelow(10)) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | +help: Remove the redundant `cast` +2 | from typing import cast +3 | +4 | # snapshot: redundant-cast + - cast(int, secrets.randbelow(10)) +5 + secrets.randbelow(10) +6 | # snapshot: redundant-cast +7 | cast(val=secrets.randbelow(10), typ=int) ``` ```py diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Redundant_cast_warni\342\200\246_(75ac240a2d1f7108).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Redundant_cast_warni\342\200\246_(75ac240a2d1f7108).snap" index 4efaf32b8aef6f..4606501edfaf6f 100644 --- "a/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Redundant_cast_warni\342\200\246_(75ac240a2d1f7108).snap" +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/typed_dict.md_-_`TypedDict`_-_Redundant_cast_warni\342\200\246_(75ac240a2d1f7108).snap" @@ -1,5 +1,5 @@ --- -source: crates/ty_test/src/lib.rs +source: crates/mdtest/src/lib.rs expression: snapshot --- @@ -35,6 +35,13 @@ warning[redundant-cast]: Value is already of type `Foo2` 10 | _ = cast(Foo2, foo) # error: [redundant-cast] | ^^^^^^^^^^^^^^^ | +help: Remove the redundant `cast` +7 | x: int +8 | +9 | foo: Foo2 = {"x": 1} + - _ = cast(Foo2, foo) # error: [redundant-cast] +10 + _ = foo # error: [redundant-cast] +11 | _ = cast(Bar2, foo) # error: [redundant-cast] ``` @@ -46,5 +53,11 @@ warning[redundant-cast]: Value is already of type `Bar2` | ^^^^^^^^^^^^^^^ | info: `Bar2` is equivalent to `Foo2` +help: Remove the redundant `cast` +8 | +9 | foo: Foo2 = {"x": 1} +10 | _ = cast(Foo2, foo) # error: [redundant-cast] + - _ = cast(Bar2, foo) # error: [redundant-cast] +11 + _ = foo # error: [redundant-cast] ``` diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs index d8214d6e511c5f..faeb1c44ad879d 100644 --- a/crates/ty_python_semantic/src/types/function.rs +++ b/crates/ty_python_semantic/src/types/function.rs @@ -55,6 +55,7 @@ use bitflags::bitflags; use ruff_db::diagnostic::{Annotation, DiagnosticId, Severity, Span}; use ruff_db::files::{File, FileRange}; use ruff_db::parsed::{ParsedModuleRef, parsed_module}; +use ruff_diagnostics::{Edit, Fix}; use ruff_python_ast::{self as ast, ParameterWithDefault}; use ruff_text_size::Ranged; use salsa::plumbing::AsId; @@ -2232,6 +2233,13 @@ impl KnownFunction { "`{casted_display}` is equivalent to `{source_display}`", )); } + if let Some(source_expr) = call_expression.arguments.args.get(1) { + diagnostic.help("Remove the redundant `cast`"); + diagnostic.set_fix(Fix::safe_edits( + Edit::deletion(call_expression.start(), source_expr.start()), + [Edit::deletion(source_expr.end(), call_expression.end())], + )); + } } } } From f589887aaa833134e960fa3a33e7c933e6ecaebb Mon Sep 17 00:00:00 2001 From: Emily Yao Date: Mon, 18 May 2026 22:58:00 -0400 Subject: [PATCH 2/5] Fix bug from keyword args --- .../ty_python_semantic/resources/mdtest/directives/cast.md | 6 ++++++ crates/ty_python_semantic/src/types/function.rs | 4 +++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/crates/ty_python_semantic/resources/mdtest/directives/cast.md b/crates/ty_python_semantic/resources/mdtest/directives/cast.md index ec604cc755a19b..2e55aafff0a11a 100644 --- a/crates/ty_python_semantic/resources/mdtest/directives/cast.md +++ b/crates/ty_python_semantic/resources/mdtest/directives/cast.md @@ -136,4 +136,10 @@ warning[redundant-cast]: Value is already of type `int` 7 | cast(val=secrets.randbelow(10), typ=int) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | +help: Remove the redundant `cast` +4 | # snapshot: redundant-cast +5 | cast(int, secrets.randbelow(10)) +6 | # snapshot: redundant-cast + - cast(val=secrets.randbelow(10), typ=int) +7 + secrets.randbelow(10) ``` diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs index faeb1c44ad879d..73f5f6ecffb6a9 100644 --- a/crates/ty_python_semantic/src/types/function.rs +++ b/crates/ty_python_semantic/src/types/function.rs @@ -2233,7 +2233,9 @@ impl KnownFunction { "`{casted_display}` is equivalent to `{source_display}`", )); } - if let Some(source_expr) = call_expression.arguments.args.get(1) { + if let Some(source_expr) = + call_expression.arguments.find_argument_value("val", 1) + { diagnostic.help("Remove the redundant `cast`"); diagnostic.set_fix(Fix::safe_edits( Edit::deletion(call_expression.start(), source_expr.start()), From e4a2d23bf459b84299a27d5eecd3d8f7629ccbd4 Mon Sep 17 00:00:00 2001 From: Emily Yao Date: Tue, 19 May 2026 22:35:41 -0400 Subject: [PATCH 3/5] Consider operator precedence for parentheses --- .../resources/mdtest/directives/cast.md | 83 +++++++++++++++++-- .../ty_python_semantic/src/types/function.rs | 37 +++++++-- 2 files changed, 110 insertions(+), 10 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/directives/cast.md b/crates/ty_python_semantic/resources/mdtest/directives/cast.md index 2e55aafff0a11a..797026a1e86bae 100644 --- a/crates/ty_python_semantic/resources/mdtest/directives/cast.md +++ b/crates/ty_python_semantic/resources/mdtest/directives/cast.md @@ -122,6 +122,7 @@ help: Remove the redundant `cast` 5 + secrets.randbelow(10) 6 | # snapshot: redundant-cast 7 | cast(val=secrets.randbelow(10), typ=int) +8 | def f(x: int, y: int, z: int) -> int: ``` ```py @@ -137,9 +138,81 @@ warning[redundant-cast]: Value is already of type `int` | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | help: Remove the redundant `cast` -4 | # snapshot: redundant-cast -5 | cast(int, secrets.randbelow(10)) -6 | # snapshot: redundant-cast - - cast(val=secrets.randbelow(10), typ=int) -7 + secrets.randbelow(10) +4 | # snapshot: redundant-cast +5 | cast(int, secrets.randbelow(10)) +6 | # snapshot: redundant-cast + - cast(val=secrets.randbelow(10), typ=int) +7 + secrets.randbelow(10) +8 | def f(x: int, y: int, z: int) -> int: +9 | # snapshot: redundant-cast +10 | return cast(int, x + y) * z +``` + +```py +def f(x: int, y: int, z: int) -> int: + # snapshot: redundant-cast + return cast(int, x + y) * z +``` + +```snapshot +warning[redundant-cast]: Value is already of type `int` + --> src/mdtest_snippet.py:10:12 + | +10 | return cast(int, x + y) * z + | ^^^^^^^^^^^^^^^^ + | +help: Remove the redundant `cast` +7 | cast(val=secrets.randbelow(10), typ=int) +8 | def f(x: int, y: int, z: int) -> int: +9 | # snapshot: redundant-cast + - return cast(int, x + y) * z +10 + return (x + y) * z +11 | def g(x: int, y: int) -> int: +12 | # snapshot: redundant-cast +13 | return -cast(int, x + y) +``` + +```py +def g(x: int, y: int) -> int: + # snapshot: redundant-cast + return -cast(int, x + y) +``` + +```snapshot +warning[redundant-cast]: Value is already of type `int` + --> src/mdtest_snippet.py:13:13 + | +13 | return -cast(int, x + y) + | ^^^^^^^^^^^^^^^^ + | +help: Remove the redundant `cast` +10 | return cast(int, x + y) * z +11 | def g(x: int, y: int) -> int: +12 | # snapshot: redundant-cast + - return -cast(int, x + y) +13 + return -(x + y) +14 | def h(x: int, y: int) -> None: +15 | # snapshot: redundant-cast +16 | print(cast(int, x + y)) +``` + +```py +def h(x: int, y: int) -> None: + # snapshot: redundant-cast + print(cast(int, x + y)) +``` + +```snapshot +warning[redundant-cast]: Value is already of type `int` + --> src/mdtest_snippet.py:16:11 + | +16 | print(cast(int, x + y)) + | ^^^^^^^^^^^^^^^^ + | +help: Remove the redundant `cast` +13 | return -cast(int, x + y) +14 | def h(x: int, y: int) -> None: +15 | # snapshot: redundant-cast + - print(cast(int, x + y)) +16 + print(x + y) ``` diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs index 73f5f6ecffb6a9..c3156027506e72 100644 --- a/crates/ty_python_semantic/src/types/function.rs +++ b/crates/ty_python_semantic/src/types/function.rs @@ -56,7 +56,8 @@ use ruff_db::diagnostic::{Annotation, DiagnosticId, Severity, Span}; use ruff_db::files::{File, FileRange}; use ruff_db::parsed::{ParsedModuleRef, parsed_module}; use ruff_diagnostics::{Edit, Fix}; -use ruff_python_ast::{self as ast, ParameterWithDefault}; +use ruff_python_ast::find_node::covering_node; +use ruff_python_ast::{self as ast, OperatorPrecedence, ParameterWithDefault}; use ruff_text_size::Ranged; use salsa::plumbing::AsId; use ty_module_resolver::{KnownModule, ModuleName, file_to_module, resolve_module}; @@ -2236,11 +2237,37 @@ impl KnownFunction { if let Some(source_expr) = call_expression.arguments.find_argument_value("val", 1) { + let module = parsed_module(db, file).load(db); + let covering = + covering_node(module.syntax().into(), call_expression.range()); + let source_precedence = OperatorPrecedence::from_expr(source_expr); + let needs_parens = covering + .parent() + .and_then(ast::AnyNodeRef::as_expr_ref) + .is_some_and(|parent| { + OperatorPrecedence::from_expr_ref(&parent) > source_precedence + }); + let (leading, trailing) = if needs_parens { + ( + Edit::replacement( + "(".to_string(), + call_expression.start(), + source_expr.start(), + ), + Edit::replacement( + ")".to_string(), + source_expr.end(), + call_expression.end(), + ), + ) + } else { + ( + Edit::deletion(call_expression.start(), source_expr.start()), + Edit::deletion(source_expr.end(), call_expression.end()), + ) + }; diagnostic.help("Remove the redundant `cast`"); - diagnostic.set_fix(Fix::safe_edits( - Edit::deletion(call_expression.start(), source_expr.start()), - [Edit::deletion(source_expr.end(), call_expression.end())], - )); + diagnostic.set_fix(Fix::safe_edits(leading, [trailing])); } } } From e5511b2e81100066f5d9c35edcc511fe73ec57ea Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Wed, 20 May 2026 11:13:43 +0100 Subject: [PATCH 4/5] Simplify --- crates/ruff_python_ast/src/nodes.rs | 2 +- .../src/operator_precedence.rs | 8 ++-- .../ty_python_semantic/src/types/function.rs | 41 ++++++++----------- .../src/types/infer/builder.rs | 1 + 4 files changed, 22 insertions(+), 30 deletions(-) diff --git a/crates/ruff_python_ast/src/nodes.rs b/crates/ruff_python_ast/src/nodes.rs index c91689e3c6b46b..d7f5c2f8faf940 100644 --- a/crates/ruff_python_ast/src/nodes.rs +++ b/crates/ruff_python_ast/src/nodes.rs @@ -116,7 +116,7 @@ impl ExprRef<'_> { } pub fn precedence(&self) -> OperatorPrecedence { - OperatorPrecedence::from(self) + OperatorPrecedence::from(*self) } } diff --git a/crates/ruff_python_ast/src/operator_precedence.rs b/crates/ruff_python_ast/src/operator_precedence.rs index 6b652847e9a574..8fba7184af6055 100644 --- a/crates/ruff_python_ast/src/operator_precedence.rs +++ b/crates/ruff_python_ast/src/operator_precedence.rs @@ -54,7 +54,7 @@ pub enum OperatorPrecedence { } impl OperatorPrecedence { - pub fn from_expr_ref(expr: &ExprRef) -> Self { + pub fn from_expr_ref(expr: ExprRef) -> Self { match expr { // Binding or parenthesized expression, list display, dictionary display, set display ExprRef::Tuple(_) @@ -130,7 +130,7 @@ impl OperatorPrecedence { } pub fn from_expr(expr: &Expr) -> Self { - Self::from(&ExprRef::from(expr)) + Self::from(ExprRef::from(expr)) } /// Returns `true` if the precedence is right-associative i.e., the operations are evaluated @@ -146,8 +146,8 @@ impl From<&Expr> for OperatorPrecedence { } } -impl<'a> From<&ExprRef<'a>> for OperatorPrecedence { - fn from(expr_ref: &ExprRef<'a>) -> Self { +impl<'a> From> for OperatorPrecedence { + fn from(expr_ref: ExprRef<'a>) -> Self { Self::from_expr_ref(expr_ref) } } diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs index c3156027506e72..7807fab3bd1d2d 100644 --- a/crates/ty_python_semantic/src/types/function.rs +++ b/crates/ty_python_semantic/src/types/function.rs @@ -55,6 +55,7 @@ use bitflags::bitflags; use ruff_db::diagnostic::{Annotation, DiagnosticId, Severity, Span}; use ruff_db::files::{File, FileRange}; use ruff_db::parsed::{ParsedModuleRef, parsed_module}; +use ruff_db::source::source_text; use ruff_diagnostics::{Edit, Fix}; use ruff_python_ast::find_node::covering_node; use ruff_python_ast::{self as ast, OperatorPrecedence, ParameterWithDefault}; @@ -2234,40 +2235,30 @@ impl KnownFunction { "`{casted_display}` is equivalent to `{source_display}`", )); } - if let Some(source_expr) = - call_expression.arguments.find_argument_value("val", 1) + if let Some(value) = call_expression.arguments.find_argument_value("val", 1) { - let module = parsed_module(db, file).load(db); - let covering = - covering_node(module.syntax().into(), call_expression.range()); - let source_precedence = OperatorPrecedence::from_expr(source_expr); + let covering = covering_node( + context.module().syntax().into(), + call_expression.range(), + ); let needs_parens = covering .parent() .and_then(ast::AnyNodeRef::as_expr_ref) .is_some_and(|parent| { - OperatorPrecedence::from_expr_ref(&parent) > source_precedence + let value_precedence = OperatorPrecedence::from_expr(value); + OperatorPrecedence::from_expr_ref(parent) >= value_precedence }); - let (leading, trailing) = if needs_parens { - ( - Edit::replacement( - "(".to_string(), - call_expression.start(), - source_expr.start(), - ), - Edit::replacement( - ")".to_string(), - source_expr.end(), - call_expression.end(), - ), - ) + let value_text = &source_text(db, file)[value.range()]; + let replacement = if needs_parens { + format!("({value_text})") } else { - ( - Edit::deletion(call_expression.start(), source_expr.start()), - Edit::deletion(source_expr.end(), call_expression.end()), - ) + value_text.to_string() }; diagnostic.help("Remove the redundant `cast`"); - diagnostic.set_fix(Fix::safe_edits(leading, [trailing])); + diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement( + replacement, + call_expression.range(), + ))); } } } diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index bc185c37087b41..799d4188322510 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -7736,6 +7736,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { Type::FunctionLiteral(function_literal) => { if let Some(known_function) = function_literal.known(self.db()) { known_function.check_call( + &self.context, overload, &call_arguments, From b5a2a99759e2a1911dabb7e8926c89c842f9ec69 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Wed, 20 May 2026 11:14:24 +0100 Subject: [PATCH 5/5] Discard changes to crates/ty_python_semantic/src/types/infer/builder.rs --- crates/ty_python_semantic/src/types/infer/builder.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 799d4188322510..bc185c37087b41 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -7736,7 +7736,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { Type::FunctionLiteral(function_literal) => { if let Some(known_function) = function_literal.known(self.db()) { known_function.check_call( - &self.context, overload, &call_arguments,