Maintain/Eliminate/Transform/
Patch.rs1use proc_macro2::Span;
25use quote::ToTokens;
26use syn::{Expr, Stmt, visit::Visit};
27
28#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct Patch {
35 pub Start:usize,
37
38 pub End:usize,
40
41 pub Replacement:String,
43}
44
45pub fn ApplyPatches(Source:&str, Patches:&[Patch]) -> Option<String> {
56 if Patches.is_empty() {
57 return Some(Source.to_string());
58 }
59
60 let Bytes = Source.as_bytes();
61
62 let mut Result = String::with_capacity(Source.len());
63
64 let mut Cursor = 0usize;
65
66 for P in Patches {
67 if P.Start > P.End || P.End > Bytes.len() {
68 return None;
69 }
70
71 if P.Start < Cursor {
72 return None;
74 }
75
76 Result.push_str(&Source[Cursor..P.Start]);
78
79 Result.push_str(&P.Replacement);
81
82 Cursor = P.End;
83 }
84
85 Result.push_str(&Source[Cursor..]);
87
88 Some(Result)
89}
90
91pub fn SpanBytes(Span:Span, Source:&str) -> Option<(usize, usize)> {
97 let Start = Span.start();
98
99 let End = Span.end();
100
101 let StartByte = LineColToByte(Source, Start.line, Start.column)?;
103
104 let EndByte = LineColToByte(Source, End.line, End.column)?;
105
106 if StartByte == 0 && EndByte == 0 {
107 return None;
109 }
110
111 Some((StartByte, EndByte))
112}
113
114pub fn LineColToByte(Source:&str, Line:usize, Col:usize) -> Option<usize> {
117 if Line == 0 {
118 return None;
119 }
120
121 let mut CurrentLine = 1usize;
122
123 let mut LineStart = 0usize;
124
125 for (ByteIdx, Ch) in Source.char_indices() {
126 if CurrentLine == Line {
127 LineStart = ByteIdx;
128
129 break;
130 }
131
132 if Ch == '\n' {
133 CurrentLine += 1;
134 }
135 }
136
137 if CurrentLine != Line {
138 return None;
139 }
140
141 let mut ByteOffset = LineStart;
143
144 for _ in 0..Col {
145 if ByteOffset >= Source.len() {
146 return None;
147 }
148
149 let Ch = Source[ByteOffset..].chars().next()?;
150
151 ByteOffset += Ch.len_utf8();
152 }
153
154 Some(ByteOffset)
155}
156
157pub fn StmtLineRange(Stmt:&Stmt, Source:&str) -> Option<(usize, usize)> {
160 let LocalSpan = StmtSpan(Stmt)?;
161
162 let (Start, End) = SpanBytes(LocalSpan, Source)?;
163
164 let Bytes = Source.as_bytes();
166
167 let mut LineEnd = End;
168
169 while LineEnd < Bytes.len() && Bytes[LineEnd] != b'\n' {
170 LineEnd += 1;
171 }
172
173 if LineEnd < Bytes.len() && Bytes[LineEnd] == b'\n' {
174 LineEnd += 1;
175 }
176
177 let mut LineStart = Start;
180
181 while LineStart > 0 && Bytes[LineStart - 1] != b'\n' {
182 LineStart -= 1;
183 }
184
185 Some((LineStart, LineEnd))
186}
187
188fn StmtSpan(Stmt:&Stmt) -> Option<Span> {
193 let mut Tokens = proc_macro2::TokenStream::new();
194
195 Stmt.to_tokens(&mut Tokens);
196
197 let Trees:Vec<_> = Tokens.into_iter().collect();
198
199 if Trees.is_empty() {
200 return None;
201 }
202
203 let First = Trees.first()?.span();
204
205 let Last = Trees.last()?.span();
206
207 First.join(Last)
208}
209
210pub struct SpanCollector<'a> {
213 pub Target:&'a str,
214
215 pub Spans:Vec<Span>,
216}
217
218impl<'a, 'ast> Visit<'ast> for SpanCollector<'a> {
219 fn visit_expr_path(&mut self, Node:&'ast syn::ExprPath) {
220 if Node.qself.is_none() {
221 if let Some(I) = Node.path.get_ident() {
222 if I == self.Target {
223 self.Spans.push(I.span());
224 }
225 }
226 }
227 }
228}
229
230#[cfg(test)]
235mod Tests {
236
237 use super::*;
238
239 #[test]
240 fn ApplyNoPatches() {
241 let Src = "hello world";
242
243 assert_eq!(ApplyPatches(Src, &[]).unwrap(), Src);
244 }
245
246 #[test]
247 fn ApplySingleDelete() {
248 let Src = "abc def ghi";
249
250 let P = Patch { Start:4, End:8, Replacement:String::new() };
251
252 assert_eq!(ApplyPatches(Src, &[P]).unwrap(), "abc ghi");
253 }
254
255 #[test]
256 fn ApplySingleReplace() {
257 let Src = "let X = 5;\nuse_x(X);";
258
259 let P = Patch { Start:16, End:17, Replacement:"5".to_string() };
261
262 assert_eq!(ApplyPatches(Src, &[P]).unwrap(), "let X = 5;\nuse_x(5);");
263 }
264
265 #[test]
266 fn ApplyTwoPatches() {
267 let Src = "AABBCC";
268
269 let Patches = vec![
270 Patch { Start:0, End:2, Replacement:"XX".to_string() },
271 Patch { Start:4, End:6, Replacement:"YY".to_string() },
272 ];
273
274 assert_eq!(ApplyPatches(Src, &Patches).unwrap(), "XXBBYY");
275 }
276
277 #[test]
278 fn OverlappingPatchesReturnNone() {
279 let Src = "ABCDE";
280
281 let Patches = vec![
282 Patch { Start:1, End:4, Replacement:"X".to_string() },
283 Patch { Start:3, End:5, Replacement:"Y".to_string() },
284 ];
285
286 assert!(ApplyPatches(Src, &Patches).is_none());
287 }
288
289 #[test]
290 fn OutOfBoundsPatchReturnsNone() {
291 let Src = "ABC";
292
293 let P = Patch { Start:2, End:10, Replacement:String::new() };
294
295 assert!(ApplyPatches(Src, &[P]).is_none());
296 }
297
298 #[test]
299 fn LineColToByteFirstLine() {
300 let Src = "hello\nworld";
301
302 assert_eq!(LineColToByte(Src, 1, 0), Some(0));
303
304 assert_eq!(LineColToByte(Src, 1, 5), Some(5));
305 }
306
307 #[test]
308 fn LineColToByteSecondLine() {
309 let Src = "hello\nworld";
310
311 assert_eq!(LineColToByte(Src, 2, 0), Some(6));
312
313 assert_eq!(LineColToByte(Src, 2, 5), Some(11));
314 }
315
316 #[test]
317 fn LineColToByteOutOfRange() {
318 let Src = "hi";
319
320 assert_eq!(LineColToByte(Src, 5, 0), None);
321 }
322}