harper_core/linting/
missing_preposition.rs1use harper_brill::UPOS;
2
3use crate::expr::AnchorStart;
4use crate::expr::Expr;
5use crate::expr::OwnedExprExt;
6use crate::expr::SequenceExpr;
7use crate::patterns::AnyPattern;
8use crate::patterns::UPOSSet;
9use crate::{Token, TokenStringExt};
10
11use super::{ExprLinter, Lint, LintKind};
12
13pub struct MissingPreposition {
14 expr: Box<dyn Expr>,
15}
16
17impl Default for MissingPreposition {
18 fn default() -> Self {
19 let expr = SequenceExpr::default()
20 .then(
21 AnchorStart.or_longest(
22 SequenceExpr::default()
23 .then_non_quantifier_determiner()
24 .t_ws(),
25 ),
26 )
27 .then(UPOSSet::new(&[UPOS::NOUN, UPOS::PRON, UPOS::PROPN]))
28 .t_ws()
29 .then(UPOSSet::new(&[UPOS::AUX]))
30 .t_ws()
31 .then(UPOSSet::new(&[UPOS::ADJ]))
32 .t_ws()
33 .then(UPOSSet::new(&[UPOS::NOUN, UPOS::PRON, UPOS::PROPN]))
34 .then_optional(AnyPattern)
35 .then_optional(AnyPattern);
36
37 Self {
38 expr: Box::new(expr),
39 }
40 }
41}
42
43impl ExprLinter for MissingPreposition {
44 fn expr(&self) -> &dyn Expr {
45 self.expr.as_ref()
46 }
47
48 fn match_to_lint(&self, matched_tokens: &[Token], _source: &[char]) -> Option<Lint> {
49 if matched_tokens.last()?.kind.is_upos(UPOS::ADP) {
50 return None;
51 }
52
53 Some({
54 Lint {
55 span: matched_tokens[2..4].span()?,
56 lint_kind: LintKind::Miscellaneous,
57 suggestions: vec![],
58 message: "You may be missing a preposition here.".to_owned(),
59 priority: 31,
60 }
61 })
62 }
63
64 fn description(&self) -> &'static str {
65 "Locates potentially missing prepositions."
66 }
67}
68
69#[cfg(test)]
70mod tests {
71 use crate::linting::tests::{assert_lint_count, assert_no_lints};
72
73 use super::MissingPreposition;
74
75 #[test]
76 fn fixes_issue_1513() {
77 assert_lint_count(
78 "The city is famous its beaches.",
79 MissingPreposition::default(),
80 1,
81 );
82 assert_lint_count(
83 "The students are interested learning.",
84 MissingPreposition::default(),
85 1,
86 );
87 }
88
89 #[test]
90 fn allows_corrected_issue_1513() {
91 assert_no_lints(
92 "The city is famous for its beaches.",
93 MissingPreposition::default(),
94 );
95 assert_no_lints(
96 "The students are interested in learning.",
97 MissingPreposition::default(),
98 );
99 }
100
101 #[test]
102 fn no_lint_without_adj_noun_sequence() {
103 assert_lint_count("She is happy.", MissingPreposition::default(), 0);
104 }
105
106 #[test]
107 fn no_lint_with_preposition_present() {
108 assert_lint_count("They are fond of music.", MissingPreposition::default(), 0);
109 assert_lint_count(
110 "Students are interested in history.",
111 MissingPreposition::default(),
112 0,
113 );
114 }
115
116 #[test]
117 fn flag_adj_pron_pair() {
118 assert_lint_count("He was angry him.", MissingPreposition::default(), 1);
119 }
120
121 #[test]
122 fn no_lint_empty() {
123 assert_lint_count("", MissingPreposition::default(), 0);
124 }
125
126 #[test]
127 fn allows_tired_herself() {
128 assert_no_lints(
129 "She had tired herself out with trying.",
130 MissingPreposition::default(),
131 );
132 }
133
134 #[test]
135 fn allows_terrible_stuff() {
136 assert_no_lints(
137 "Either it was terrible stuff or the whiskey distorted things.",
138 MissingPreposition::default(),
139 );
140 }
141
142 #[test]
143 fn allows_issue_1585() {
144 assert_no_lints(
145 "Each agent has specific tools and tasks orchestrated through a crew workflow.",
146 MissingPreposition::default(),
147 );
148 }
149}