1use std::path::Path;
2
3use lib_ruby_parser::{ErrorLevel, Parser};
4
5#[derive(Debug, Clone)]
7pub struct Replacement {
8 pub start: usize,
9 pub end: usize,
10 pub text: String,
11}
12
13#[derive(Debug, Clone)]
15pub struct Fix {
16 pub replacements: Vec<Replacement>,
17}
18
19impl Fix {
20 pub fn single(start: usize, end: usize, text: impl Into<String>) -> Self {
21 Self {
22 replacements: vec![Replacement {
23 start,
24 end,
25 text: text.into(),
26 }],
27 }
28 }
29
30 pub fn two(
31 start1: usize,
32 end1: usize,
33 text1: impl Into<String>,
34 start2: usize,
35 end2: usize,
36 text2: impl Into<String>,
37 ) -> Self {
38 Self {
39 replacements: vec![
40 Replacement {
41 start: start1,
42 end: end1,
43 text: text1.into(),
44 },
45 Replacement {
46 start: start2,
47 end: end2,
48 text: text2.into(),
49 },
50 ],
51 }
52 }
53}
54
55pub fn apply_fixes(source: &[u8], fixes: &[Fix]) -> Vec<u8> {
59 let mut replacements: Vec<&Replacement> = fixes.iter().flat_map(|f| &f.replacements).collect();
61
62 replacements.sort_by(|a, b| b.start.cmp(&a.start));
63
64 let mut result = source.to_vec();
65 let mut last_start = usize::MAX;
66
67 for r in &replacements {
68 if r.end > last_start || r.start > result.len() || r.end > result.len() {
70 continue;
71 }
72 result.splice(r.start..r.end, r.text.bytes());
73 last_start = r.start;
74 }
75
76 result
77}
78
79pub fn verify_syntax(source: &[u8]) -> bool {
81 let result = Parser::new(source.to_vec(), Default::default()).do_parse();
82 !result
83 .diagnostics
84 .iter()
85 .any(|d| d.level == ErrorLevel::Error)
86}
87
88pub fn apply_fixes_to_file(path: &Path, fixes: &[Fix]) -> Result<usize, String> {
91 if fixes.is_empty() {
92 return Ok(0);
93 }
94
95 let source =
96 std::fs::read(path).map_err(|e| format!("Failed to read {}: {}", path.display(), e))?;
97 let fixed = apply_fixes(&source, fixes);
98
99 if !verify_syntax(&fixed) {
100 return Err(format!(
101 "Fix would produce invalid syntax in {}; skipping",
102 path.display()
103 ));
104 }
105
106 std::fs::write(path, &fixed)
107 .map_err(|e| format!("Failed to write {}: {}", path.display(), e))?;
108
109 Ok(fixes.len())
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115
116 #[test]
117 fn single_replacement() {
118 let source = b"hello world";
119 let fix = Fix::single(6, 11, "rust");
120 let result = apply_fixes(source, &[fix]);
121 assert_eq!(result, b"hello rust");
122 }
123
124 #[test]
125 fn multiple_non_overlapping() {
126 let source = b"foo.bar.baz";
127 let fixes = vec![Fix::single(0, 3, "qux"), Fix::single(8, 11, "quux")];
128 let result = apply_fixes(source, &fixes);
129 assert_eq!(result, b"qux.bar.quux");
130 }
131
132 #[test]
133 fn overlapping_skipped() {
134 let source = b"abcdefgh";
135 let fixes = vec![
136 Fix::single(2, 6, "XX"), Fix::single(4, 8, "YY"), ];
139 let result = apply_fixes(source, &fixes);
144 assert_eq!(result, b"abcdYY");
145 }
146
147 #[test]
148 fn verify_valid_ruby() {
149 assert!(verify_syntax(b"x = 1 + 2"));
150 }
151
152 #[test]
153 fn verify_invalid_ruby() {
154 assert!(!verify_syntax(b"def def def"));
155 }
156
157 #[test]
158 fn two_replacements_in_one_fix() {
159 let source = b"arr.map { |x| [x] }.flatten(1)";
160 let fix = Fix::two(
162 4, 7, "flat_map", 19, 30, "", );
165 let result = apply_fixes(source, &[fix]);
166 assert_eq!(result, b"arr.flat_map { |x| [x] }");
167 }
168
169 #[test]
170 fn apply_fixes_empty_fixes() {
171 let source = b"hello world";
172 let result = apply_fixes(source, &[]);
173 assert_eq!(result, source);
174 }
175
176 #[test]
177 fn apply_fixes_out_of_bounds_skipped() {
178 let source = b"short";
179 let fix = Fix::single(10, 20, "big");
180 let result = apply_fixes(source, &[fix]);
181 assert_eq!(result, b"short");
182 }
183
184 #[test]
185 fn apply_fixes_to_file_no_fixes() {
186 let dir = tempfile::TempDir::new().unwrap();
187 let file = dir.path().join("test.rb");
188 std::fs::write(&file, "x = 1").unwrap();
189 let result = apply_fixes_to_file(&file, &[]).unwrap();
190 assert_eq!(result, 0);
191 }
192
193 #[test]
194 fn apply_fixes_to_file_valid_fix() {
195 let dir = tempfile::TempDir::new().unwrap();
196 let file = dir.path().join("test.rb");
197 std::fs::write(&file, "for x in [1]; end").unwrap();
198 let fix = Fix::single(0, 14, "[1].each do |x|;");
200 let result = apply_fixes_to_file(&file, &[fix]).unwrap();
201 assert_eq!(result, 1);
202 }
203
204 #[test]
205 fn apply_fixes_to_file_syntax_error_skipped() {
206 let dir = tempfile::TempDir::new().unwrap();
207 let file = dir.path().join("test.rb");
208 std::fs::write(&file, "x = 1 + 2").unwrap();
209 let fix = Fix::single(0, 9, "def def def");
211 let result = apply_fixes_to_file(&file, &[fix]);
212 assert!(result.is_err());
213 }
214
215 #[test]
216 fn apply_fixes_to_file_nonexistent_file() {
217 let fix = Fix::single(0, 3, "x");
218 let result = apply_fixes_to_file(Path::new("/nonexistent.rb"), &[fix]);
219 assert!(result.is_err());
220 }
221}