Skip to content

Commit d699371

Browse files
"Inline variable" when on a use of the variable
1 parent 15b3466 commit d699371

File tree

1 file changed

+185
-38
lines changed

1 file changed

+185
-38
lines changed

crates/ide_assists/src/handlers/inline_local_variable.rs

Lines changed: 185 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
use ide_db::{defs::Definition, search::FileReference};
1+
use either::Either;
2+
use hir::PathResolution;
3+
use ide_db::{base_db::FileId, defs::Definition, search::FileReference};
24
use rustc_hash::FxHashMap;
35
use syntax::{
4-
ast::{self, AstNode, AstToken},
6+
ast::{self, AstNode, AstToken, NameOwner},
57
TextRange,
68
};
79

@@ -27,44 +29,28 @@ use crate::{
2729
// }
2830
// ```
2931
pub(crate) fn inline_local_variable(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
30-
let let_stmt = ctx.find_node_at_offset::<ast::LetStmt>()?;
31-
let bind_pat = match let_stmt.pat()? {
32-
ast::Pat::IdentPat(pat) => pat,
33-
_ => return None,
34-
};
35-
if bind_pat.mut_token().is_some() {
36-
cov_mark::hit!(test_not_inline_mut_variable);
37-
return None;
38-
}
39-
if !bind_pat.syntax().text_range().contains_inclusive(ctx.offset()) {
40-
cov_mark::hit!(not_applicable_outside_of_bind_pat);
41-
return None;
42-
}
32+
let InlineData { let_stmt, delete_let, replace_usages, target } =
33+
inline_let(ctx).or_else(|| inline_usage(ctx))?;
4334
let initializer_expr = let_stmt.initializer()?;
4435

45-
let def = ctx.sema.to_def(&bind_pat)?;
46-
let def = Definition::Local(def);
47-
let usages = def.usages(&ctx.sema).all();
48-
if usages.is_empty() {
49-
cov_mark::hit!(test_not_applicable_if_variable_unused);
50-
return None;
51-
};
52-
53-
let delete_range = if let Some(whitespace) = let_stmt
54-
.syntax()
55-
.next_sibling_or_token()
56-
.and_then(|it| ast::Whitespace::cast(it.as_token()?.clone()))
57-
{
58-
TextRange::new(
59-
let_stmt.syntax().text_range().start(),
60-
whitespace.syntax().text_range().end(),
61-
)
36+
let delete_range = if delete_let {
37+
if let Some(whitespace) = let_stmt
38+
.syntax()
39+
.next_sibling_or_token()
40+
.and_then(|it| ast::Whitespace::cast(it.as_token()?.clone()))
41+
{
42+
Some(TextRange::new(
43+
let_stmt.syntax().text_range().start(),
44+
whitespace.syntax().text_range().end(),
45+
))
46+
} else {
47+
Some(let_stmt.syntax().text_range())
48+
}
6249
} else {
63-
let_stmt.syntax().text_range()
50+
None
6451
};
6552

66-
let wrap_in_parens = usages
67-
.references
53+
let wrap_in_parens = replace_usages
6854
.iter()
6955
.map(|(&file_id, refs)| {
7056
refs.iter()
@@ -114,14 +100,20 @@ pub(crate) fn inline_local_variable(acc: &mut Assists, ctx: &AssistContext) -> O
114100
let init_str = initializer_expr.syntax().text().to_string();
115101
let init_in_paren = format!("({})", &init_str);
116102

117-
let target = bind_pat.syntax().text_range();
103+
let target = match target {
104+
ast::NameOrNameRef::Name(it) => it.syntax().text_range(),
105+
ast::NameOrNameRef::NameRef(it) => it.syntax().text_range(),
106+
};
107+
118108
acc.add(
119109
AssistId("inline_local_variable", AssistKind::RefactorInline),
120110
"Inline variable",
121111
target,
122112
move |builder| {
123-
builder.delete(delete_range);
124-
for (file_id, references) in usages.references {
113+
if let Some(range) = delete_range {
114+
builder.delete(range);
115+
}
116+
for (file_id, references) in replace_usages {
125117
for (&should_wrap, reference) in wrap_in_parens[&file_id].iter().zip(references) {
126118
let replacement =
127119
if should_wrap { init_in_paren.clone() } else { init_str.clone() };
@@ -140,6 +132,81 @@ pub(crate) fn inline_local_variable(acc: &mut Assists, ctx: &AssistContext) -> O
140132
)
141133
}
142134

135+
struct InlineData {
136+
let_stmt: ast::LetStmt,
137+
delete_let: bool,
138+
target: ast::NameOrNameRef,
139+
replace_usages: FxHashMap<FileId, Vec<FileReference>>,
140+
}
141+
142+
fn inline_let(ctx: &AssistContext) -> Option<InlineData> {
143+
let let_stmt = ctx.find_node_at_offset::<ast::LetStmt>()?;
144+
let bind_pat = match let_stmt.pat()? {
145+
ast::Pat::IdentPat(pat) => pat,
146+
_ => return None,
147+
};
148+
if bind_pat.mut_token().is_some() {
149+
cov_mark::hit!(test_not_inline_mut_variable);
150+
return None;
151+
}
152+
if !bind_pat.syntax().text_range().contains_inclusive(ctx.offset()) {
153+
cov_mark::hit!(not_applicable_outside_of_bind_pat);
154+
return None;
155+
}
156+
157+
let def = ctx.sema.to_def(&bind_pat)?;
158+
let def = Definition::Local(def);
159+
let usages = def.usages(&ctx.sema).all();
160+
if usages.is_empty() {
161+
cov_mark::hit!(test_not_applicable_if_variable_unused);
162+
return None;
163+
};
164+
165+
Some(InlineData {
166+
let_stmt,
167+
delete_let: true,
168+
target: ast::NameOrNameRef::Name(bind_pat.name()?),
169+
replace_usages: usages.references,
170+
})
171+
}
172+
173+
fn inline_usage(ctx: &AssistContext) -> Option<InlineData> {
174+
let path_expr = ctx.find_node_at_offset::<ast::PathExpr>()?;
175+
let path = path_expr.path()?;
176+
let name = match path.as_single_segment()?.kind()? {
177+
ast::PathSegmentKind::Name(name) => name,
178+
_ => return None,
179+
};
180+
181+
let local = match ctx.sema.resolve_path(&path)? {
182+
PathResolution::Local(local) => local,
183+
_ => return None,
184+
};
185+
186+
let bind_pat = match local.source(ctx.db()).value {
187+
Either::Left(ident) => ident,
188+
_ => return None,
189+
};
190+
191+
let let_stmt = ast::LetStmt::cast(bind_pat.syntax().parent()?)?;
192+
193+
let def = Definition::Local(local);
194+
let mut usages = def.usages(&ctx.sema).all();
195+
196+
let delete_let = usages.references.values().map(|v| v.len()).sum::<usize>() == 1;
197+
198+
for references in usages.references.values_mut() {
199+
references.retain(|reference| reference.name.as_name_ref() == Some(&name));
200+
}
201+
202+
Some(InlineData {
203+
let_stmt,
204+
delete_let,
205+
target: ast::NameOrNameRef::NameRef(name),
206+
replace_usages: usages.references,
207+
})
208+
}
209+
143210
#[cfg(test)]
144211
mod tests {
145212
use crate::tests::{check_assist, check_assist_not_applicable};
@@ -726,4 +793,84 @@ fn main() {
726793
",
727794
)
728795
}
796+
797+
#[test]
798+
fn works_on_local_usage() {
799+
check_assist(
800+
inline_local_variable,
801+
r#"
802+
fn f() {
803+
let xyz = 0;
804+
xyz$0;
805+
}
806+
"#,
807+
r#"
808+
fn f() {
809+
0;
810+
}
811+
"#,
812+
);
813+
}
814+
815+
#[test]
816+
fn does_not_remove_let_when_multiple_usages() {
817+
check_assist(
818+
inline_local_variable,
819+
r#"
820+
fn f() {
821+
let xyz = 0;
822+
xyz$0;
823+
xyz;
824+
}
825+
"#,
826+
r#"
827+
fn f() {
828+
let xyz = 0;
829+
0;
830+
xyz;
831+
}
832+
"#,
833+
);
834+
}
835+
836+
#[test]
837+
fn not_applicable_with_non_ident_pattern() {
838+
check_assist_not_applicable(
839+
inline_local_variable,
840+
r#"
841+
fn main() {
842+
let (x, y) = (0, 1);
843+
x$0;
844+
}
845+
"#,
846+
);
847+
}
848+
849+
#[test]
850+
fn not_applicable_on_local_usage_in_macro() {
851+
check_assist_not_applicable(
852+
inline_local_variable,
853+
r#"
854+
macro_rules! m {
855+
($i:ident) => { $i }
856+
}
857+
fn f() {
858+
let xyz = 0;
859+
m!(xyz$0); // replacing it would break the macro
860+
}
861+
"#,
862+
);
863+
check_assist_not_applicable(
864+
inline_local_variable,
865+
r#"
866+
macro_rules! m {
867+
($i:ident) => { $i }
868+
}
869+
fn f() {
870+
let xyz$0 = 0;
871+
m!(xyz); // replacing it would break the macro
872+
}
873+
"#,
874+
);
875+
}
729876
}

0 commit comments

Comments
 (0)