]> git.lizzy.rs Git - rust.git/blob - src/tools/rustfmt/src/issues.rs
d369b75541ef90fa2756bde652dad1c8073f64ba
[rust.git] / src / tools / rustfmt / src / issues.rs
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 }