diff --git a/c2rust-refactor/src/ast_builder/builder.rs b/c2rust-refactor/src/ast_builder/builder.rs index 501c6c1fdf..075f70e054 100644 --- a/c2rust-refactor/src/ast_builder/builder.rs +++ b/c2rust-refactor/src/ast_builder/builder.rs @@ -360,6 +360,21 @@ pub struct Builder { #[allow(dead_code)] impl Builder { + /// Pick the span we should attach to a freshly minted statement. + /// + /// We want new statements produced by transforms such as `sink_lets` or + /// `fold_let_assign` to retain the source span of the code they were cloned + /// from so that later pretty-print/reparse passes can recover the original + /// text instead of seeing an empty buffer from a `DUMMY_SP`. + #[inline] + fn stmt_span(&self, fallback: Span) -> Span { + if self.span == DUMMY_SP { + fallback + } else { + self.span + } + } + pub fn new() -> Builder { Builder { vis: Visibility { @@ -1552,10 +1567,11 @@ impl Builder { L: Make>, { let local = local.make(&self); + let stmt_span = self.stmt_span(local.span); Stmt { id: self.id, kind: StmtKind::Local(local), - span: self.span, + span: stmt_span, } } @@ -1564,10 +1580,11 @@ impl Builder { E: Make>, { let expr = expr.make(&self); + let stmt_span = self.stmt_span(expr.span); Stmt { id: self.id, kind: StmtKind::Expr(expr), - span: self.span, + span: stmt_span, } } @@ -1576,10 +1593,11 @@ impl Builder { E: Make>, { let expr = expr.make(&self); + let stmt_span = self.stmt_span(expr.span); Stmt { id: self.id, kind: StmtKind::Semi(expr), - span: self.span, + span: stmt_span, } } @@ -1588,10 +1606,11 @@ impl Builder { I: Make>, { let item = item.make(&self); + let stmt_span = self.stmt_span(item.span); Stmt { id: self.id, kind: StmtKind::Item(item), - span: self.span, + span: stmt_span, } } diff --git a/c2rust-refactor/src/rewrite/strategy/print.rs b/c2rust-refactor/src/rewrite/strategy/print.rs index 36f73fa609..d25023002e 100644 --- a/c2rust-refactor/src/rewrite/strategy/print.rs +++ b/c2rust-refactor/src/rewrite/strategy/print.rs @@ -9,7 +9,7 @@ //! pretty-printer output, since it likely has nicer formatting, comments, etc. So there is some //! logic in this module for "recovering" from needing to use this strategy by splicing old AST //! text back into the new AST's pretty printer output. -use log::{info, warn}; +use log::{debug, info, warn}; use rustc_ast::attr; use rustc_ast::ptr::P; use rustc_ast::token::{BinOpToken, CommentKind, Delimiter, Nonterminal, Token, TokenKind}; @@ -95,6 +95,45 @@ impl PrintParse for Ty { } } +/// Parse a snippet that contains only outer attributes (e.g. a relocated `#[derive]` line). +/// +/// Transforms such as `fold_let_assign` or `remove_redundant_let_types` may move attributes around +/// on their own, which means `driver::parse_stmts` sees no statement tokens at all and would panic. +/// By parsing the snippet as `attrs + dummy item` we can synthesize an empty statement that still +/// carries the attribute span for recovery. +fn parse_attr_only_stmt(sess: &Session, src: &str) -> Option { + let trimmed = src.trim_start(); + if !(trimmed.starts_with("#[") || trimmed.starts_with("#!")) { + return None; + } + + // Provide a dummy item so Rust's parser accepts the attributes. We only use the attribute spans, + // so the fabricated item never shows up in the recorded rewrite. + let wrapped_src = format!( + "{src}\nstruct __c2rust_dummy_for_attr_only_reparse;", + src = src + ); + let mut items = driver::parse_items(sess, &wrapped_src); + if items.len() != 1 { + return None; + } + let item = items.pop().unwrap(); + if item.attrs.is_empty() { + return None; + } + let span = item + .attrs + .iter() + .map(|attr| attr.span) + .reduce(|acc, s| acc.to(s)) + .unwrap_or(item.span); + Some(Stmt { + id: DUMMY_NODE_ID, + span, + kind: StmtKind::Empty, + }) +} + impl PrintParse for Stmt { fn to_string(&self) -> String { // pprust::stmt_to_string appends a semicolon to Expr kind statements, @@ -110,7 +149,51 @@ impl PrintParse for Stmt { type Parsed = Stmt; fn parse(sess: &Session, src: &str) -> Self::Parsed { - driver::parse_stmts(sess, src).lone() + let mut stmts = driver::parse_stmts(sess, src); + match stmts.len() { + 1 => stmts.pop().unwrap(), + 0 => { + if src.trim().is_empty() { + // ASTBuilder assigns DUMMY_SP to freshly synthesized statements (e.g. from + // `mk().local_stmt(...)`), so pretty-printing them can legitimately produce an + // empty buffer even though the inner `Local`/`Expr` still carries the original + // span. Preserve that slot explicitly so Recover still sees the expected single + // statement node and doesn’t panic on len()==0. + return Stmt { + id: DUMMY_NODE_ID, + span: DUMMY_SP, + kind: StmtKind::Empty, + }; + } + if let Some(stmt) = parse_attr_only_stmt(sess, src) { + return stmt; + } + let mut items = driver::parse_items(sess, src); + if items.len() != 1 { + panic!( + "PrintParse expected 1 statement or item but parsed {} items from:\n{}", + items.len(), + src + ); + } + let item = items.pop().unwrap(); + // Rust’s parser treats outer attributes as part of the following item. If recover + // reused the old struct body text but the attrs need reprinting, the snippet that + // reaches us can literally be just `#[derive(...)]`. Wrap the parsed item so the + // enclosing `Block` continues to hold a `StmtKind::Item`, matching rustc’s AST. + Stmt { + id: DUMMY_NODE_ID, + span: item.span, + kind: StmtKind::Item(item), + } + } + n => { + panic!( + "PrintParse expected 1 statement but parsed {} from:\n{}", + n, src + ); + } + } } } @@ -252,7 +335,40 @@ impl Splice for Ty { impl Splice for Stmt { fn splice_span(&self) -> Span { - self.span + match &self.kind { + StmtKind::Local(local) => { + // Newly inserted locals often have DUMMY_SP; fall back to the local span so we can + // recover text/attrs from the original source. + let base = if self.span == DUMMY_SP { + local.span + } else { + self.span + }; + extend_span_attrs(base, &local.attrs) + } + StmtKind::Item(item) => { + // Same fallback for items whose wrapper stmt lost its span. + let base = if self.span == DUMMY_SP { + item.span + } else { + self.span + }; + extend_span_attrs(base, &item.attrs) + } + StmtKind::Expr(expr) | StmtKind::Semi(expr) => { + // Expression statements carry attrs/comments on the child `Expr`. Use that span when + // the wrapper is dummy so `extend_span_attrs` still captures outer attributes and any + // trailing comments during rewrites. + let base = if self.span == DUMMY_SP { + expr.span + } else { + self.span + }; + extend_span_attrs(base, &expr.attrs) + } + StmtKind::MacCall(mac) => extend_span_attrs(self.span, &mac.attrs), + StmtKind::Empty => self.span, + } } } @@ -674,7 +790,27 @@ fn rewrite_at_impl(old_span: Span, new: &T, mut rcx: RewriteCtxtRef) -> bool where T: PrintParse + RecoverChildren + Splice + MaybeGetNodeId, { - let printed = add_comments(new.to_string(), new, &rcx); + let mut printed = add_comments(new.to_string(), new, &rcx); + if printed.trim().is_empty() { + // When the statement wrapper has DUMMY_SP the pretty printer outputs nothing even though the + // original source had a full `let`. Pull the old snippet (which still contains the attrs/body) + // so `parse()` sees valid syntax and the reparsed AST matches what we’re trying to insert. + if let Ok(snippet) = rcx + .session() + .source_map() + .span_to_snippet(new.splice_span()) + { + if !snippet.trim().is_empty() { + printed = snippet; + } + } + if printed.trim().is_empty() { + debug!( + "rewrite_at_impl: empty printed text {:?} for {:?}", + printed, new + ); + } + } let reparsed = T::parse(rcx.session(), &printed); let reparsed = reparsed.ast_deref();