diff --git a/crates/ide-assists/src/handlers/remove_parentheses.rs b/crates/ide-assists/src/handlers/remove_parentheses.rs new file mode 100644 index 00000000000..185beda9d07 --- /dev/null +++ b/crates/ide-assists/src/handlers/remove_parentheses.rs @@ -0,0 +1,91 @@ +use syntax::{ast, AstNode}; + +use crate::{AssistContext, AssistId, AssistKind, Assists}; + +// Assist: remove_parentheses +// +// Removes redundant parentheses. +// +// ``` +// fn main() { +// _ = $0(2) + 2; +// } +// ``` +// -> +// ``` +// fn main() { +// _ = 2 + 2; +// } +// ``` +pub(crate) fn remove_parentheses(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> { + let parens = ctx.find_node_at_offset::()?; + + let cursor_in_range = + parens.l_paren_token()?.text_range().contains_range(ctx.selection_trimmed()) + || parens.r_paren_token()?.text_range().contains_range(ctx.selection_trimmed()); + if !cursor_in_range { + return None; + } + + let expr = parens.expr()?; + + let parent = ast::Expr::cast(parens.syntax().parent()?); + let is_ok_to_remove = expr.precedence() >= parent.as_ref().and_then(ast::Expr::precedence); + if !is_ok_to_remove { + return None; + } + + let target = parens.syntax().text_range(); + acc.add( + AssistId("remove_parentheses", AssistKind::Refactor), + "Remove redundant parentheses", + target, + |builder| builder.replace_ast(parens.into(), expr), + ) +} + +#[cfg(test)] +mod tests { + use crate::tests::{check_assist, check_assist_not_applicable}; + + use super::*; + + #[test] + fn remove_parens_simple() { + check_assist(remove_parentheses, r#"fn f() { $0(2) + 2; }"#, r#"fn f() { 2 + 2; }"#); + check_assist(remove_parentheses, r#"fn f() { ($02) + 2; }"#, r#"fn f() { 2 + 2; }"#); + check_assist(remove_parentheses, r#"fn f() { (2)$0 + 2; }"#, r#"fn f() { 2 + 2; }"#); + check_assist(remove_parentheses, r#"fn f() { (2$0) + 2; }"#, r#"fn f() { 2 + 2; }"#); + } + + #[test] + fn remove_parens_precedence() { + check_assist( + remove_parentheses, + r#"fn f() { $0(2 * 3) + 1; }"#, + r#"fn f() { 2 * 3 + 1; }"#, + ); + check_assist(remove_parentheses, r#"fn f() { ( $0(2) ); }"#, r#"fn f() { ( 2 ); }"#); + check_assist(remove_parentheses, r#"fn f() { $0(2?)?; }"#, r#"fn f() { 2??; }"#); + check_assist(remove_parentheses, r#"fn f() { f(($02 + 2)); }"#, r#"fn f() { f(2 + 2); }"#); + check_assist( + remove_parentheses, + r#"fn f() { (1<2)&&$0(3>4); }"#, + r#"fn f() { (1<2)&&3>4; }"#, + ); + } + + #[test] + fn remove_parens_doesnt_apply_precedence() { + check_assist_not_applicable(remove_parentheses, r#"fn f() { $0(2 + 2) * 8; }"#); + check_assist_not_applicable(remove_parentheses, r#"fn f() { $0(2 + 2).f(); }"#); + check_assist_not_applicable(remove_parentheses, r#"fn f() { $0(2 + 2).await; }"#); + check_assist_not_applicable(remove_parentheses, r#"fn f() { $0!(2..2); }"#); + } + + #[test] + fn remove_parens_doesnt_apply_with_cursor_not_on_paren() { + check_assist_not_applicable(remove_parentheses, r#"fn f() { (2 +$0 2) }"#); + check_assist_not_applicable(remove_parentheses, r#"fn f() {$0 (2 + 2) }"#); + } +} diff --git a/crates/ide-assists/src/lib.rs b/crates/ide-assists/src/lib.rs index a55de800b39..8b1247c640a 100644 --- a/crates/ide-assists/src/lib.rs +++ b/crates/ide-assists/src/lib.rs @@ -179,6 +179,7 @@ mod handlers { mod remove_dbg; mod remove_mut; mod remove_unused_param; + mod remove_parentheses; mod reorder_fields; mod reorder_impl_items; mod replace_try_expr_with_match; @@ -280,6 +281,7 @@ mod handlers { remove_dbg::remove_dbg, remove_mut::remove_mut, remove_unused_param::remove_unused_param, + remove_parentheses::remove_parentheses, reorder_fields::reorder_fields, reorder_impl_items::reorder_impl_items, replace_try_expr_with_match::replace_try_expr_with_match, diff --git a/crates/ide-assists/src/tests/generated.rs b/crates/ide-assists/src/tests/generated.rs index ccd38119c4a..80b8c27c7c0 100644 --- a/crates/ide-assists/src/tests/generated.rs +++ b/crates/ide-assists/src/tests/generated.rs @@ -1978,6 +1978,23 @@ impl Walrus { ) } +#[test] +fn doctest_remove_parentheses() { + check_doc_test( + "remove_parentheses", + r#####" +fn main() { + _ = $0(2) + 2; +} +"#####, + r#####" +fn main() { + _ = 2 + 2; +} +"#####, + ) +} + #[test] fn doctest_remove_unused_param() { check_doc_test( diff --git a/crates/syntax/src/ast.rs b/crates/syntax/src/ast.rs index 4aa64d0d6e8..10c04575833 100644 --- a/crates/syntax/src/ast.rs +++ b/crates/syntax/src/ast.rs @@ -9,6 +9,7 @@ mod operators; pub mod edit; pub mod edit_in_place; pub mod make; +pub mod prec; use std::marker::PhantomData; diff --git a/crates/syntax/src/ast/prec.rs b/crates/syntax/src/ast/prec.rs new file mode 100644 index 00000000000..6253c4dc3e7 --- /dev/null +++ b/crates/syntax/src/ast/prec.rs @@ -0,0 +1,115 @@ +//! Precedence representation. + +use crate::ast::{self, BinExpr, Expr}; + +/// Precedence of an expression. +#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)] +pub enum ExprPrecedence { + // N.B.: Order is important + Closure, + Jump, + Range, + Bin(BinOpPresedence), + Prefix, + Postfix, + Paren, +} + +/// Precedence of a binary operator. +#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)] +pub enum BinOpPresedence { + // N.B.: Order is important + /// `=`, `+=`, `-=`, `*=`, `/=`, `%=`, `|=`, `&=` + Assign, + /// `||` + LOr, + /// `&&` + LAnd, + /// `<`, `<=`, `>`, `>=`, `==` and `!=` + Cmp, + /// `|` + BitOr, + /// `^` + BitXor, + /// `&` + BitAnd, + /// `<<` and `>>` + Shift, + /// `+` and `-` + Add, + /// `*`, `/` and `%` + Mul, + /// `as` + As, +} + +impl Expr { + /// Returns precedence of this expression. + /// Usefull to preserve semantics in assists. + /// + /// Returns `None` if this is a [`BinExpr`] and its [`op_kind`] returns `None`. + /// + /// [`op_kind`]: BinExpr::op_kind + /// [`BinExpr`]: Expr::BinExpr + pub fn precedence(&self) -> Option { + // Copied from + use Expr::*; + + let prec = match self { + ClosureExpr(_) => ExprPrecedence::Closure, + + ContinueExpr(_) | ReturnExpr(_) | YieldExpr(_) | BreakExpr(_) => ExprPrecedence::Jump, + + RangeExpr(_) => ExprPrecedence::Range, + + BinExpr(bin_expr) => return bin_expr.precedence().map(ExprPrecedence::Bin), + CastExpr(_) => ExprPrecedence::Bin(BinOpPresedence::As), + + BoxExpr(_) | RefExpr(_) | LetExpr(_) | PrefixExpr(_) => ExprPrecedence::Prefix, + + AwaitExpr(_) | CallExpr(_) | MethodCallExpr(_) | FieldExpr(_) | IndexExpr(_) + | TryExpr(_) | MacroExpr(_) => ExprPrecedence::Postfix, + + ArrayExpr(_) | TupleExpr(_) | Literal(_) | PathExpr(_) | ParenExpr(_) | IfExpr(_) + | WhileExpr(_) | ForExpr(_) | LoopExpr(_) | MatchExpr(_) | BlockExpr(_) + | RecordExpr(_) | UnderscoreExpr(_) => ExprPrecedence::Paren, + }; + + Some(prec) + } +} + +impl BinExpr { + /// Returns precedence of this binary expression. + /// Usefull to preserve semantics in assists. + /// + /// Returns `None` if [`op_kind`] returns `None`. + /// + /// [`op_kind`]: BinExpr::op_kind + pub fn precedence(&self) -> Option { + use ast::{ArithOp::*, BinaryOp::*, LogicOp::*}; + + let prec = match self.op_kind()? { + LogicOp(op) => match op { + And => BinOpPresedence::LAnd, + Or => BinOpPresedence::LOr, + }, + ArithOp(op) => match op { + Add => BinOpPresedence::Add, + Mul => BinOpPresedence::Mul, + Sub => BinOpPresedence::Add, + Div => BinOpPresedence::Mul, + Rem => BinOpPresedence::Mul, + Shl => BinOpPresedence::Shift, + Shr => BinOpPresedence::Shift, + BitXor => BinOpPresedence::BitXor, + BitOr => BinOpPresedence::BitOr, + BitAnd => BinOpPresedence::BitAnd, + }, + CmpOp(_) => BinOpPresedence::Cmp, + Assignment { .. } => BinOpPresedence::Assign, + }; + + Some(prec) + } +}