1use crate::error::{Applicability, CompileError};
21use crate::span::Span;
22
23pub fn lone_fix(error: &CompileError) -> Option<&crate::error::Suggestion> {
27 let mut applicable = error
28 .suggestions
29 .iter()
30 .filter(|s| s.applicability == Applicability::MachineApplicable);
31 match (applicable.next(), applicable.next()) {
32 (Some(s), None) => Some(s),
33 _ => None,
34 }
35}
36
37#[derive(Debug, Clone, Default, PartialEq, Eq)]
40pub struct Selection {
41 pub edits: Vec<(Span, String)>,
43 pub categories: Vec<&'static str>,
44}
45
46pub fn edits<'a>(
51 text: &str,
52 errors: impl IntoIterator<Item = &'a CompileError>,
53) -> Vec<(Span, String)> {
54 select(text, errors).edits
55}
56
57pub fn select<'a>(text: &str, errors: impl IntoIterator<Item = &'a CompileError>) -> Selection {
59 let mut taken: Vec<(Span, String)> = Vec::new();
60 let mut categories = Vec::new();
61 for e in errors {
62 let Some(s) = lone_fix(e) else {
63 continue; };
65 if !s.edits.iter().all(|(span, _)| valid(text, *span)) {
66 continue;
67 }
68 let fresh: Vec<&(Span, String)> = s
69 .edits
70 .iter()
71 .filter(|e| !taken.iter().any(|t| t.0 == e.0 && t.1 == e.1))
72 .collect();
73 if fresh
74 .iter()
75 .any(|e| taken.iter().any(|t| overlaps(t.0, e.0)))
76 || fresh
77 .iter()
78 .enumerate()
79 .any(|(i, a)| fresh[i + 1..].iter().any(|b| overlaps(a.0, b.0)))
80 {
81 continue;
82 }
83 if !fresh.is_empty() {
84 categories.push(e.category);
85 }
86 taken.extend(fresh.into_iter().cloned());
87 }
88 taken.sort_by_key(|(span, _)| (span.start, span.end));
89 Selection {
90 edits: taken,
91 categories,
92 }
93}
94
95fn overlaps(a: Span, b: Span) -> bool {
99 if a.start == a.end && b.start == b.end {
100 return a.start == b.start;
101 }
102 a.start < b.end && b.start < a.end
103}
104
105fn valid(text: &str, span: Span) -> bool {
107 span.start <= span.end && text.get(span.start..span.end).is_some()
108}
109
110pub fn apply(text: &str, edits: &[(Span, String)]) -> String {
114 let mut out = String::with_capacity(text.len());
115 let mut last = 0;
116 for (span, new_text) in edits {
117 let Some(before) = text.get(last..span.start) else {
118 continue;
119 };
120 if !valid(text, *span) {
121 continue;
122 }
123 out.push_str(before);
124 out.push_str(new_text);
125 last = span.end;
126 }
127 out.push_str(&text[last..]);
128 out
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134
135 type Fix<'a> = (&'a [((usize, usize), &'a str)], Applicability);
137
138 fn diag(span: (usize, usize), fixes: &[Fix]) -> CompileError {
139 let mut error = CompileError::new("bynk.test", Span::new(span.0, span.1), "msg");
140 for (edits, applicability) in fixes {
141 error = error.with_suggestion(
142 "fix",
143 edits
144 .iter()
145 .map(|((s, e), t)| (Span::new(*s, *e), t.to_string()))
146 .collect(),
147 *applicability,
148 );
149 }
150 error
151 }
152
153 const TEXT: &str = "0123456789012345678901234567890123456789";
155 const M: Applicability = Applicability::MachineApplicable;
156 const P: Applicability = Applicability::HasPlaceholders;
157
158 #[test]
159 fn takes_every_lone_machine_applicable_suggestion_in_position_order() {
160 let ds = [
161 diag((10, 12), &[(&[((10, 12), "b")], M)]),
162 diag((0, 2), &[(&[((0, 2), "a")], M)]),
163 ];
164 assert_eq!(
165 edits(TEXT, &ds),
166 vec![
167 (Span::new(0, 2), "a".to_string()),
168 (Span::new(10, 12), "b".to_string())
169 ]
170 );
171 }
172
173 #[test]
174 fn skips_placeholders_and_choices() {
175 let ds = [
176 diag((0, 2), &[(&[((0, 2), "<T>")], P)]),
177 diag((5, 6), &[(&[((5, 6), "x")], M), (&[((5, 6), "y")], M)]),
179 diag((8, 9), &[(&[((8, 9), "z")], M), (&[((8, 9), "<T>")], P)]),
181 ];
182 assert_eq!(edits(TEXT, &ds), vec![(Span::new(8, 9), "z".to_string())]);
183 }
184
185 #[test]
186 fn an_overlapping_suggestion_is_skipped_whole() {
187 let ds = [
188 diag((0, 4), &[(&[((0, 4), "a")], M)]),
189 diag((10, 12), &[(&[((10, 12), "b"), ((2, 3), "c")], M)]),
192 ];
193 assert_eq!(edits(TEXT, &ds), vec![(Span::new(0, 4), "a".to_string())]);
194 }
195
196 #[test]
197 fn an_identical_edit_is_taken_once() {
198 let ds = [
201 diag((20, 24), &[(&[((14, 14), ", Used")], M)]),
202 diag((30, 34), &[(&[((14, 14), ", Used")], M)]),
203 ];
204 assert_eq!(
205 edits(TEXT, &ds),
206 vec![(Span::new(14, 14), ", Used".to_string())]
207 );
208 }
209
210 #[test]
211 fn different_insertions_at_one_point_conflict() {
212 let ds = [
213 diag((20, 24), &[(&[((14, 14), ", A")], M)]),
214 diag((30, 34), &[(&[((14, 14), ", B")], M)]),
215 ];
216 assert_eq!(
217 edits(TEXT, &ds),
218 vec![(Span::new(14, 14), ", A".to_string())]
219 );
220 }
221
222 #[test]
223 fn touching_edits_do_not_conflict() {
224 let ds = [
225 diag((0, 2), &[(&[((0, 2), "a")], M)]),
226 diag((2, 4), &[(&[((2, 4), "b")], M)]),
227 diag((4, 4), &[(&[((4, 4), "c")], M)]),
228 ];
229 assert_eq!(edits(TEXT, &ds).len(), 3);
230 assert_eq!(apply("0123456", &edits(TEXT, &ds)), "abc456");
231 }
232
233 #[test]
234 fn an_insertion_inside_a_replacement_conflicts() {
235 let ds = [
236 diag((0, 4), &[(&[((0, 4), "a")], M)]),
237 diag((2, 2), &[(&[((2, 2), "b")], M)]),
238 ];
239 assert_eq!(edits(TEXT, &ds), vec![(Span::new(0, 4), "a".to_string())]);
240 }
241
242 #[test]
243 fn a_suggestion_with_a_bad_span_is_skipped() {
244 let ds = [
245 diag((0, 1), &[(&[((38, 99), "x")], M)]),
247 diag((0, 1), &[(&[((5, 3), "y")], M)]),
249 diag((0, 1), &[(&[((0, 1), "z")], M)]),
250 ];
251 assert_eq!(edits(TEXT, &ds), vec![(Span::new(0, 1), "z".to_string())]);
252 let ds = [diag((0, 1), &[(&[((2, 2), "w")], M)])];
254 assert!(edits("aé", &ds).is_empty());
255 }
256
257 #[test]
258 fn apply_skips_a_bad_span_instead_of_panicking() {
259 let bad = [
260 (Span::new(0, 1), "A".to_string()),
261 (Span::new(2, 99), "!".to_string()),
262 ];
263 assert_eq!(apply("abc", &bad), "Abc");
264 assert_eq!(apply("aé", &[(Span::new(2, 2), "w".to_string())]), "aé");
265 }
266
267 #[test]
268 fn nothing_to_fix_is_empty() {
269 assert!(edits(TEXT, &[] as &[CompileError]).is_empty());
270 assert!(edits(TEXT, &[diag((0, 1), &[])]).is_empty());
271 }
272
273 #[test]
274 fn select_names_the_diagnostics_it_took_from() {
275 let ds = [
276 diag((0, 2), &[(&[((0, 2), "a")], M)]),
277 diag((1, 3), &[(&[((1, 3), "b")], M)]),
279 diag((10, 12), &[(&[((10, 12), "c")], M)]),
280 ];
281 let s = select(TEXT, &ds);
282 assert_eq!(s.edits.len(), 2);
283 assert_eq!(s.categories, vec!["bynk.test", "bynk.test"]);
284 }
285
286 #[test]
287 fn lone_fix_is_only_a_single_machine_applicable_suggestion() {
288 assert!(lone_fix(&diag((0, 1), &[(&[((0, 1), "a")], M)])).is_some());
289 assert!(lone_fix(&diag((0, 1), &[(&[((0, 1), "a")], P)])).is_none());
290 assert!(
291 lone_fix(&diag(
292 (0, 1),
293 &[(&[((0, 1), "a")], M), (&[((0, 1), "b")], M)]
294 ))
295 .is_none()
296 );
297 assert!(lone_fix(&diag((0, 1), &[])).is_none());
298 }
299}