harper_core/linting/
its_contraction.rs

1use harper_brill::UPOS;
2
3use crate::Document;
4use crate::TokenStringExt;
5use crate::expr::All;
6use crate::expr::Expr;
7use crate::expr::ExprExt;
8use crate::expr::OwnedExprExt;
9use crate::expr::SequenceExpr;
10use crate::patterns::NominalPhrase;
11use crate::patterns::Pattern;
12use crate::patterns::UPOSSet;
13use crate::patterns::WordSet;
14use crate::{
15    Token,
16    linting::{Lint, LintKind, Suggestion},
17};
18
19use super::Linter;
20
21pub struct ItsContraction {
22    expr: Box<dyn Expr>,
23}
24
25impl Default for ItsContraction {
26    fn default() -> Self {
27        let positive = SequenceExpr::default().t_aco("its").then_whitespace().then(
28            UPOSSet::new(&[UPOS::VERB, UPOS::AUX, UPOS::DET, UPOS::PRON])
29                .or(WordSet::new(&["because"])),
30        );
31
32        let exceptions = SequenceExpr::default()
33            .then_anything()
34            .then_anything()
35            .then(WordSet::new(&["own", "intended"]));
36
37        let inverted = SequenceExpr::default().then_unless(exceptions);
38
39        let expr = All::new(vec![Box::new(positive), Box::new(inverted)]).or_longest(
40            SequenceExpr::aco("its")
41                .t_ws()
42                .then(UPOSSet::new(&[UPOS::ADJ]))
43                .t_ws()
44                .then(UPOSSet::new(&[UPOS::SCONJ, UPOS::PART])),
45        );
46
47        Self {
48            expr: Box::new(expr),
49        }
50    }
51}
52
53impl Linter for ItsContraction {
54    fn lint(&mut self, document: &Document) -> Vec<Lint> {
55        let mut lints = Vec::new();
56        let source = document.get_source();
57
58        for chunk in document.iter_chunks() {
59            lints.extend(
60                self.expr
61                    .iter_matches(chunk, source)
62                    .filter_map(|match_span| {
63                        self.match_to_lint(&chunk[match_span.start..], source)
64                    }),
65            );
66        }
67
68        lints
69    }
70
71    fn description(&self) -> &str {
72        "Detects the possessive `its` before `had`, `been`, or `got` and offers `it's` or `it has`."
73    }
74}
75
76impl ItsContraction {
77    fn match_to_lint(&self, toks: &[Token], source: &[char]) -> Option<Lint> {
78        let offender = toks.first()?;
79        let offender_chars = offender.span.get_content(source);
80
81        if toks.get(2)?.kind.is_upos(UPOS::VERB)
82            && NominalPhrase.matches(&toks[2..], source).is_some()
83        {
84            return None;
85        }
86
87        Some(Lint {
88            span: offender.span,
89            lint_kind: LintKind::WordChoice,
90            suggestions: vec![
91                Suggestion::replace_with_match_case_str("it's", offender_chars),
92                Suggestion::replace_with_match_case_str("it has", offender_chars),
93            ],
94            message: "Use `it's` (short for `it has` or `it is`) here, not the possessive `its`."
95                .to_owned(),
96            priority: 54,
97        })
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use super::ItsContraction;
104    use crate::linting::tests::{assert_lint_count, assert_no_lints, assert_suggestion_result};
105
106    #[test]
107    fn fix_had() {
108        assert_suggestion_result(
109            "Its had an enormous effect.",
110            ItsContraction::default(),
111            "It's had an enormous effect.",
112        );
113    }
114
115    #[test]
116    fn fix_been() {
117        assert_suggestion_result(
118            "Its been months since we spoke.",
119            ItsContraction::default(),
120            "It's been months since we spoke.",
121        );
122    }
123
124    #[test]
125    fn fix_got() {
126        assert_suggestion_result(
127            "I think its got nothing to do with us.",
128            ItsContraction::default(),
129            "I think it's got nothing to do with us.",
130        );
131    }
132
133    #[test]
134    fn fixes_its_common() {
135        assert_suggestion_result(
136            "Its common for users to get frustrated.",
137            ItsContraction::default(),
138            "It's common for users to get frustrated.",
139        );
140    }
141
142    #[test]
143    fn ignore_correct_contraction() {
144        assert_lint_count(
145            "It's been a long year for everyone.",
146            ItsContraction::default(),
147            0,
148        );
149    }
150
151    #[test]
152    fn ignore_possessive() {
153        assert_lint_count(
154            "The company revised its policies last week.",
155            ItsContraction::default(),
156            0,
157        );
158    }
159
160    #[test]
161    fn ignore_coroutine() {
162        assert_lint_count(
163            "Launch each task within its own child coroutine.",
164            ItsContraction::default(),
165            0,
166        );
167    }
168
169    #[test]
170    fn issue_381() {
171        assert_suggestion_result(
172            "Its a nice day.",
173            ItsContraction::default(),
174            "It's a nice day.",
175        );
176    }
177
178    #[test]
179    fn ignore_nominal_progressive() {
180        assert_lint_count(
181            "The class preserves its existing properties.",
182            ItsContraction::default(),
183            0,
184        );
185    }
186
187    #[test]
188    #[ignore = "past participles are not always adjectives ('cared' for instance)"]
189    fn ignore_nominal_perfect() {
190        assert_lint_count(
191            "The robot followed its predetermined route.",
192            ItsContraction::default(),
193            0,
194        );
195    }
196
197    #[test]
198    fn ignore_nominal_long() {
199        assert_lint_count(
200            "I think of its exploding marvelous spectacular output.",
201            ItsContraction::default(),
202            0,
203        );
204    }
205
206    #[test]
207    fn corrects_because() {
208        assert_suggestion_result(
209            "Its because they don't want to.",
210            ItsContraction::default(),
211            "It's because they don't want to.",
212        );
213    }
214
215    #[test]
216    fn corrects_its_hard() {
217        assert_suggestion_result(
218            "Its hard to believe that.",
219            ItsContraction::default(),
220            "It's hard to believe that.",
221        );
222    }
223
224    #[test]
225    fn corrects_its_easy() {
226        assert_suggestion_result(
227            "Its easy if you try.",
228            ItsContraction::default(),
229            "It's easy if you try.",
230        );
231    }
232
233    #[test]
234    fn corrects_its_a_picnic() {
235        assert_suggestion_result(
236            "Its a beautiful day for a picnic",
237            ItsContraction::default(),
238            "It's a beautiful day for a picnic",
239        );
240    }
241
242    #[test]
243    fn corrects_its_my() {
244        assert_suggestion_result(
245            "Its my favorite song.",
246            ItsContraction::default(),
247            "It's my favorite song.",
248        );
249    }
250
251    #[test]
252    fn allows_its_new() {
253        assert_no_lints(
254            "The company announced its new product line. ",
255            ItsContraction::default(),
256        );
257    }
258
259    #[test]
260    fn allows_its_own_charm() {
261        assert_no_lints("The house has its own charm. ", ItsContraction::default());
262    }
263
264    #[test]
265    fn allows_its_victory() {
266        assert_no_lints(
267            "The team celebrated its victory. ",
268            ItsContraction::default(),
269        );
270    }
271
272    #[test]
273    fn allows_its_history() {
274        assert_no_lints(
275            "The country is proud of its history. ",
276            ItsContraction::default(),
277        );
278    }
279
280    #[test]
281    fn allows_its_secrets() {
282        assert_no_lints(
283            "The book contains its own secrets. ",
284            ItsContraction::default(),
285        );
286    }
287}