harper_core/linting/
no_oxford_comma.rs1use crate::expr::ExprExt;
2use crate::expr::SequenceExpr;
3use crate::{
4 Document, Token, TokenStringExt,
5 patterns::{NominalPhrase, WordSet},
6};
7
8use super::{Lint, LintKind, Linter, Suggestion};
9
10pub struct NoOxfordComma {
11 expr: SequenceExpr,
12}
13
14impl NoOxfordComma {
15 pub fn new() -> Self {
16 Self {
17 expr: {
18 let this = {
19 let this = SequenceExpr::default();
20 this.then(NominalPhrase)
21 }
22 .then_comma()
23 .then_whitespace();
24 this.then(NominalPhrase)
25 }
26 .then_comma()
27 .then_whitespace()
28 .then(WordSet::new(&["and", "or", "nor"])),
29 }
30 }
31
32 fn match_to_lint(&self, matched_toks: &[Token], _source: &[char]) -> Option<Lint> {
33 let last_comma_index = matched_toks.last_comma_index()?;
34 let offender = &matched_toks[last_comma_index];
35
36 Some(Lint {
37 span: offender.span,
38 lint_kind: LintKind::Style,
39 suggestions: vec![Suggestion::Remove],
40 message: "Remove the Oxford comma here.".to_owned(),
41 priority: 31,
42 })
43 }
44}
45
46impl Default for NoOxfordComma {
47 fn default() -> Self {
48 Self::new()
49 }
50}
51
52impl Linter for NoOxfordComma {
53 fn lint(&mut self, document: &Document) -> Vec<Lint> {
54 let mut lints = Vec::new();
55
56 for sentence in document.iter_sentences() {
57 for match_span in self.expr.iter_matches(sentence, document.get_source()) {
58 let lint = self.match_to_lint(
59 &sentence[match_span.start..match_span.end],
60 document.get_source(),
61 );
62 lints.extend(lint);
63 }
64 }
65
66 lints
67 }
68
69 fn description(&self) -> &str {
70 "The Oxford comma is one of the more controversial rules in common use today. Enabling this lint checks that there is no comma before `and`, `or` or `nor` when listing out more than two ideas."
71 }
72}
73
74#[cfg(test)]
75mod tests {
76 use crate::linting::tests::{assert_lint_count, assert_suggestion_result};
77
78 use super::NoOxfordComma;
79
80 #[test]
81 fn fruits() {
82 assert_lint_count(
83 "An apple, a banana, and a pear",
84 NoOxfordComma::default(),
85 1,
86 );
87 }
88
89 #[test]
90 fn people() {
91 assert_suggestion_result(
92 "Nancy, Steve, and Carl are going to the coffee shop.",
93 NoOxfordComma::default(),
94 "Nancy, Steve and Carl are going to the coffee shop.",
95 );
96 }
97
98 #[test]
99 fn places() {
100 assert_suggestion_result(
101 "I've always wanted to visit Paris, Tokyo, and Rome.",
102 NoOxfordComma::default(),
103 "I've always wanted to visit Paris, Tokyo and Rome.",
104 );
105 }
106
107 #[test]
108 fn foods() {
109 assert_suggestion_result(
110 "My favorite foods are pizza, sushi, tacos, and burgers.",
111 NoOxfordComma::default(),
112 "My favorite foods are pizza, sushi, tacos and burgers.",
113 );
114 }
115
116 #[test]
117 fn allows_clean_music() {
118 assert_lint_count(
119 "I enjoy listening to pop music, rock, hip-hop, electronic dance and classical music.",
120 NoOxfordComma::default(),
121 0,
122 );
123 }
124
125 #[test]
126 fn allows_clean_nations() {
127 assert_lint_count(
128 "The team consists of players from different countries: France, Germany, Italy and Spain.",
129 NoOxfordComma::default(),
130 0,
131 );
132 }
133
134 #[test]
135 fn or_writing() {
136 assert_suggestion_result(
137 "Harper can be a lifesaver when writing technical documents, emails, or other formal forms of communication.",
138 NoOxfordComma::default(),
139 "Harper can be a lifesaver when writing technical documents, emails or other formal forms of communication.",
140 );
141 }
142
143 #[test]
144 fn sports() {
145 assert_suggestion_result(
146 "They enjoy playing soccer, basketball, or tennis.",
147 NoOxfordComma::default(),
148 "They enjoy playing soccer, basketball or tennis.",
149 );
150 }
151
152 #[test]
153 fn nor_vegetables() {
154 assert_suggestion_result(
155 "I like carrots, kale, nor broccoli.",
156 NoOxfordComma::default(),
157 "I like carrots, kale nor broccoli.",
158 );
159 }
160}