harper_core/linting/
discourse_markers.rs

1use harper_brill::UPOS;
2
3use crate::expr::{Expr, FirstMatchOf, FixedPhrase, SequenceExpr};
4use crate::patterns::UPOSSet;
5use crate::{Document, Token, TokenStringExt};
6
7use super::{Lint, LintKind, Linter, Suggestion};
8
9pub struct DiscourseMarkers {
10    expr: SequenceExpr,
11}
12
13impl DiscourseMarkers {
14    pub fn new() -> Self {
15        let phrases = &[
16            "however",
17            "therefore",
18            "meanwhile",
19            "furthermore",
20            "nevertheless",
21            "consequently",
22            "thus",
23            "instead",
24            "moreover",
25            "honestly",
26            "alternatively",
27            "frankly",
28            "additionally",
29            "subsequently",
30            "accordingly",
31            "otherwise",
32            "incidentally",
33            "conversely",
34            "notwithstanding",
35            "hence",
36            "indeed",
37            "for example",
38            "on the other hand",
39        ];
40
41        let phrases_expr = FirstMatchOf::new(
42            phrases
43                .iter()
44                .map(|text: &&str| Box::new(FixedPhrase::from_phrase(text)) as Box<dyn Expr>)
45                .collect(),
46        );
47
48        Self {
49            expr: SequenceExpr::default()
50                .then(phrases_expr)
51                .t_ws()
52                .then_unless(UPOSSet::new(&[UPOS::ADJ, UPOS::ADV, UPOS::ADP])),
53        }
54    }
55
56    fn lint_sentence(&self, sent: &[Token], source: &[char]) -> Option<Lint> {
57        let first_word_idx = sent.iter_word_indices().next()?;
58
59        if let Some(matched_phrase) = self.expr.run(first_word_idx, sent, source) {
60            Some(Lint {
61                span: sent[matched_phrase.start..matched_phrase.end - 2].span()?,
62                lint_kind: LintKind::Punctuation,
63                suggestions: vec![Suggestion::InsertAfter(vec![','])],
64                message: "Discourse markers at the beginning of a sentence should be followed by a comma.".into(),
65                priority: 31,
66            })
67        } else {
68            None
69        }
70    }
71}
72
73impl Default for DiscourseMarkers {
74    fn default() -> Self {
75        Self::new()
76    }
77}
78
79impl Linter for DiscourseMarkers {
80    fn lint(&mut self, document: &Document) -> Vec<Lint> {
81        document
82            .iter_sentences()
83            .flat_map(|sent| self.lint_sentence(sent, document.get_source()))
84            .collect()
85    }
86
87    fn description(&self) -> &str {
88        "Flags sentences that begin with a discourse marker but omit the required following comma."
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use crate::linting::tests::{assert_no_lints, assert_suggestion_result};
95
96    use super::DiscourseMarkers;
97
98    #[test]
99    fn corrects_frankly() {
100        assert_suggestion_result(
101            "Frankly I think he is wrong.",
102            DiscourseMarkers::default(),
103            "Frankly, I think he is wrong.",
104        );
105    }
106
107    #[test]
108    fn corrects_however() {
109        assert_suggestion_result(
110            "However I disagree with your conclusion.",
111            DiscourseMarkers::default(),
112            "However, I disagree with your conclusion.",
113        );
114    }
115
116    #[test]
117    fn corrects_therefore() {
118        assert_suggestion_result(
119            "Therefore we must act now.",
120            DiscourseMarkers::default(),
121            "Therefore, we must act now.",
122        );
123    }
124
125    #[test]
126    fn corrects_meanwhile() {
127        assert_suggestion_result(
128            "Meanwhile preparations continued in the background.",
129            DiscourseMarkers::default(),
130            "Meanwhile, preparations continued in the background.",
131        );
132    }
133
134    #[test]
135    fn corrects_furthermore() {
136        assert_suggestion_result(
137            "Furthermore this approach reduces complexity.",
138            DiscourseMarkers::default(),
139            "Furthermore, this approach reduces complexity.",
140        );
141    }
142
143    #[test]
144    fn corrects_nevertheless() {
145        assert_suggestion_result(
146            "Nevertheless we persevered despite the odds.",
147            DiscourseMarkers::default(),
148            "Nevertheless, we persevered despite the odds.",
149        );
150    }
151
152    #[test]
153    fn corrects_consequently() {
154        assert_suggestion_result(
155            "Consequently the system halted unexpectedly.",
156            DiscourseMarkers::default(),
157            "Consequently, the system halted unexpectedly.",
158        );
159    }
160
161    #[test]
162    fn corrects_thus() {
163        assert_suggestion_result(
164            "Thus we arrive at the final verdict.",
165            DiscourseMarkers::default(),
166            "Thus, we arrive at the final verdict.",
167        );
168    }
169
170    #[test]
171    fn allows_thus_far() {
172        assert_no_lints(
173            "Thus far there have been no problems.",
174            DiscourseMarkers::default(),
175        );
176    }
177
178    #[test]
179    fn corrects_instead() {
180        assert_suggestion_result(
181            "Instead he chose a different path.",
182            DiscourseMarkers::default(),
183            "Instead, he chose a different path.",
184        );
185    }
186
187    #[test]
188    fn corrects_moreover() {
189        assert_suggestion_result(
190            "Moreover this solution is more efficient.",
191            DiscourseMarkers::default(),
192            "Moreover, this solution is more efficient.",
193        );
194    }
195
196    #[test]
197    fn corrects_alternatively() {
198        assert_suggestion_result(
199            "Alternatively we could defer the decision.",
200            DiscourseMarkers::default(),
201            "Alternatively, we could defer the decision.",
202        );
203    }
204
205    #[test]
206    fn no_suggestion_if_comma_present() {
207        assert_no_lints(
208            "However, I disagree with your point.",
209            DiscourseMarkers::default(),
210        );
211    }
212
213    #[test]
214    fn no_lint_for_mid_sentence_marker() {
215        assert_no_lints(
216            "I said however I would consider it.",
217            DiscourseMarkers::default(),
218        );
219    }
220
221    #[test]
222    fn preserves_whitespace() {
223        assert_suggestion_result(
224            "However   I disagree.",
225            DiscourseMarkers::default(),
226            "However,   I disagree.",
227        );
228    }
229
230    #[test]
231    fn corrects_semicolon_case() {
232        assert_suggestion_result(
233            "However I disagree.",
234            DiscourseMarkers::default(),
235            "However, I disagree.",
236        );
237    }
238
239    #[test]
240    fn corrects_multiple_sentences() {
241        assert_suggestion_result(
242            "However I disagree. Therefore I propose an alternative.",
243            DiscourseMarkers::default(),
244            "However, I disagree. Therefore, I propose an alternative.",
245        );
246    }
247
248    #[test]
249    fn allows_single_word_sentence() {
250        assert_no_lints("Thus", DiscourseMarkers::default());
251    }
252
253    #[test]
254    fn corrects_for_example() {
255        assert_suggestion_result(
256            "For example I recommend updating the configuration.",
257            DiscourseMarkers::default(),
258            "For example, I recommend updating the configuration.",
259        );
260    }
261
262    #[test]
263    fn no_suggestion_if_comma_after_for_example() {
264        assert_no_lints(
265            "For example, I recommend updating the configuration.",
266            DiscourseMarkers::default(),
267        );
268    }
269
270    #[test]
271    fn preserves_whitespace_for_example() {
272        assert_suggestion_result(
273            "For example   the outcome was unexpected.",
274            DiscourseMarkers::default(),
275            "For example,   the outcome was unexpected.",
276        );
277    }
278
279    #[test]
280    fn corrects_on_the_other_hand() {
281        assert_suggestion_result(
282            "On the other hand we could delay the deployment.",
283            DiscourseMarkers::default(),
284            "On the other hand, we could delay the deployment.",
285        );
286    }
287
288    #[test]
289    fn no_lint_for_mid_sentence_on_the_other_hand() {
290        assert_no_lints(
291            "We might postpone, on the other hand this introduces risk.",
292            DiscourseMarkers::default(),
293        );
294    }
295}