1#![warn(missing_docs)]
23#![allow(
24 clippy::cargo_common_metadata,
25 reason = "workspace-wide dependency-graph check, not something a single-crate pass can fix or meaningfully scope"
26)]
27
28use proc_macro2::Span;
29use syn::parse::Parser;
30use syn::{Attribute, Item};
31
32#[derive(Debug, Clone)]
35pub struct SourceEdit {
36 pub start: usize,
38 pub end: usize,
40 pub replacement: String,
42}
43
44#[must_use]
50pub fn byte_offset(source: &str, line: usize, column: usize) -> usize {
51 let mut offset = 0usize;
52 for (idx, line_str) in source.split('\n').enumerate() {
53 if idx + 1 == line {
54 let prefix_len: usize = line_str.chars().take(column).map(char::len_utf8).sum();
55 return offset + prefix_len;
56 }
57 offset += line_str.len() + 1; }
59 source.len()
60}
61
62#[must_use]
64pub fn span_byte_range(source: &str, span: Span) -> (usize, usize) {
65 let start = span.start();
66 let end = span.end();
67 (
68 byte_offset(source, start.line, start.column),
69 byte_offset(source, end.line, end.column),
70 )
71}
72
73#[must_use]
77pub fn print_item(item: &Item) -> String {
78 let file = syn::File {
79 shebang: None,
80 attrs: Vec::new(),
81 items: vec![item.clone()],
82 };
83 prettyplease::unparse(&file)
84 .trim_end_matches('\n')
85 .to_string()
86}
87
88pub fn parse_attribute(text: &str) -> syn::Result<Attribute> {
101 let mut attrs = Attribute::parse_outer.parse_str(text)?;
102 if attrs.len() != 1 {
103 return Err(syn::Error::new(
104 Span::call_site(),
105 format!("expected exactly one attribute, got {}", attrs.len()),
106 ));
107 }
108 Ok(attrs.remove(0))
109}
110
111#[must_use]
120pub fn apply_edits(source: &str, edits: &[SourceEdit]) -> String {
121 let mut ordered: Vec<&SourceEdit> = edits.iter().collect();
122 ordered.sort_by(|a, b| b.start.cmp(&a.start));
123
124 let mut result = source.to_string();
125 for edit in ordered {
126 result.replace_range(edit.start..edit.end, &edit.replacement);
127 }
128 result
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134 use syn::spanned::Spanned;
135
136 #[test]
137 fn byte_offset_finds_start_of_a_later_line() {
138 let source = "fn a() {}\nfn b() {}\n";
139 assert_eq!(byte_offset(source, 2, 0), 10);
140 }
141
142 #[test]
143 fn byte_offset_handles_a_mid_line_column() {
144 let source = "fn a() {}\nfn b() {}\n";
145 assert_eq!(byte_offset(source, 2, 3), 13);
146 }
147
148 #[test]
149 fn span_byte_range_covers_exactly_the_item_text() {
150 let source = "fn a() {}\nfn b() {}\n";
151 let file: syn::File = syn::parse_str(source).unwrap();
152 let second = &file.items[1];
153 let (start, end) = span_byte_range(source, second.span());
154 assert_eq!(&source[start..end], "fn b() {}");
155 }
156
157 #[test]
158 fn print_item_renders_valid_rust_with_no_trailing_blank_line() {
159 let item: Item = syn::parse_quote! {
160 fn hello() {
161 println!("hi");
162 }
163 };
164 let rendered = print_item(&item);
165 assert!(rendered.starts_with("fn hello"));
166 assert!(!rendered.ends_with('\n'));
167 }
168
169 #[test]
170 fn parse_attribute_parses_a_single_outer_attribute() {
171 let attr = parse_attribute("#[capability(alloc(none), io(none), ptr(none))]").unwrap();
172 assert!(attr.path().is_ident("capability"));
173 }
174
175 #[test]
176 fn parse_attribute_rejects_malformed_text() {
177 assert!(parse_attribute("not an attribute").is_err());
178 }
179
180 #[test]
181 fn apply_edits_replaces_the_targeted_item_and_nothing_else() {
182 let source = "fn a() {}\nfn b() {}\nfn c() {}\n";
183 let file: syn::File = syn::parse_str(source).unwrap();
184 let (start, end) = span_byte_range(source, file.items[1].span());
185 let rewritten = apply_edits(
186 source,
187 &[SourceEdit {
188 start,
189 end,
190 replacement: "fn b() { /* patched */ }".to_string(),
191 }],
192 );
193 assert_eq!(
194 rewritten,
195 "fn a() {}\nfn b() { /* patched */ }\nfn c() {}\n"
196 );
197 }
198
199 #[test]
200 fn apply_edits_handles_multiple_disjoint_edits_in_one_pass() {
201 let source = "fn a() {}\nfn b() {}\nfn c() {}\n";
202 let file: syn::File = syn::parse_str(source).unwrap();
203 let (a_start, a_end) = span_byte_range(source, file.items[0].span());
204 let (c_start, c_end) = span_byte_range(source, file.items[2].span());
205 let rewritten = apply_edits(
206 source,
207 &[
208 SourceEdit {
209 start: a_start,
210 end: a_end,
211 replacement: "fn a() { /* one */ }".to_string(),
212 },
213 SourceEdit {
214 start: c_start,
215 end: c_end,
216 replacement: "fn c() { /* two */ }".to_string(),
217 },
218 ],
219 );
220 assert_eq!(
221 rewritten,
222 "fn a() { /* one */ }\nfn b() {}\nfn c() { /* two */ }\n"
223 );
224 }
225}