Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: for loop to while let assist #19271

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
383 changes: 383 additions & 0 deletions crates/ide-assists/src/handlers/convert_for_to_while_let.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,383 @@
use hir::{sym, Name};
use ide_db::famous_defs::FamousDefs;
use syntax::{
ast::{
self,
edit::{AstNodeEdit, IndentLevel},
make, HasLoopBody,
},
AstNode,
};

use crate::{AssistContext, AssistId, AssistKind, Assists};

// Assist: convert_for_loop_to_while_let
//
// Converts a for loop into a while let on the Iterator.
//
// ```
// fn main() {
// let x = vec![1, 2, 3];
// for$0 v in x {
// let y = v * 2;
// };
// }
// ```
// ->
// ```
// fn main() {
// let x = vec![1, 2, 3];
// while let Some(v) = x.into_iter().next() {
// let y = v * 2;
// };
// }
// ```
pub(crate) fn convert_for_loop_to_while_let(
acc: &mut Assists,
ctx: &AssistContext<'_>,
) -> Option<()> {
let for_loop = ctx.find_node_at_offset::<ast::ForExpr>()?;
let iterable = for_loop.iterable()?;
let pat = for_loop.pat()?;
let body = for_loop.loop_body()?;
if body.syntax().text_range().start() < ctx.offset() {
cov_mark::hit!(not_available_in_body);
return None;
}

acc.add(
AssistId("convert_for_loop_to_while_let", AssistKind::RefactorRewrite),
"Replace this for loop with `while let`",
for_loop.syntax().text_range(),
|builder| {
let iter = if let Some((expr_behind_ref, method)) =
is_ref_and_impls_iter_method(&ctx.sema, &iterable)
{
// We have either "for x in &col" and col implements a method called iter
// or "for x in &mut col" and col implements a method called iter_mut
make::expr_method_call(
expr_behind_ref,
make::name_ref(method.as_str()),
make::arg_list([]),
)
} else if let ast::Expr::RangeExpr(..) = iterable {
// range expressions need to be parenthesized for the syntax to be correct
make::expr_paren(iterable)
} else if impls_core_iter(&ctx.sema, &iterable) {
// We can't use `iterable` if its immutable, so we create a tmp mutable variable
let mut_expr = make::let_stmt(
make::ident_pat(false, true, make::name("tmp")).into(),
None,
Some(iterable),
);
builder.insert(
for_loop.syntax().text_range().start(),
mut_expr.reset_indent().to_string(),
);
builder.insert(
for_loop.syntax().text_range().start(),
make::tokens::whitespace(
format!("\n{}", IndentLevel::from_node(for_loop.syntax())).as_str(),
)
.to_string(),
);
make::expr_path(make::ext::ident_path("tmp"))
} else if let ast::Expr::RefExpr(_) = iterable {
make::expr_method_call(iterable, make::name_ref("into_iter"), make::arg_list([]))
} else {
make::expr_method_call(iterable, make::name_ref("into_iter"), make::arg_list([]))
};

let opt_pat = make::tuple_struct_pat(make::ext::ident_path("Some"), [pat]);
let iter_next_expr =
make::expr_method_call(iter, make::name_ref("next"), make::arg_list([]));
let cond = make::expr_let(opt_pat.into(), iter_next_expr);

let expr_for_loop = make::expr_while_loop(cond.into(), body);
builder.replace(for_loop.syntax().text_range(), expr_for_loop.to_string())
},
)
}

/// If iterable is a reference where the expression behind the reference implements a method
/// returning an Iterator called iter or iter_mut (depending on the type of reference) then return
/// the expression behind the reference and the method name
fn is_ref_and_impls_iter_method(
sema: &hir::Semantics<'_, ide_db::RootDatabase>,
iterable: &ast::Expr,
) -> Option<(ast::Expr, hir::Name)> {
let ref_expr = match iterable {
ast::Expr::RefExpr(r) => r,
_ => return None,
};
let wanted_method = Name::new_symbol_root(if ref_expr.mut_token().is_some() {
sym::iter_mut.clone()
} else {
sym::iter.clone()
});
let expr_behind_ref = ref_expr.expr()?;
let ty = sema.type_of_expr(&expr_behind_ref)?.adjusted();
let scope = sema.scope(iterable.syntax())?;
let krate = scope.krate();
let iter_trait = FamousDefs(sema, krate).core_iter_Iterator()?;

let has_wanted_method = ty
.iterate_method_candidates(sema.db, &scope, None, Some(&wanted_method), |func| {
if func.ret_type(sema.db).impls_trait(sema.db, iter_trait, &[]) {
return Some(());
}
None
})
.is_some();
if !has_wanted_method {
return None;
}

Some((expr_behind_ref, wanted_method))
}

/// Whether iterable implements core::Iterator
fn impls_core_iter(sema: &hir::Semantics<'_, ide_db::RootDatabase>, iterable: &ast::Expr) -> bool {
(|| {
let it_typ = sema.type_of_expr(iterable)?.adjusted();

let module = sema.scope(iterable.syntax())?.module();

let krate = module.krate();
let iter_trait = FamousDefs(sema, krate).core_iter_Iterator()?;
cov_mark::hit!(test_already_impls_iterator);
Some(it_typ.impls_trait(sema.db, iter_trait, &[]))
})()
.unwrap_or(false)
}

#[cfg(test)]
mod tests {
use crate::tests::{check_assist, check_assist_not_applicable};

use super::*;

#[test]
fn each_to_for_simple_for() {
check_assist(
convert_for_loop_to_while_let,
r"
fn main() {
let x = vec![1, 2, 3];
for $0v in x {
v *= 2;
};
}",
r"
fn main() {
let x = vec![1, 2, 3];
while let Some(v) = x.into_iter().next() {
v *= 2;
};
}",
)
}

#[test]
fn each_to_for_for_in_range() {
check_assist(
convert_for_loop_to_while_let,
r#"
//- minicore: range, iterators
impl<T> core::iter::Iterator for core::ops::Range<T> {
type Item = T;

fn next(&mut self) -> Option<Self::Item> {
None
}
}

fn main() {
for $0x in 0..92 {
print!("{}", x);
}
}"#,
r#"
impl<T> core::iter::Iterator for core::ops::Range<T> {
type Item = T;

fn next(&mut self) -> Option<Self::Item> {
None
}
}

fn main() {
while let Some(x) = (0..92).next() {
print!("{}", x);
}
}"#,
)
}

#[test]
fn each_to_for_not_available_in_body() {
cov_mark::check!(not_available_in_body);
check_assist_not_applicable(
convert_for_loop_to_while_let,
r"
fn main() {
let x = vec![1, 2, 3];
for v in x {
$0v *= 2;
}
}",
)
}

#[test]
fn each_to_for_for_borrowed() {
check_assist(
convert_for_loop_to_while_let,
r#"
//- minicore: iterators
use core::iter::{Repeat, repeat};

struct S;
impl S {
fn iter(&self) -> Repeat<i32> { repeat(92) }
fn iter_mut(&mut self) -> Repeat<i32> { repeat(92) }
}

fn main() {
let x = S;
for $0v in &x {
let a = v * 2;
}
}
"#,
r#"
use core::iter::{Repeat, repeat};

struct S;
impl S {
fn iter(&self) -> Repeat<i32> { repeat(92) }
fn iter_mut(&mut self) -> Repeat<i32> { repeat(92) }
}

fn main() {
let x = S;
while let Some(v) = x.iter().next() {
let a = v * 2;
}
}
"#,
)
}

#[test]
fn each_to_for_for_borrowed_no_iter_method() {
check_assist(
convert_for_loop_to_while_let,
r"
struct NoIterMethod;
fn main() {
let x = NoIterMethod;
for $0v in &x {
let a = v * 2;
}
}
",
r"
struct NoIterMethod;
fn main() {
let x = NoIterMethod;
while let Some(v) = &x.into_iter().next() {
let a = v * 2;
}
}
",
)
}

#[test]
fn each_to_for_for_borrowed_mut() {
check_assist(
convert_for_loop_to_while_let,
r#"
//- minicore: iterators
use core::iter::{Repeat, repeat};

struct S;
impl S {
fn iter(&self) -> Repeat<i32> { repeat(92) }
fn iter_mut(&mut self) -> Repeat<i32> { repeat(92) }
}

fn main() {
let x = S;
for $0v in &mut x {
let a = v * 2;
}
}
"#,
r#"
use core::iter::{Repeat, repeat};

struct S;
impl S {
fn iter(&self) -> Repeat<i32> { repeat(92) }
fn iter_mut(&mut self) -> Repeat<i32> { repeat(92) }
}

fn main() {
let x = S;
while let Some(v) = x.iter_mut().next() {
let a = v * 2;
}
}
"#,
)
}

#[test]
fn each_to_for_for_borrowed_mut_behind_var() {
check_assist(
convert_for_loop_to_while_let,
r"
fn main() {
let x = vec![1, 2, 3];
let y = &mut x;
for $0v in y {
*v *= 2;
}
}",
r"
fn main() {
let x = vec![1, 2, 3];
let y = &mut x;
while let Some(v) = y.into_iter().next() {
*v *= 2;
}
}",
)
}

#[test]
fn each_to_for_already_impls_iterator() {
cov_mark::check!(test_already_impls_iterator);
check_assist(
convert_for_loop_to_while_let,
r#"
//- minicore: iterators
fn main() {
for$0 a in core::iter::repeat(92).take(1) {
println!("{}", a);
}
}
"#,
r#"
fn main() {
let mut tmp = core::iter::repeat(92).take(1);
while let Some(a) = tmp.next() {
println!("{}", a);
}
}
"#,
);
}
}
2 changes: 2 additions & 0 deletions crates/ide-assists/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ mod handlers {
mod convert_closure_to_fn;
mod convert_comment_block;
mod convert_comment_from_or_to_doc;
mod convert_for_to_while_let;
mod convert_from_to_tryfrom;
mod convert_integer_literal;
mod convert_into_to_from;
Expand Down Expand Up @@ -252,6 +253,7 @@ mod handlers {
convert_closure_to_fn::convert_closure_to_fn,
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,
convert_from_to_tryfrom::convert_from_to_tryfrom,
convert_integer_literal::convert_integer_literal,
convert_into_to_from::convert_into_to_from,
Expand Down
Loading
Loading