1use std::{borrow::Cow, cmp::Ordering, fs};
2
3use anyhow::Result;
4use bon::bon;
5use log::{debug, error, trace};
6use serde::{Deserialize, Serialize};
7
8use crate::{
9 app_error::AppError,
10 context::Context,
11 location::{AdjustedRange, DenormalizedLocation, Offsets},
12 output::LintOutput,
13 rope::Rope,
14 utils::words::{is_sentence_start, WordIterator},
15 Linter,
16};
17
18#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)]
19pub enum LintCorrection {
20 Insert(LintCorrectionInsert),
21 Delete(LintCorrectionDelete),
22 Replace(LintCorrectionReplace),
23}
24
25#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)]
26pub struct LintCorrectionInsert {
27 pub(crate) location: DenormalizedLocation,
29 pub(crate) text: String,
30}
31
32#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)]
33pub struct LintCorrectionDelete {
34 pub(crate) location: DenormalizedLocation,
35}
36
37impl crate::private::Sealed for LintCorrectionDelete {}
39
40impl Offsets for LintCorrectionDelete {
41 fn start(&self) -> usize {
42 self.location.offset_range.start.into()
43 }
44
45 fn end(&self) -> usize {
46 self.location.offset_range.end.into()
47 }
48}
49
50#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)]
51pub struct LintCorrectionReplace {
52 pub(crate) location: DenormalizedLocation,
53 pub(crate) text: String,
54}
55
56impl crate::private::Sealed for LintCorrectionInsert {}
58
59impl Offsets for LintCorrectionInsert {
60 fn start(&self) -> usize {
61 self.location.offset_range.start.into()
62 }
63
64 fn end(&self) -> usize {
65 self.location.offset_range.end.into()
66 }
67}
68
69impl LintCorrectionInsert {
70 pub fn text(&self) -> &str {
71 &self.text
72 }
73}
74
75impl crate::private::Sealed for LintCorrectionReplace {}
77
78impl Offsets for LintCorrectionReplace {
79 fn start(&self) -> usize {
80 self.location.offset_range.start.into()
81 }
82
83 fn end(&self) -> usize {
84 self.location.offset_range.end.into()
85 }
86}
87
88impl LintCorrectionReplace {
89 pub fn text(&self) -> &str {
90 &self.text
91 }
92}
93
94impl PartialOrd for LintCorrection {
95 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
96 Some(self.cmp(other))
97 }
98}
99
100impl Ord for LintCorrection {
101 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
102 match (self, other) {
103 (LintCorrection::Insert(insert_a), LintCorrection::Insert(insert_b)) => {
104 insert_a.location.start.cmp(&insert_b.location.start)
105 }
106 (LintCorrection::Insert(insert), LintCorrection::Delete(delete)) => {
107 if delete.location.start.le(&insert.location.start)
108 && delete.location.end.gt(&insert.location.start)
109 {
110 return Ordering::Equal;
113 }
114
115 delete.location.start.cmp(&insert.location.start)
119 }
120 (LintCorrection::Insert(insert), LintCorrection::Replace(replace)) => {
121 if replace.location.start.le(&insert.location.start)
122 && replace.location.end.gt(&insert.location.start)
123 {
124 return Ordering::Equal;
127 }
128
129 replace.location.start.cmp(&insert.location.start)
133 }
134 (LintCorrection::Delete(_), LintCorrection::Insert(_)) => other.cmp(self).reverse(),
135 (LintCorrection::Delete(delete_a), LintCorrection::Delete(delete_b)) => {
136 let flip = delete_a.location.start.gt(&delete_b.location.start);
137 if flip {
138 return other.cmp(self).reverse();
139 }
140
141 if delete_a.location.end.gt(&delete_b.location.start) {
142 return Ordering::Equal;
146 }
147
148 Ordering::Less
149 }
150 (LintCorrection::Delete(delete), LintCorrection::Replace(replace)) => {
151 let flip = delete.location.start.gt(&replace.location.start);
152 if flip {
153 return other.cmp(self).reverse();
154 }
155
156 if delete.location.end.gt(&replace.location.start) {
157 return Ordering::Equal;
161 }
162
163 Ordering::Less
164 }
165 (LintCorrection::Replace(_), LintCorrection::Insert(_)) => other.cmp(self).reverse(),
166 (LintCorrection::Replace(replace), LintCorrection::Delete(delete)) => {
167 let flip = replace.location.start.gt(&delete.location.start);
168 if flip {
169 return other.cmp(self).reverse();
170 }
171
172 if replace.location.end.gt(&delete.location.start) {
173 return Ordering::Equal;
177 }
178
179 Ordering::Less
180 }
181 (LintCorrection::Replace(replace_a), LintCorrection::Replace(replace_b)) => {
182 let flip = replace_a.location.start.gt(&replace_b.location.start);
183 if flip {
184 return other.cmp(self).reverse();
185 }
186
187 if replace_a.location.end.gt(&replace_b.location.start) {
188 return Ordering::Equal;
192 }
193
194 Ordering::Less
195 }
196 }
197 }
198}
199
200#[bon]
201impl LintCorrection {
202 fn choose_or_merge(self, other: Self) -> Option<Self> {
208 match (self, other) {
209 (LintCorrection::Insert(_), LintCorrection::Insert(_)) => {
210 None
214 }
215 (LintCorrection::Insert(_), LintCorrection::Delete(delete)) => {
216 Some(LintCorrection::Delete(delete))
218 }
219 (LintCorrection::Insert(_), LintCorrection::Replace(replace)) => {
220 Some(LintCorrection::Replace(replace))
222 }
223 (LintCorrection::Delete(delete), LintCorrection::Insert(_)) => {
224 Some(LintCorrection::Delete(delete))
226 }
227 (LintCorrection::Delete(delete_a), LintCorrection::Delete(delete_b)) => {
228 let new_range = AdjustedRange::span_between(
230 &delete_a.location.offset_range,
231 &delete_b.location.offset_range,
232 );
233 let start = if delete_a.location.offset_range.start
234 < delete_b.location.offset_range.start
235 {
236 delete_a.location.start
237 } else {
238 delete_b.location.start
239 };
240 let end = if delete_a.location.offset_range.end > delete_b.location.offset_range.end
241 {
242 delete_a.location.end
243 } else {
244 delete_b.location.end
245 };
246 let location = DenormalizedLocation {
247 offset_range: new_range,
248 start,
249 end,
250 };
251
252 Some(LintCorrection::Delete(LintCorrectionDelete { location }))
253 }
254 (LintCorrection::Delete(delete), LintCorrection::Replace(replace)) => {
255 if delete.location.start.lt(&replace.location.start)
258 && delete.location.end.gt(&replace.location.end)
259 {
260 Some(LintCorrection::Delete(delete))
262 } else if replace.location.start.lt(&delete.location.start)
263 && replace.location.end.gt(&delete.location.end)
264 {
265 Some(LintCorrection::Replace(replace))
267 } else {
268 None
269 }
270 }
271 (LintCorrection::Replace(replace), LintCorrection::Insert(_)) => {
272 Some(LintCorrection::Replace(replace))
274 }
275 (LintCorrection::Replace(replace), LintCorrection::Delete(delete)) => {
276 if delete.location.start.lt(&replace.location.start)
279 && delete.location.end.gt(&replace.location.end)
280 {
281 Some(LintCorrection::Delete(delete))
283 } else if replace.location.start.lt(&delete.location.start)
284 && replace.location.end.gt(&delete.location.end)
285 {
286 Some(LintCorrection::Replace(replace))
288 } else {
289 None
290 }
291 }
292 (LintCorrection::Replace(replace_a), LintCorrection::Replace(replace_b)) => {
293 if replace_b.location.start.lt(&replace_a.location.start)
296 && replace_b.location.end.gt(&replace_a.location.end)
297 {
298 Some(LintCorrection::Replace(replace_b))
300 } else if replace_a.location.start.lt(&replace_b.location.start)
301 && replace_a.location.end.gt(&replace_b.location.end)
302 {
303 Some(LintCorrection::Replace(replace_a))
305 } else {
306 None
307 }
308 }
309 }
310 }
311
312 #[builder]
313 pub(crate) fn create_word_splice_correction(
314 context: &Context<'_>,
315 outer_range: &AdjustedRange,
316 splice_range: &AdjustedRange,
317 #[builder(default = true)] count_beginning_as_sentence_start: bool,
318 replace: Option<Cow<'_, str>>,
319 ) -> Self {
320 let outer_text = context.rope().byte_slice(outer_range.to_usize_range());
321 let is_sentence_start = is_sentence_start()
322 .slice(outer_text)
323 .query_offset(splice_range.start.into_usize() - outer_range.start.into_usize())
324 .count_beginning_as_sentence_start(count_beginning_as_sentence_start)
325 .call();
326
327 let location = DenormalizedLocation::from_offset_range(splice_range.clone(), context);
328
329 match replace {
330 Some(replace) => {
331 let replace = if is_sentence_start {
332 replace.chars().next().unwrap().to_uppercase().to_string() + &replace[1..]
333 } else {
334 replace.to_string()
335 };
336
337 LintCorrection::Replace(LintCorrectionReplace {
338 location,
339 text: replace,
340 })
341 }
342 None => {
343 let mut iter = WordIterator::new(
344 context.rope().byte_slice(splice_range.end.into_usize()..),
345 splice_range.end.into(),
346 Default::default(),
347 );
348
349 if let Some((offset, _, _)) = iter.next() {
350 let mut between = context
351 .rope()
352 .byte_slice(splice_range.end.into()..offset)
353 .chars();
354 if between.all(|c| c.is_whitespace()) {
355 if is_sentence_start {
356 let location = DenormalizedLocation::from_offset_range(
357 AdjustedRange::new(splice_range.start, (offset + 1).into()),
358 context,
359 );
360 LintCorrection::Replace(LintCorrectionReplace {
361 location,
362 text: context
363 .rope()
364 .byte_slice(offset..)
365 .chars()
366 .next()
367 .unwrap()
368 .to_string()
369 .to_uppercase(),
370 })
371 } else {
372 LintCorrection::Delete(LintCorrectionDelete {
373 location: DenormalizedLocation::from_offset_range(
374 AdjustedRange::new(splice_range.start, offset.into()),
375 context,
376 ),
377 })
378 }
379 } else {
380 LintCorrection::Delete(LintCorrectionDelete { location })
381 }
382 } else {
383 LintCorrection::Delete(LintCorrectionDelete { location })
384 }
385 }
386 }
387 }
388}
389
390impl Linter {
391 pub fn fix(&self, diagnostics: &[LintOutput]) -> Result<(usize, usize)> {
395 let mut files_fixed: usize = 0;
396 let mut errors_fixed: usize = 0;
397
398 let fixable_outputs: Vec<&LintOutput> = diagnostics
399 .iter()
400 .filter(|diagnostic| diagnostic.errors().iter().any(|error| error.fix.is_some()))
401 .collect();
402 if fixable_outputs.is_empty() {
403 debug!("No fixable errors found for this set of diagnostics.");
404 trace!("Diagnostics: {:#?}", diagnostics);
405 return Ok((files_fixed, errors_fixed));
406 }
407
408 for diagnostic in fixable_outputs {
409 let local_errors_fixed = Self::fix_single_file(diagnostic).inspect_err(|err| {
410 error!("Error fixing file {}: {}", diagnostic.file_path(), err)
411 })?;
412 errors_fixed += local_errors_fixed;
413 files_fixed += 1;
414 }
415
416 Ok((files_fixed, errors_fixed))
417 }
418
419 fn fix_single_file(diagnostic: &LintOutput) -> Result<usize> {
420 let mut errors_fixed = 0;
421
422 let file = diagnostic.file_path();
423 debug!("Fixing errors in {file}");
424
425 let content = fs::read_to_string(file).map_err(|err| {
426 AppError::FileSystemError(format!("reading file {file} for auto-fixing"), err)
427 })?;
428 let mut rope = Rope::from(content.as_str());
429
430 let fixes_to_apply = Self::calculate_fixes_to_apply(file, diagnostic);
431 debug!("Fixes to apply for file {file}: {fixes_to_apply:#?}");
432
433 for fix in fixes_to_apply {
434 match fix {
435 LintCorrection::Insert(lint_fix_insert) => {
436 rope.insert(
437 lint_fix_insert.location.offset_range.start.into(),
438 lint_fix_insert.text,
439 );
440 errors_fixed += 1;
441 }
442 LintCorrection::Delete(lint_fix_delete) => {
443 let start: usize = lint_fix_delete.location.offset_range.start.into();
444 let end: usize = lint_fix_delete.location.offset_range.end.into();
445 rope.replace(start..end, "");
446 errors_fixed += 1;
447 }
448 LintCorrection::Replace(lint_fix_replace) => {
449 let start: usize = lint_fix_replace.location.offset_range.start.into();
450 let end: usize = lint_fix_replace.location.offset_range.end.into();
451 rope.replace(start..end, lint_fix_replace.text.as_str());
452 errors_fixed += 1;
453 }
454 }
455 }
456
457 let content = rope.to_string();
458 fs::write(diagnostic.file_path(), content).map_err(|err| {
459 AppError::FileSystemError(format!("writing file {file} post-fixing"), err)
460 })?;
461
462 Ok(errors_fixed)
463 }
464
465 fn calculate_fixes_to_apply(file: &str, diagnostic: &LintOutput) -> Vec<LintCorrection> {
466 let mut requested_fixes: Vec<LintCorrection> = diagnostic
467 .errors()
468 .iter()
469 .filter_map(|err| err.fix.clone())
470 .flatten()
471 .collect();
472 requested_fixes.sort();
473 let requested_fixes = requested_fixes.into_iter().rev();
476 debug!("Requested fixes for file {file}: {requested_fixes:#?}");
477
478 let mut fixes_to_apply: Vec<LintCorrection> = Vec::new();
479 for fix in requested_fixes {
480 if let Some(last_scheduled_fix) = fixes_to_apply.last() {
481 if last_scheduled_fix.eq(&fix) {
482 let last_scheduled_fix = fixes_to_apply.pop().unwrap();
485 if let Some(new_fix) = last_scheduled_fix.choose_or_merge(fix) {
486 fixes_to_apply.push(new_fix);
487 }
488 } else {
489 fixes_to_apply.push(fix.clone());
491 }
492 } else {
493 fixes_to_apply.push(fix.clone());
494 }
495 }
496
497 fixes_to_apply
498 }
499}
500
501#[cfg(test)]
502mod tests {
503 use crate::parse;
504
505 use super::*;
506
507 #[test]
508 fn test_create_word_splice_correction_midsentence() {
509 let parsed = parse("Here is a simple sentence.").unwrap();
510 let context = Context::builder().parse_result(&parsed).build().unwrap();
511
512 let outer_range = AdjustedRange::new(0.into(), 26.into());
513 let splice_range = AdjustedRange::new(10.into(), 16.into());
514
515 let expected = LintCorrection::Delete(LintCorrectionDelete {
516 location: DenormalizedLocation::from_offset_range(
517 AdjustedRange::new(10.into(), 17.into()),
518 &context,
519 ),
520 });
521 let actual = LintCorrection::create_word_splice_correction()
522 .context(&context)
523 .outer_range(&outer_range)
524 .splice_range(&splice_range)
525 .call();
526 assert_eq!(expected, actual);
527 }
528
529 #[test]
530 fn test_create_word_splice_correction_midsentence_replace() {
531 let parsed = parse("Here is a simple sentence.").unwrap();
532 let context = Context::builder().parse_result(&parsed).build().unwrap();
533
534 let outer_range = AdjustedRange::new(0.into(), 26.into());
535 let splice_range = AdjustedRange::new(10.into(), 16.into());
536
537 let expected = LintCorrection::Replace(LintCorrectionReplace {
538 text: "lovely".to_string(),
539 location: DenormalizedLocation::from_offset_range(
540 AdjustedRange::new(10.into(), 16.into()),
541 &context,
542 ),
543 });
544 let actual = LintCorrection::create_word_splice_correction()
545 .context(&context)
546 .outer_range(&outer_range)
547 .splice_range(&splice_range)
548 .replace("lovely".into())
549 .call();
550 assert_eq!(expected, actual);
551 }
552
553 #[test]
554 fn test_create_word_splice_correction_new_sentence() {
555 let parsed = parse("What a lovely day. Please take a biscuit.").unwrap();
556 let context = Context::builder().parse_result(&parsed).build().unwrap();
557
558 let outer_range = AdjustedRange::new(0.into(), 41.into());
559 let splice_range = AdjustedRange::new(19.into(), 25.into());
560
561 let expected = LintCorrection::Replace(LintCorrectionReplace {
562 text: "T".to_string(),
563 location: DenormalizedLocation::from_offset_range(
564 AdjustedRange::new(19.into(), 27.into()),
565 &context,
566 ),
567 });
568 let actual = LintCorrection::create_word_splice_correction()
569 .context(&context)
570 .outer_range(&outer_range)
571 .splice_range(&splice_range)
572 .call();
573 assert_eq!(expected, actual);
574 }
575
576 #[test]
577 fn test_create_word_splice_correction_new_sentence_replace() {
578 let parsed = parse("What a lovely day. Please take a biscuit.").unwrap();
579 let context = Context::builder().parse_result(&parsed).build().unwrap();
580
581 let outer_range = AdjustedRange::new(0.into(), 41.into());
582 let splice_range = AdjustedRange::new(19.into(), 25.into());
583
584 let expected = LintCorrection::Replace(LintCorrectionReplace {
585 text: "Kindly".to_string(),
586 location: DenormalizedLocation::from_offset_range(
587 AdjustedRange::new(19.into(), 25.into()),
588 &context,
589 ),
590 });
591 let actual = LintCorrection::create_word_splice_correction()
592 .context(&context)
593 .outer_range(&outer_range)
594 .splice_range(&splice_range)
595 .replace("kindly".into())
596 .call();
597 assert_eq!(expected, actual);
598 }
599
600 #[test]
601 fn test_create_word_splice_correction_start() {
602 let parsed = parse("Please take a biscuit.").unwrap();
603 let context = Context::builder().parse_result(&parsed).build().unwrap();
604
605 let outer_range = AdjustedRange::new(0.into(), 22.into());
606 let splice_range = AdjustedRange::new(0.into(), 6.into());
607
608 let expected = LintCorrection::Replace(LintCorrectionReplace {
609 text: "T".to_string(),
610 location: DenormalizedLocation::from_offset_range(
611 AdjustedRange::new(0.into(), 8.into()),
612 &context,
613 ),
614 });
615 let actual = LintCorrection::create_word_splice_correction()
616 .context(&context)
617 .outer_range(&outer_range)
618 .splice_range(&splice_range)
619 .call();
620 assert_eq!(expected, actual);
621 }
622}