Something went wrong. Try again.
AST-based search and replace to refactor to new `Playground` impl
Something went wrong. Try again.
16 kB · 394 lines
Rust
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395use itertools::Itertools;use ra_ap_syntax::{ AstNode, AstToken, NodeOrToken, SyntaxKind, SyntaxToken, T, ast::{ self, HasArgList, HasAttrs, edit::{AstNodeEdit, IndentLevel}, }, syntax_editor::{Element, Position, SyntaxEditor},};
use crate::{ meta::PlaygroundMeta, method_call::{method_call_stmt, method_named, receiver_matches_name},};
pub fn replace_playground_setup_with_inner( editor: &SyntaxEditor, playground_meta: &PlaygroundMeta, body: &ast::StmtList,) { let body = normalize_multiline_string_literals(body.dedent(IndentLevel::from(1)));
// collect all nodes and tokens let replacements = body.syntax().children_with_tokens().collect::<Vec<_>>();
// filter out opening and closing bracket let [opening, replacements @ .., closing] = replacements.as_slice() else { unreachable!("closures always have an opening and closing bracket"); }; assert_eq!(opening.kind(), T!['{']); assert_eq!(closing.kind(), T!['}']);
// filter out starting whitespace let mut replacements = replacements; while let [start, rest @ ..] = replacements { if start.kind() != SyntaxKind::WHITESPACE { break; }
replacements = rest; }
// filter out closing whitespace let mut replacements = replacements; while let [rest @ .., closing] = replacements { if closing.kind() != SyntaxKind::WHITESPACE { break; }
replacements = rest; }
let replacements = replacements .iter() .map(|node_or_token| node_or_token.syntax_element()) .collect(); editor.replace_with_many(playground_meta.setup.syntax(), replacements);}
fn normalize_multiline_string_literals(body: ast::StmtList) -> ast::StmtList { let (editor, body) = SyntaxEditor::with_ast_node(&body);
for token in body .syntax() .descendants_with_tokens() .filter_map(|it| it.into_token()) { if !matches!( token.kind(), SyntaxKind::STRING | SyntaxKind::BYTE_STRING | SyntaxKind::C_STRING ) { continue; }
let text = token.text(); let Some(normalized) = normalize_multiline_string_literal(text, &token) else { continue; };
if normalized != text { let new_token = ast::make::tokens::literal(&normalized); editor.replace(&token, &new_token); } }
ast::StmtList::cast(editor.finish().new_root().clone()).unwrap()}
fn normalize_multiline_string_literal(text: &str, token: &SyntaxToken) -> Option<String> { if !text.contains('\n') { return None; }
let opening_quote = text.find('"')?; let closing_quote = text.rfind('"')?; if opening_quote == closing_quote { return None; }
let indent = IndentLevel::from_token(token); let base_indent = indent.to_string(); let content_indent = (indent + 1).to_string(); let prefix = &text[..=opening_quote]; let content = &text[opening_quote + 1..closing_quote]; let suffix = &text[closing_quote..];
let lines = content.split('\n').collect::<Vec<_>>(); let common_indent = lines .iter() .skip(1) .filter(|line| !line.trim().is_empty()) .map(|line| leading_whitespace_len(line)) .min()?; let last = lines.len().saturating_sub(1); let normalized = lines .iter() .enumerate() .map(|(index, line)| { if index == 0 { return line.to_string(); }
if index == last && line.trim().is_empty() { base_indent.clone() } else { let remove = leading_whitespace_len(line).min(common_indent); format!("{content_indent}{}", &line[remove..]) } }) .collect::<Vec<_>>() .join("\n");
Some(format!("{prefix}{normalized}{suffix}"))}
fn leading_whitespace_len(text: &str) -> usize { text.char_indices() .find_map(|(index, ch)| (!ch.is_whitespace()).then_some(index)) .unwrap_or(text.len())}
pub fn update_playground_body(playground_meta: &PlaygroundMeta) -> ast::StmtList { let (editor, body) = SyntaxEditor::with_ast_node(&playground_meta.body); let factory = editor.make();
let playground_expr = factory.expr_path(factory.path_from_text("playground"));
if let Some(dirs) = &playground_meta.args.dirs { let method_calls: Vec<_> = body .syntax() .descendants() .flat_map(ast::MethodCallExpr::cast) .filter(|expr| receiver_matches_name(expr, dirs).unwrap_or(false)) .collect();
// handle `dirs.test()` -> `playground.path()` let path_name_ref = factory.name_ref("path"); let path_arg_list = factory.arg_list([]); let playground_path_method_call_expr = factory.expr_method_call(playground_expr.clone(), path_name_ref, path_arg_list); method_calls .iter() .filter(method_named("test")) .for_each(|expr| { editor.replace(expr.syntax(), playground_path_method_call_expr.syntax()) }) }
if let Some(playground) = &playground_meta.args.playground { let method_call_exprs: Vec<_> = body .syntax() .descendants() .flat_map(ast::MethodCallExpr::cast) .filter(|method_call_expr| { receiver_matches_name(method_call_expr, playground).unwrap_or(false) }) .collect();
let method_call_stmts: Vec<_> = method_call_exprs .iter() .flat_map(method_call_stmt) .collect();
// handle `playground.mkdir()` -> `playground.dir()` method_call_stmts .iter() .filter(method_named("mkdir")) .for_each(|method_call_stmt| { let dir_name_ref = factory.name_ref("dir"); let dir_expr = factory.expr_method_call( playground_expr.clone(), dir_name_ref, method_call_stmt.method_call_expr.arg_list().unwrap(), ); let dir_stmt = factory.expr_stmt(dir_expr.into()); editor.replace(method_call_stmt.expr_stmt.syntax(), dir_stmt.syntax()); });
// handle `playground.cwd()` -> `playground.path()` method_call_exprs .iter() .filter(method_named("cwd")) .for_each(|method_call_expr| { let path_name_ref = factory.name_ref("path"); let path_expr = factory.expr_method_call( playground_expr.clone(), path_name_ref, method_call_expr.arg_list().unwrap(), ); editor.replace(method_call_expr.syntax(), path_expr.syntax()); });
// handle `playground.symlink()` -> `playground.symlink()` method_call_stmts .iter() .filter(method_named("symlink")) .for_each(|method_call_stmt| { let symlink_name_ref = factory.name_ref("symlink"); let symlink_expr = factory.expr_method_call( playground_expr.clone(), symlink_name_ref, method_call_stmt.method_call_expr.arg_list().unwrap(), ); let symlink_stmt = factory.expr_stmt(symlink_expr.into()); editor.replace(method_call_stmt.expr_stmt.syntax(), symlink_stmt.syntax()); });
// handle `playground.with_files()` -> n × `playground.file()` method_call_stmts .iter() .filter(method_named("with_files")) .for_each(|method_call_stmt| { let arg_expr = method_call_stmt .method_call_expr .arg_list() .unwrap() .args() .next() .unwrap(); let arg_ref_expr = ast::RefExpr::cast(arg_expr.syntax().clone()).unwrap(); let array_expr = ast::ArrayExpr::cast(arg_ref_expr.expr().unwrap().syntax().clone()).unwrap(); let replacements: Vec<_> = array_expr .exprs() .map(|expr| { let call_expr = ast::CallExpr::cast(expr.syntax().clone()).unwrap(); let path_expr = ast::PathExpr::cast(call_expr.expr().unwrap().syntax().clone()) .unwrap(); let path = path_expr.path().unwrap(); let last_path = path.segments().last().unwrap().name_ref().unwrap(); match last_path.text_non_mutable() { "FileWithContent" => { let file_name_ref = factory.name_ref("file"); let expr = factory.expr_method_call( playground_expr.clone(), file_name_ref, call_expr.arg_list().unwrap(), ); factory.expr_stmt(expr.into()).syntax().syntax_element() } "EmptyFile" => { let file_name_ref = factory.name_ref("empty_file"); let expr = factory.expr_method_call( playground_expr.clone(), file_name_ref, call_expr.arg_list().unwrap(), ); factory.expr_stmt(expr.into()).syntax().syntax_element() } "FileWithContentToBeTrimmed" => { let [file_name, file_content] = call_expr .arg_list() .unwrap() .args() .collect_array() .unwrap();
let indoc_path = factory.path_from_idents(["indoc", "indoc"]).unwrap(); let indoc_tt = factory.token_tree( T!['{'], file_content .syntax() .descendants_with_tokens() .filter_map(|it| it.into_token()) .map(NodeOrToken::Token), ); let indoc_macro_expr = factory.expr_macro(indoc_path, indoc_tt);
let file_name_ref = factory.name_ref("file"); let arg_list = factory.arg_list([file_name, indoc_macro_expr.into()]); let expr = factory.expr_method_call( playground_expr.clone(), file_name_ref, arg_list, ); factory.expr_stmt(expr.into()).syntax().syntax_element() } "FileWithPermission" => { let [file_name, is_writable] = call_expr .arg_list() .unwrap() .args() .collect_array() .unwrap();
let is_writable = ast::Literal::cast(is_writable.syntax().clone()).unwrap(); assert_eq!(is_writable.token().text(), "false");
let readonly_file_name_ref = factory.name_ref("readonly_file"); let arg_list = factory.arg_list([file_name]); let expr = factory.expr_method_call( playground_expr.clone(), readonly_file_name_ref, arg_list, ); factory.expr_stmt(expr.into()).syntax().syntax_element() } _ => unimplemented!(), } }) .collect(); editor.replace_with_many( method_call_stmt.expr_stmt.syntax().syntax_element(), replacements .into_iter() .intersperse(method_call_stmt.indent.syntax().syntax_element()) .collect(), ); }); }
ast::StmtList::cast(editor.finish().new_root().clone()).unwrap()}
pub fn insert_playground_into_params(editor: &SyntaxEditor, playground_meta: &PlaygroundMeta) { let param_list = playground_meta.outer.param_list().unwrap(); let name = editor.make().name("playground"); let ident_pat = editor.make().ident_pat(false, false, name); let path = editor.make().path_from_text("Playground"); let ty_path = editor.make().ty_path(path); let mut playground_param = editor.make().param(ident_pat.into(), ty_path.into());
let has_rstest = playground_meta .outer .attrs() .flat_map(|attr| attr.path()?.segments().last()?.name_ref()) .any(|name_ref| name_ref.text_non_mutable() == "rstest"); if has_rstest { let (editor, _) = SyntaxEditor::with_ast_node(&playground_param); let path = ast::make::path_from_text("ignore"); let meta = ast::make::meta_path(path); let attr = editor.make().attr_outer(meta); editor.insert_all( Position::first_child_of(playground_param.syntax()), vec![ attr.syntax().syntax_element(), editor.make().whitespace(" ").syntax_element(), ], ); playground_param = ast::Param::cast(editor.finish().new_root().clone()).unwrap(); }
let leading_whitespace = param_list .syntax() .children_with_tokens() .find_map(|element| ast::Param::cast(element.into_node()?)) .and_then(|param| param.syntax().prev_sibling_or_token()) .and_then(|element| ast::Whitespace::cast(element.into_token()?));
let comma = param_list .params() .next() .map(|_| editor.make().token(T![,]));
let trailing_whitespace = (leading_whitespace.is_none() && comma.is_some()) .then(|| ast::make::tokens::whitespace(" "));
editor.insert_all( Position::after(param_list.l_paren_token().unwrap()), [ leading_whitespace.map(|ws| ws.syntax().syntax_element()), Some(playground_param.syntax().syntax_element()), comma.map(|comma| comma.syntax_element()), trailing_whitespace.map(|ws| ws.syntax_element()), ] .into_iter() .flatten() .collect(), );}