harper_core/linting/
expand_time_shorthands.rs1use 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 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}