]>
Commit | Line | Data |
---|---|---|
f20569fa XL |
1 | // Objects for seeking through a char stream for occurrences of TODO and FIXME. |
2 | // Depending on the loaded configuration, may also check that these have an | |
3 | // associated issue number. | |
4 | ||
5 | use std::fmt; | |
6 | ||
7 | use crate::config::ReportTactic; | |
8 | ||
9 | const TO_DO_CHARS: &[char] = &['t', 'o', 'd', 'o']; | |
10 | const FIX_ME_CHARS: &[char] = &['f', 'i', 'x', 'm', 'e']; | |
11 | ||
12 | // Enabled implementation detail is here because it is | |
13 | // irrelevant outside the issues module | |
14 | fn is_enabled(report_tactic: ReportTactic) -> bool { | |
15 | report_tactic != ReportTactic::Never | |
16 | } | |
17 | ||
18 | #[derive(Clone, Copy)] | |
19 | enum Seeking { | |
20 | Issue { todo_idx: usize, fixme_idx: usize }, | |
21 | Number { issue: Issue, part: NumberPart }, | |
22 | } | |
23 | ||
24 | #[derive(Clone, Copy)] | |
25 | enum NumberPart { | |
26 | OpenParen, | |
27 | Pound, | |
28 | Number, | |
29 | CloseParen, | |
30 | } | |
31 | ||
32 | #[derive(PartialEq, Eq, Debug, Clone, Copy)] | |
33 | pub struct Issue { | |
34 | issue_type: IssueType, | |
35 | // Indicates whether we're looking for issues with missing numbers, or | |
36 | // all issues of this type. | |
37 | missing_number: bool, | |
38 | } | |
39 | ||
40 | impl fmt::Display for Issue { | |
41 | fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { | |
42 | let msg = match self.issue_type { | |
43 | IssueType::Todo => "TODO", | |
44 | IssueType::Fixme => "FIXME", | |
45 | }; | |
46 | let details = if self.missing_number { | |
47 | " without issue number" | |
48 | } else { | |
49 | "" | |
50 | }; | |
51 | ||
52 | write!(fmt, "{}{}", msg, details) | |
53 | } | |
54 | } | |
55 | ||
56 | #[derive(PartialEq, Eq, Debug, Clone, Copy)] | |
57 | enum IssueType { | |
58 | Todo, | |
59 | Fixme, | |
60 | } | |
61 | ||
62 | enum IssueClassification { | |
63 | Good, | |
64 | Bad(Issue), | |
65 | None, | |
66 | } | |
67 | ||
68 | pub(crate) struct BadIssueSeeker { | |
69 | state: Seeking, | |
70 | report_todo: ReportTactic, | |
71 | report_fixme: ReportTactic, | |
72 | } | |
73 | ||
74 | impl BadIssueSeeker { | |
75 | pub(crate) fn new(report_todo: ReportTactic, report_fixme: ReportTactic) -> BadIssueSeeker { | |
76 | BadIssueSeeker { | |
77 | state: Seeking::Issue { | |
78 | todo_idx: 0, | |
79 | fixme_idx: 0, | |
80 | }, | |
81 | report_todo, | |
82 | report_fixme, | |
83 | } | |
84 | } | |
85 | ||
86 | pub(crate) fn is_disabled(&self) -> bool { | |
87 | !is_enabled(self.report_todo) && !is_enabled(self.report_fixme) | |
88 | } | |
89 | ||
90 | // Check whether or not the current char is conclusive evidence for an | |
91 | // unnumbered TO-DO or FIX-ME. | |
92 | pub(crate) fn inspect(&mut self, c: char) -> Option<Issue> { | |
93 | match self.state { | |
94 | Seeking::Issue { | |
95 | todo_idx, | |
96 | fixme_idx, | |
97 | } => { | |
98 | self.state = self.inspect_issue(c, todo_idx, fixme_idx); | |
99 | } | |
100 | Seeking::Number { issue, part } => { | |
101 | let result = self.inspect_number(c, issue, part); | |
102 | ||
103 | if let IssueClassification::None = result { | |
104 | return None; | |
105 | } | |
106 | ||
107 | self.state = Seeking::Issue { | |
108 | todo_idx: 0, | |
109 | fixme_idx: 0, | |
110 | }; | |
111 | ||
112 | if let IssueClassification::Bad(issue) = result { | |
113 | return Some(issue); | |
114 | } | |
115 | } | |
116 | } | |
117 | ||
118 | None | |
119 | } | |
120 | ||
121 | fn inspect_issue(&mut self, c: char, mut todo_idx: usize, mut fixme_idx: usize) -> Seeking { | |
122 | if let Some(lower_case_c) = c.to_lowercase().next() { | |
123 | if is_enabled(self.report_todo) && lower_case_c == TO_DO_CHARS[todo_idx] { | |
124 | todo_idx += 1; | |
125 | if todo_idx == TO_DO_CHARS.len() { | |
126 | return Seeking::Number { | |
127 | issue: Issue { | |
128 | issue_type: IssueType::Todo, | |
129 | missing_number: if let ReportTactic::Unnumbered = self.report_todo { | |
130 | true | |
131 | } else { | |
132 | false | |
133 | }, | |
134 | }, | |
135 | part: NumberPart::OpenParen, | |
136 | }; | |
137 | } | |
138 | fixme_idx = 0; | |
139 | } else if is_enabled(self.report_fixme) && lower_case_c == FIX_ME_CHARS[fixme_idx] { | |
140 | // Exploit the fact that the character sets of todo and fixme | |
141 | // are disjoint by adding else. | |
142 | fixme_idx += 1; | |
143 | if fixme_idx == FIX_ME_CHARS.len() { | |
144 | return Seeking::Number { | |
145 | issue: Issue { | |
146 | issue_type: IssueType::Fixme, | |
147 | missing_number: if let ReportTactic::Unnumbered = self.report_fixme { | |
148 | true | |
149 | } else { | |
150 | false | |
151 | }, | |
152 | }, | |
153 | part: NumberPart::OpenParen, | |
154 | }; | |
155 | } | |
156 | todo_idx = 0; | |
157 | } else { | |
158 | todo_idx = 0; | |
159 | fixme_idx = 0; | |
160 | } | |
161 | } | |
162 | ||
163 | Seeking::Issue { | |
164 | todo_idx, | |
165 | fixme_idx, | |
166 | } | |
167 | } | |
168 | ||
169 | fn inspect_number( | |
170 | &mut self, | |
171 | c: char, | |
172 | issue: Issue, | |
173 | mut part: NumberPart, | |
174 | ) -> IssueClassification { | |
175 | if !issue.missing_number || c == '\n' { | |
176 | return IssueClassification::Bad(issue); | |
177 | } else if c == ')' { | |
178 | return if let NumberPart::CloseParen = part { | |
179 | IssueClassification::Good | |
180 | } else { | |
181 | IssueClassification::Bad(issue) | |
182 | }; | |
183 | } | |
184 | ||
185 | match part { | |
186 | NumberPart::OpenParen => { | |
187 | if c != '(' { | |
188 | return IssueClassification::Bad(issue); | |
189 | } else { | |
190 | part = NumberPart::Pound; | |
191 | } | |
192 | } | |
193 | NumberPart::Pound => { | |
194 | if c == '#' { | |
195 | part = NumberPart::Number; | |
196 | } | |
197 | } | |
198 | NumberPart::Number => { | |
199 | if c >= '0' && c <= '9' { | |
200 | part = NumberPart::CloseParen; | |
201 | } else { | |
202 | return IssueClassification::Bad(issue); | |
203 | } | |
204 | } | |
205 | NumberPart::CloseParen => {} | |
206 | } | |
207 | ||
208 | self.state = Seeking::Number { part, issue }; | |
209 | ||
210 | IssueClassification::None | |
211 | } | |
212 | } | |
213 | ||
214 | #[test] | |
215 | fn find_unnumbered_issue() { | |
216 | fn check_fail(text: &str, failing_pos: usize) { | |
217 | let mut seeker = BadIssueSeeker::new(ReportTactic::Unnumbered, ReportTactic::Unnumbered); | |
218 | assert_eq!( | |
219 | Some(failing_pos), | |
220 | text.find(|c| seeker.inspect(c).is_some()) | |
221 | ); | |
222 | } | |
223 | ||
224 | fn check_pass(text: &str) { | |
225 | let mut seeker = BadIssueSeeker::new(ReportTactic::Unnumbered, ReportTactic::Unnumbered); | |
226 | assert_eq!(None, text.find(|c| seeker.inspect(c).is_some())); | |
227 | } | |
228 | ||
229 | check_fail("TODO\n", 4); | |
230 | check_pass(" TO FIX DOME\n"); | |
231 | check_fail(" \n FIXME\n", 8); | |
232 | check_fail("FIXME(\n", 6); | |
233 | check_fail("FIXME(#\n", 7); | |
234 | check_fail("FIXME(#1\n", 8); | |
235 | check_fail("FIXME(#)1\n", 7); | |
236 | check_pass("FIXME(#1222)\n"); | |
237 | check_fail("FIXME(#12\n22)\n", 9); | |
238 | check_pass("FIXME(@maintainer, #1222, hello)\n"); | |
239 | check_fail("TODO(#22) FIXME\n", 15); | |
240 | } | |
241 | ||
242 | #[test] | |
243 | fn find_issue() { | |
244 | fn is_bad_issue(text: &str, report_todo: ReportTactic, report_fixme: ReportTactic) -> bool { | |
245 | let mut seeker = BadIssueSeeker::new(report_todo, report_fixme); | |
246 | text.chars().any(|c| seeker.inspect(c).is_some()) | |
247 | } | |
248 | ||
249 | assert!(is_bad_issue( | |
250 | "TODO(@maintainer, #1222, hello)\n", | |
251 | ReportTactic::Always, | |
252 | ReportTactic::Never, | |
253 | )); | |
254 | ||
255 | assert!(!is_bad_issue( | |
256 | "TODO: no number\n", | |
257 | ReportTactic::Never, | |
258 | ReportTactic::Always, | |
259 | )); | |
260 | ||
261 | assert!(!is_bad_issue( | |
262 | "Todo: mixed case\n", | |
263 | ReportTactic::Never, | |
264 | ReportTactic::Always, | |
265 | )); | |
266 | ||
267 | assert!(is_bad_issue( | |
268 | "This is a FIXME(#1)\n", | |
269 | ReportTactic::Never, | |
270 | ReportTactic::Always, | |
271 | )); | |
272 | ||
273 | assert!(is_bad_issue( | |
274 | "This is a FixMe(#1) mixed case\n", | |
275 | ReportTactic::Never, | |
276 | ReportTactic::Always, | |
277 | )); | |
278 | ||
279 | assert!(!is_bad_issue( | |
280 | "bad FIXME\n", | |
281 | ReportTactic::Always, | |
282 | ReportTactic::Never, | |
283 | )); | |
284 | } | |
285 | ||
286 | #[test] | |
287 | fn issue_type() { | |
288 | let mut seeker = BadIssueSeeker::new(ReportTactic::Always, ReportTactic::Never); | |
289 | let expected = Some(Issue { | |
290 | issue_type: IssueType::Todo, | |
291 | missing_number: false, | |
292 | }); | |
293 | ||
294 | assert_eq!( | |
295 | expected, | |
296 | "TODO(#100): more awesomeness" | |
297 | .chars() | |
298 | .map(|c| seeker.inspect(c)) | |
299 | .find(Option::is_some) | |
300 | .unwrap() | |
301 | ); | |
302 | ||
303 | let mut seeker = BadIssueSeeker::new(ReportTactic::Never, ReportTactic::Unnumbered); | |
304 | let expected = Some(Issue { | |
305 | issue_type: IssueType::Fixme, | |
306 | missing_number: true, | |
307 | }); | |
308 | ||
309 | assert_eq!( | |
310 | expected, | |
311 | "Test. FIXME: bad, bad, not good" | |
312 | .chars() | |
313 | .map(|c| seeker.inspect(c)) | |
314 | .find(Option::is_some) | |
315 | .unwrap() | |
316 | ); | |
317 | } |