Skip to content

Commit e6159d5

Browse files
committed
refactor: keep attr-only statements and cloned lets printable
1 parent ec3e109 commit e6159d5

File tree

2 files changed

+163
-7
lines changed

2 files changed

+163
-7
lines changed

c2rust-refactor/src/ast_builder/builder.rs

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,21 @@ pub struct Builder {
360360

361361
#[allow(dead_code)]
362362
impl Builder {
363+
/// Pick the span we should attach to a freshly minted statement.
364+
///
365+
/// We want new statements produced by transforms such as `sink_lets` or
366+
/// `fold_let_assign` to retain the source span of the code they were cloned
367+
/// from so that later pretty-print/reparse passes can recover the original
368+
/// text instead of seeing an empty buffer from a `DUMMY_SP`.
369+
#[inline]
370+
fn stmt_span(&self, fallback: Span) -> Span {
371+
if self.span == DUMMY_SP {
372+
fallback
373+
} else {
374+
self.span
375+
}
376+
}
377+
363378
pub fn new() -> Builder {
364379
Builder {
365380
vis: Visibility {
@@ -1552,10 +1567,11 @@ impl Builder {
15521567
L: Make<P<Local>>,
15531568
{
15541569
let local = local.make(&self);
1570+
let stmt_span = self.stmt_span(local.span);
15551571
Stmt {
15561572
id: self.id,
15571573
kind: StmtKind::Local(local),
1558-
span: self.span,
1574+
span: stmt_span,
15591575
}
15601576
}
15611577

@@ -1564,10 +1580,11 @@ impl Builder {
15641580
E: Make<P<Expr>>,
15651581
{
15661582
let expr = expr.make(&self);
1583+
let stmt_span = self.stmt_span(expr.span);
15671584
Stmt {
15681585
id: self.id,
15691586
kind: StmtKind::Expr(expr),
1570-
span: self.span,
1587+
span: stmt_span,
15711588
}
15721589
}
15731590

@@ -1576,10 +1593,11 @@ impl Builder {
15761593
E: Make<P<Expr>>,
15771594
{
15781595
let expr = expr.make(&self);
1596+
let stmt_span = self.stmt_span(expr.span);
15791597
Stmt {
15801598
id: self.id,
15811599
kind: StmtKind::Semi(expr),
1582-
span: self.span,
1600+
span: stmt_span,
15831601
}
15841602
}
15851603

@@ -1588,10 +1606,11 @@ impl Builder {
15881606
I: Make<P<Item>>,
15891607
{
15901608
let item = item.make(&self);
1609+
let stmt_span = self.stmt_span(item.span);
15911610
Stmt {
15921611
id: self.id,
15931612
kind: StmtKind::Item(item),
1594-
span: self.span,
1613+
span: stmt_span,
15951614
}
15961615
}
15971616

c2rust-refactor/src/rewrite/strategy/print.rs

Lines changed: 140 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
//! pretty-printer output, since it likely has nicer formatting, comments, etc. So there is some
1010
//! logic in this module for "recovering" from needing to use this strategy by splicing old AST
1111
//! text back into the new AST's pretty printer output.
12-
use log::{info, warn};
12+
use log::{debug, info, warn};
1313
use rustc_ast::attr;
1414
use rustc_ast::ptr::P;
1515
use rustc_ast::token::{BinOpToken, CommentKind, Delimiter, Nonterminal, Token, TokenKind};
@@ -95,6 +95,45 @@ impl PrintParse for Ty {
9595
}
9696
}
9797

98+
/// Parse a snippet that contains only outer attributes (e.g. a relocated `#[derive]` line).
99+
///
100+
/// Transforms such as `fold_let_assign` or `remove_redundant_let_types` may move attributes around
101+
/// on their own, which means `driver::parse_stmts` sees no statement tokens at all and would panic.
102+
/// By parsing the snippet as `attrs + dummy item` we can synthesize an empty statement that still
103+
/// carries the attribute span for recovery.
104+
fn parse_attr_only_stmt(sess: &Session, src: &str) -> Option<Stmt> {
105+
let trimmed = src.trim_start();
106+
if !(trimmed.starts_with("#[") || trimmed.starts_with("#!")) {
107+
return None;
108+
}
109+
110+
// Provide a dummy item so Rust's parser accepts the attributes. We only use the attribute spans,
111+
// so the fabricated item never shows up in the recorded rewrite.
112+
let wrapped_src = format!(
113+
"{src}\nstruct __c2rust_dummy_for_attr_only_reparse;",
114+
src = src
115+
);
116+
let mut items = driver::parse_items(sess, &wrapped_src);
117+
if items.len() != 1 {
118+
return None;
119+
}
120+
let item = items.pop().unwrap();
121+
if item.attrs.is_empty() {
122+
return None;
123+
}
124+
let span = item
125+
.attrs
126+
.iter()
127+
.map(|attr| attr.span)
128+
.reduce(|acc, s| acc.to(s))
129+
.unwrap_or(item.span);
130+
Some(Stmt {
131+
id: DUMMY_NODE_ID,
132+
span,
133+
kind: StmtKind::Empty,
134+
})
135+
}
136+
98137
impl PrintParse for Stmt {
99138
fn to_string(&self) -> String {
100139
// pprust::stmt_to_string appends a semicolon to Expr kind statements,
@@ -110,7 +149,51 @@ impl PrintParse for Stmt {
110149

111150
type Parsed = Stmt;
112151
fn parse(sess: &Session, src: &str) -> Self::Parsed {
113-
driver::parse_stmts(sess, src).lone()
152+
let mut stmts = driver::parse_stmts(sess, src);
153+
match stmts.len() {
154+
1 => stmts.pop().unwrap(),
155+
0 => {
156+
if src.trim().is_empty() {
157+
// ASTBuilder assigns DUMMY_SP to freshly synthesized statements (e.g. from
158+
// `mk().local_stmt(...)`), so pretty-printing them can legitimately produce an
159+
// empty buffer even though the inner `Local`/`Expr` still carries the original
160+
// span. Preserve that slot explicitly so Recover still sees the expected single
161+
// statement node and doesn’t panic on len()==0.
162+
return Stmt {
163+
id: DUMMY_NODE_ID,
164+
span: DUMMY_SP,
165+
kind: StmtKind::Empty,
166+
};
167+
}
168+
if let Some(stmt) = parse_attr_only_stmt(sess, src) {
169+
return stmt;
170+
}
171+
let mut items = driver::parse_items(sess, src);
172+
if items.len() != 1 {
173+
panic!(
174+
"PrintParse<Stmt> expected 1 statement or item but parsed {} items from:\n{}",
175+
items.len(),
176+
src
177+
);
178+
}
179+
let item = items.pop().unwrap();
180+
// Rust’s parser treats outer attributes as part of the following item. If recover
181+
// reused the old struct body text but the attrs need reprinting, the snippet that
182+
// reaches us can literally be just `#[derive(...)]`. Wrap the parsed item so the
183+
// enclosing `Block` continues to hold a `StmtKind::Item`, matching rustc’s AST.
184+
Stmt {
185+
id: DUMMY_NODE_ID,
186+
span: item.span,
187+
kind: StmtKind::Item(item),
188+
}
189+
}
190+
n => {
191+
panic!(
192+
"PrintParse<Stmt> expected 1 statement but parsed {} from:\n{}",
193+
n, src
194+
);
195+
}
196+
}
114197
}
115198
}
116199

@@ -252,7 +335,40 @@ impl Splice for Ty {
252335

253336
impl Splice for Stmt {
254337
fn splice_span(&self) -> Span {
255-
self.span
338+
match &self.kind {
339+
StmtKind::Local(local) => {
340+
// Newly inserted locals often have DUMMY_SP; fall back to the local span so we can
341+
// recover text/attrs from the original source.
342+
let base = if self.span == DUMMY_SP {
343+
local.span
344+
} else {
345+
self.span
346+
};
347+
extend_span_attrs(base, &local.attrs)
348+
}
349+
StmtKind::Item(item) => {
350+
// Same fallback for items whose wrapper stmt lost its span.
351+
let base = if self.span == DUMMY_SP {
352+
item.span
353+
} else {
354+
self.span
355+
};
356+
extend_span_attrs(base, &item.attrs)
357+
}
358+
StmtKind::Expr(expr) | StmtKind::Semi(expr) => {
359+
// Expression statements carry attrs/comments on the child `Expr`. Use that span when
360+
// the wrapper is dummy so `extend_span_attrs` still captures outer attributes and any
361+
// trailing comments during rewrites.
362+
let base = if self.span == DUMMY_SP {
363+
expr.span
364+
} else {
365+
self.span
366+
};
367+
extend_span_attrs(base, &expr.attrs)
368+
}
369+
StmtKind::MacCall(mac) => extend_span_attrs(self.span, &mac.attrs),
370+
StmtKind::Empty => self.span,
371+
}
256372
}
257373
}
258374

@@ -675,6 +791,27 @@ where
675791
T: PrintParse + RecoverChildren + Splice + MaybeGetNodeId,
676792
{
677793
let printed = add_comments(new.to_string(), new, &rcx);
794+
let mut printed = printed;
795+
if printed.trim().is_empty() {
796+
// When the statement wrapper has DUMMY_SP the pretty printer outputs nothing even though the
797+
// original source had a full `let`. Pull the old snippet (which still contains the attrs/body)
798+
// so `parse()` sees valid syntax and the reparsed AST matches what we’re trying to insert.
799+
if let Ok(snippet) = rcx
800+
.session()
801+
.source_map()
802+
.span_to_snippet(new.splice_span())
803+
{
804+
if !snippet.trim().is_empty() {
805+
printed = snippet;
806+
}
807+
}
808+
if printed.trim().is_empty() {
809+
debug!(
810+
"rewrite_at_impl: empty printed text {:?} for {:?}",
811+
printed, new
812+
);
813+
}
814+
}
678815
let reparsed = T::parse(rcx.session(), &printed);
679816
let reparsed = reparsed.ast_deref();
680817

0 commit comments

Comments
 (0)