1use crate::expr::Expr;
2use crate::expr::ExprExt;
3use crate::expr::OwnedExprExt;
4use crate::expr::SequenceExpr;
5use crate::{Lrc, Token, TokenStringExt, linting::Linter, patterns::WordSet};
6
7use super::{super::Lint, LintKind, Suggestion};
8
9pub struct OxfordComma {
10 expr: Box<dyn Expr>,
11}
12
13impl Default for OxfordComma {
14 fn default() -> Self {
15 let item = Lrc::new(
16 SequenceExpr::default()
17 .then_determiner()
18 .then_whitespace()
19 .then_nominal()
20 .or_longest(SequenceExpr::default().then_nominal()),
21 );
22
23 let item_chunk = SequenceExpr::default()
24 .then(item.clone())
25 .then_comma()
26 .then_whitespace();
27
28 let pattern = SequenceExpr::default()
29 .then_one_or_more(item_chunk)
30 .then(item.clone())
31 .then_whitespace()
32 .then(WordSet::new(&["and", "or", "nor"]))
33 .then_whitespace()
34 .then(item.clone());
35
36 Self {
37 expr: Box::new(pattern),
38 }
39 }
40}
41
42impl OxfordComma {
43 fn match_to_lint(&self, matched_toks: &[Token], _source: &[char]) -> Option<Lint> {
44 let conj_index = matched_toks.last_conjunction_index()?;
45 let offender = &matched_toks[conj_index - 2];
46
47 Some(Lint {
48 span: offender.span,
49 lint_kind: LintKind::Style,
50 suggestions: vec![Suggestion::InsertAfter(vec![','])],
51 message: "An Oxford comma is necessary here.".to_owned(),
52 priority: 31,
53 })
54 }
55}
56
57impl Linter for OxfordComma {
58 fn lint(&mut self, document: &crate::Document) -> Vec<crate::linting::Lint> {
59 let mut lints = Vec::new();
60 for sentence in document.iter_sentences() {
61 let mut skip = 0;
62
63 let mut words = sentence
64 .iter_words()
65 .filter_map(|v| v.kind.as_word())
66 .flatten();
67
68 if let (Some(first), Some(second)) = (words.next(), words.next())
69 && first.preposition
70 && second.is_likely_homograph()
71 {
72 skip = sentence
73 .iter()
74 .position(|t| t.kind.is_comma())
75 .unwrap_or(sentence.iter().len())
76 }
77
78 let sentence = &sentence[skip..];
79
80 for match_span in self.expr.iter_matches(sentence, document.get_source()) {
81 let lint = self.match_to_lint(
82 &sentence[match_span.start..match_span.end],
83 document.get_source(),
84 );
85 lints.extend(lint);
86 }
87 }
88
89 lints
90 }
91
92 fn description(&self) -> &str {
93 "The Oxford comma is one of the more controversial rules in common use today. Enabling this lint checks that there is a comma before `and`, `or`, or `nor` when listing out more than two ideas."
94 }
95}
96
97#[cfg(test)]
98mod tests {
99 use crate::linting::tests::{assert_lint_count, assert_suggestion_result};
100
101 use super::OxfordComma;
102
103 #[test]
104 fn fruits() {
105 assert_lint_count(
106 "An apple, a banana and a pear walk into a bar.",
107 OxfordComma::default(),
108 1,
109 );
110 }
111
112 #[test]
113 fn people() {
114 assert_suggestion_result(
115 "Nancy, Steve and Carl are going to the coffee shop.",
116 OxfordComma::default(),
117 "Nancy, Steve, and Carl are going to the coffee shop.",
118 );
119 }
120
121 #[test]
122 fn places() {
123 assert_suggestion_result(
124 "I've always wanted to visit Paris, Tokyo and Rome.",
125 OxfordComma::default(),
126 "I've always wanted to visit Paris, Tokyo, and Rome.",
127 );
128 }
129
130 #[test]
131 fn foods() {
132 assert_suggestion_result(
133 "My favorite foods are pizza, sushi, tacos and burgers.",
134 OxfordComma::default(),
135 "My favorite foods are pizza, sushi, tacos, and burgers.",
136 );
137 }
138
139 #[test]
140 fn allows_clean_music() {
141 assert_lint_count(
142 "I enjoy listening to pop music, rock, hip-hop, electronic dance, and classical music.",
143 OxfordComma::default(),
144 0,
145 );
146 }
147
148 #[test]
149 fn allows_clean_nations() {
150 assert_lint_count(
151 "The team consists of players from different countries: France, Germany, Italy, and Spain.",
152 OxfordComma::default(),
153 0,
154 );
155 }
156
157 #[test]
158 fn or_writing() {
159 assert_suggestion_result(
160 "Harper can be a lifesaver when writing technical documents, emails or other formal forms of communication.",
161 OxfordComma::default(),
162 "Harper can be a lifesaver when writing technical documents, emails, or other formal forms of communication.",
163 );
164 }
165
166 #[test]
167 fn sports() {
168 assert_suggestion_result(
169 "They enjoy playing soccer, basketball or tennis.",
170 OxfordComma::default(),
171 "They enjoy playing soccer, basketball, or tennis.",
172 );
173 }
174
175 #[test]
176 fn nor_vegetables() {
177 assert_suggestion_result(
178 "I like carrots, kale nor broccoli.",
179 OxfordComma::default(),
180 "I like carrots, kale, nor broccoli.",
181 );
182 }
183
184 #[test]
185 fn allow_non_list_transportation() {
186 assert_lint_count(
187 "In transportation, autonomous vehicles and smart traffic management systems promise to reduce accidents and optimize travel routes.",
188 OxfordComma::default(),
189 0,
190 );
191 }
192
193 #[test]
194 fn allow_pill() {
195 assert_lint_count(
196 "Develop a pill that causes partial amnesia, affecting relationships and identity.",
197 OxfordComma::default(),
198 0,
199 );
200 }
201
202 #[test]
203 fn allow_at_first() {
204 assert_lint_count(
205 "In the heart of a bustling city, Sarah finds herself trapped in an endless cycle of the same day. Each morning, she awakens to find the date unchanged, her life on repeat. At first, confusion and frustration cloud her thoughts, but soon she notices something peculiar—each day has tiny differences, subtle changes that hint at a larger pattern.",
206 OxfordComma::default(),
207 0,
208 );
209 }
210
211 #[test]
212 fn allow_standoff() {
213 assert_lint_count(
214 "In a tense standoff, Alex and his reflection engage in a battle of wills.",
215 OxfordComma::default(),
216 0,
217 );
218 }
219}