use std::{
io::{BufRead as _, BufReader},
path::PathBuf,
};
use dyn_iter::{DynIter, IntoDynIterator as _};
use md5::Digest;
use crate::{Category, Location, Severity, TextRange};
const TYPOS_ENGINE: &str = "typos";
pub struct Typos {
issues: DynIter<'static, Issue>,
}
impl Iterator for Typos {
type Item = Issue;
#[inline]
fn next(&mut self) -> Option<Self::Item> {
self.issues.next()
}
}
impl Typos {
#[inline]
pub fn try_new<R>(json_read: R) -> eyre::Result<Self>
where
R: std::io::Read + 'static,
{
let reader = BufReader::new(json_read);
let issues = reader
.lines()
.map_while(Result::ok)
.flat_map(|line| serde_json::from_str::<Issue>(&line))
.into_dyn_iter();
let typos = Self { issues };
Ok(typos)
}
}
#[derive(serde::Deserialize)]
pub struct Issue {
pub path: PathBuf,
pub line_num: usize,
pub byte_offset: usize,
pub typo: String,
pub corrections: Vec<String>,
}
impl crate::Issue for Issue {
#[inline]
fn analyzer_id(&self) -> String {
TYPOS_ENGINE.to_owned()
}
#[inline]
fn issue_id(&self) -> String {
"typo".to_owned()
}
#[inline]
fn fingerprint(&self) -> Digest {
md5::compute(format!(
"{}::{}::{}",
self.path.to_string_lossy(),
self.line_num,
self.byte_offset
))
}
#[inline]
fn category(&self) -> Category {
Category::Style
}
#[inline]
fn severity(&self) -> Severity {
Severity::Info
}
#[inline]
fn location(&self) -> Option<Location> {
let message = format!(
"‘{}’ might be misspelled. Did you mean: {}",
self.typo,
self.corrections.join(", "),
);
let path = self.path.clone();
let range = TextRange::new(
(self.line_num, self.byte_offset),
(
self.line_num,
self.byte_offset.saturating_add(self.typo.len()),
),
);
let location = Location {
path,
range,
message,
};
Some(location)
}
}
#[cfg(test)]
mod tests {
use crate::{typos::Typos, Category, Issue as _, Severity};
use std::{io::Write as _, path::Path};
use test_log::test;
#[test]
fn single_issue() {
let json = r#"{
"type":"typo",
"path":"./CHANGELOG.md",
"line_num":89,
"byte_offset":32,
"typo":"ba",
"corrections":["be", "by"]
}"#;
let json = json.to_owned().replace('\n', "");
let mut typos_json = tempfile::NamedTempFile::new().unwrap();
write!(typos_json, "{}", json).unwrap();
let typos_json = typos_json.reopen().unwrap();
let mut typos = Typos::try_new(typos_json).unwrap();
let issue = typos.next().unwrap();
assert_eq!(issue.analyzer_id(), "typos");
assert_eq!(issue.issue_uid(), "typos::typo");
assert!(matches!(issue.severity(), Severity::Info));
assert!(matches!(issue.category(), Category::Style));
let location = issue.location().unwrap();
assert_eq!(location.path, Path::new("./CHANGELOG.md"));
assert_eq!(
location.message,
"‘ba’ might be misspelled. Did you mean: be, by",
);
assert_eq!(location.range.start.line, 89);
assert_eq!(location.range.end.line, 89);
assert_eq!(location.range.start.column, 32);
assert_eq!(location.range.end.column, 34);
}
}