1use super::*;
4use crate::checker::Checkers;
5use crate::errors::*;
6use crate::reflow::Reflow;
7
8use fs_err as fs;
9use futures::stream::{self, StreamExt};
10use rayon::iter::ParallelIterator;
11
12use std::io::{Read, Write};
13use std::path::PathBuf;
14
15pub mod bandaid;
16pub mod interactive;
17
18pub(crate) use bandaid::*;
19
20use interactive::{UserPicked, UserSelection};
21
22#[derive(Debug, Clone, Copy)]
24pub enum Finish {
25 Success,
27 Abort,
29 MistakeCount(usize),
32}
33
34impl Finish {
35 pub fn found_any(&self) -> bool {
37 match *self {
38 Self::MistakeCount(n) if n > 0 => true,
39 _ => false,
40 }
41 }
42}
43
44#[derive(Debug, Clone, Eq, PartialEq)]
48pub(crate) enum Patch {
49 Replace {
53 replace_span: Span,
54 replacement: String,
55 },
56 Insert {
58 insert_at: LineColumn,
59 content: String,
60 },
61}
62
63impl<'a> From<&'a BandAid> for Patch {
64 fn from(bandaid: &'a BandAid) -> Self {
65 Self::from(bandaid.clone())
67 }
68}
69
70impl From<BandAid> for Patch {
71 fn from(bandaid: BandAid) -> Self {
72 match bandaid {
73 bandaid if bandaid.span.start == bandaid.span.end => Self::Insert {
74 insert_at: bandaid.span.start,
75 content: bandaid.content,
76 },
77 _ => Self::Replace {
78 replace_span: bandaid.span,
79 replacement: bandaid.content,
80 },
81 }
82 }
83}
84
85pub(crate) fn apply_patches<'s, II, I>(
94 patches: II,
95 source_buffer: &str,
96 mut sink: impl Write,
97) -> Result<()>
98where
99 II: IntoIterator<IntoIter = I, Item = Patch>,
100 I: Iterator<Item = Patch>,
101{
102 let mut patches = patches.into_iter().peekable();
103
104 let mut source_iter =
105 iter_with_line_column_from(source_buffer, LineColumn { line: 1, column: 0 }).peekable();
106
107 const TARGET: &str = "patch";
108 let mut write_to_sink = |topic: &str, data: &str| -> Result<()> {
109 log::trace!(target: TARGET, "w<{}>: {}", topic, data.escape_debug());
110 sink.write_all(data.as_bytes())?;
111 Ok(())
112 };
113
114 let mut cc_end_byte_offset = 0;
115
116 let mut current = None;
117 let mut byte_cursor = 0usize;
118 loop {
119 let cc_start_byte_offset = if let Some(ref current) = current {
120 let (cc_start, data, insertion) = match current {
121 Patch::Replace {
122 replace_span,
123 replacement,
124 } => (replace_span.end, replacement.as_str(), false),
125 Patch::Insert { insert_at, content } => (*insert_at, content.as_str(), true),
126 };
127
128 write_to_sink("new", data)?;
129
130 if insertion {
131 byte_cursor
133 } else {
134 let mut cc_start_byte_offset = byte_cursor;
137 'skip: while let Some((c, byte_offset, _idx, linecol)) = source_iter.peek() {
138 let byte_offset = *byte_offset;
139 let linecol = *linecol;
140
141 cc_start_byte_offset = byte_offset + c.len_utf8();
142
143 if linecol >= cc_start {
144 log::trace!(
145 target: TARGET,
146 "skip buffer: >{}<",
147 &source_buffer[cc_end_byte_offset..cc_start_byte_offset].escape_debug()
148 );
149
150 break 'skip;
151 }
152
153 log::trace!(target: TARGET, "skip[{}]: >{}<", _idx, c.escape_debug());
154
155 let _ = source_iter.next();
156 }
157 cc_start_byte_offset
158 }
159 } else {
160 byte_cursor
161 };
162 debug_assert!(byte_cursor <= cc_start_byte_offset);
163 byte_cursor = cc_start_byte_offset;
164
165 cc_end_byte_offset = if let Some(upcoming) = patches.peek() {
166 let cc_end = match upcoming {
167 Patch::Replace { replace_span, .. } => replace_span.start,
168 Patch::Insert { insert_at, .. } => *insert_at,
169 };
170
171 let mut cc_end_byte_offset = byte_cursor;
175 'cc: while let Some((c, byte_offset, _idx, linecol)) = source_iter.peek() {
176 let byte_offset = *byte_offset;
177 let linecol = *linecol;
178
179 if linecol >= cc_end {
180 log::trace!(
181 target: TARGET,
182 "copy buffer: >{}<",
183 &source_buffer[cc_start_byte_offset..cc_end_byte_offset].escape_debug()
184 );
185 break 'cc;
186 }
187
188 cc_end_byte_offset = byte_offset + c.len_utf8();
189
190 log::trace!(target: TARGET, "copy[{}]: >{}<", _idx, c.escape_debug());
191
192 let _ = source_iter.next();
193 }
195 std::cmp::min(cc_end_byte_offset, source_buffer.len())
197 } else {
198 source_buffer.len()
199 };
200 debug_assert!(byte_cursor <= cc_end_byte_offset);
201
202 byte_cursor = cc_end_byte_offset;
203
204 let cc_range = cc_start_byte_offset..cc_end_byte_offset;
205
206 write_to_sink("cc", &source_buffer[cc_range])?;
207
208 current = patches.next();
210
211 if current.is_none() {
212 break;
214 }
215 }
216
217 Ok(())
218}
219
220#[derive(Debug, Clone, Copy, Eq, PartialEq)]
224pub enum Action {
225 Check,
227
228 Fix,
230
231 Reflow,
233
234 ListFiles,
237}
238
239impl Action {
240 pub fn write_changes_to_disk(
242 &self,
243 origin: ContentOrigin,
244 bandaids: impl IntoIterator<Item = BandAid>,
245 ) -> Result<()> {
246 match origin {
247 ContentOrigin::CargoManifestDescription(path) => self.correct_file(path, bandaids),
248 ContentOrigin::CommonMarkFile(path) => self.correct_file(path, bandaids),
249 ContentOrigin::RustSourceFile(path) => self.correct_file(path, bandaids),
250 ContentOrigin::RustDocTest(path, _span) => self.correct_file(path, bandaids),
251 ContentOrigin::TestEntityRust => unreachable!("Use a proper file"),
252 ContentOrigin::TestEntityCommonMark => unreachable!("Use a proper file"),
253 }
254 }
255
256 fn correct_file(
259 &self,
260 path: PathBuf,
261 bandaids: impl IntoIterator<Item = BandAid>,
262 ) -> Result<()> {
263 let path = fs::canonicalize(path.as_path())?;
264 let path = path.as_path();
265 log::trace!("Attempting to open {} as read", path.display());
266 let ro = fs::OpenOptions::new().read(true).open(path)?;
267
268 let mut reader = std::io::BufReader::new(ro);
269
270 const TEMPORARY: &str = ".spellcheck.tmp";
271
272 let tmp_name = TEMPORARY.to_owned() + uuid::Uuid::new_v4().to_string().as_str();
274
275 let tmp = std::env::current_dir()
276 .expect("Must have cwd")
277 .join(tmp_name);
278 let wr = fs::OpenOptions::new()
279 .write(true)
280 .truncate(true)
281 .create(true)
282 .open(&tmp)?;
283
284 let mut writer = std::io::BufWriter::with_capacity(1024, wr);
285
286 let mut content = String::with_capacity(2e6 as usize);
287 reader.get_mut().read_to_string(&mut content)?;
288
289 {
290 let th = crate::TinHat::on();
291
292 apply_patches(
293 bandaids.into_iter().map(Patch::from),
294 content.as_str(), &mut writer,
296 )?;
297
298 writer.flush()?;
299 drop(writer);
303 drop(reader);
304 fs::rename(tmp, path)?;
305
306 drop(th);
308 }
309
310 Ok(())
311 }
312
313 pub fn write_user_pick_changes_to_disk(
318 &self,
319 userpicked: interactive::UserPicked,
320 ) -> Result<()> {
321 if userpicked.total_count() > 0 {
322 log::debug!("Writing changes back to disk");
323 for (origin, bandaids) in userpicked.bandaids.into_iter() {
324 self.write_changes_to_disk(origin, bandaids.into_iter())?;
325 }
326 } else {
327 log::debug!("No band aids to apply");
328 }
329 Ok(())
330 }
331 pub async fn run(self, documents: Documentation, config: Config) -> Result<Finish> {
333 let fin = match self {
334 Self::ListFiles { .. } => self.run_list_files(documents, &config)?,
335 Self::Reflow { .. } => self.run_reflow(documents, config).await?,
336 Self::Check { .. } => self.run_check(documents, config).await?,
337 Self::Fix { .. } => self.run_fix_interactive(documents, config).await?,
338 };
339 Ok(fin)
340 }
341
342 fn run_list_files(self, documents: Documentation, _config: &Config) -> Result<Finish> {
344 for (origin, _chunks) in documents.iter() {
345 println!("{}", origin.as_path().display())
346 }
347 Ok(Finish::Success)
348 }
349
350 async fn run_fix_interactive(self, documents: Documentation, config: Config) -> Result<Finish> {
352 let n_cpus = num_cpus::get();
353
354 let checkers = Checkers::new(config)?;
355
356 let n = documents.entry_count();
357 log::debug!("Running checkers on all documents {n}");
358 let mut pick_stream = stream::iter(documents.iter().enumerate())
359 .map(|(mut idx, (origin, chunks))| {
360 idx += 1;
362 log::trace!("Running checkers on {idx}/{n},{origin:?}");
363 let suggestions = checkers.check(origin, &chunks[..]);
364 async move { Ok::<_, color_eyre::eyre::Report>((idx, origin, suggestions?)) }
365 })
366 .buffered(n_cpus)
367 .fuse();
368
369 let mut collected_picks = UserPicked::default();
370 while let Some(result) = pick_stream.next().await {
371 match result {
372 Ok((idx, origin, suggestions)) => {
373 let (picked, user_sel) =
374 interactive::UserPicked::select_interactive(origin.clone(), suggestions)?;
375
376 match user_sel {
377 UserSelection::Quit => break,
378 UserSelection::Abort => return Ok(Finish::Abort),
379 UserSelection::Nop if !picked.is_empty() => {
380 log::debug!(
381 "User picked patches to be applied for {idx}/{n},{origin:?}"
382 );
383 collected_picks.extend(picked);
384 }
385 UserSelection::Nop => {
386 log::debug!("Nothing to do for {idx}/{n},{origin:?}");
387 }
388 _ => unreachable!(
389 "All other variants are only internal to `select_interactive`. qed"
390 ),
391 }
392 }
393 Err(e) => Err(e)?,
394 }
395 }
396 let total = collected_picks.total_count();
397 self.write_user_pick_changes_to_disk(collected_picks)?;
401
402 Ok(Finish::MistakeCount(total))
403 }
404
405 async fn run_check(self, documents: Documentation, config: Config) -> Result<Finish> {
407 let checkers = Checkers::new(config)?;
408 let num_mistakes = documents
409 .into_par_iter()
410 .map(|(origin, chunks)| {
411 checkers.check(&origin, &chunks).map(|suggestions| {
412 let path = origin.as_path();
413 let n = suggestions.len();
414 match suggestions.is_empty() {
415 true => log::info!("✅ {}", path.display()),
416 false => log::info!("❌ {} : {}", path.display(), n),
417 };
418 for suggestion in suggestions {
419 println!("{suggestion}");
420 }
421 n
422 })
423 })
424 .try_fold_with(0, |count, res| res.map(|it| it + count))
425 .try_reduce(|| 0, |l, r| Ok(l + r))?;
426
427 if num_mistakes > 0 {
428 Ok(Finish::MistakeCount(num_mistakes))
429 } else {
430 Ok(Finish::Success)
431 }
432 }
433
434 async fn run_reflow(self, documents: Documentation, config: Config) -> Result<Finish> {
436 let reflow_config = config.reflow.clone().unwrap_or_default();
437 let reflow = Reflow::new(reflow_config)?;
438
439 documents
440 .into_par_iter()
441 .map(|(origin, chunks)| {
442 let mut picked = UserPicked::default();
443 let suggestions = reflow.check(&origin, &chunks[..])?;
444 for suggestion in suggestions {
445 let bandaids = suggestion.replacements.first().map(|replacement| {
446 super::BandAid::from((replacement.to_owned(), &suggestion.span))
447 });
448
449 picked.add_bandaids(&origin, bandaids);
450 }
451 Ok::<_, color_eyre::eyre::Report>(picked)
452 })
453 .try_for_each(move |picked| {
454 self.write_user_pick_changes_to_disk(picked?)?;
455 Ok::<_, color_eyre::eyre::Report>(())
456 })?;
457
458 Ok(Finish::Success)
459 }
460}
461
462#[cfg(test)]
463mod tests {
464 use super::*;
465 use std::convert::TryInto;
466
467 macro_rules! verify_correction {
468 ($text:literal, $bandaids:expr, $expected:literal) => {
469 let mut sink: Vec<u8> = Vec::with_capacity(1024);
470
471 apply_patches(
472 $bandaids.into_iter().map(|bandaid| Patch::from(bandaid)),
473 $text,
474 &mut sink,
475 )
476 .expect("Line correction must work in unit test!");
477
478 assert_eq!(String::from_utf8_lossy(sink.as_slice()), $expected);
479 };
480 }
481
482 #[test]
483 fn patch_full() {
484 let _ = env_logger::Builder::new()
485 .filter_level(log::LevelFilter::Trace)
486 .is_test(true)
487 .try_init();
488
489 let patches = vec![
490 Patch::Replace {
491 replace_span: Span {
492 start: LineColumn { line: 1, column: 6 },
493 end: LineColumn {
494 line: 2,
495 column: 12,
496 },
497 },
498 replacement: "& Omega".to_owned(),
499 },
500 Patch::Insert {
501 insert_at: LineColumn { line: 3, column: 0 },
502 content: "Icecream truck".to_owned(),
503 },
504 ];
505 verify_correction!(
506 r#"Alpha beta gamma
507zeta eta beta.
508"#,
509 patches,
510 r#"Alpha & Omega.
511Icecream truck"#
512 );
513 }
514
515 #[test]
516 fn patch_replace_1() {
517 let _ = env_logger::Builder::new()
518 .filter_level(log::LevelFilter::Trace)
519 .is_test(true)
520 .try_init();
521 let bandaids = vec![Patch::Replace {
522 replace_span: (1_usize, 0..1).try_into().unwrap(),
523 replacement: "Y".to_owned(),
524 }];
525 verify_correction!("T🐠🐠U", bandaids, "Y🐠🐠U");
526 }
527
528 #[test]
529 fn patch_replace_2() {
530 let _ = env_logger::Builder::new()
531 .filter_level(log::LevelFilter::Trace)
532 .is_test(true)
533 .try_init();
534 let bandaids = vec![Patch::Replace {
535 replace_span: (1_usize, 1..3).try_into().unwrap(),
536 replacement: "Y".to_owned(),
537 }];
538 verify_correction!("T🐠🐠U", bandaids, "TYU");
539 }
540
541 #[test]
542 fn patch_replace_3() {
543 let _ = env_logger::Builder::new()
544 .filter_level(log::LevelFilter::Trace)
545 .is_test(true)
546 .try_init();
547 let bandaids = vec![Patch::Replace {
548 replace_span: (1_usize, 3..4).try_into().unwrap(),
549 replacement: "Y".to_owned(),
550 }];
551 verify_correction!("T🐠🐠U", bandaids, "T🐠🐠Y");
552 }
553
554 #[test]
555 fn patch_injection_1() {
556 let _ = env_logger::Builder::new()
557 .filter_level(log::LevelFilter::Trace)
558 .is_test(true)
559 .try_init();
560
561 let patches = vec![Patch::Insert {
562 insert_at: LineColumn {
563 line: 1_usize,
564 column: 0,
565 },
566 content: "Q".to_owned(),
567 }];
568 verify_correction!("A🐢C", patches, "QA🐢C");
569 }
570
571 #[test]
572 fn patch_injection_2() {
573 let _ = env_logger::Builder::new()
574 .filter_level(log::LevelFilter::Trace)
575 .is_test(true)
576 .try_init();
577
578 let patches = vec![Patch::Insert {
579 insert_at: LineColumn {
580 line: 1_usize,
581 column: 2,
582 },
583 content: "Q".to_owned(),
584 }];
585 verify_correction!("A🐢C", patches, "A🐢QC");
586 }
587 #[test]
588 fn patch_injection_3() {
589 let _ = env_logger::Builder::new()
590 .filter_level(log::LevelFilter::Trace)
591 .is_test(true)
592 .try_init();
593
594 let patches = vec![Patch::Insert {
595 insert_at: LineColumn {
596 line: 1_usize,
597 column: 3,
598 },
599 content: "Q".to_owned(),
600 }];
601 verify_correction!("A🐢C", patches, "A🐢CQ");
602 }
603}