harper_core/linting/
expand_time_shorthands.rs

1use std::sync::Arc;
2
3use super::{Lint, LintKind, PatternLinter};
4use crate::Token;
5use crate::linting::Suggestion;
6use crate::patterns::{EitherPattern, ImpliesQuantity, Pattern, SequencePattern, WordSet};
7
8pub struct ExpandTimeShorthands {
9    pattern: Box<dyn Pattern>,
10}
11
12impl ExpandTimeShorthands {
13    pub fn new() -> Self {
14        let hotwords = Arc::new(WordSet::new(&[
15            "hr", "hrs", "min", "mins", "sec", "secs", "ms", "msec", "msecs",
16        ]));
17
18        Self {
19            pattern: Box::new(SequencePattern::default().then(ImpliesQuantity).then(
20                EitherPattern::new(vec![
21                        Box::new(SequencePattern::default().then(hotwords.clone())),
22                        Box::new(
23                            SequencePattern::default()
24                                .then_whitespace()
25                                .then(hotwords.clone()),
26                        ),
27                        Box::new(
28                            SequencePattern::default()
29                                .then_hyphen()
30                                .then(hotwords.clone()),
31                        ),
32                    ]),
33            )),
34        }
35    }
36
37    fn get_replacement(abbreviation: &str, plural: Option<bool>) -> Option<&'static str> {
38        let is_plural = plural.unwrap_or(matches!(abbreviation, "hrs" | "mins" | "secs" | "msecs"));
39        match abbreviation {
40            "hr" | "hrs" => Some(if is_plural { "hours" } else { "hour" }),
41            "min" | "mins" => Some(if is_plural { "minutes" } else { "minute" }),
42            "sec" | "secs" => Some(if is_plural { "seconds" } else { "second" }),
43            "ms" | "msec" | "msecs" => Some(if is_plural {
44                "milliseconds"
45            } else {
46                "millisecond"
47            }),
48            _ => None,
49        }
50    }
51}
52
53impl Default for ExpandTimeShorthands {
54    fn default() -> Self {
55        Self::new()
56    }
57}
58
59impl PatternLinter for ExpandTimeShorthands {
60    fn pattern(&self) -> &dyn Pattern {
61        self.pattern.as_ref()
62    }
63
64    fn match_to_lint(&self, matched_tokens: &[Token], source: &[char]) -> Option<Lint> {
65        let offending_span = matched_tokens.last()?.span;
66        let implies_plural = ImpliesQuantity::implies_plurality(matched_tokens, source);
67
68        let offending_text = offending_span.get_content(source);
69
70        let replacement =
71            Self::get_replacement(&offending_text.iter().collect::<String>(), implies_plural)?;
72
73        let mut replacement_chars = Vec::new();
74
75        // If there isn't spacing, insert a space
76        if matched_tokens.len() == 2 {
77            replacement_chars.push(' ');
78        }
79
80        replacement_chars.extend(replacement.chars());
81
82        if replacement_chars == offending_text {
83            return None;
84        }
85
86        Some(Lint {
87            span: offending_span,
88            lint_kind: LintKind::WordChoice,
89            suggestions: vec![Suggestion::ReplaceWith(replacement_chars)],
90            message: format!("Did you mean `{}`?", replacement),
91            priority: 31,
92        })
93    }
94
95    fn description(&self) -> &str {
96        "Expands time-related abbreviations (`hr`, `hrs`, `min`, `mins`, `sec`, `secs`, `ms`, `msec`, `msecs`) to their full forms (`hour`, `hours`, `minute`, `minutes`, `second`, `seconds`, `millisecond`, `milliseconds`)."
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use crate::linting::tests::assert_suggestion_result;
103
104    use super::ExpandTimeShorthands;
105
106    #[test]
107    fn detects_singular_hour() {
108        assert_suggestion_result("5 hr", ExpandTimeShorthands::new(), "5 hours");
109    }
110
111    #[test]
112    fn detects_singular_minute() {
113        assert_suggestion_result("10 min", ExpandTimeShorthands::new(), "10 minutes");
114    }
115
116    #[test]
117    fn detects_singular_second() {
118        assert_suggestion_result("30 sec", ExpandTimeShorthands::new(), "30 seconds");
119    }
120
121    #[test]
122    fn detects_plural_hours() {
123        assert_suggestion_result("5 hrs", ExpandTimeShorthands::new(), "5 hours");
124    }
125
126    #[test]
127    fn detects_plural_minutes() {
128        assert_suggestion_result("10 mins", ExpandTimeShorthands::new(), "10 minutes");
129    }
130
131    #[test]
132    fn detects_plural_seconds() {
133        assert_suggestion_result("30 secs", ExpandTimeShorthands::new(), "30 seconds");
134    }
135
136    #[test]
137    fn detects_millisecond() {
138        assert_suggestion_result("5 ms", ExpandTimeShorthands::new(), "5 milliseconds");
139    }
140
141    #[test]
142    fn detects_milliseconds() {
143        assert_suggestion_result("10 msecs", ExpandTimeShorthands::new(), "10 milliseconds");
144    }
145
146    #[test]
147    fn handles_punctuation_hour() {
148        assert_suggestion_result("5 hr.", ExpandTimeShorthands::new(), "5 hours.");
149    }
150
151    #[test]
152    fn handles_punctuation_minute() {
153        assert_suggestion_result("10 min,", ExpandTimeShorthands::new(), "10 minutes,");
154    }
155
156    #[test]
157    fn handles_punctuation_second() {
158        assert_suggestion_result("30 sec!", ExpandTimeShorthands::new(), "30 seconds!");
159    }
160
161    #[test]
162    fn handles_adjacent_number_hour() {
163        assert_suggestion_result("5hr", ExpandTimeShorthands::new(), "5 hours");
164    }
165
166    #[test]
167    fn handles_adjacent_number_minute() {
168        assert_suggestion_result("10-min", ExpandTimeShorthands::new(), "10-minutes");
169    }
170
171    #[test]
172    fn handles_adjacent_number_second() {
173        assert_suggestion_result("30sec", ExpandTimeShorthands::new(), "30 seconds");
174    }
175}