Skip to main content

cargo_sonar/
typos.rs

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    /// Create a typos parser for issues
28    ///
29    /// # Errors
30    /// May fail reading and parsing the file (IO errors).
31    #[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}