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}