Repository: rust-analyzer/rust-analyzer
Pull request #7147 (2021-01-04 09:57:47)

ssr: Allow replacing expressions with statements
Depends on #6587

Until that is merged, the diff is https://github.com/MarijnS95/rust-analyzer/compare/stmt..replace-expr-with-stmt

---

Now that statements can be matched and replaced (#6587) some usecases require expressions to be replaced with statements as well. This happens when something that can ambiguously be an expression or statement like `if` and loop blocks appear in the last position of a block, as trailing expression. In this case a replacement pattern of the form `if foo(){$a();}==>>$a();` will only substitute `if` blocks in the list of statements but not if they (implicitly) end up in the trailing expression, where they are not wrapped by an EXPR_STMT (but the pattern and template are, as parsing only succeeds for the `stmt ==>> stmt` case).

Instead of adding two rules that match an expression - and emit duplicate matching errors - allow the template for expressions to be a statement if it fails to parse as an expression.

---

Another gross change that does not seem to break any tests currently, but perhaps a safeguard should be added to only allow this kind of replacement in blocks by "pushing" the replacement template to the statement list and clearing the trailing expression?

CC @davidlattimore 


Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 4aaee3ab1d..1aa0c072d4 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -386,11 +386,11 @@ dependencies = [
 
 [[package]]
 name = "expect-test"
-version = "1.1.0"
+version = "1.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2300477aab3a378f2ca00a4fbd4dc713654ab7ed790e4017493cb33656280633"
+checksum = "eab78d4c727fa879de40951ffc01ed781a231b28786b19d6bfbdc0d1ec92eb3e"
 dependencies = [
- "dissimilar",
+ "difference",
  "once_cell",
 ]
 
diff --git a/crates/assists/src/handlers/extract_assignment.rs b/crates/assists/src/handlers/extract_assignment.rs
index ae99598c00..281cf5d24e 100644
--- a/crates/assists/src/handlers/extract_assignment.rs
+++ b/crates/assists/src/handlers/extract_assignment.rs
@@ -1,3 +1,4 @@
+use hir::AsName;
 use syntax::{
     ast::{self, edit::AstNodeEdit, make},
     AstNode,
@@ -37,23 +38,15 @@ use crate::{
 // }
 // ```
 pub(crate) fn extract_assigment(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
-    let assign_expr = ctx.find_node_at_offset::<ast::BinExpr>()?;
-    let name_expr = if assign_expr.op_kind()? == ast::BinOp::Assignment {
-        assign_expr.lhs()?
-    } else {
-        return None;
-    };
+    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, &ctx.sema, &name_expr)?.indent(if_expr.indent_level()),
+            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, &ctx.sema, &name_expr)?,
-        )
+        (ast::Expr::cast(match_expr.syntax().to_owned())?, exprify_match(&match_expr, &name)?)
     } else {
         return None;
     };
@@ -65,22 +58,18 @@ pub(crate) fn extract_assigment(acc: &mut Assists, ctx: &AssistContext) -> Optio
         "Extract assignment",
         old_stmt.syntax().text_range(),
         move |edit| {
-            edit.replace(old_stmt.syntax().text_range(), format!("{} = {};", name_expr, expr_stmt));
+            edit.replace(old_stmt.syntax().text_range(), format!("{} = {};", name, expr_stmt));
         },
     )
 }
 
-fn exprify_match(
-    match_expr: &ast::MatchExpr,
-    sema: &hir::Semantics<ide_db::RootDatabase>,
-    name: &ast::Expr,
-) -> Option<ast::Expr> {
+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, sema, name)?.indent(block.indent_level());
+                let new_block = exprify_block(&block, name)?.indent(block.indent_level());
                 Some(arm.replace_descendant(block, new_block))
             } else {
                 None
@@ -93,31 +82,21 @@ fn exprify_match(
     Some(make::expr_match(match_expr.expr()?, new_arm_list))
 }
 
-fn exprify_if(
-    statement: &ast::IfExpr,
-    sema: &hir::Semantics<ide_db::RootDatabase>,
-    name: &ast::Expr,
-) -> Option<ast::Expr> {
-    let then_branch = exprify_block(&statement.then_branch()?, sema, name)?;
+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, sema, name)?)
-        }
+        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, sema, name)?.syntax().to_owned(),
+                exprify_if(&expr, name)?.syntax().to_owned(),
             )?)
         }
     };
     Some(make::expr_if(statement.condition()?, then_branch, Some(else_branch)))
 }
 
-fn exprify_block(
-    block: &ast::BlockExpr,
-    sema: &hir::Semantics<ide_db::RootDatabase>,
-    name: &ast::Expr,
-) -> Option<ast::BlockExpr> {
+fn exprify_block(block: &ast::BlockExpr, name: &hir::Name) -> Option<ast::BlockExpr> {
     if block.expr().is_some() {
         return None;
     }
@@ -127,7 +106,8 @@ fn exprify_block(
 
     if let ast::Stmt::ExprStmt(stmt) = stmt {
         if let ast::Expr::BinExpr(expr) = stmt.expr()? {
-            if expr.op_kind()? == ast::BinOp::Assignment && is_equivalent(sema, &expr.lhs()?, name)
+            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()?)));
@@ -137,29 +117,6 @@ fn exprify_block(
     None
 }
 
-fn is_equivalent(
-    sema: &hir::Semantics<ide_db::RootDatabase>,
-    expr0: &ast::Expr,
-    expr1: &ast::Expr,
-) -> bool {
-    match (expr0, expr1) {
-        (ast::Expr::FieldExpr(field_expr0), ast::Expr::FieldExpr(field_expr1)) => {
-            mark::hit!(test_extract_assignment_field_assignment);
-            sema.resolve_field(field_expr0) == sema.resolve_field(field_expr1)
-        }
-        (ast::Expr::PathExpr(path0), ast::Expr::PathExpr(path1)) => {
-            let path0 = path0.path();
-            let path1 = path1.path();
-            if let (Some(path0), Some(path1)) = (path0, path1) {
-                sema.resolve_path(&path0) == sema.resolve_path(&path1)
-            } else {
-                false
-            }
-        }
-        _ => false,
-    }
-}
-
 #[cfg(test)]
 mod tests {
     use super::*;
@@ -362,38 +319,6 @@ fn foo() {
         },
         3 => {},
     }
-}"#,
-        )
-    }
-
-    #[test]
-    fn test_extract_assignment_field_assignment() {
-        mark::check!(test_extract_assignment_field_assignment);
-        check_assist(
-            extract_assigment,
-            r#"
-struct A(usize);
-
-fn foo() {
-    let mut a = A(1);
-
-    if true {
-        <|>a.0 = 2;
-    } else {
-        a.0 = 3;
-    }
-}"#,
-            r#"
-struct A(usize);
-
-fn foo() {
-    let mut a = A(1);
-
-    a.0 = if true {
-        2
-    } else {
-        3
-    };
 }"#,
         )
     }
diff --git a/crates/cfg/Cargo.toml b/crates/cfg/Cargo.toml
index 73247d1302..c68e391c1b 100644
--- a/crates/cfg/Cargo.toml
+++ b/crates/cfg/Cargo.toml
@@ -17,4 +17,4 @@ tt = { path = "../tt", version = "0.0.0" }
 [dev-dependencies]
 mbe = { path = "../mbe" }
 syntax = { path = "../syntax" }
-expect-test = "1.1"
+expect-test = "1.0"
diff --git a/crates/completion/Cargo.toml b/crates/completion/Cargo.toml
index 99a1bdd544..78e93e78ea 100644
--- a/crates/completion/Cargo.toml
+++ b/crates/completion/Cargo.toml
@@ -28,4 +28,4 @@ test_utils = { path = "../test_utils", version = "0.0.0" }
 hir = { path = "../hir", version = "0.0.0" }
 
 [dev-dependencies]
-expect-test = "1.1"
+expect-test = "1.0"
diff --git a/crates/hir_def/Cargo.toml b/crates/hir_def/Cargo.toml
index 7ef966cd22..e8b581e2fa 100644
--- a/crates/hir_def/Cargo.toml
+++ b/crates/hir_def/Cargo.toml
@@ -33,4 +33,4 @@ cfg = { path = "../cfg", version = "0.0.0" }
 tt = { path = "../tt", version = "0.0.0" }
 
 [dev-dependencies]
-expect-test = "1.1"
+expect-test = "1.0"
diff --git a/crates/hir_def/src/diagnostics.rs b/crates/hir_def/src/diagnostics.rs
index ab3f059cef..c71266dc09 100644
--- a/crates/hir_def/src/diagnostics.rs
+++ b/crates/hir_def/src/diagnostics.rs
@@ -133,10 +133,6 @@ impl Diagnostic for InactiveCode {
 // This diagnostic is shown when a procedural macro can not be found. This usually means that
 // procedural macro support is simply disabled (and hence is only a weak hint instead of an error),
 // but can also indicate project setup problems.
-//
-// If you are seeing a lot of "proc macro not expanded" warnings, you can add this option to the
-// `rust-analyzer.diagnostics.disabled` list to prevent them from showing. Alternatively you can
-// enable support for procedural macros (see `rust-analyzer.procMacro.enable`).
 #[derive(Debug, Clone, Eq, PartialEq)]
 pub struct UnresolvedProcMacro {
     pub file: HirFileId,
diff --git a/crates/hir_def/src/path/lower.rs b/crates/hir_def/src/path/lower.rs
index 8a01e6eead..9518ac109b 100644
--- a/crates/hir_def/src/path/lower.rs
+++ b/crates/hir_def/src/path/lower.rs
@@ -123,7 +123,7 @@ pub(super) fn lower_path(mut path: ast::Path, hygiene: &Hygiene) -> Option<Path>
     // 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);
             }
         }
diff --git a/crates/hir_expand/src/hygiene.rs b/crates/hir_expand/src/hygiene.rs
index 7ab0a5e52e..6042e15b22 100644
--- a/crates/hir_expand/src/hygiene.rs
+++ b/crates/hir_expand/src/hygiene.rs
@@ -2,30 +2,94 @@
 //!
 //! 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 {
@@ -36,31 +100,68 @@ impl Hygiene {
                         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
     }
 }
diff --git a/crates/hir_expand/src/lib.rs b/crates/hir_expand/src/lib.rs
index 3fa1b1d776..5b6734a5f4 100644
--- a/crates/hir_expand/src/lib.rs
+++ b/crates/hir_expand/src/lib.rs
@@ -340,11 +340,8 @@ impl ExpansionInfo {
         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 {
@@ -359,7 +356,7 @@ impl ExpansionInfo {
             ),
         };
 
-        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))
@@ -495,7 +492,7 @@ fn ascend_call_token(
     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;
     }
diff --git a/crates/hir_ty/Cargo.toml b/crates/hir_ty/Cargo.toml
index 3d17785902..2dfccd191b 100644
--- a/crates/hir_ty/Cargo.toml
+++ b/crates/hir_ty/Cargo.toml
@@ -31,7 +31,7 @@ syntax = { path = "../syntax", version = "0.0.0" }
 test_utils = { path = "../test_utils", version = "0.0.0" }
 
 [dev-dependencies]
-expect-test = "1.1"
+expect-test = "1.0"
 tracing = "0.1"
 tracing-subscriber = { version = "0.2", default-features = false, features = ["env-filter", "registry"] }
 tracing-tree = { version = "0.1.4" }
diff --git a/crates/ide/Cargo.toml b/crates/ide/Cargo.toml
index bb28cca4d4..f1544dbe01 100644
--- a/crates/ide/Cargo.toml
+++ b/crates/ide/Cargo.toml
@@ -36,4 +36,4 @@ completion = { path = "../completion", version = "0.0.0" }
 hir = { path = "../hir", version = "0.0.0" }
 
 [dev-dependencies]
-expect-test = "1.1"
+expect-test = "1.0"
diff --git a/crates/ide_db/Cargo.toml b/crates/ide_db/Cargo.toml
index d3d3dc6889..ebe53c8ee8 100644
--- a/crates/ide_db/Cargo.toml
+++ b/crates/ide_db/Cargo.toml
@@ -32,4 +32,4 @@ test_utils = { path = "../test_utils", version = "0.0.0" }
 hir = { path = "../hir", version = "0.0.0" }
 
 [dev-dependencies]
-expect-test = "1.1"
+expect-test = "1.0"
diff --git a/crates/mbe/src/mbe_expander/matcher.rs b/crates/mbe/src/mbe_expander/matcher.rs
index ab5f87c487..385b466018 100644
--- a/crates/mbe/src/mbe_expander/matcher.rs
+++ b/crates/mbe/src/mbe_expander/matcher.rs
@@ -150,7 +150,7 @@ fn match_subtree(
                     res.add_err(err!("leftover tokens"));
                 }
             }
-            Op::Var { name, kind } => {
+            Op::Var { name, kind, .. } => {
                 let kind = match kind {
                     Some(k) => k,
                     None => {
diff --git a/crates/mbe/src/mbe_expander/transcriber.rs b/crates/mbe/src/mbe_expander/transcriber.rs
index 7205312371..57f3f104dc 100644
--- a/crates/mbe/src/mbe_expander/transcriber.rs
+++ b/crates/mbe/src/mbe_expander/transcriber.rs
@@ -100,8 +100,8 @@ fn expand_subtree(
                 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);
             }
@@ -118,12 +118,10 @@ fn expand_subtree(
     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.
@@ -142,14 +140,8 @@ fn expand_var(ctx: &mut ExpandCtx, v: &SmolStr) -> ExpandResult<Fragment> {
         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();
diff --git a/crates/mbe/src/parser.rs b/crates/mbe/src/parser.rs
index 2f3ebc8313..77cc739b65 100644
--- a/crates/mbe/src/parser.rs
+++ b/crates/mbe/src/parser.rs
@@ -8,7 +8,7 @@ use crate::{tt_iter::TtIter, ExpandError, MetaTemplate};
 
 #[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),
@@ -106,18 +106,21 @@ fn next_op<'a>(first: &tt::TokenTree, src: &mut TtIter<'a>, mode: Mode) -> Resul
                         }
                         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");
                         }
diff --git a/crates/parser/src/grammar.rs b/crates/parser/src/grammar.rs
index 63cc900272..bb9ffea8be 100644
--- a/crates/parser/src/grammar.rs
+++ b/crates/parser/src/grammar.rs
@@ -66,6 +66,10 @@ pub(crate) mod fragments {
         expressions::stmt(p, expressions::StmtWithSemi::No)
     }
 
+    pub(crate) fn stmt_optional_semi(p: &mut Parser) {
+        expressions::stmt(p, expressions::StmtWithSemi::Optional)
+    }
+
     pub(crate) fn opt_visibility(p: &mut Parser) {
         let _ = super::opt_visibility(p);
     }
diff --git a/crates/parser/src/lib.rs b/crates/parser/src/lib.rs
index 811e740f98..9dfe63028c 100644
--- a/crates/parser/src/lib.rs
+++ b/crates/parser/src/lib.rs
@@ -88,6 +88,7 @@ pub enum FragmentKind {
     Path,
     Expr,
     Statement,
+    StatementOptionalSemi,
     Type,
     Pattern,
     Item,
@@ -118,6 +119,7 @@ pub fn parse_fragment(
         FragmentKind::Visibility => grammar::fragments::opt_visibility,
         FragmentKind::MetaItem => grammar::fragments::meta_item,
         FragmentKind::Statement => grammar::fragments::stmt,
+        FragmentKind::StatementOptionalSemi => grammar::fragments::stmt_optional_semi,
         FragmentKind::Items => grammar::fragments::macro_items,
         FragmentKind::Statements => grammar::fragments::macro_stmts,
         FragmentKind::Attr => grammar::fragments::attr,
diff --git a/crates/rust-analyzer/Cargo.toml b/crates/rust-analyzer/Cargo.toml
index af7b86ead5..0a63593fb4 100644
--- a/crates/rust-analyzer/Cargo.toml
+++ b/crates/rust-analyzer/Cargo.toml
@@ -62,7 +62,7 @@ proc_macro_srv = { path = "../proc_macro_srv", version = "0.0.0" }
 winapi = "0.3.8"
 
 [dev-dependencies]
-expect-test = "1.1"
+expect-test = "1.0"
 test_utils = { path = "../test_utils" }
 mbe = { path = "../mbe" }
 tt = { path = "../tt" }
diff --git a/crates/ssr/Cargo.toml b/crates/ssr/Cargo.toml
index cc8136d223..339eda86a3 100644
--- a/crates/ssr/Cargo.toml
+++ b/crates/ssr/Cargo.toml
@@ -21,4 +21,4 @@ hir = { path = "../hir", version = "0.0.0" }
 test_utils = { path = "../test_utils", version = "0.0.0" }
 
 [dev-dependencies]
-expect-test = "1.1"
+expect-test = "1.0"
diff --git a/crates/ssr/src/parsing.rs b/crates/ssr/src/parsing.rs
index f3b084bafe..3d5e4feb7d 100644
--- a/crates/ssr/src/parsing.rs
+++ b/crates/ssr/src/parsing.rs
@@ -73,11 +73,18 @@ impl ParsedRule {
             placeholders_by_stand_in: pattern.placeholders_by_stand_in(),
             rules: Vec::new(),
         };
-        builder.try_add(ast::Expr::parse(&raw_pattern), raw_template.map(ast::Expr::parse));
+
+        let raw_template_stmt = raw_template.map(ast::Stmt::parse);
+        if let raw_template_expr @ Some(Ok(_)) = raw_template.map(ast::Expr::parse) {
+            builder.try_add(ast::Expr::parse(&raw_pattern), raw_template_expr);
+        } else {
+            builder.try_add(ast::Expr::parse(&raw_pattern), raw_template_stmt.clone());
+        }
         builder.try_add(ast::Type::parse(&raw_pattern), raw_template.map(ast::Type::parse));
         builder.try_add(ast::Item::parse(&raw_pattern), raw_template.map(ast::Item::parse));
         builder.try_add(ast::Path::parse(&raw_pattern), raw_template.map(ast::Path::parse));
         builder.try_add(ast::Pat::parse(&raw_pattern), raw_template.map(ast::Pat::parse));
+        builder.try_add(ast::Stmt::parse(&raw_pattern), raw_template_stmt);
         builder.build()
     }
 }
@@ -88,7 +95,11 @@ struct RuleBuilder {
 }
 
 impl RuleBuilder {
-    fn try_add<T: AstNode>(&mut self, pattern: Result<T, ()>, template: Option<Result<T, ()>>) {
+    fn try_add<T: AstNode, T2: AstNode>(
+        &mut self,
+        pattern: Result<T, ()>,
+        template: Option<Result<T2, ()>>,
+    ) {
         match (pattern, template) {
             (Ok(pattern), Some(Ok(template))) => self.rules.push(ParsedRule {
                 placeholders_by_stand_in: self.placeholders_by_stand_in.clone(),
diff --git a/crates/syntax/Cargo.toml b/crates/syntax/Cargo.toml
index 1810779449..5d8389ade5 100644
--- a/crates/syntax/Cargo.toml
+++ b/crates/syntax/Cargo.toml
@@ -33,4 +33,4 @@ profile = { path = "../profile", version = "0.0.0" }
 [dev-dependencies]
 walkdir = "2.3.1"
 rayon = "1"
-expect-test = "1.1"
+expect-test = "1.0"
diff --git a/crates/syntax/src/lib.rs b/crates/syntax/src/lib.rs
index 4d272f367a..da151e3289 100644
--- a/crates/syntax/src/lib.rs
+++ b/crates/syntax/src/lib.rs
@@ -212,6 +212,13 @@ impl ast::Attr {
     }
 }
 
+impl ast::Stmt {
+    /// Returns `text`, parsed as statement, but only if it has no errors.
+    pub fn parse(text: &str) -> Result<Self, ()> {
+        parsing::parse_text_fragment(text, parser::FragmentKind::StatementOptionalSemi)
+    }
+}
+
 /// Matches a `SyntaxNode` against an `ast` type.
 ///
 /// # Example:
diff --git a/docs/user/manual.adoc b/docs/user/manual.adoc
index 266baffa17..75445a02e5 100644
--- a/docs/user/manual.adoc
+++ b/docs/user/manual.adoc
@@ -18,9 +18,6 @@ The LSP allows various code editors, like VS Code, Emacs or Vim, to implement se
 [.lead]
 To improve this document, send a pull request: +
 https://github.com/rust-analyzer/rust-analyzer/blob/master/docs/user/manual.adoc[https://github.com/rust-analyzer/.../manual.adoc]
-
-The manual is written in https://asciidoc.org[AsciiDoc] and includes some extra files which are generated from the source code. Run `cargo test` and `cargo xtask codegen` to create these and then `asciidoctor manual.adoc` to create an HTML copy.
-
 ====
 
 If you have questions about using rust-analyzer, please ask them in the https://users.rust-lang.org/c/ide/14["`IDEs and Editors`"] topic of Rust users forum.


Test patch:
diff --git a/crates/hir_ty/src/tests/macros.rs b/crates/hir_ty/src/tests/macros.rs
index 1953da7beb..c64f0b5b53 100644
--- a/crates/hir_ty/src/tests/macros.rs
+++ b/crates/hir_ty/src/tests/macros.rs
@@ -370,6 +370,37 @@ expand!();
     );
 }
 
+#[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(
diff --git a/crates/ssr/src/tests.rs b/crates/ssr/src/tests.rs
index 63131f6ca5..db9cb8ca15 100644
--- a/crates/ssr/src/tests.rs
+++ b/crates/ssr/src/tests.rs
@@ -159,6 +159,97 @@ fn assert_match_failure_reason(pattern: &str, code: &str, snippet: &str, expecte
     assert_eq!(reasons, vec![expected_reason]);
 }
 
+#[test]
+fn ssr_let_stmt_in_macro_match() {
+    assert_matches(
+        "let a = 0",
+        r#"
+            macro_rules! m1 { ($a:stmt) => {$a}; }
+            fn f() {m1!{ let a = 0 };}"#,
+        // FIXME: Whitespace is not part of the matched block
+        &["leta=0"],
+    );
+}
+
+#[test]
+fn ssr_let_stmt_in_fn_match() {
+    assert_matches("let $a = 10;", "fn main() { let x = 10; x }", &["let x = 10;"]);
+    assert_matches("let $a = $b;", "fn main() { let x = 10; x }", &["let x = 10;"]);
+}
+
+#[test]
+fn ssr_block_expr_match() {
+    assert_matches("{ let $a = $b; }", "fn main() { let x = 10; }", &["{ let x = 10; }"]);
+    assert_matches("{ let $a = $b; $c }", "fn main() { let x = 10; x }", &["{ let x = 10; x }"]);
+}
+
+#[test]
+fn ssr_let_stmt_replace() {
+    // Pattern and template with trailing semicolon
+    assert_ssr_transform(
+        "let $a = $b; ==>> let $a = 11;",
+        "fn main() { let x = 10; x }",
+        expect![["fn main() { let x = 11; x }"]],
+    );
+}
+
+#[test]
+fn ssr_let_stmt_replace_expr() {
+    // Trailing semicolon should be dropped from the new expression
+    assert_ssr_transform(
+        "let $a = $b; ==>> $b",
+        "fn main() { let x = 10; }",
+        expect![["fn main() { 10 }"]],
+    );
+}
+
+#[test]
+fn ssr_blockexpr_replace_stmt_with_stmt() {
+    assert_ssr_transform(
+        "if $a() {$b;} ==>> $b;",
+        "{
+    if foo() {
+        bar();
+    }
+    Ok(())
+}",
+        expect![[r#"{
+    bar();
+    Ok(())
+}"#]],
+    );
+}
+
+#[test]
+fn ssr_blockexpr_match_trailing_expr() {
+    assert_matches(
+        "if $a() {$b;}",
+        "{
+    if foo() {
+        bar();
+    }
+}",
+        &["if foo() {
+        bar();
+    }"],
+    );
+}
+
+#[test]
+fn ssr_blockexpr_replace_trailing_expr_with_stmt() {
+    assert_ssr_transform(
+        "if $a() {$b;} ==>> $b;",
+        "{
+    if foo() {
+        bar();
+    }
+}",
+        expect![["{
+    bar();
+}"]],
+    );
+}
+
 #[test]
 fn ssr_function_to_method() {
     assert_ssr_transform(
diff --git a/crates/syntax/src/tests.rs b/crates/syntax/src/tests.rs
index 8c217dfe01..9d3433c9db 100644
--- a/crates/syntax/src/tests.rs
+++ b/crates/syntax/src/tests.rs
@@ -102,6 +102,15 @@ fn type_parser_tests() {
     );
 }
 
+#[test]
+fn stmt_parser_tests() {
+    fragment_parser_dir_test(
+        &["parser/fragments/stmt/ok"],
+        &["parser/fragments/stmt/err"],
+        crate::ast::Stmt::parse,
+    );
+}
+
 #[test]
 fn parser_fuzz_tests() {
     for (_, text) in collect_rust_files(&test_data_dir(), &["parser/fuzz-failures"]) {
diff --git a/crates/syntax/test_data/parser/fragments/stmt/err/0000_attr.rast b/crates/syntax/test_data/parser/fragments/stmt/err/0000_attr.rast
new file mode 100644
index 0000000000..5df7507e2d
--- /dev/null
+++ b/crates/syntax/test_data/parser/fragments/stmt/err/0000_attr.rast
@@ -0,0 +1,1 @@
+ERROR
diff --git a/crates/syntax/test_data/parser/fragments/stmt/err/0000_attr.rs b/crates/syntax/test_data/parser/fragments/stmt/err/0000_attr.rs
new file mode 100644
index 0000000000..988df07059
--- /dev/null
+++ b/crates/syntax/test_data/parser/fragments/stmt/err/0000_attr.rs
@@ -0,0 +1,1 @@
+#[foo]
diff --git a/crates/syntax/test_data/parser/fragments/stmt/err/0000_multiple_stmts.rast b/crates/syntax/test_data/parser/fragments/stmt/err/0000_multiple_stmts.rast
new file mode 100644
index 0000000000..5df7507e2d
--- /dev/null
+++ b/crates/syntax/test_data/parser/fragments/stmt/err/0000_multiple_stmts.rast
@@ -0,0 +1,1 @@
+ERROR
diff --git a/crates/syntax/test_data/parser/fragments/stmt/err/0000_multiple_stmts.rs b/crates/syntax/test_data/parser/fragments/stmt/err/0000_multiple_stmts.rs
new file mode 100644
index 0000000000..7e3b2fd493
--- /dev/null
+++ b/crates/syntax/test_data/parser/fragments/stmt/err/0000_multiple_stmts.rs
@@ -0,0 +1,1 @@
+a(); b(); c()
diff --git a/crates/syntax/test_data/parser/fragments/stmt/err/0000_open_parenthesis.rast b/crates/syntax/test_data/parser/fragments/stmt/err/0000_open_parenthesis.rast
new file mode 100644
index 0000000000..5df7507e2d
--- /dev/null
+++ b/crates/syntax/test_data/parser/fragments/stmt/err/0000_open_parenthesis.rast
@@ -0,0 +1,1 @@
+ERROR
diff --git a/crates/syntax/test_data/parser/fragments/stmt/err/0000_open_parenthesis.rs b/crates/syntax/test_data/parser/fragments/stmt/err/0000_open_parenthesis.rs
new file mode 100644
index 0000000000..2d06f37663
--- /dev/null
+++ b/crates/syntax/test_data/parser/fragments/stmt/err/0000_open_parenthesis.rs
@@ -0,0 +1,1 @@
+(
diff --git a/crates/syntax/test_data/parser/fragments/stmt/err/0000_semicolon.rast b/crates/syntax/test_data/parser/fragments/stmt/err/0000_semicolon.rast
new file mode 100644
index 0000000000..5df7507e2d
--- /dev/null
+++ b/crates/syntax/test_data/parser/fragments/stmt/err/0000_semicolon.rast
@@ -0,0 +1,1 @@
+ERROR
diff --git a/crates/syntax/test_data/parser/fragments/stmt/err/0000_semicolon.rs b/crates/syntax/test_data/parser/fragments/stmt/err/0000_semicolon.rs
new file mode 100644
index 0000000000..092bc2b041
--- /dev/null
+++ b/crates/syntax/test_data/parser/fragments/stmt/err/0000_semicolon.rs
@@ -0,0 +1,1 @@
+;
diff --git a/crates/syntax/test_data/parser/fragments/stmt/err/0000_unterminated_expr.rast b/crates/syntax/test_data/parser/fragments/stmt/err/0000_unterminated_expr.rast
new file mode 100644
index 0000000000..5df7507e2d
--- /dev/null
+++ b/crates/syntax/test_data/parser/fragments/stmt/err/0000_unterminated_expr.rast
@@ -0,0 +1,1 @@
+ERROR
diff --git a/crates/syntax/test_data/parser/fragments/stmt/err/0000_unterminated_expr.rs b/crates/syntax/test_data/parser/fragments/stmt/err/0000_unterminated_expr.rs
new file mode 100644
index 0000000000..ca49acb079
--- /dev/null
+++ b/crates/syntax/test_data/parser/fragments/stmt/err/0000_unterminated_expr.rs
@@ -0,0 +1,1 @@
+1 +
diff --git a/crates/syntax/test_data/parser/fragments/stmt/ok/0000_expr.rast b/crates/syntax/test_data/parser/fragments/stmt/ok/0000_expr.rast
new file mode 100644
index 0000000000..274fdf16de
--- /dev/null
+++ b/crates/syntax/test_data/parser/fragments/stmt/ok/0000_expr.rast
@@ -0,0 +1,9 @@
+EXPR_STMT@0..5
+  BIN_EXPR@0..5
+    LITERAL@0..1
+      INT_NUMBER@0..1 "1"
+    WHITESPACE@1..2 " "
+    PLUS@2..3 "+"
+    WHITESPACE@3..4 " "
+    LITERAL@4..5
+      INT_NUMBER@4..5 "1"
diff --git a/crates/syntax/test_data/parser/fragments/stmt/ok/0000_expr.rs b/crates/syntax/test_data/parser/fragments/stmt/ok/0000_expr.rs
new file mode 100644
index 0000000000..8d2f0971e2
--- /dev/null
+++ b/crates/syntax/test_data/parser/fragments/stmt/ok/0000_expr.rs
@@ -0,0 +1,1 @@
+1 + 1
diff --git a/crates/syntax/test_data/parser/fragments/stmt/ok/0000_expr_block.rast b/crates/syntax/test_data/parser/fragments/stmt/ok/0000_expr_block.rast
new file mode 100644
index 0000000000..6c946091f3
--- /dev/null
+++ b/crates/syntax/test_data/parser/fragments/stmt/ok/0000_expr_block.rast
@@ -0,0 +1,69 @@
+EXPR_STMT@0..55
+  BLOCK_EXPR@0..55
+    L_CURLY@0..1 "{"
+    WHITESPACE@1..6 "\n    "
+    LET_STMT@6..20
+      LET_KW@6..9 "let"
+      WHITESPACE@9..10 " "
+      IDENT_PAT@10..11
+        NAME@10..11
+          IDENT@10..11 "x"
+      WHITESPACE@11..12 " "
+      EQ@12..13 "="
+      WHITESPACE@13..14 " "
+      CALL_EXPR@14..19
+        PATH_EXPR@14..17
+          PATH@14..17
+            PATH_SEGMENT@14..17
+              NAME_REF@14..17
+                IDENT@14..17 "foo"
+        ARG_LIST@17..19
+          L_PAREN@17..18 "("
+          R_PAREN@18..19 ")"
+      SEMICOLON@19..20 ";"
+    WHITESPACE@20..25 "\n    "
+    LET_STMT@25..39
+      LET_KW@25..28 "let"
+      WHITESPACE@28..29 " "
+      IDENT_PAT@29..30
+        NAME@29..30
+          IDENT@29..30 "y"
+      WHITESPACE@30..31 " "
+      EQ@31..32 "="
+      WHITESPACE@32..33 " "
+      CALL_EXPR@33..38
+        PATH_EXPR@33..36
+          PATH@33..36
+            PATH_SEGMENT@33..36
+              NAME_REF@33..36
+                IDENT@33..36 "bar"
+        ARG_LIST@36..38
+          L_PAREN@36..37 "("
+          R_PAREN@37..38 ")"
+      SEMICOLON@38..39 ";"
+    WHITESPACE@39..44 "\n    "
+    CALL_EXPR@44..53
+      PATH_EXPR@44..46
+        PATH@44..46
+          PATH_SEGMENT@44..46
+            NAME_REF@44..46
+              IDENT@44..46 "Ok"
+      ARG_LIST@46..53
+        L_PAREN@46..47 "("
+        BIN_EXPR@47..52
+          PATH_EXPR@47..48
+            PATH@47..48
+              PATH_SEGMENT@47..48
+                NAME_REF@47..48
+                  IDENT@47..48 "x"
+          WHITESPACE@48..49 " "
+          PLUS@49..50 "+"
+          WHITESPACE@50..51 " "
+          PATH_EXPR@51..52
+            PATH@51..52
+              PATH_SEGMENT@51..52
+                NAME_REF@51..52
+                  IDENT@51..52 "y"
+        R_PAREN@52..53 ")"
+    WHITESPACE@53..54 "\n"
+    R_CURLY@54..55 "}"
diff --git a/crates/syntax/test_data/parser/fragments/stmt/ok/0000_expr_block.rs b/crates/syntax/test_data/parser/fragments/stmt/ok/0000_expr_block.rs
new file mode 100644
index 0000000000..ffa5c1e66e
--- /dev/null
+++ b/crates/syntax/test_data/parser/fragments/stmt/ok/0000_expr_block.rs
@@ -0,0 +1,5 @@
+{
+    let x = foo();
+    let y = bar();
+    Ok(x + y)
+}
diff --git a/crates/syntax/test_data/parser/fragments/stmt/ok/0000_fn_call.rast b/crates/syntax/test_data/parser/fragments/stmt/ok/0000_fn_call.rast
new file mode 100644
index 0000000000..8c186da93e
--- /dev/null
+++ b/crates/syntax/test_data/parser/fragments/stmt/ok/0000_fn_call.rast
@@ -0,0 +1,11 @@
+EXPR_STMT@0..6
+  CALL_EXPR@0..5
+    PATH_EXPR@0..3
+      PATH@0..3
+        PATH_SEGMENT@0..3
+          NAME_REF@0..3
+            IDENT@0..3 "foo"
+    ARG_LIST@3..5
+      L_PAREN@3..4 "("
+      R_PAREN@4..5 ")"
+  SEMICOLON@5..6 ";"
diff --git a/crates/syntax/test_data/parser/fragments/stmt/ok/0000_fn_call.rs b/crates/syntax/test_data/parser/fragments/stmt/ok/0000_fn_call.rs
new file mode 100644
index 0000000000..a280f9a5cc
--- /dev/null
+++ b/crates/syntax/test_data/parser/fragments/stmt/ok/0000_fn_call.rs
@@ -0,0 +1,1 @@
+foo();
diff --git a/crates/syntax/test_data/parser/fragments/stmt/ok/0000_let_stmt.rast b/crates/syntax/test_data/parser/fragments/stmt/ok/0000_let_stmt.rast
new file mode 100644
index 0000000000..8ab38da21c
--- /dev/null
+++ b/crates/syntax/test_data/parser/fragments/stmt/ok/0000_let_stmt.rast
@@ -0,0 +1,12 @@
+LET_STMT@0..11
+  LET_KW@0..3 "let"
+  WHITESPACE@3..4 " "
+  IDENT_PAT@4..5
+    NAME@4..5
+      IDENT@4..5 "x"
+  WHITESPACE@5..6 " "
+  EQ@6..7 "="
+  WHITESPACE@7..8 " "
+  LITERAL@8..10
+    INT_NUMBER@8..10 "10"
+  SEMICOLON@10..11 ";"
diff --git a/crates/syntax/test_data/parser/fragments/stmt/ok/0000_let_stmt.rs b/crates/syntax/test_data/parser/fragments/stmt/ok/0000_let_stmt.rs
new file mode 100644
index 0000000000..de8a7f1fc6
--- /dev/null
+++ b/crates/syntax/test_data/parser/fragments/stmt/ok/0000_let_stmt.rs
@@ -0,0 +1,1 @@
+let x = 10;
diff --git a/crates/syntax/test_data/parser/fragments/stmt/ok/0000_macro_let_stmt.rast b/crates/syntax/test_data/parser/fragments/stmt/ok/0000_macro_let_stmt.rast
new file mode 100644
index 0000000000..81d6df29a5
--- /dev/null
+++ b/crates/syntax/test_data/parser/fragments/stmt/ok/0000_macro_let_stmt.rast
@@ -0,0 +1,21 @@
+EXPR_STMT@0..18
+  MACRO_CALL@0..17
+    PATH@0..2
+      PATH_SEGMENT@0..2
+        NAME_REF@0..2
+          IDENT@0..2 "m1"
+    BANG@2..3 "!"
+    TOKEN_TREE@3..17
+      L_CURLY@3..4 "{"
+      WHITESPACE@4..5 " "
+      LET_KW@5..8 "let"
+      WHITESPACE@8..9 " "
+      IDENT@9..10 "a"
+      WHITESPACE@10..11 " "
+      EQ@11..12 "="
+      WHITESPACE@12..13 " "
+      INT_NUMBER@13..14 "0"
+      SEMICOLON@14..15 ";"
+      WHITESPACE@15..16 " "
+      R_CURLY@16..17 "}"
+  SEMICOLON@17..18 ";"
diff --git a/crates/syntax/test_data/parser/fragments/stmt/ok/0000_macro_let_stmt.rs b/crates/syntax/test_data/parser/fragments/stmt/ok/0000_macro_let_stmt.rs
new file mode 100644
index 0000000000..075f30159b
--- /dev/null
+++ b/crates/syntax/test_data/parser/fragments/stmt/ok/0000_macro_let_stmt.rs
@@ -0,0 +1,1 @@
+m1!{ let a = 0; };
diff --git a/crates/syntax/test_data/parser/fragments/stmt/ok/0000_macro_unterminated_let_stmt.rast b/crates/syntax/test_data/parser/fragments/stmt/ok/0000_macro_unterminated_let_stmt.rast
new file mode 100644
index 0000000000..81d6df29a5
--- /dev/null
+++ b/crates/syntax/test_data/parser/fragments/stmt/ok/0000_macro_unterminated_let_stmt.rast
@@ -0,0 +1,21 @@
+EXPR_STMT@0..18
+  MACRO_CALL@0..17
+    PATH@0..2
+      PATH_SEGMENT@0..2
+        NAME_REF@0..2
+          IDENT@0..2 "m1"
+    BANG@2..3 "!"
+    TOKEN_TREE@3..17
+      L_CURLY@3..4 "{"
+      WHITESPACE@4..5 " "
+      LET_KW@5..8 "let"
+      WHITESPACE@8..9 " "
+      IDENT@9..10 "a"
+      WHITESPACE@10..11 " "
+      EQ@11..12 "="
+      WHITESPACE@12..13 " "
+      INT_NUMBER@13..14 "0"
+      SEMICOLON@14..15 ";"
+      WHITESPACE@15..16 " "
+      R_CURLY@16..17 "}"
+  SEMICOLON@17..18 ";"
diff --git a/crates/syntax/test_data/parser/fragments/stmt/ok/0000_macro_unterminated_let_stmt.rs b/crates/syntax/test_data/parser/fragments/stmt/ok/0000_macro_unterminated_let_stmt.rs
new file mode 100644
index 0000000000..075f30159b
--- /dev/null
+++ b/crates/syntax/test_data/parser/fragments/stmt/ok/0000_macro_unterminated_let_stmt.rs
@@ -0,0 +1,1 @@
+m1!{ let a = 0; };
diff --git a/crates/syntax/test_data/parser/fragments/stmt/ok/0000_struct_item.rast b/crates/syntax/test_data/parser/fragments/stmt/ok/0000_struct_item.rast
new file mode 100644
index 0000000000..64c5d29691
--- /dev/null
+++ b/crates/syntax/test_data/parser/fragments/stmt/ok/0000_struct_item.rast
@@ -0,0 +1,22 @@
+STRUCT@0..28
+  STRUCT_KW@0..6 "struct"
+  WHITESPACE@6..7 " "
+  NAME@7..10
+    IDENT@7..10 "Foo"
+  WHITESPACE@10..11 " "
+  RECORD_FIELD_LIST@11..28
+    L_CURLY@11..12 "{"
+    WHITESPACE@12..17 "\n    "
+    RECORD_FIELD@17..25
+      NAME@17..20
+        IDENT@17..20 "bar"
+      COLON@20..21 ":"
+      WHITESPACE@21..22 " "
+      PATH_TYPE@22..25
+        PATH@22..25
+          PATH_SEGMENT@22..25
+            NAME_REF@22..25
+              IDENT@22..25 "u32"
+    COMMA@25..26 ","
+    WHITESPACE@26..27 "\n"
+    R_CURLY@27..28 "}"
diff --git a/crates/syntax/test_data/parser/fragments/stmt/ok/0000_struct_item.rs b/crates/syntax/test_data/parser/fragments/stmt/ok/0000_struct_item.rs
new file mode 100644
index 0000000000..e5473e3ac8
--- /dev/null
+++ b/crates/syntax/test_data/parser/fragments/stmt/ok/0000_struct_item.rs
@@ -0,0 +1,3 @@
+struct Foo {
+    bar: u32,
+}
diff --git a/crates/syntax/test_data/parser/fragments/stmt/ok/0000_unterminated_fn_call.rast b/crates/syntax/test_data/parser/fragments/stmt/ok/0000_unterminated_fn_call.rast
new file mode 100644
index 0000000000..9089906bce
--- /dev/null
+++ b/crates/syntax/test_data/parser/fragments/stmt/ok/0000_unterminated_fn_call.rast
@@ -0,0 +1,10 @@
+EXPR_STMT@0..5
+  CALL_EXPR@0..5
+    PATH_EXPR@0..3
+      PATH@0..3
+        PATH_SEGMENT@0..3
+          NAME_REF@0..3
+            IDENT@0..3 "foo"
+    ARG_LIST@3..5
+      L_PAREN@3..4 "("
+      R_PAREN@4..5 ")"
diff --git a/crates/syntax/test_data/parser/fragments/stmt/ok/0000_unterminated_fn_call.rs b/crates/syntax/test_data/parser/fragments/stmt/ok/0000_unterminated_fn_call.rs
new file mode 100644
index 0000000000..eb28ef4401
--- /dev/null
+++ b/crates/syntax/test_data/parser/fragments/stmt/ok/0000_unterminated_fn_call.rs
@@ -0,0 +1,1 @@
+foo()
diff --git a/crates/syntax/test_data/parser/fragments/stmt/ok/0000_unterminated_let_stmt.rast b/crates/syntax/test_data/parser/fragments/stmt/ok/0000_unterminated_let_stmt.rast
new file mode 100644
index 0000000000..37663671fa
--- /dev/null
+++ b/crates/syntax/test_data/parser/fragments/stmt/ok/0000_unterminated_let_stmt.rast
@@ -0,0 +1,11 @@
+LET_STMT@0..10
+  LET_KW@0..3 "let"
+  WHITESPACE@3..4 " "
+  IDENT_PAT@4..5
+    NAME@4..5
+      IDENT@4..5 "x"
+  WHITESPACE@5..6 " "
+  EQ@6..7 "="
+  WHITESPACE@7..8 " "
+  LITERAL@8..10
+    INT_NUMBER@8..10 "10"
diff --git a/crates/syntax/test_data/parser/fragments/stmt/ok/0000_unterminated_let_stmt.rs b/crates/syntax/test_data/parser/fragments/stmt/ok/0000_unterminated_let_stmt.rs
new file mode 100644
index 0000000000..78364b2a96
--- /dev/null
+++ b/crates/syntax/test_data/parser/fragments/stmt/ok/0000_unterminated_let_stmt.rs
@@ -0,0 +1,1 @@
+let x = 10