1use std::{
2 io::{BufRead as _, BufReader},
3 path::PathBuf,
4};
5
6use dyn_iter::{DynIter, IntoDynIterator as _};
7use md5::Digest;
8
9use crate::{Category, Location, Severity, TextRange};
10
11const TYPOS_ENGINE: &str = "typos";
12
13pub struct Typos {
14 issues: DynIter<'static, Issue>,
15}
16
17impl Iterator for Typos {
18 type Item = Issue;
19
20 #[inline]
21 fn next(&mut self) -> Option<Self::Item> {
22 self.issues.next()
23 }
24}
25
26impl Typos {
27 #[inline]
32 pub fn try_new<R>(json_read: R) -> eyre::Result<Self>
33 where
34 R: std::io::Read + 'static,
35 {
36 let reader = BufReader::new(json_read);
37 let issues = reader
38 .lines()
39 .map_while(Result::ok)
40 .flat_map(|line| serde_json::from_str::<Issue>(&line))
41 .into_dyn_iter();
42 let typos = Self { issues };
43 Ok(typos)
44 }
45}
46
47#[derive(serde::Deserialize)]
48pub struct Issue {
49 pub path: PathBuf,
50 pub line_num: usize,
51 pub byte_offset: usize,
52 pub typo: String,
53 pub corrections: Vec<String>,
54}
55
56impl crate::Issue for Issue {
57 #[inline]
58 fn analyzer_id(&self) -> String {
59 TYPOS_ENGINE.to_owned()
60 }
61 #[inline]
62 fn issue_id(&self) -> String {
63 "typo".to_owned()
64 }
65 #[inline]
66 fn fingerprint(&self) -> Digest {
67 md5::compute(format!(
68 "{}::{}::{}",
69 self.path.to_string_lossy(),
70 self.line_num,
71 self.byte_offset
72 ))
73 }
74 #[inline]
75 fn category(&self) -> Category {
76 Category::Style
77 }
78 #[inline]
79 fn severity(&self) -> Severity {
80 Severity::Info
81 }
82 #[inline]
83 fn location(&self) -> Option<Location> {
84 let message = format!(
85 "‘{}’ might be misspelled. Did you mean: {}",
86 self.typo,
87 self.corrections.join(", "),
88 );
89 let path = self.path.clone();
90 let range = TextRange::new(
91 (self.line_num, self.byte_offset),
92 (
93 self.line_num,
94 self.byte_offset.saturating_add(self.typo.len()),
95 ),
96 );
97 let location = Location {
98 path,
99 range,
100 message,
101 };
102 Some(location)
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use crate::{typos::Typos, Category, Issue as _, Severity};
109 use std::{io::Write as _, path::Path};
110 use test_log::test;
111
112 #[test]
113 fn single_issue() {
114 let json = r#"{
115 "type":"typo",
116 "path":"./CHANGELOG.md",
117 "line_num":89,
118 "byte_offset":32,
119 "typo":"ba",
120 "corrections":["be", "by"]
121 }"#;
122 let json = json.to_owned().replace('\n', "");
123 let mut typos_json = tempfile::NamedTempFile::new().unwrap();
124 write!(typos_json, "{}", json).unwrap();
125 let typos_json = typos_json.reopen().unwrap();
126
127 let mut typos = Typos::try_new(typos_json).unwrap();
128 let issue = typos.next().unwrap();
129 assert_eq!(issue.analyzer_id(), "typos");
130 assert_eq!(issue.issue_uid(), "typos::typo");
131 assert!(matches!(issue.severity(), Severity::Info));
132 assert!(matches!(issue.category(), Category::Style));
133 let location = issue.location().unwrap();
134 assert_eq!(location.path, Path::new("./CHANGELOG.md"));
135 assert_eq!(
136 location.message,
137 "‘ba’ might be misspelled. Did you mean: be, by",
138 );
139 assert_eq!(location.range.start.line, 89);
140 assert_eq!(location.range.end.line, 89);
141 assert_eq!(location.range.start.column, 32);
142 assert_eq!(location.range.end.column, 34);
143 }
144}