harper_core/linting/
missing_preposition.rs

1use 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}