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