use ide_db::{defs::Definition, search::FileReference}; use rustc_hash::FxHashMap; use syntax::{ ast::{self, AstNode, AstToken}, TextRange, }; use test_utils::mark; use crate::{ assist_context::{AssistContext, Assists}, AssistId, AssistKind, }; // Assist: inline_local_variable // // Inlines local variable. // // ``` // fn main() { // let x$0 = 1 + 2; // x * 4; // } // ``` // -> // ``` // fn main() { // (1 + 2) * 4; // } // ``` pub(crate) fn inline_local_variable(acc: &mut Assists, ctx: &AssistContext) -> Option<()> { let let_stmt = ctx.find_node_at_offset::()?; let bind_pat = match let_stmt.pat()? { ast::Pat::IdentPat(pat) => pat, _ => return None, }; if bind_pat.mut_token().is_some() { mark::hit!(test_not_inline_mut_variable); return None; } if !bind_pat.syntax().text_range().contains_inclusive(ctx.offset()) { mark::hit!(not_applicable_outside_of_bind_pat); return None; } let initializer_expr = let_stmt.initializer()?; let def = ctx.sema.to_def(&bind_pat)?; let def = Definition::Local(def); let usages = def.usages(&ctx.sema).all(); if usages.is_empty() { mark::hit!(test_not_applicable_if_variable_unused); return None; }; let delete_range = if let Some(whitespace) = let_stmt .syntax() .next_sibling_or_token() .and_then(|it| ast::Whitespace::cast(it.as_token()?.clone())) { TextRange::new( let_stmt.syntax().text_range().start(), whitespace.syntax().text_range().end(), ) } else { let_stmt.syntax().text_range() }; let wrap_in_parens = usages .references .iter() .map(|(&file_id, refs)| { refs.iter() .map(|&FileReference { range, .. }| { let usage_node = ctx .covering_node_for_range(range) .ancestors() .find_map(ast::PathExpr::cast)?; let usage_parent_option = usage_node.syntax().parent().and_then(ast::Expr::cast); let usage_parent = match usage_parent_option { Some(u) => u, None => return Some(false), }; Some(!matches!( (&initializer_expr, usage_parent), (ast::Expr::CallExpr(_), _) | (ast::Expr::IndexExpr(_), _) | (ast::Expr::MethodCallExpr(_), _) | (ast::Expr::FieldExpr(_), _) | (ast::Expr::TryExpr(_), _) | (ast::Expr::RefExpr(_), _) | (ast::Expr::Literal(_), _) | (ast::Expr::TupleExpr(_), _) | (ast::Expr::ArrayExpr(_), _) | (ast::Expr::ParenExpr(_), _) | (ast::Expr::PathExpr(_), _) | (ast::Expr::BlockExpr(_), _) | (ast::Expr::EffectExpr(_), _) | (_, ast::Expr::CallExpr(_)) | (_, ast::Expr::TupleExpr(_)) | (_, ast::Expr::ArrayExpr(_)) | (_, ast::Expr::ParenExpr(_)) | (_, ast::Expr::ForExpr(_)) | (_, ast::Expr::WhileExpr(_)) | (_, ast::Expr::BreakExpr(_)) | (_, ast::Expr::ReturnExpr(_)) | (_, ast::Expr::MatchExpr(_)) )) }) .collect::>() .map(|b| (file_id, b)) }) .collect::>>>()?; let init_str = initializer_expr.syntax().text().to_string(); let init_in_paren = format!("({})", &init_str); let target = bind_pat.syntax().text_range(); acc.add( AssistId("inline_local_variable", AssistKind::RefactorInline), "Inline variable", target, move |builder| { builder.delete(delete_range); for (file_id, references) in usages.references { for (&should_wrap, reference) in wrap_in_parens[&file_id].iter().zip(references) { let replacement = if should_wrap { init_in_paren.clone() } else { init_str.clone() }; match reference.name.as_name_ref() { Some(name_ref) if ast::RecordExprField::for_field_name(name_ref).is_some() => { mark::hit!(inline_field_shorthand); builder.insert(reference.range.end(), format!(": {}", replacement)); } _ => builder.replace(reference.range, replacement), } } } }, ) } #[cfg(test)] mod tests { use test_utils::mark; use crate::tests::{check_assist, check_assist_not_applicable}; use super::*; #[test] fn test_inline_let_bind_literal_expr() { check_assist( inline_local_variable, r" fn bar(a: usize) {} fn foo() { let a$0 = 1; a + 1; if a > 10 { } while a > 10 { } let b = a * 10; bar(a); }", r" fn bar(a: usize) {} fn foo() { 1 + 1; if 1 > 10 { } while 1 > 10 { } let b = 1 * 10; bar(1); }", ); } #[test] fn test_inline_let_bind_bin_expr() { check_assist( inline_local_variable, r" fn bar(a: usize) {} fn foo() { let a$0 = 1 + 1; a + 1; if a > 10 { } while a > 10 { } let b = a * 10; bar(a); }", r" fn bar(a: usize) {} fn foo() { (1 + 1) + 1; if (1 + 1) > 10 { } while (1 + 1) > 10 { } let b = (1 + 1) * 10; bar(1 + 1); }", ); } #[test] fn test_inline_let_bind_function_call_expr() { check_assist( inline_local_variable, r" fn bar(a: usize) {} fn foo() { let a$0 = bar(1); a + 1; if a > 10 { } while a > 10 { } let b = a * 10; bar(a); }", r" fn bar(a: usize) {} fn foo() { bar(1) + 1; if bar(1) > 10 { } while bar(1) > 10 { } let b = bar(1) * 10; bar(bar(1)); }", ); } #[test] fn test_inline_let_bind_cast_expr() { check_assist( inline_local_variable, r" fn bar(a: usize): usize { a } fn foo() { let a$0 = bar(1) as u64; a + 1; if a > 10 { } while a > 10 { } let b = a * 10; bar(a); }", r" fn bar(a: usize): usize { a } fn foo() { (bar(1) as u64) + 1; if (bar(1) as u64) > 10 { } while (bar(1) as u64) > 10 { } let b = (bar(1) as u64) * 10; bar(bar(1) as u64); }", ); } #[test] fn test_inline_let_bind_block_expr() { check_assist( inline_local_variable, r" fn foo() { let a$0 = { 10 + 1 }; a + 1; if a > 10 { } while a > 10 { } let b = a * 10; bar(a); }", r" fn foo() { { 10 + 1 } + 1; if { 10 + 1 } > 10 { } while { 10 + 1 } > 10 { } let b = { 10 + 1 } * 10; bar({ 10 + 1 }); }", ); } #[test] fn test_inline_let_bind_paren_expr() { check_assist( inline_local_variable, r" fn foo() { let a$0 = ( 10 + 1 ); a + 1; if a > 10 { } while a > 10 { } let b = a * 10; bar(a); }", r" fn foo() { ( 10 + 1 ) + 1; if ( 10 + 1 ) > 10 { } while ( 10 + 1 ) > 10 { } let b = ( 10 + 1 ) * 10; bar(( 10 + 1 )); }", ); } #[test] fn test_not_inline_mut_variable() { mark::check!(test_not_inline_mut_variable); check_assist_not_applicable( inline_local_variable, r" fn foo() { let mut a$0 = 1 + 1; a + 1; }", ); } #[test] fn test_call_expr() { check_assist( inline_local_variable, r" fn foo() { let a$0 = bar(10 + 1); let b = a * 10; let c = a as usize; }", r" fn foo() { let b = bar(10 + 1) * 10; let c = bar(10 + 1) as usize; }", ); } #[test] fn test_index_expr() { check_assist( inline_local_variable, r" fn foo() { let x = vec![1, 2, 3]; let a$0 = x[0]; let b = a * 10; let c = a as usize; }", r" fn foo() { let x = vec![1, 2, 3]; let b = x[0] * 10; let c = x[0] as usize; }", ); } #[test] fn test_method_call_expr() { check_assist( inline_local_variable, r" fn foo() { let bar = vec![1]; let a$0 = bar.len(); let b = a * 10; let c = a as usize; }", r" fn foo() { let bar = vec![1]; let b = bar.len() * 10; let c = bar.len() as usize; }", ); } #[test] fn test_field_expr() { check_assist( inline_local_variable, r" struct Bar { foo: usize } fn foo() { let bar = Bar { foo: 1 }; let a$0 = bar.foo; let b = a * 10; let c = a as usize; }", r" struct Bar { foo: usize } fn foo() { let bar = Bar { foo: 1 }; let b = bar.foo * 10; let c = bar.foo as usize; }", ); } #[test] fn test_try_expr() { check_assist( inline_local_variable, r" fn foo() -> Option { let bar = Some(1); let a$0 = bar?; let b = a * 10; let c = a as usize; None }", r" fn foo() -> Option { let bar = Some(1); let b = bar? * 10; let c = bar? as usize; None }", ); } #[test] fn test_ref_expr() { check_assist( inline_local_variable, r" fn foo() { let bar = 10; let a$0 = &bar; let b = a * 10; }", r" fn foo() { let bar = 10; let b = &bar * 10; }", ); } #[test] fn test_tuple_expr() { check_assist( inline_local_variable, r" fn foo() { let a$0 = (10, 20); let b = a[0]; }", r" fn foo() { let b = (10, 20)[0]; }", ); } #[test] fn test_array_expr() { check_assist( inline_local_variable, r" fn foo() { let a$0 = [1, 2, 3]; let b = a.len(); }", r" fn foo() { let b = [1, 2, 3].len(); }", ); } #[test] fn test_paren() { check_assist( inline_local_variable, r" fn foo() { let a$0 = (10 + 20); let b = a * 10; let c = a as usize; }", r" fn foo() { let b = (10 + 20) * 10; let c = (10 + 20) as usize; }", ); } #[test] fn test_path_expr() { check_assist( inline_local_variable, r" fn foo() { let d = 10; let a$0 = d; let b = a * 10; let c = a as usize; }", r" fn foo() { let d = 10; let b = d * 10; let c = d as usize; }", ); } #[test] fn test_block_expr() { check_assist( inline_local_variable, r" fn foo() { let a$0 = { 10 }; let b = a * 10; let c = a as usize; }", r" fn foo() { let b = { 10 } * 10; let c = { 10 } as usize; }", ); } #[test] fn test_used_in_different_expr1() { check_assist( inline_local_variable, r" fn foo() { let a$0 = 10 + 20; let b = a * 10; let c = (a, 20); let d = [a, 10]; let e = (a); }", r" fn foo() { let b = (10 + 20) * 10; let c = (10 + 20, 20); let d = [10 + 20, 10]; let e = (10 + 20); }", ); } #[test] fn test_used_in_for_expr() { check_assist( inline_local_variable, r" fn foo() { let a$0 = vec![10, 20]; for i in a {} }", r" fn foo() { for i in vec![10, 20] {} }", ); } #[test] fn test_used_in_while_expr() { check_assist( inline_local_variable, r" fn foo() { let a$0 = 1 > 0; while a {} }", r" fn foo() { while 1 > 0 {} }", ); } #[test] fn test_used_in_break_expr() { check_assist( inline_local_variable, r" fn foo() { let a$0 = 1 + 1; loop { break a; } }", r" fn foo() { loop { break 1 + 1; } }", ); } #[test] fn test_used_in_return_expr() { check_assist( inline_local_variable, r" fn foo() { let a$0 = 1 > 0; return a; }", r" fn foo() { return 1 > 0; }", ); } #[test] fn test_used_in_match_expr() { check_assist( inline_local_variable, r" fn foo() { let a$0 = 1 > 0; match a {} }", r" fn foo() { match 1 > 0 {} }", ); } #[test] fn inline_field_shorthand() { mark::check!(inline_field_shorthand); check_assist( inline_local_variable, r" struct S { foo: i32} fn main() { let $0foo = 92; S { foo } } ", r" struct S { foo: i32} fn main() { S { foo: 92 } } ", ); } #[test] fn test_not_applicable_if_variable_unused() { mark::check!(test_not_applicable_if_variable_unused); check_assist_not_applicable( inline_local_variable, r" fn foo() { let $0a = 0; } ", ) } #[test] fn not_applicable_outside_of_bind_pat() { mark::check!(not_applicable_outside_of_bind_pat); check_assist_not_applicable( inline_local_variable, r" fn main() { let x = $01 + 2; x * 4; } ", ) } }