]> git.proxmox.com Git - rustc.git/blame - src/tools/rustfmt/src/issues.rs
New upstream version 1.52.1+dfsg1
[rustc.git] / src / tools / rustfmt / src / issues.rs
CommitLineData
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
5use std::fmt;
6
7use crate::config::ReportTactic;
8
9const TO_DO_CHARS: &[char] = &['t', 'o', 'd', 'o'];
10const 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
14fn is_enabled(report_tactic: ReportTactic) -> bool {
15 report_tactic != ReportTactic::Never
16}
17
18#[derive(Clone, Copy)]
19enum Seeking {
20 Issue { todo_idx: usize, fixme_idx: usize },
21 Number { issue: Issue, part: NumberPart },
22}
23
24#[derive(Clone, Copy)]
25enum NumberPart {
26 OpenParen,
27 Pound,
28 Number,
29 CloseParen,
30}
31
32#[derive(PartialEq, Eq, Debug, Clone, Copy)]
33pub 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
40impl 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)]
57enum IssueType {
58 Todo,
59 Fixme,
60}
61
62enum IssueClassification {
63 Good,
64 Bad(Issue),
65 None,
66}
67
68pub(crate) struct BadIssueSeeker {
69 state: Seeking,
70 report_todo: ReportTactic,
71 report_fixme: ReportTactic,
72}
73
74impl 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]
215fn 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]
243fn 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]
287fn 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}