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}