aboutsummaryrefslogtreecommitdiff
path: root/crates/assists/src/handlers/pull_assignment_up.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/assists/src/handlers/pull_assignment_up.rs')
-rw-r--r--crates/assists/src/handlers/pull_assignment_up.rs400
1 files changed, 400 insertions, 0 deletions
diff --git a/crates/assists/src/handlers/pull_assignment_up.rs b/crates/assists/src/handlers/pull_assignment_up.rs
new file mode 100644
index 000000000..560d93e10
--- /dev/null
+++ b/crates/assists/src/handlers/pull_assignment_up.rs
@@ -0,0 +1,400 @@
1use syntax::{
2 ast::{self, edit::AstNodeEdit, make},
3 AstNode,
4};
5use test_utils::mark;
6
7use crate::{
8 assist_context::{AssistContext, Assists},
9 AssistId, AssistKind,
10};
11
12// Assist: pull_assignment_up
13//
14// Extracts variable assignment to outside an if or match statement.
15//
16// ```
17// fn main() {
18// let mut foo = 6;
19//
20// if true {
21// <|>foo = 5;
22// } else {
23// foo = 4;
24// }
25// }
26// ```
27// ->
28// ```
29// fn main() {
30// let mut foo = 6;
31//
32// foo = if true {
33// 5
34// } else {
35// 4
36// };
37// }
38// ```
39pub(crate) fn pull_assignment_up(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
40 let assign_expr = ctx.find_node_at_offset::<ast::BinExpr>()?;
41 let name_expr = if assign_expr.op_kind()? == ast::BinOp::Assignment {
42 assign_expr.lhs()?
43 } else {
44 return None;
45 };
46
47 let (old_stmt, new_stmt) = if let Some(if_expr) = ctx.find_node_at_offset::<ast::IfExpr>() {
48 (
49 ast::Expr::cast(if_expr.syntax().to_owned())?,
50 exprify_if(&if_expr, &ctx.sema, &name_expr)?.indent(if_expr.indent_level()),
51 )
52 } else if let Some(match_expr) = ctx.find_node_at_offset::<ast::MatchExpr>() {
53 (
54 ast::Expr::cast(match_expr.syntax().to_owned())?,
55 exprify_match(&match_expr, &ctx.sema, &name_expr)?,
56 )
57 } else {
58 return None;
59 };
60
61 let expr_stmt = make::expr_stmt(new_stmt);
62
63 acc.add(
64 AssistId("pull_assignment_up", AssistKind::RefactorExtract),
65 "Pull assignment up",
66 old_stmt.syntax().text_range(),
67 move |edit| {
68 edit.replace(old_stmt.syntax().text_range(), format!("{} = {};", name_expr, expr_stmt));
69 },
70 )
71}
72
73fn exprify_match(
74 match_expr: &ast::MatchExpr,
75 sema: &hir::Semantics<ide_db::RootDatabase>,
76 name: &ast::Expr,
77) -> Option<ast::Expr> {
78 let new_arm_list = match_expr
79 .match_arm_list()?
80 .arms()
81 .map(|arm| {
82 if let ast::Expr::BlockExpr(block) = arm.expr()? {
83 let new_block = exprify_block(&block, sema, name)?.indent(block.indent_level());
84 Some(arm.replace_descendant(block, new_block))
85 } else {
86 None
87 }
88 })
89 .collect::<Option<Vec<_>>>()?;
90 let new_arm_list = match_expr
91 .match_arm_list()?
92 .replace_descendants(match_expr.match_arm_list()?.arms().zip(new_arm_list));
93 Some(make::expr_match(match_expr.expr()?, new_arm_list))
94}
95
96fn exprify_if(
97 statement: &ast::IfExpr,
98 sema: &hir::Semantics<ide_db::RootDatabase>,
99 name: &ast::Expr,
100) -> Option<ast::Expr> {
101 let then_branch = exprify_block(&statement.then_branch()?, sema, name)?;
102 let else_branch = match statement.else_branch()? {
103 ast::ElseBranch::Block(ref block) => {
104 ast::ElseBranch::Block(exprify_block(block, sema, name)?)
105 }
106 ast::ElseBranch::IfExpr(expr) => {
107 mark::hit!(test_pull_assignment_up_chained_if);
108 ast::ElseBranch::IfExpr(ast::IfExpr::cast(
109 exprify_if(&expr, sema, name)?.syntax().to_owned(),
110 )?)
111 }
112 };
113 Some(make::expr_if(statement.condition()?, then_branch, Some(else_branch)))
114}
115
116fn exprify_block(
117 block: &ast::BlockExpr,
118 sema: &hir::Semantics<ide_db::RootDatabase>,
119 name: &ast::Expr,
120) -> Option<ast::BlockExpr> {
121 if block.expr().is_some() {
122 return None;
123 }
124
125 let mut stmts: Vec<_> = block.statements().collect();
126 let stmt = stmts.pop()?;
127
128 if let ast::Stmt::ExprStmt(stmt) = stmt {
129 if let ast::Expr::BinExpr(expr) = stmt.expr()? {
130 if expr.op_kind()? == ast::BinOp::Assignment && is_equivalent(sema, &expr.lhs()?, name)
131 {
132 // The last statement in the block is an assignment to the name we want
133 return Some(make::block_expr(stmts, Some(expr.rhs()?)));
134 }
135 }
136 }
137 None
138}
139
140fn is_equivalent(
141 sema: &hir::Semantics<ide_db::RootDatabase>,
142 expr0: &ast::Expr,
143 expr1: &ast::Expr,
144) -> bool {
145 match (expr0, expr1) {
146 (ast::Expr::FieldExpr(field_expr0), ast::Expr::FieldExpr(field_expr1)) => {
147 mark::hit!(test_pull_assignment_up_field_assignment);
148 sema.resolve_field(field_expr0) == sema.resolve_field(field_expr1)
149 }
150 (ast::Expr::PathExpr(path0), ast::Expr::PathExpr(path1)) => {
151 let path0 = path0.path();
152 let path1 = path1.path();
153 if let (Some(path0), Some(path1)) = (path0, path1) {
154 sema.resolve_path(&path0) == sema.resolve_path(&path1)
155 } else {
156 false
157 }
158 }
159 _ => false,
160 }
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166
167 use crate::tests::{check_assist, check_assist_not_applicable};
168
169 #[test]
170 fn test_pull_assignment_up_if() {
171 check_assist(
172 pull_assignment_up,
173 r#"
174fn foo() {
175 let mut a = 1;
176
177 if true {
178 <|>a = 2;
179 } else {
180 a = 3;
181 }
182}"#,
183 r#"
184fn foo() {
185 let mut a = 1;
186
187 a = if true {
188 2
189 } else {
190 3
191 };
192}"#,
193 );
194 }
195
196 #[test]
197 fn test_pull_assignment_up_match() {
198 check_assist(
199 pull_assignment_up,
200 r#"
201fn foo() {
202 let mut a = 1;
203
204 match 1 {
205 1 => {
206 <|>a = 2;
207 },
208 2 => {
209 a = 3;
210 },
211 3 => {
212 a = 4;
213 }
214 }
215}"#,
216 r#"
217fn foo() {
218 let mut a = 1;
219
220 a = match 1 {
221 1 => {
222 2
223 },
224 2 => {
225 3
226 },
227 3 => {
228 4
229 }
230 };
231}"#,
232 );
233 }
234
235 #[test]
236 fn test_pull_assignment_up_not_last_not_applicable() {
237 check_assist_not_applicable(
238 pull_assignment_up,
239 r#"
240fn foo() {
241 let mut a = 1;
242
243 if true {
244 <|>a = 2;
245 b = a;
246 } else {
247 a = 3;
248 }
249}"#,
250 )
251 }
252
253 #[test]
254 fn test_pull_assignment_up_chained_if() {
255 mark::check!(test_pull_assignment_up_chained_if);
256 check_assist(
257 pull_assignment_up,
258 r#"
259fn foo() {
260 let mut a = 1;
261
262 if true {
263 <|>a = 2;
264 } else if false {
265 a = 3;
266 } else {
267 a = 4;
268 }
269}"#,
270 r#"
271fn foo() {
272 let mut a = 1;
273
274 a = if true {
275 2
276 } else if false {
277 3
278 } else {
279 4
280 };
281}"#,
282 );
283 }
284
285 #[test]
286 fn test_pull_assignment_up_retains_stmts() {
287 check_assist(
288 pull_assignment_up,
289 r#"
290fn foo() {
291 let mut a = 1;
292
293 if true {
294 let b = 2;
295 <|>a = 2;
296 } else {
297 let b = 3;
298 a = 3;
299 }
300}"#,
301 r#"
302fn foo() {
303 let mut a = 1;
304
305 a = if true {
306 let b = 2;
307 2
308 } else {
309 let b = 3;
310 3
311 };
312}"#,
313 )
314 }
315
316 #[test]
317 fn pull_assignment_up_let_stmt_not_applicable() {
318 check_assist_not_applicable(
319 pull_assignment_up,
320 r#"
321fn foo() {
322 let mut a = 1;
323
324 let b = if true {
325 <|>a = 2
326 } else {
327 a = 3
328 };
329}"#,
330 )
331 }
332
333 #[test]
334 fn pull_assignment_up_if_missing_assigment_not_applicable() {
335 check_assist_not_applicable(
336 pull_assignment_up,
337 r#"
338fn foo() {
339 let mut a = 1;
340
341 if true {
342 <|>a = 2;
343 } else {}
344}"#,
345 )
346 }
347
348 #[test]
349 fn pull_assignment_up_match_missing_assigment_not_applicable() {
350 check_assist_not_applicable(
351 pull_assignment_up,
352 r#"
353fn foo() {
354 let mut a = 1;
355
356 match 1 {
357 1 => {
358 <|>a = 2;
359 },
360 2 => {
361 a = 3;
362 },
363 3 => {},
364 }
365}"#,
366 )
367 }
368
369 #[test]
370 fn test_pull_assignment_up_field_assignment() {
371 mark::check!(test_pull_assignment_up_field_assignment);
372 check_assist(
373 pull_assignment_up,
374 r#"
375struct A(usize);
376
377fn foo() {
378 let mut a = A(1);
379
380 if true {
381 <|>a.0 = 2;
382 } else {
383 a.0 = 3;
384 }
385}"#,
386 r#"
387struct A(usize);
388
389fn foo() {
390 let mut a = A(1);
391
392 a.0 = if true {
393 2
394 } else {
395 3
396 };
397}"#,
398 )
399 }
400}