1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
//! Verbatim source slicing: map `proc_macro2::Span` (line/column) back to exact
//! original source byte ranges so split items can be emitted byte-for-byte,
//! preserving inline `//` comments and original formatting that `prettyplease`
//! would otherwise strip or reflow.
// `span_slice` / `item_verbatim` are part of this module's public API and are
// exercised by the unit tests, but the binary target currently only calls
// `item_verbatim_with_indent` / `impl_header_verbatim`. The binary recompiles
// these modules directly (separate from the lib target), so the unused public
// methods would emit `dead_code` on the bin build. Mirror the `file_analyzer`
// pattern and allow it: these items are an intentional shared internal API.
#![allow(dead_code)]
use proc_macro2::{LineColumn, Span};
/// Index over the original source enabling (line, column) -> byte-offset mapping
/// and exact slice extraction for spans.
pub struct SourceMap<'a> {
source: &'a str,
/// Byte offset at which each line starts. `line_starts[0]` == 0 (line 1).
line_starts: Vec<usize>,
}
impl<'a> SourceMap<'a> {
/// Build a line-offset index over `source`. Line 1 starts at byte 0; each
/// subsequent entry is the byte offset immediately AFTER a `\n`.
pub fn new(source: &'a str) -> Self {
let mut line_starts = vec![0usize];
for (i, b) in source.bytes().enumerate() {
if b == b'\n' {
line_starts.push(i + 1);
}
}
Self {
source,
line_starts,
}
}
/// Convert a 1-based line / 0-based UTF-8 *char* column to a byte offset.
/// Walks `column` chars from the line start so multibyte chars are handled.
/// Returns `None` if the line is out of range. Clamps a column past
/// end-of-line to the line's end (defensive; proc-macro2 columns are valid).
fn line_col_to_byte(&self, line: usize, column: usize) -> Option<usize> {
if line == 0 || line > self.line_starts.len() {
return None;
}
let line_start = self.line_starts[line - 1];
// Determine this line's exclusive end (start of next line, or source end).
let line_end = self
.line_starts
.get(line) // next line's start
.copied()
.unwrap_or(self.source.len());
let line_slice = &self.source[line_start..line_end];
// Walk `column` chars.
let mut byte = line_start;
let mut chars = line_slice.char_indices();
for _ in 0..column {
match chars.next() {
Some((_, c)) => byte += c.len_utf8(),
None => return Some(line_end), // clamp
}
}
Some(byte)
}
/// Exact source slice for `span` (start..end), or `None` if either endpoint
/// maps out of range. Uses `Span::start()`/`Span::end()` (real `LineColumn`
/// when `span-locations` is enabled).
pub fn span_slice(&self, span: Span) -> Option<&'a str> {
let start = span.start();
let end = span.end();
let start_byte = self.line_col_to_byte(start.line, start.column)?;
let end_byte = self.line_col_to_byte(end.line, end.column)?;
if start_byte > end_byte || end_byte > self.source.len() {
return None;
}
Some(&self.source[start_byte..end_byte])
}
/// Earliest start `LineColumn` among an item's own span start and all its
/// attributes' span starts. Captures leading `#[...]` and `///` doc comments
/// (attributes ARE part of the item and carry spans), which begin BEFORE the
/// item's main token span.
fn earliest_start(item_span: Span, attrs: &[syn::Attribute]) -> LineColumn {
use syn::spanned::Spanned;
let mut earliest = item_span.start();
for attr in attrs {
let s = attr.span().start();
if (s.line, s.column) < (earliest.line, earliest.column) {
earliest = s;
}
}
earliest
}
/// Verbatim slice for an item INCLUDING its leading attributes/doc comments,
/// from the earliest attribute/item start through the item span end (closing
/// brace). Inner `//` comments fall naturally within the braces.
///
/// NOTE: starts at the first non-whitespace token column, so the FIRST line's
/// leading indentation is not included. Use [`Self::item_verbatim_with_indent`]
/// when you need the original indentation preserved (e.g. emitting methods
/// inside a re-synthesised `impl` block).
pub fn item_verbatim(&self, item_span: Span, attrs: &[syn::Attribute]) -> Option<&'a str> {
let start = Self::earliest_start(item_span, attrs);
let end = item_span.end();
let start_byte = self.line_col_to_byte(start.line, start.column)?;
let end_byte = self.line_col_to_byte(end.line, end.column)?;
if start_byte > end_byte || end_byte > self.source.len() {
return None;
}
Some(&self.source[start_byte..end_byte])
}
/// Like [`Self::item_verbatim`] but starts at the BEGINNING of the line the
/// earliest token sits on, preserving that line's original leading
/// indentation. Subsequent lines already retain their indentation (they are
/// mid-slice). Used for methods/trait-impls so they keep original layout.
pub fn item_verbatim_with_indent(
&self,
item_span: Span,
attrs: &[syn::Attribute],
) -> Option<&'a str> {
let start = Self::earliest_start(item_span, attrs);
if start.line == 0 || start.line > self.line_starts.len() {
return None;
}
let start_byte = self.line_starts[start.line - 1]; // include leading indentation
let end = item_span.end();
let end_byte = self.line_col_to_byte(end.line, end.column)?;
if start_byte > end_byte || end_byte > self.source.len() {
return None;
}
Some(&self.source[start_byte..end_byte])
}
/// Verbatim text of an impl block's header: from the impl's earliest start
/// (incl. attributes) up to AND INCLUDING the opening `{`. Lets callers wrap
/// verbatim method bodies in the byte-faithful original `impl ... {` line.
pub fn impl_header_verbatim(&self, item: &syn::ItemImpl) -> Option<String> {
use syn::spanned::Spanned;
let start = Self::earliest_start(item.span(), &item.attrs);
let start_byte = self.line_col_to_byte(start.line, start.column)?;
// Opening-brace location via the brace token's delimiter span.
let brace_open = item.brace_token.span.open();
let bo = brace_open.start();
let brace_open_byte = self.line_col_to_byte(bo.line, bo.column)?;
// `brace_open_byte` points AT the `{`; include it (ASCII `{` is 1 byte).
let end = brace_open_byte + 1;
if start_byte > end || end > self.source.len() {
return None;
}
Some(self.source[start_byte..end].to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
use syn::spanned::Spanned;
#[test]
fn test_line_starts() {
// "ab\ncd\n\nef" -> lines start at 0, 3, 6, 7
let sm = SourceMap::new("ab\ncd\n\nef");
assert_eq!(sm.line_starts, vec![0, 3, 6, 7]);
}
#[test]
fn test_line_col_to_byte_ascii() {
let src = "abc\ndef\n";
let sm = SourceMap::new(src);
// line 1 col 0 -> 0 ; line 1 col 2 -> 2 ; line 2 col 0 -> 4 ; line 2 col 1 -> 5
assert_eq!(sm.line_col_to_byte(1, 0), Some(0));
assert_eq!(sm.line_col_to_byte(1, 2), Some(2));
assert_eq!(sm.line_col_to_byte(2, 0), Some(4));
assert_eq!(sm.line_col_to_byte(2, 1), Some(5));
assert_eq!(sm.line_col_to_byte(0, 0), None);
assert_eq!(sm.line_col_to_byte(99, 0), None);
}
#[test]
fn test_line_col_to_byte_multibyte() {
// line 2 is "αβ" — each Greek letter is 2 bytes in UTF-8.
let src = "a\nαβ\nz";
let sm = SourceMap::new(src);
// line 2 starts after "a\n" = byte 2.
// col 0 -> byte 2 ; col 1 -> byte 2 + 2 = 4 ; col 2 -> byte 6 (end of αβ).
assert_eq!(sm.line_col_to_byte(2, 0), Some(2));
assert_eq!(sm.line_col_to_byte(2, 1), Some(4));
assert_eq!(sm.line_col_to_byte(2, 2), Some(6));
// verify the slice is exactly "αβ"
let (a, b) = (
sm.line_col_to_byte(2, 0).expect("start"),
sm.line_col_to_byte(2, 2).expect("end"),
);
assert_eq!(&src[a..b], "αβ");
}
#[test]
fn test_item_verbatim_preserves_doc_and_inner_comment() {
let src = "/// outer doc\npub fn f() {\n // inner note\n let x = 1;\n}\n";
let item: syn::ItemFn = syn::parse_str(src).expect("parse ItemFn");
let sm = SourceMap::new(src);
let slice = sm
.item_verbatim(item.span(), &item.attrs)
.expect("verbatim slice");
assert!(slice.contains("/// outer doc"), "missing doc: {slice:?}");
assert!(slice.contains("// inner note"), "missing inner: {slice:?}");
assert!(slice.contains("let x = 1;"), "missing body: {slice:?}");
}
#[test]
fn test_item_verbatim_with_indent_preserves_indentation() {
// The fn is indented 4 spaces. parse_str on a single indented fn keeps
// spans relative to THIS string, so the indent precedes col-of-`pub`.
let src = " pub fn f() {\n let x = 1;\n }\n";
let item: syn::ItemFn = syn::parse_str(src).expect("parse ItemFn");
let sm = SourceMap::new(src);
let with_indent = sm
.item_verbatim_with_indent(item.span(), &item.attrs)
.expect("indented slice");
assert!(
with_indent.starts_with(" pub fn f()"),
"indentation not preserved: {with_indent:?}"
);
// The non-indent variant should NOT start with spaces.
let no_indent = sm.item_verbatim(item.span(), &item.attrs).expect("slice");
assert!(no_indent.starts_with("pub fn f()"), "got: {no_indent:?}");
}
#[test]
fn test_span_slice_exact() {
let src = "pub fn g() {}\n";
let item: syn::ItemFn = syn::parse_str(src).expect("parse");
let sm = SourceMap::new(src);
let slice = sm.span_slice(item.span()).expect("slice");
assert_eq!(slice, "pub fn g() {}");
}
#[test]
fn test_impl_header_verbatim() {
let src = "impl Foo {\n fn a(&self) {}\n}\n";
let item: syn::ItemImpl = syn::parse_str(src).expect("parse impl");
let sm = SourceMap::new(src);
let header = sm.impl_header_verbatim(&item).expect("header");
assert_eq!(header, "impl Foo {");
}
}