Skip to content

Commit c7ecf74

Browse files
committed
Merge pull request #106 from marcusklaas/issue-number-check-2
Implement checks for unnumbered TODOs and FIXMEs
2 parents 9da7be9 + d335d04 commit c7ecf74

File tree

8 files changed

+435
-22
lines changed

8 files changed

+435
-22
lines changed

src/changes.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ use std::fs::File;
2121
use std::io::{Write, stdout};
2222
use WriteMode;
2323
use NewlineStyle;
24+
use utils::round_up_to_power_of_two;
2425

2526
// This is basically a wrapper around a bunch of Ropes which makes it convenient
2627
// to work with libsyntax. It is badly named.
@@ -41,11 +42,10 @@ impl<'a> ChangeSet<'a> {
4142

4243
for f in codemap.files.borrow().iter() {
4344
// Use the length of the file as a heuristic for how much space we
44-
// need. I hope that at some stage someone rounds this up to the next
45-
// power of two. TODO check that or do it here.
46-
result.file_map.insert(f.name.clone(),
47-
StringBuffer::with_capacity(f.src.as_ref().unwrap().len()));
45+
// need. Round to the next power of two.
46+
let buffer_cap = round_up_to_power_of_two(f.src.as_ref().unwrap().len());
4847

48+
result.file_map.insert(f.name.clone(), StringBuffer::with_capacity(buffer_cap));
4949
result.file_spans.push((f.start_pos.0, f.end_pos.0));
5050
}
5151

src/config.rs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,25 @@
1010

1111
extern crate toml;
1212

13+
use {NewlineStyle, BraceStyle, ReturnIndent};
14+
use lists::SeparatorTactic;
15+
use issues::ReportTactic;
16+
1317
#[derive(RustcDecodable)]
1418
pub struct Config {
1519
pub max_width: usize,
1620
pub ideal_width: usize,
1721
pub leeway: usize,
1822
pub tab_spaces: usize,
19-
pub newline_style: ::NewlineStyle,
20-
pub fn_brace_style: ::BraceStyle,
21-
pub fn_return_indent: ::ReturnIndent,
23+
pub newline_style: NewlineStyle,
24+
pub fn_brace_style: BraceStyle,
25+
pub fn_return_indent: ReturnIndent,
2226
pub fn_args_paren_newline: bool,
2327
pub struct_trailing_comma: bool,
24-
pub struct_lit_trailing_comma: ::lists::SeparatorTactic,
28+
pub struct_lit_trailing_comma: SeparatorTactic,
2529
pub enum_trailing_comma: bool,
30+
pub report_todo: ReportTactic,
31+
pub report_fixme: ReportTactic,
2632
}
2733

2834
impl Config {

src/default.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,5 @@ fn_args_paren_newline = true
99
struct_trailing_comma = true
1010
struct_lit_trailing_comma = "Vertical"
1111
enum_trailing_comma = true
12+
report_todo = "Always"
13+
report_fixme = "Never"

src/issues.rs

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

0 commit comments

Comments
 (0)