harper_core/linting/
expr_linter.rs1use crate::expr::{Expr, ExprExt};
2use blanket::blanket;
3
4use crate::{Document, LSend, Token, TokenStringExt};
5
6use super::{Lint, Linter};
7
8#[blanket(derive(Box))]
13pub trait ExprLinter: LSend {
14 fn expr(&self) -> &dyn Expr;
16 fn match_to_lint(&self, matched_tokens: &[Token], source: &[char]) -> Option<Lint> {
26 self.match_to_lint_with_context(matched_tokens, source, None)
27 }
28
29 fn match_to_lint_with_context(
36 &self,
37 matched_tokens: &[Token],
38 source: &[char],
39 _context: Option<(&[Token], &[Token])>,
40 ) -> Option<Lint> {
41 self.match_to_lint(matched_tokens, source)
43 }
44 fn description(&self) -> &str;
47}
48
49pub fn find_the_only_token_matching<'a, F>(
54 tokens: &'a [Token],
55 source: &[char],
56 predicate: F,
57) -> Option<&'a Token>
58where
59 F: Fn(&Token, &[char]) -> bool,
60{
61 let mut matches = tokens.iter().filter(|&tok| predicate(tok, source));
62 match (matches.next(), matches.next()) {
63 (Some(tok), None) => Some(tok),
64 _ => None,
65 }
66}
67
68impl<L> Linter for L
69where
70 L: ExprLinter,
71{
72 fn lint(&mut self, document: &Document) -> Vec<Lint> {
73 let mut lints = Vec::new();
74 let source = document.get_source();
75
76 for chunk in document.iter_chunks() {
77 lints.extend(run_on_chunk(self, chunk, source));
78 }
79
80 lints
81 }
82
83 fn description(&self) -> &str {
84 self.description()
85 }
86}
87
88pub fn run_on_chunk<'a>(
89 linter: &'a impl ExprLinter,
90 chunk: &'a [Token],
91 source: &'a [char],
92) -> impl Iterator<Item = Lint> + 'a {
93 linter
94 .expr()
95 .iter_matches(chunk, source)
96 .filter_map(|match_span| {
97 linter.match_to_lint_with_context(
98 &chunk[match_span.start..match_span.end],
99 source,
100 Some((&chunk[..match_span.start], &chunk[match_span.end..])),
101 )
102 })
103}
104
105#[cfg(test)]
106mod tests {
107 use crate::expr::{Expr, FixedPhrase};
108 use crate::linting::tests::assert_suggestion_result;
109 use crate::linting::{ExprLinter, Suggestion};
110 use crate::token_string_ext::TokenStringExt;
111 use crate::{Lint, Token};
112
113 pub struct TestSimpleLinter {
114 expr: Box<dyn Expr>,
115 }
116
117 impl Default for TestSimpleLinter {
118 fn default() -> Self {
119 Self {
120 expr: Box::new(FixedPhrase::from_phrase("two")),
121 }
122 }
123 }
124
125 impl ExprLinter for TestSimpleLinter {
126 fn expr(&self) -> &dyn Expr {
127 &*self.expr
128 }
129
130 fn match_to_lint(&self, toks: &[Token], _src: &[char]) -> Option<Lint> {
131 Some(Lint {
132 span: toks.span()?,
133 message: "simple".to_string(),
134 suggestions: vec![Suggestion::ReplaceWith(vec!['2'])],
135 ..Default::default()
136 })
137 }
138
139 fn description(&self) -> &str {
140 "test linter"
141 }
142 }
143
144 pub struct TestContextLinter {
145 expr: Box<dyn Expr>,
146 }
147
148 impl Default for TestContextLinter {
149 fn default() -> Self {
150 Self {
151 expr: Box::new(FixedPhrase::from_phrase("two")),
152 }
153 }
154 }
155
156 impl ExprLinter for TestContextLinter {
157 fn expr(&self) -> &dyn Expr {
158 &*self.expr
159 }
160
161 fn match_to_lint_with_context(
162 &self,
163 toks: &[Token],
164 src: &[char],
165 context: Option<(&[Token], &[Token])>,
166 ) -> Option<Lint> {
167 if let Some((before, after)) = context {
168 let before = before.span()?.get_content_string(src);
169 let after = after.span()?.get_content_string(src);
170
171 let (message, suggestions) = if before.eq_ignore_ascii_case("one ")
172 && after.eq_ignore_ascii_case(" three")
173 {
174 (
175 "ascending".to_string(),
176 vec![Suggestion::ReplaceWith(vec!['>'])],
177 )
178 } else if before.eq_ignore_ascii_case("three ")
179 && after.eq_ignore_ascii_case(" one")
180 {
181 (
182 "descending".to_string(),
183 vec![Suggestion::ReplaceWith(vec!['<'])],
184 )
185 } else {
186 (
187 "dunno".to_string(),
188 vec![Suggestion::ReplaceWith(vec!['?'])],
189 )
190 };
191
192 return Some(Lint {
193 span: toks.span()?,
194 message,
195 suggestions,
196 ..Default::default()
197 });
198 } else {
199 None
200 }
201 }
202
203 fn description(&self) -> &str {
204 "context linter"
205 }
206 }
207
208 #[test]
209 fn simple_test_123() {
210 assert_suggestion_result("one two three", TestSimpleLinter::default(), "one 2 three");
211 }
212
213 #[test]
214 fn context_test_123() {
215 assert_suggestion_result("one two three", TestContextLinter::default(), "one > three");
216 }
217
218 #[test]
219 fn context_test_321() {
220 assert_suggestion_result("three two one", TestContextLinter::default(), "three < one");
221 }
222}