diff --git a/crates/ide-assists/src/handlers/convert_attr_cfg_to_if.rs b/crates/ide-assists/src/handlers/convert_attr_cfg_to_if.rs new file mode 100644 index 000000000000..3da2f8b020fc --- /dev/null +++ b/crates/ide-assists/src/handlers/convert_attr_cfg_to_if.rs @@ -0,0 +1,845 @@ +use either::Either::{self, Left, Right}; +use ide_db::assists::AssistId; +use itertools::Itertools; +use syntax::{ + AstNode, + NodeOrToken::{self, Node, Token}, + SyntaxKind, SyntaxNode, SyntaxToken, T, + ast::{self, HasAttrs, edit_in_place::Indent, make}, + syntax_editor::{Position, SyntaxEditor}, +}; + +use crate::assist_context::{AssistContext, Assists}; + +// Assist: convert_attr_cfg_to_if +// +// Convert `#[cfg(...)] {}` to `if cfg!(...) {}`. +// +// ``` +// fn foo() { +// $0#[cfg(feature = "foo")] +// { +// let _x = 2; +// } +// } +// ``` +// -> +// ``` +// fn foo() { +// if cfg!(feature = "foo") { +// let _x = 2; +// } +// } +// ``` +pub(crate) fn convert_attr_cfg_to_if(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> { + let cfg = ctx.find_node_at_offset::()?; + let target = cfg.syntax().text_range(); + let (origin, cfg, stmt) = find_stmt(cfg)?; + + if !is_cfg(&cfg) { + return None; + } + + let cfg_tt = cfg.token_tree()?; + let cfg_expr = make::expr_macro(cfg.path()?, cfg_tt.clone()).into(); + + acc.add( + AssistId::refactor_rewrite("convert_attr_cfg_to_if"), + "Convert `#[cfg()]` to `if cfg!()`", + target, + |builder| { + let mut edit = builder.make_editor(origin.syntax()); + + let block = process_stmt(&cfg, stmt, &mut edit); + let else_branch = take_else_branch(&origin, cfg_tt, &mut edit); + let if_expr = make::expr_if(cfg_expr, block, else_branch).clone_for_update(); + + if_expr.indent(origin.indent_level()); + edit.replace(origin.syntax(), if_expr.syntax()); + + builder.add_file_edits(ctx.vfs_file_id(), edit); + }, + ) +} + +// Assist: convert_if_cfg_to_attr +// +// Convert `if cfg!(...) {}` to `#[cfg(...)] {}`. +// +// ``` +// fn foo() { +// if $0cfg!(feature = "foo") { +// let _x = 2; +// } +// } +// ``` +// -> +// ``` +// fn foo() { +// #[cfg(feature = "foo")] +// { +// let _x = 2; +// } +// } +// ``` +pub(crate) fn convert_if_cfg_to_attr(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> { + let if_ = ctx.find_node_at_offset::()?; + let ast::Expr::MacroExpr(cfg) = if_.condition()? else { + return None; + }; + let macro_call = cfg.macro_call()?; + + let block_expr = if_.then_branch()?; + let else_block = match if_.else_branch() { + Some(ast::ElseBranch::Block(block)) => Some(block), + None => None, + Some(ast::ElseBranch::IfExpr(_)) => return None, + }; + if macro_call.path()?.segment()?.name_ref()?.text() != "cfg" + || ctx.offset() > block_expr.stmt_list()?.l_curly_token()?.text_range().end() + { + return None; + } + let cfg_cond = macro_call.token_tree()?; + + acc.add( + AssistId::refactor_rewrite("convert_if_cfg_to_attr"), + "Convert `if cfg!()` to `#[cfg()]`", + macro_call.syntax().text_range(), + |builder| { + let mut edit = builder.make_editor(if_.syntax()); + + let indent = format!("\n{}", if_.indent_level()); + if let Some(else_block) = else_block { + let attr_cfg_not = make_cfg(make::token_tree( + T!['('], + [Token(make::tokens::ident("not")), Node(cfg_cond.clone())], + )); + edit.insert_all( + Position::before(if_.syntax()), + vec![ + make::tokens::whitespace(&indent).into(), + attr_cfg_not.syntax().clone_for_update().into(), + make::tokens::whitespace(&indent).into(), + else_block.syntax().clone_for_update().into(), + ], + ); + edit.insert_all( + Position::before(if_.syntax()), + indent_attributes(&if_, &indent, false).clone(), + ); + } + + let attr_cfg = make_cfg(cfg_cond); + edit.insert_all( + Position::before(if_.syntax()), + vec![ + attr_cfg.syntax().clone_for_update().into(), + make::tokens::whitespace(&indent).into(), + block_expr.syntax().clone_for_update().into(), + ], + ); + edit.insert_all(Position::before(if_.syntax()), indent_attributes(&if_, &indent, true)); + + edit.delete(if_.syntax()); + builder.add_file_edits(ctx.vfs_file_id(), edit); + }, + ) +} + +fn take_else_branch( + origin: &impl AstNode, + cfg_tt: ast::TokenTree, + edit: &mut SyntaxEditor, +) -> Option { + let attr_cfg_not = next_attr_cfg_not(origin, &cfg_tt)?; + let (else_origin, else_cfg, else_stmt) = find_stmt(attr_cfg_not)?; + let block = process_stmt(&else_cfg, else_stmt, edit); + + remove_next_ws(origin.syntax(), edit); + edit.delete(else_origin.syntax()); + + Some(ast::ElseBranch::Block(block)) +} + +fn make_block( + stmts: impl IntoIterator, + tail_expr: Option, +) -> ast::BlockExpr { + let tail_expr = tail_expr.inspect(|expr| expr.reindent_to(1.into())); + let stmts = stmts.into_iter().inspect(|stmt| stmt.reindent_to(1.into())); + + make::block_expr(stmts, tail_expr) +} + +fn process_stmt( + cfg: &ast::Attr, + stmt: Either, + edit: &mut SyntaxEditor, +) -> ast::BlockExpr { + match stmt { + Left(stmt_list) => { + remove_cfg(cfg, edit); + make_block(stmt_list.statements(), stmt_list.tail_expr()) + } + Right(stmt) => make_block([stmt], None), + } +} + +fn find_stmt( + attr: ast::Attr, +) -> Option<(impl Indent, ast::Attr, Either)> { + if let Some(stmt_list) = find_stmt_list(&attr) { + let new_stmt_list = stmt_list.clone_for_update(); + new_stmt_list.dedent(stmt_list.indent_level()); + return Some((Left(stmt_list), attr.clone(), Left(new_stmt_list))); + } + + let node = + match attr.syntax().ancestors().find_map(Either::::cast)? { + Left(expr_stmt) => Left(expr_stmt), + Right(list) => Right(list.tail_expr()?), + }; + + let new_node = node.syntax().clone_subtree(); + let attr = new_node + .descendants() + .filter(|node| node.text() == attr.syntax().text()) + .find_map(ast::Attr::cast)?; + + let mut edit = SyntaxEditor::new(new_node); + remove_cfg(&attr, &mut edit); + let new_node = edit.finish().new_root().clone(); + let new_stmt = match Either::::cast(new_node)? { + Left(expr_stmt) => ast::Stmt::from(expr_stmt), + Right(expr) => make::expr_stmt(expr).clone_for_update().into(), + }; + new_stmt.dedent(node.indent_level()); + Some((Right(node), attr, Right(new_stmt))) +} + +fn find_stmt_list(attr: &ast::Attr) -> Option { + let mut node = attr.syntax().clone(); + + while node.kind().is_trivia() || node.kind() == SyntaxKind::ATTR { + node = node.next_sibling()?; + } + + AstNode::cast(node) +} + +fn next_attr_cfg_not(node: &impl AstNode, cond: &ast::TokenTree) -> Option { + let attr = node + .syntax() + .ancestors() + .filter_map(|node| node.next_sibling()) + .flat_map(|x| x.descendants()) + .filter_map(ast::Attr::cast) + .find(is_cfg)?; + + let tts = attr + .token_tree()? + .token_trees_and_tokens() + .filter(|tt| tt.as_token().is_none_or(|t| !t.kind().is_trivia())) + .collect_array()?; + if let [Token(_lparen), Token(not), Node(not_cond), Token(_rparen)] = tts + && not.text() == "not" + && not_cond.syntax().text() == cond.syntax().text() + { + Some(attr) + } else { + None + } +} + +fn remove_cfg(cfg: &ast::Attr, edit: &mut SyntaxEditor) { + remove_next_ws(cfg.syntax(), edit); + edit.delete(cfg.syntax()); +} + +fn remove_next_ws(node: &SyntaxNode, edit: &mut SyntaxEditor) { + if let Some(Token(next)) = node.next_sibling_or_token() + && next.kind() == SyntaxKind::WHITESPACE + { + edit.delete(next); + } else if let Some(parent) = node.parent() + && parent.last_child().is_some_and(|it| it == *node) + { + remove_next_ws(&parent, edit); + } +} + +fn is_cfg(attr: &ast::Attr) -> bool { + attr.path().and_then(|p| p.as_single_name_ref()).is_some_and(|name| name.text() == "cfg") +} + +fn make_cfg(tt: ast::TokenTree) -> ast::Attr { + let cfg_path = make::ext::ident_path("cfg"); + make::attr_outer(make::meta_token_tree(cfg_path, tt)) +} + +fn indent_attributes( + if_: &ast::IfExpr, + indent: &str, + before: bool, +) -> Vec> { + if_.attrs() + .flat_map(|attr| { + let mut tts = + [Token(make::tokens::whitespace(indent)), Node(attr.syntax().clone_for_update())]; + if before { + tts.reverse(); + } + tts + }) + .collect_vec() +} + +#[cfg(test)] +mod tests { + use crate::tests::{check_assist, check_assist_not_applicable}; + + use super::*; + + #[test] + fn test_stmt_list() { + check_assist( + convert_attr_cfg_to_if, + r#" +fn foo() { + $0#[cfg(feature = "foo")] + { + let x = 2; + let _ = x+1; + } +} + "#, + r#" +fn foo() { + if cfg!(feature = "foo") { + let x = 2; + let _ = x+1; + } +} + "#, + ); + } + + #[test] + fn test_stmt_list_else() { + check_assist( + convert_attr_cfg_to_if, + r#" +fn foo() { + $0#[cfg(feature = "foo")] + { + let x = 2; + let _ = x+1; + } + #[cfg(not(feature = "foo"))] + { + let _ = 3; + } + // needless comment +} + "#, + r#" +fn foo() { + if cfg!(feature = "foo") { + let x = 2; + let _ = x+1; + } else { + let _ = 3; + } + // needless comment +} + "#, + ); + } + + #[test] + fn test_expr_stmt() { + check_assist( + convert_attr_cfg_to_if, + r#" +fn bar() {} +fn foo() { + $0#[cfg(feature = "foo")] + bar(); +} + "#, + r#" +fn bar() {} +fn foo() { + if cfg!(feature = "foo") { + bar(); + } +} + "#, + ); + } + + #[test] + fn test_other_attr() { + check_assist( + convert_attr_cfg_to_if, + r#" +fn foo() { + $0#[cfg(feature = "foo")] + #[allow(unused)] + { + let x = 2; + let _ = x+1; + } +} + "#, + r#" +fn foo() { + #[allow(unused)] + if cfg!(feature = "foo") { + let x = 2; + let _ = x+1; + } +} + "#, + ); + + check_assist( + convert_attr_cfg_to_if, + r#" +fn bar() {} +fn foo() { + #[allow(unused)] + $0#[cfg(feature = "foo")] + bar(); +} + "#, + r#" +fn bar() {} +fn foo() { + if cfg!(feature = "foo") { + #[allow(unused)] + bar(); + } +} + "#, + ); + + check_assist( + convert_attr_cfg_to_if, + r#" +fn bar() {} +fn baz() {} +fn foo() { + #[allow(unused)] + $0#[cfg(feature = "foo")] + bar(); + #[allow(unused)] + #[cfg(not(feature = "foo"))] + baz(); +} + "#, + r#" +fn bar() {} +fn baz() {} +fn foo() { + if cfg!(feature = "foo") { + #[allow(unused)] + bar(); + } else { + #[allow(unused)] + baz(); + } +} + "#, + ); + } + + #[test] + fn test_stmt_list_indent() { + check_assist( + convert_attr_cfg_to_if, + r#" +mod a { + fn foo() { + #[allow(unused)] + $0#[cfg(feature = "foo")] + { + let _ = match () { + () => { + todo!() + }, + }; + match () { + () => { + todo!() + }, + } + } + } +} + "#, + r#" +mod a { + fn foo() { + #[allow(unused)] + if cfg!(feature = "foo") { + let _ = match () { + () => { + todo!() + }, + }; + match () { + () => { + todo!() + }, + } + } + } +} + "#, + ); + + check_assist( + convert_attr_cfg_to_if, + r#" +mod a { + fn foo() { + #[allow(unused)] + $0#[cfg(feature = "foo")] + { + let _ = match () { + () => { + todo!() + }, + }; + match () { + () => { + todo!() + }, + } + } + #[cfg(not(feature = "foo"))] + { + let _ = match () { + () => { + todo!("") + }, + }; + match () { + () => { + todo!("") + }, + } + } + } +} + "#, + r#" +mod a { + fn foo() { + #[allow(unused)] + if cfg!(feature = "foo") { + let _ = match () { + () => { + todo!() + }, + }; + match () { + () => { + todo!() + }, + } + } else { + let _ = match () { + () => { + todo!("") + }, + }; + match () { + () => { + todo!("") + }, + } + } + } +} + "#, + ); + } + + #[test] + fn test_expr_indent() { + check_assist( + convert_attr_cfg_to_if, + r#" +mod a { + fn foo() { + #[allow(unused)] + $0#[cfg(feature = "foo")] + match () { + () => { + todo!() + }, + } + } +} + "#, + r#" +mod a { + fn foo() { + if cfg!(feature = "foo") { + #[allow(unused)] + match () { + () => { + todo!() + }, + } + } + } +} + "#, + ); + + check_assist( + convert_attr_cfg_to_if, + r#" +mod a { + fn foo() { + $0#[cfg(feature = "foo")] + match () { + () => { + todo!() + }, + }; + } +} + "#, + r#" +mod a { + fn foo() { + if cfg!(feature = "foo") { + match () { + () => { + todo!() + }, + }; + } + } +} + "#, + ); + + check_assist( + convert_attr_cfg_to_if, + r#" +mod a { + fn foo() { + $0#[cfg(feature = "foo")] + match () { + () => { + todo!() + }, + }; + #[cfg(not(feature = "foo"))] + match () { + () => { + todo!("") + }, + }; + } +} + "#, + r#" +mod a { + fn foo() { + if cfg!(feature = "foo") { + match () { + () => { + todo!() + }, + }; + } else { + match () { + () => { + todo!("") + }, + }; + } + } +} + "#, + ); + } + + #[test] + fn test_convert_if_cfg_to_attr_else_block() { + check_assist( + convert_if_cfg_to_attr, + r#" +fn foo() { + $0if cfg!(feature = "foo") { + let x = 2; + let _ = x+1; + } else { + let _ = 3; + } + // needless comment +} + "#, + r#" +fn foo() { + #[cfg(feature = "foo")] + { + let x = 2; + let _ = x+1; + } + #[cfg(not(feature = "foo"))] + { + let _ = 3; + } + // needless comment +} + "#, + ); + } + + #[test] + fn test_convert_if_cfg_to_attr_not_applicable_after_curly() { + check_assist_not_applicable( + convert_if_cfg_to_attr, + r#" +fn foo() { + if cfg!(feature = "foo") { + $0let x = 2; + let _ = x+1; + } else { + let _ = 3; + } + // needless comment +} + "#, + ); + } + + #[test] + fn test_convert_if_cfg_to_attr_indent() { + check_assist( + convert_if_cfg_to_attr, + r#" +mod a { + fn foo() { + $0if cfg!(feature = "foo") { + #[allow(unused)] + match () { + () => { + todo!() + }, + } + } + } +} + "#, + r#" +mod a { + fn foo() { + #[cfg(feature = "foo")] + { + #[allow(unused)] + match () { + () => { + todo!() + }, + } + } + } +} + "#, + ); + + check_assist( + convert_if_cfg_to_attr, + r#" +mod a { + fn foo() { + $0if cfg!(feature = "foo") { + #[allow(unused)] + match () { + () => { + todo!() + }, + } + } else { + #[allow(unused)] + match () { + () => { + todo!("") + }, + } + } + } +} + "#, + r#" +mod a { + fn foo() { + #[cfg(feature = "foo")] + { + #[allow(unused)] + match () { + () => { + todo!() + }, + } + } + #[cfg(not(feature = "foo"))] + { + #[allow(unused)] + match () { + () => { + todo!("") + }, + } + } + } +} + "#, + ); + } + + #[test] + fn test_convert_if_cfg_to_attr_attributes() { + check_assist( + convert_if_cfg_to_attr, + r#" +fn foo() { + #[foo] + #[allow(unused)] + $0if cfg!(feature = "foo") { + let x = 2; + let _ = x+1; + } else { + let _ = 3; + } +} + "#, + r#" +fn foo() { + #[foo] + #[allow(unused)] + #[cfg(feature = "foo")] + { + let x = 2; + let _ = x+1; + } + #[foo] + #[allow(unused)] + #[cfg(not(feature = "foo"))] + { + let _ = 3; + } +} + "#, + ); + } +} diff --git a/crates/ide-assists/src/lib.rs b/crates/ide-assists/src/lib.rs index 4682c0473238..2aef3c3ff10b 100644 --- a/crates/ide-assists/src/lib.rs +++ b/crates/ide-assists/src/lib.rs @@ -117,6 +117,7 @@ mod handlers { mod auto_import; mod bind_unused_param; mod change_visibility; + mod convert_attr_cfg_to_if; mod convert_bool_then; mod convert_bool_to_enum; mod convert_closure_to_fn; @@ -254,6 +255,8 @@ mod handlers { convert_bool_then::convert_if_to_bool_then, convert_bool_to_enum::convert_bool_to_enum, convert_closure_to_fn::convert_closure_to_fn, + convert_attr_cfg_to_if::convert_attr_cfg_to_if, + convert_attr_cfg_to_if::convert_if_cfg_to_attr, convert_comment_block::convert_comment_block, convert_comment_from_or_to_doc::convert_comment_from_or_to_doc, convert_for_to_while_let::convert_for_loop_to_while_let, diff --git a/crates/ide-assists/src/tests/generated.rs b/crates/ide-assists/src/tests/generated.rs index 91348be97eb7..0113d0aebbab 100644 --- a/crates/ide-assists/src/tests/generated.rs +++ b/crates/ide-assists/src/tests/generated.rs @@ -355,6 +355,28 @@ fn doctest_comment_to_doc() { ) } +#[test] +fn doctest_convert_attr_cfg_to_if() { + check_doc_test( + "convert_attr_cfg_to_if", + r#####" +fn foo() { + $0#[cfg(feature = "foo")] + { + let _x = 2; + } +} +"#####, + r#####" +fn foo() { + if cfg!(feature = "foo") { + let _x = 2; + } +} +"#####, + ) +} + #[test] fn doctest_convert_bool_then_to_if() { check_doc_test( @@ -516,6 +538,28 @@ impl TryFrom for Thing { ) } +#[test] +fn doctest_convert_if_cfg_to_attr() { + check_doc_test( + "convert_if_cfg_to_attr", + r#####" +fn foo() { + if $0cfg!(feature = "foo") { + let _x = 2; + } +} +"#####, + r#####" +fn foo() { + #[cfg(feature = "foo")] + { + let _x = 2; + } +} +"#####, + ) +} + #[test] fn doctest_convert_if_to_bool_then() { check_doc_test(