--- /dev/null
+use hir::AsName;
+use syntax::{
+ ast::{self, edit::AstNodeEdit, make},
+ AstNode,
+};
+use test_utils::mark;
+
+use crate::{
+ assist_context::{AssistContext, Assists},
+ AssistId, AssistKind,
+};
+
+// Assist: extract_assignment
+//
+// Extracts variable assigment to outside an if or match statement.
+//
+// ```
+// fn main() {
+// let mut foo = 6;
+//
+// if true {
+// <|>foo = 5;
+// } else {
+// foo = 4;
+// }
+// }
+// ```
+// ->
+// ```
+// fn main() {
+// let mut foo = 6;
+//
+// foo = if true {
+// 5
+// } else {
+// 4
+// };
+// }
+// ```
+pub(crate) fn extract_assigment(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
+ let name = ctx.find_node_at_offset::<ast::NameRef>()?.as_name();
+
+ let (old_stmt, new_stmt) = if let Some(if_expr) = ctx.find_node_at_offset::<ast::IfExpr>() {
+ (
+ ast::Expr::cast(if_expr.syntax().to_owned())?,
+ exprify_if(&if_expr, &name)?.indent(if_expr.indent_level()),
+ )
+ } else if let Some(match_expr) = ctx.find_node_at_offset::<ast::MatchExpr>() {
+ (ast::Expr::cast(match_expr.syntax().to_owned())?, exprify_match(&match_expr, &name)?)
+ } else {
+ return None;
+ };
+
+ let expr_stmt = make::expr_stmt(new_stmt);
+
+ acc.add(
+ AssistId("extract_assignment", AssistKind::RefactorExtract),
+ "Extract assignment",
+ old_stmt.syntax().text_range(),
+ move |edit| {
+ edit.replace(old_stmt.syntax().text_range(), format!("{} = {};", name, expr_stmt));
+ },
+ )
+}
+
+fn exprify_match(match_expr: &ast::MatchExpr, name: &hir::Name) -> Option<ast::Expr> {
+ let new_arm_list = match_expr
+ .match_arm_list()?
+ .arms()
+ .map(|arm| {
+ if let ast::Expr::BlockExpr(block) = arm.expr()? {
+ let new_block = exprify_block(&block, name)?.indent(block.indent_level());
+ Some(arm.replace_descendant(block, new_block))
+ } else {
+ None
+ }
+ })
+ .collect::<Option<Vec<_>>>()?;
+ let new_arm_list = match_expr
+ .match_arm_list()?
+ .replace_descendants(match_expr.match_arm_list()?.arms().zip(new_arm_list));
+ Some(make::expr_match(match_expr.expr()?, new_arm_list))
+}
+
+fn exprify_if(statement: &ast::IfExpr, name: &hir::Name) -> Option<ast::Expr> {
+ let then_branch = exprify_block(&statement.then_branch()?, name)?;
+ let else_branch = match statement.else_branch()? {
+ ast::ElseBranch::Block(ref block) => ast::ElseBranch::Block(exprify_block(block, name)?),
+ ast::ElseBranch::IfExpr(expr) => {
+ mark::hit!(test_extract_assigment_chained_if);
+ ast::ElseBranch::IfExpr(ast::IfExpr::cast(
+ exprify_if(&expr, name)?.syntax().to_owned(),
+ )?)
+ }
+ };
+ Some(make::expr_if(statement.condition()?, then_branch, Some(else_branch)))
+}
+
+fn exprify_block(block: &ast::BlockExpr, name: &hir::Name) -> Option<ast::BlockExpr> {
+ if block.expr().is_some() {
+ return None;
+ }
+
+ let mut stmts: Vec<_> = block.statements().collect();
+ let stmt = stmts.pop()?;
+
+ if let ast::Stmt::ExprStmt(stmt) = stmt {
+ if let ast::Expr::BinExpr(expr) = stmt.expr()? {
+ if expr.op_kind()? == ast::BinOp::Assignment
+ && &expr.lhs()?.name_ref()?.as_name() == name
+ {
+ // The last statement in the block is an assignment to the name we want
+ return Some(make::block_expr(stmts, Some(expr.rhs()?)));
+ }
+ }
+ }
+ None
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ use crate::tests::{check_assist, check_assist_not_applicable};
+
+ #[test]
+ fn test_extract_assignment_if() {
+ check_assist(
+ extract_assigment,
+ r#"
+fn foo() {
+ let mut a = 1;
+
+ if true {
+ <|>a = 2;
+ } else {
+ a = 3;
+ }
+}"#,
+ r#"
+fn foo() {
+ let mut a = 1;
+
+ a = if true {
+ 2
+ } else {
+ 3
+ };
+}"#,
+ );
+ }
+
+ #[test]
+ fn test_extract_assignment_match() {
+ check_assist(
+ extract_assigment,
+ r#"
+fn foo() {
+ let mut a = 1;
+
+ match 1 {
+ 1 => {
+ <|>a = 2;
+ },
+ 2 => {
+ a = 3;
+ },
+ 3 => {
+ a = 4;
+ }
+ }
+}"#,
+ r#"
+fn foo() {
+ let mut a = 1;
+
+ a = match 1 {
+ 1 => {
+ 2
+ },
+ 2 => {
+ 3
+ },
+ 3 => {
+ 4
+ }
+ };
+}"#,
+ );
+ }
+
+ #[test]
+ fn test_extract_assignment_not_last_not_applicable() {
+ check_assist_not_applicable(
+ extract_assigment,
+ r#"
+fn foo() {
+ let mut a = 1;
+
+ if true {
+ <|>a = 2;
+ b = a;
+ } else {
+ a = 3;
+ }
+}"#,
+ )
+ }
+
+ #[test]
+ fn test_extract_assignment_chained_if() {
+ mark::check!(test_extract_assigment_chained_if);
+ check_assist(
+ extract_assigment,
+ r#"
+fn foo() {
+ let mut a = 1;
+
+ if true {
+ <|>a = 2;
+ } else if false {
+ a = 3;
+ } else {
+ a = 4;
+ }
+}"#,
+ r#"
+fn foo() {
+ let mut a = 1;
+
+ a = if true {
+ 2
+ } else if false {
+ 3
+ } else {
+ 4
+ };
+}"#,
+ );
+ }
+
+ #[test]
+ fn test_extract_assigment_retains_stmts() {
+ check_assist(
+ extract_assigment,
+ r#"
+fn foo() {
+ let mut a = 1;
+
+ if true {
+ let b = 2;
+ <|>a = 2;
+ } else {
+ let b = 3;
+ a = 3;
+ }
+}"#,
+ r#"
+fn foo() {
+ let mut a = 1;
+
+ a = if true {
+ let b = 2;
+ 2
+ } else {
+ let b = 3;
+ 3
+ };
+}"#,
+ )
+ }
+
+ #[test]
+ fn extract_assignment_let_stmt_not_applicable() {
+ check_assist_not_applicable(
+ extract_assigment,
+ r#"
+fn foo() {
+ let mut a = 1;
+
+ let b = if true {
+ <|>a = 2
+ } else {
+ a = 3
+ };
+}"#,
+ )
+ }
+
+ #[test]
+ fn extract_assignment_if_missing_assigment_not_applicable() {
+ check_assist_not_applicable(
+ extract_assigment,
+ r#"
+fn foo() {
+ let mut a = 1;
+
+ if true {
+ <|>a = 2;
+ } else {}
+}"#,
+ )
+ }
+
+ #[test]
+ fn extract_assignment_match_missing_assigment_not_applicable() {
+ check_assist_not_applicable(
+ extract_assigment,
+ r#"
+fn foo() {
+ let mut a = 1;
+
+ match 1 {
+ 1 => {
+ <|>a = 2;
+ },
+ 2 => {
+ a = 3;
+ },
+ 3 => {},
+ }
+}"#,
+ )
+ }
+}
mod convert_integer_literal;
mod early_return;
mod expand_glob_import;
+ mod extract_assignment;
mod extract_module_to_file;
mod extract_struct_from_enum_variant;
mod extract_variable;
convert_integer_literal::convert_integer_literal,
early_return::convert_to_guarded_return,
expand_glob_import::expand_glob_import,
+ extract_assignment::extract_assigment,
extract_module_to_file::extract_module_to_file,
extract_struct_from_enum_variant::extract_struct_from_enum_variant,
extract_variable::extract_variable,
)
}
+#[test]
+fn doctest_extract_assignment() {
+ check_doc_test(
+ "extract_assignment",
+ r#####"
+fn main() {
+ let mut foo = 6;
+
+ if true {
+ <|>foo = 5;
+ } else {
+ foo = 4;
+ }
+}
+"#####,
+ r#####"
+fn main() {
+ let mut foo = 6;
+
+ foo = if true {
+ 5
+ } else {
+ 4
+ };
+}
+"#####,
+ )
+}
+
#[test]
fn doctest_extract_module_to_file() {
check_doc_test(
ast::AssocItem::MacroCall(_) => None,
}
.is_some()
- };
+ }
items
.iter()
}
pub fn docs(&self) -> Option<Documentation> {
- let docs = self
- .by_key("doc")
- .attrs()
- .flat_map(|attr| match attr.input.as_ref()? {
- AttrInput::Literal(s) => Some(s),
- AttrInput::TokenTree(_) => None,
- })
- .intersperse(&SmolStr::new_inline("\n"))
+ let docs = self.by_key("doc").attrs().flat_map(|attr| match attr.input.as_ref()? {
+ AttrInput::Literal(s) => Some(s),
+ AttrInput::TokenTree(_) => None,
+ });
+ // FIXME: Replace `Itertools::intersperse` with `Iterator::intersperse[_with]` until the
+ // libstd api gets stabilized (https://github.com/rust-lang/rust/issues/79524).
+ let docs = Itertools::intersperse(docs, &SmolStr::new_inline("\n"))
.map(|it| it.as_str())
.collect::<String>();
if docs.is_empty() {
// We follow what it did anyway :)
if segments.len() == 1 && kind == PathKind::Plain {
if let Some(_macro_call) = path.syntax().parent().and_then(ast::MacroCall::cast) {
- if let Some(crate_id) = hygiene.local_inner_macros() {
+ if let Some(crate_id) = hygiene.local_inner_macros(path) {
kind = PathKind::DollarCrate(crate_id);
}
}
TRY_EXPR => FragmentKind::Expr,
TUPLE_EXPR => FragmentKind::Expr,
PAREN_EXPR => FragmentKind::Expr,
-
+ ARRAY_EXPR => FragmentKind::Expr,
FOR_EXPR => FragmentKind::Expr,
PATH_EXPR => FragmentKind::Expr,
CLOSURE_EXPR => FragmentKind::Expr,
//!
//! Specifically, `ast` + `Hygiene` allows you to create a `Name`. Note that, at
//! this moment, this is horribly incomplete and handles only `$crate`.
+use std::sync::Arc;
+
+use arena::{Arena, Idx};
use base_db::CrateId;
use either::Either;
-use syntax::ast;
+use mbe::Origin;
+use syntax::{ast, AstNode};
use crate::{
db::AstDatabase,
name::{AsName, Name},
- HirFileId, HirFileIdRepr, MacroCallId, MacroDefKind,
+ ExpansionInfo, HirFileId, HirFileIdRepr, MacroCallId, MacroDefKind,
};
#[derive(Clone, Debug)]
pub struct Hygiene {
- // This is what `$crate` expands to
- def_crate: Option<CrateId>,
+ frames: Option<Arc<HygieneFrames>>,
+}
+
+impl Hygiene {
+ pub fn new(db: &dyn AstDatabase, file_id: HirFileId) -> Hygiene {
+ Hygiene { frames: Some(Arc::new(HygieneFrames::new(db, file_id.clone()))) }
+ }
+
+ pub fn new_unhygienic() -> Hygiene {
+ Hygiene { frames: None }
+ }
+
+ // FIXME: this should just return name
+ pub fn name_ref_to_name(&self, name_ref: ast::NameRef) -> Either<Name, CrateId> {
+ if let Some(frames) = &self.frames {
+ if name_ref.text() == "$crate" {
+ if let Some(krate) = frames.root_crate(&name_ref) {
+ return Either::Right(krate);
+ }
+ }
+ }
+
+ Either::Left(name_ref.as_name())
+ }
+
+ pub fn local_inner_macros(&self, path: ast::Path) -> Option<CrateId> {
+ let frames = self.frames.as_ref()?;
+
+ let mut token = path.syntax().first_token()?;
+ let mut current = frames.first();
+
+ while let Some((frame, data)) =
+ current.and_then(|it| Some((it, it.expansion.as_ref()?.map_token_up(&token)?)))
+ {
+ let (mapped, origin) = data;
+ if origin == Origin::Def {
+ return if frame.local_inner { frame.krate } else { None };
+ }
+ current = Some(&frames.0[frame.call_site?]);
+ token = mapped.value;
+ }
+ None
+ }
+}
+
+#[derive(Default, Debug)]
+struct HygieneFrames(Arena<HygieneFrame>);
+
+#[derive(Clone, Debug)]
+struct HygieneFrame {
+ expansion: Option<ExpansionInfo>,
// Indicate this is a local inner macro
local_inner: bool,
+ krate: Option<CrateId>,
+
+ call_site: Option<Idx<HygieneFrame>>,
+ def_site: Option<Idx<HygieneFrame>>,
}
-impl Hygiene {
- pub fn new(db: &dyn AstDatabase, file_id: HirFileId) -> Hygiene {
- let (def_crate, local_inner) = match file_id.0 {
+impl HygieneFrames {
+ fn new(db: &dyn AstDatabase, file_id: HirFileId) -> Self {
+ let mut frames = HygieneFrames::default();
+ frames.add(db, file_id);
+ frames
+ }
+
+ fn add(&mut self, db: &dyn AstDatabase, file_id: HirFileId) -> Option<Idx<HygieneFrame>> {
+ let (krate, local_inner) = match file_id.0 {
HirFileIdRepr::FileId(_) => (None, false),
HirFileIdRepr::MacroFile(macro_file) => match macro_file.macro_call_id {
+ MacroCallId::EagerMacro(_id) => (None, false),
MacroCallId::LazyMacro(id) => {
let loc = db.lookup_intern_macro(id);
match loc.def.kind {
MacroDefKind::ProcMacro(_) => (None, false),
}
}
- MacroCallId::EagerMacro(_id) => (None, false),
},
};
- Hygiene { def_crate, local_inner }
- }
- pub fn new_unhygienic() -> Hygiene {
- Hygiene { def_crate: None, local_inner: false }
+ let expansion = file_id.expansion_info(db);
+ let expansion = match expansion {
+ None => {
+ return Some(self.0.alloc(HygieneFrame {
+ expansion: None,
+ local_inner,
+ krate,
+ call_site: None,
+ def_site: None,
+ }));
+ }
+ Some(it) => it,
+ };
+
+ let def_site = expansion.def.clone();
+ let call_site = expansion.arg.file_id;
+ let idx = self.0.alloc(HygieneFrame {
+ expansion: Some(expansion),
+ local_inner,
+ krate,
+ call_site: None,
+ def_site: None,
+ });
+
+ self.0[idx].call_site = self.add(db, call_site);
+ self.0[idx].def_site = def_site.and_then(|it| self.add(db, it.file_id));
+
+ Some(idx)
}
- // FIXME: this should just return name
- pub fn name_ref_to_name(&self, name_ref: ast::NameRef) -> Either<Name, CrateId> {
- if let Some(def_crate) = self.def_crate {
- if name_ref.text() == "$crate" {
- return Either::Right(def_crate);
- }
- }
- Either::Left(name_ref.as_name())
+ fn first(&self) -> Option<&HygieneFrame> {
+ self.0.iter().next().map(|it| it.1)
}
- pub fn local_inner_macros(&self) -> Option<CrateId> {
- if self.local_inner {
- self.def_crate
- } else {
- None
+ fn root_crate(&self, name_ref: &ast::NameRef) -> Option<CrateId> {
+ let mut token = name_ref.syntax().first_token()?;
+ let first = self.first()?;
+ let mut result = first.krate;
+ let mut current = Some(first);
+
+ while let Some((frame, (mapped, origin))) =
+ current.and_then(|it| Some((it, it.expansion.as_ref()?.map_token_up(&token)?)))
+ {
+ result = frame.krate;
+
+ let site = match origin {
+ Origin::Def => frame.def_site,
+ Origin::Call => frame.call_site,
+ };
+
+ let site = match site {
+ None => break,
+ Some(it) => it,
+ };
+
+ current = Some(&self.0[site]);
+ token = mapped.value;
}
+
+ result
}
}
Some(self.expanded.with_value(token))
}
- pub fn map_token_up(
- &self,
- token: InFile<&SyntaxToken>,
- ) -> Option<(InFile<SyntaxToken>, Origin)> {
- let token_id = self.exp_map.token_by_range(token.value.text_range())?;
+ pub fn map_token_up(&self, token: &SyntaxToken) -> Option<(InFile<SyntaxToken>, Origin)> {
+ let token_id = self.exp_map.token_by_range(token.text_range())?;
let (token_id, origin) = self.macro_def.0.map_id_up(token_id);
let (token_map, tt) = match origin {
),
};
- let range = token_map.range_by_token(token_id)?.by_kind(token.value.kind())?;
+ let range = token_map.range_by_token(token_id)?.by_kind(token.kind())?;
let token = algo::find_covering_element(&tt.value, range + tt.value.text_range().start())
.into_token()?;
Some((tt.with_value(token), origin))
expansion: &ExpansionInfo,
token: InFile<SyntaxToken>,
) -> Option<InFile<SyntaxToken>> {
- let (mapped, origin) = expansion.map_token_up(token.as_ref())?;
+ let (mapped, origin) = expansion.map_token_up(&token.value)?;
if origin != Origin::Call {
return None;
}
);
}
+#[test]
+fn infer_macro_with_dollar_crate_in_def_site() {
+ check_types(
+ r#"
+//- /main.rs crate:main deps:foo
+use foo::expand;
+
+macro_rules! list {
+ ($($tt:tt)*) => { $($tt)* }
+}
+
+fn test() {
+ let r = expand!();
+ r;
+ //^ u128
+}
+
+//- /lib.rs crate:foo
+#[macro_export]
+macro_rules! expand {
+ () => { list!($crate::m!()) };
+}
+
+#[macro_export]
+macro_rules! m {
+ () => { 0u128 };
+}
+"#,
+ );
+}
+
#[test]
fn infer_type_value_non_legacy_macro_use_as() {
check_infer(
);
}
+#[test]
+fn infer_array_macro_call() {
+ check_infer(
+ r#"
+ macro_rules! bar { () => {0u32} }
+ fn test() {
+ let a = [bar!()];
+ }
+ "#,
+ expect![[r#"
+ !0..4 '0u32': u32
+ 44..69 '{ ...()]; }': ()
+ 54..55 'a': [u32; _]
+ 58..66 '[bar!()]': [u32; _]
+ "#]],
+ );
+}
+
#[test]
fn bug_1030() {
check_infer(
res.add_err(err!("leftover tokens"));
}
}
- Op::Var { name, kind } => {
+ Op::Var { name, kind, .. } => {
let kind = match kind {
Some(k) => k,
None => {
err = err.or(e);
arena.push(tt.into());
}
- Op::Var { name, .. } => {
- let ExpandResult { value: fragment, err: e } = expand_var(ctx, &name);
+ Op::Var { name, id, .. } => {
+ let ExpandResult { value: fragment, err: e } = expand_var(ctx, &name, *id);
err = err.or(e);
push_fragment(arena, fragment);
}
ExpandResult { value: tt::Subtree { delimiter: template.delimiter, token_trees: tts }, err }
}
-fn expand_var(ctx: &mut ExpandCtx, v: &SmolStr) -> ExpandResult<Fragment> {
+fn expand_var(ctx: &mut ExpandCtx, v: &SmolStr, id: tt::TokenId) -> ExpandResult<Fragment> {
if v == "crate" {
// We simply produce identifier `$crate` here. And it will be resolved when lowering ast to Path.
- let tt =
- tt::Leaf::from(tt::Ident { text: "$crate".into(), id: tt::TokenId::unspecified() })
- .into();
+ let tt = tt::Leaf::from(tt::Ident { text: "$crate".into(), id }).into();
ExpandResult::ok(Fragment::Tokens(tt))
} else if !ctx.bindings.contains(v) {
// Note that it is possible to have a `$var` inside a macro which is not bound.
let tt = tt::Subtree {
delimiter: None,
token_trees: vec![
- tt::Leaf::from(tt::Punct {
- char: '$',
- spacing: tt::Spacing::Alone,
- id: tt::TokenId::unspecified(),
- })
- .into(),
- tt::Leaf::from(tt::Ident { text: v.clone(), id: tt::TokenId::unspecified() })
- .into(),
+ tt::Leaf::from(tt::Punct { char: '$', spacing: tt::Spacing::Alone, id }).into(),
+ tt::Leaf::from(tt::Ident { text: v.clone(), id }).into(),
],
}
.into();
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum Op {
- Var { name: SmolStr, kind: Option<SmolStr> },
+ Var { name: SmolStr, kind: Option<SmolStr>, id: tt::TokenId },
Repeat { subtree: MetaTemplate, kind: RepeatKind, separator: Option<Separator> },
Leaf(tt::Leaf),
Subtree(MetaTemplate),
}
let name = UNDERSCORE.clone();
let kind = eat_fragment_kind(src, mode)?;
- Op::Var { name, kind }
+ let id = punct.id;
+ Op::Var { name, kind, id }
}
tt::Leaf::Ident(ident) => {
let name = ident.text.clone();
let kind = eat_fragment_kind(src, mode)?;
- Op::Var { name, kind }
+ let id = ident.id;
+ Op::Var { name, kind, id }
}
tt::Leaf::Literal(lit) => {
if is_boolean_literal(&lit) {
let name = lit.text.clone();
let kind = eat_fragment_kind(src, mode)?;
- Op::Var { name, kind }
+ let id = lit.id;
+ Op::Var { name, kind, id }
} else {
bail!("bad var 2");
}
for ((f1, ..), (f2, ..)) in fields.iter().zip(&fields[1..]) {
fn key(f: &str) -> &str {
f.splitn(2, "_").next().unwrap()
- };
+ }
assert!(key(f1) <= key(f2), "wrong field order: {:?} {:?}", f1, f2);
}
InsertPosition::After($anchor.syntax().clone().into())
}
};
- };
+ }
let position = match position {
InsertPosition::First => after_l_curly!(),
InsertPosition::After($anchor.syntax().clone().into())
}
};
- };
+ }
let position = match self.generic_params().last() {
Some(it) => after_field!(it),
env::set_var("CC", "clang");
}
- let toolchain = toolchain(&target);
- cmd!("cargo +{toolchain} build --manifest-path ./crates/rust-analyzer/Cargo.toml --bin rust-analyzer --target {target} --release").run()?;
+ cmd!("cargo build --manifest-path ./crates/rust-analyzer/Cargo.toml --bin rust-analyzer --target {target} --release").run()?;
let suffix = exe_suffix(&target);
let src =
}
}
-fn toolchain(target: &str) -> String {
- match target {
- "aarch64-apple-darwin" => "beta".to_string(),
- _ => "stable".to_string(),
- }
-}
-
fn gzip(src_path: &Path, dest_path: &Path) -> Result<()> {
let mut encoder = GzEncoder::new(File::create(dest_path)?, Compression::best());
let mut input = io::BufReader::new(File::open(src_path)?);