harper_core/linting/
of_course.rs1use crate::{
2 Token,
3 linting::{Lint, LintKind, PatternLinter, Suggestion},
4 patterns::{SequencePattern, WordSet},
5};
6
7pub struct OfCourse {
8 pattern: Box<dyn crate::patterns::Pattern>,
9}
10
11impl Default for OfCourse {
12 fn default() -> Self {
13 let wrong_forms = WordSet::new(&["curse", "corse"]);
14 let pattern = SequencePattern::default()
15 .t_aco("of")
16 .then_whitespace()
17 .then(wrong_forms);
18
19 Self {
20 pattern: Box::new(pattern),
21 }
22 }
23}
24
25impl PatternLinter for OfCourse {
26 fn pattern(&self) -> &dyn crate::patterns::Pattern {
27 self.pattern.as_ref()
28 }
29
30 fn match_to_lint(&self, matched: &[Token], source: &[char]) -> Option<Lint> {
31 if let Some(of_idx) = matched.first().map(|t| t.span.start) {
33 match source.get(..of_idx).map(|src| {
34 let mut i = of_idx.saturating_sub(1);
36 while i > 0 && src[i].is_whitespace() {
37 i -= 1;
38 }
39 let start = src[..=i]
41 .iter()
42 .rposition(|c| c.is_whitespace())
43 .map(|p| p + 1)
44 .unwrap_or(0);
45 src[start..=i].iter().collect::<String>()
46 }) {
47 Some(prev) => {
48 let lower = prev.to_ascii_lowercase();
49 if lower == "kind" || lower == "sort" {
50 return None;
51 }
52 }
53 _ => (),
54 }
55 }
56
57 let typo_span = matched.last()?.span;
58 let original = typo_span.get_content(source);
59
60 Some(Lint {
61 span: typo_span,
62 lint_kind: LintKind::WordChoice,
63 suggestions: vec![Suggestion::replace_with_match_case(
64 "course".chars().collect(),
65 original,
66 )],
67 message: "Did you mean “of **course**” (= clearly) instead of “of curse / corse”?"
68 .to_string(),
69 priority: 31,
70 })
71 }
72
73 fn description(&self) -> &str {
74 "Corrects the common eggcorn “of curse / corse” to “of course,” ignoring phrases like “kind of curse.”"
75 }
76}
77
78#[cfg(test)]
79mod tests {
80 use super::OfCourse;
81 use crate::linting::tests::{assert_lint_count, assert_suggestion_result};
82
83 #[test]
84 fn flags_of_curse() {
85 assert_suggestion_result("Yes, of curse!", OfCourse::default(), "Yes, of course!");
86 }
87
88 #[test]
89 fn flags_of_corse() {
90 assert_suggestion_result(
91 "Well, of corse we can.",
92 OfCourse::default(),
93 "Well, of course we can.",
94 );
95 }
96
97 #[test]
98 fn ignores_kind_of_curse() {
99 assert_lint_count("This kind of curse is dangerous.", OfCourse::default(), 0);
100 }
101
102 #[test]
103 fn ignores_sort_of_curse() {
104 assert_lint_count("It's a sort of curse that lingers.", OfCourse::default(), 0);
105 }
106
107 #[test]
108 fn ignores_curse_of_title() {
109 assert_lint_count(
110 "The Curse of Strahd is a famous module.",
111 OfCourse::default(),
112 0,
113 );
114 }
115}