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