Skip to content

Commit adf42ee

Browse files
authored
feat: number agreement in this/these type(s) of thing(s) (#2400)
* feat: number agreement in this/these type(s) of thing(s) * fix: one wrong test snuck in * fix: address booboo brought to light in PR review
1 parent 02896a7 commit adf42ee

3 files changed

Lines changed: 260 additions & 0 deletions

File tree

harper-core/src/linting/lint_group.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ use super::then_than::ThenThan;
187187
use super::theres::Theres;
188188
use super::theses_these::ThesesThese;
189189
use super::thing_think::ThingThink;
190+
use super::this_type_of_thing::ThisTypeOfThing;
190191
use super::though_thought::ThoughThought;
191192
use super::throw_away::ThrowAway;
192193
use super::throw_rubbish::ThrowRubbish;
@@ -660,6 +661,7 @@ impl LintGroup {
660661
insert_expr_rule!(Theres, true);
661662
insert_expr_rule!(ThesesThese, true);
662663
insert_expr_rule!(ThingThink, true);
664+
insert_expr_rule!(ThisTypeOfThing, true);
663665
insert_expr_rule!(ThoughThought, true);
664666
insert_expr_rule!(ThrowAway, true);
665667
insert_struct_rule!(ThrowRubbish, true);

harper-core/src/linting/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ mod then_than;
199199
mod theres;
200200
mod theses_these;
201201
mod thing_think;
202+
mod this_type_of_thing;
202203
mod though_thought;
203204
mod throw_away;
204205
mod throw_rubbish;
Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
use crate::{
2+
CharStringExt, Lint, Token,
3+
expr::{Expr, SequenceExpr},
4+
linting::{ExprLinter, LintKind, Suggestion, expr_linter::Chunk},
5+
patterns::WordSet,
6+
};
7+
8+
pub struct ThisTypeOfThing {
9+
expr: Box<dyn Expr>,
10+
}
11+
12+
impl Default for ThisTypeOfThing {
13+
fn default() -> Self {
14+
Self {
15+
expr: Box::new(
16+
SequenceExpr::word_set(&["this", "these", "that", "those"])
17+
.t_ws()
18+
.then(
19+
SequenceExpr::word_set(&[
20+
"kind", "kinds", "sort", "sorts", "type", "types",
21+
])
22+
.t_ws(),
23+
)
24+
.then(SequenceExpr::aco("of").t_ws())
25+
.then_any_of(vec![
26+
// "thing" is common in this construction and won't be part of a compound noun.
27+
Box::new(WordSet::new(&["thing", "things"])),
28+
// Other singular nouns may be part of hard-to-determine compound nouns, but plural nouns won't.
29+
Box::new(
30+
SequenceExpr::default()
31+
.then_kind_where(|k| k.is_plural_noun() && !k.is_singular_noun()),
32+
),
33+
]),
34+
),
35+
}
36+
}
37+
}
38+
39+
impl ExprLinter for ThisTypeOfThing {
40+
type Unit = Chunk;
41+
42+
fn expr(&self) -> &dyn Expr {
43+
self.expr.as_ref()
44+
}
45+
46+
fn description(&self) -> &str {
47+
"Checks that the parts of `this/these type(s) of thing(s)` agree in grammatical number"
48+
}
49+
50+
fn match_to_lint(&self, toks: &[Token], src: &[char]) -> Option<Lint> {
51+
#[derive(PartialEq)]
52+
enum Num {
53+
Sg,
54+
Pl,
55+
}
56+
57+
let (det_tok, type_tok, thing_tok) = (toks.first()?, toks.get(2)?, toks.last()?);
58+
let (type_kind, thing_kind) = (&type_tok.kind, &thing_tok.kind);
59+
let (det_span, type_span) = (det_tok.span, type_tok.span);
60+
let (det_chars, type_chars) = (det_span.get_content(src), type_span.get_content(src));
61+
let (det_num, type_num, thing_num) = (
62+
if det_chars.eq_any_ignore_ascii_case_str(&["this", "that"]) {
63+
Num::Sg
64+
} else {
65+
Num::Pl
66+
},
67+
if type_kind.is_plural_noun() {
68+
Num::Pl
69+
} else {
70+
Num::Sg
71+
},
72+
if thing_kind.is_plural_noun() {
73+
Num::Pl
74+
} else {
75+
Num::Sg
76+
},
77+
);
78+
if det_num == type_num && type_num == thing_num {
79+
return None;
80+
};
81+
82+
enum Deixis {
83+
Proximal,
84+
Distal,
85+
}
86+
let deixis = if det_chars.eq_any_ignore_ascii_case_str(&["this", "these"]) {
87+
Deixis::Proximal
88+
} else {
89+
Deixis::Distal
90+
};
91+
92+
enum Specie {
93+
Kind,
94+
Sort,
95+
Type,
96+
}
97+
let specie = match type_chars.first()? {
98+
'k' | 'K' => Specie::Kind,
99+
's' | 'S' => Specie::Sort,
100+
't' | 'T' => Specie::Type,
101+
_ => return None,
102+
};
103+
104+
// Due to the logic above, when we get here we either have 1 singular and 2 plurals or 2 plurals and 1 singular.
105+
// Meaning one of the three varying words does not agree in number with the other two.
106+
let bad_tok = match (&det_num, &type_num, &thing_num) {
107+
(Num::Sg, Num::Sg, Num::Pl) => thing_tok,
108+
(Num::Sg, Num::Pl, Num::Sg) => type_tok,
109+
(Num::Sg, Num::Pl, Num::Pl) => det_tok,
110+
(Num::Pl, Num::Sg, Num::Sg) => det_tok,
111+
(Num::Pl, Num::Sg, Num::Pl) => type_tok,
112+
(Num::Pl, Num::Pl, Num::Sg) => thing_tok,
113+
_ => return None,
114+
};
115+
116+
Some(Lint {
117+
span: bad_tok.span,
118+
lint_kind: LintKind::Agreement,
119+
suggestions: vec![Suggestion::replace_with_match_case(
120+
if bad_tok == det_tok {
121+
match (det_num, deixis) {
122+
(Num::Sg, Deixis::Proximal) => "these",
123+
(Num::Sg, Deixis::Distal) => "those",
124+
(Num::Pl, Deixis::Proximal) => "this",
125+
(Num::Pl, Deixis::Distal) => "that",
126+
}
127+
} else if bad_tok == type_tok {
128+
match (type_num, specie) {
129+
(Num::Sg, Specie::Kind) => "kinds",
130+
(Num::Pl, Specie::Kind) => "kind",
131+
(Num::Sg, Specie::Sort) => "sorts",
132+
(Num::Pl, Specie::Sort) => "sort",
133+
(Num::Sg, Specie::Type) => "types",
134+
(Num::Pl, Specie::Type) => "type",
135+
}
136+
} else if bad_tok == thing_tok {
137+
match thing_num {
138+
Num::Sg => "things",
139+
Num::Pl => "thing",
140+
}
141+
} else {
142+
return None;
143+
}
144+
.chars()
145+
.collect(),
146+
bad_tok.span.get_content(src),
147+
)],
148+
message: "The grammatical number of the determiner and the two nouns must agree."
149+
.to_string(),
150+
..Default::default()
151+
})
152+
}
153+
}
154+
155+
#[cfg(test)]
156+
mod tests {
157+
use crate::linting::{tests::assert_suggestion_result, this_type_of_thing::ThisTypeOfThing};
158+
159+
#[test]
160+
fn fix_that_kind_of_things() {
161+
assert_suggestion_result(
162+
"it's specific to TypeScript and not Go nor Python can do that kind of things",
163+
ThisTypeOfThing::default(),
164+
"it's specific to TypeScript and not Go nor Python can do that kind of thing",
165+
);
166+
}
167+
168+
#[test]
169+
fn fix_that_sort_of_things() {
170+
assert_suggestion_result(
171+
"there isn't a trivial stb-like ready-to-use C++ library to do that sort of things",
172+
ThisTypeOfThing::default(),
173+
"there isn't a trivial stb-like ready-to-use C++ library to do that sort of thing",
174+
);
175+
}
176+
177+
#[test]
178+
fn fix_these_kind_of_things() {
179+
assert_suggestion_result(
180+
"For these kind of things, I think it would be great to have a user-defined field which can be used to search for files.",
181+
ThisTypeOfThing::default(),
182+
"For these kinds of things, I think it would be great to have a user-defined field which can be used to search for files.",
183+
);
184+
}
185+
186+
#[test]
187+
fn fix_these_sort_of_thing() {
188+
assert_suggestion_result(
189+
"People from npm actually get death threats for these sort of thing",
190+
ThisTypeOfThing::default(),
191+
"People from npm actually get death threats for this sort of thing",
192+
);
193+
}
194+
195+
#[test]
196+
fn fix_these_sort_of_things() {
197+
assert_suggestion_result(
198+
"I suppose doing these sort of things should be legal",
199+
ThisTypeOfThing::default(),
200+
"I suppose doing these sorts of things should be legal",
201+
);
202+
}
203+
204+
#[test]
205+
fn fix_these_sorts_of_thing() {
206+
assert_suggestion_result(
207+
"What I would like to understand is what the syntactic structure is for these sorts of things.",
208+
ThisTypeOfThing::default(),
209+
"What I would like to understand is what the syntactic structure is for these sorts of things.",
210+
);
211+
}
212+
213+
#[test]
214+
fn fix_these_type_of_things() {
215+
assert_suggestion_result(
216+
"You can use the Symfony validator to validate these type of things easily.",
217+
ThisTypeOfThing::default(),
218+
"You can use the Symfony validator to validate these types of things easily.",
219+
);
220+
}
221+
222+
#[test]
223+
fn fix_this_kind_of_things() {
224+
assert_suggestion_result(
225+
"this kind of things could exists in languages like Haskell which supports higher kinded types",
226+
ThisTypeOfThing::default(),
227+
"this kind of thing could exists in languages like Haskell which supports higher kinded types",
228+
);
229+
}
230+
231+
#[test]
232+
fn fix_this_sort_of_things() {
233+
assert_suggestion_result(
234+
"I have heard this sort of things happening in the movie industry but it's appalling that it happens in the business world too",
235+
ThisTypeOfThing::default(),
236+
"I have heard this sort of thing happening in the movie industry but it's appalling that it happens in the business world too",
237+
);
238+
}
239+
240+
#[test]
241+
fn fix_this_type_of_things() {
242+
assert_suggestion_result(
243+
"how to handle this type of things",
244+
ThisTypeOfThing::default(),
245+
"how to handle this type of thing",
246+
);
247+
}
248+
249+
#[test]
250+
fn fix_those_kind_of_things() {
251+
assert_suggestion_result(
252+
"uh, so I was playing both of those kind of things",
253+
ThisTypeOfThing::default(),
254+
"uh, so I was playing both of those kinds of things",
255+
);
256+
}
257+
}

0 commit comments

Comments
 (0)