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.
7 use crate::config
::ReportTactic
;
9 const TO_DO_CHARS
: &[char] = &['t'
, 'o'
, 'd'
, 'o'
];
10 const FIX_ME_CHARS
: &[char] = &['f'
, 'i'
, 'x'
, 'm'
, 'e'
];
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
18 #[derive(Clone, Copy)]
20 Issue { todo_idx: usize, fixme_idx: usize }
,
21 Number { issue: Issue, part: NumberPart }
,
24 #[derive(Clone, Copy)]
32 #[derive(PartialEq, Eq, Debug, Clone, Copy)]
34 issue_type
: IssueType
,
35 // Indicates whether we're looking for issues with missing numbers, or
36 // all issues of this type.
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",
46 let details
= if self.missing_number
{
47 " without issue number"
52 write
!(fmt
, "{}{}", msg
, details
)
56 #[derive(PartialEq, Eq, Debug, Clone, Copy)]
62 enum IssueClassification
{
68 pub(crate) struct BadIssueSeeker
{
70 report_todo
: ReportTactic
,
71 report_fixme
: ReportTactic
,
75 pub(crate) fn new(report_todo
: ReportTactic
, report_fixme
: ReportTactic
) -> BadIssueSeeker
{
77 state
: Seeking
::Issue
{
86 pub(crate) fn is_disabled(&self) -> bool
{
87 !is_enabled(self.report_todo
) && !is_enabled(self.report_fixme
)
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
> {
98 self.state
= self.inspect_issue(c
, todo_idx
, fixme_idx
);
100 Seeking
::Number { issue, part }
=> {
101 let result
= self.inspect_number(c
, issue
, part
);
103 if let IssueClassification
::None
= result
{
107 self.state
= Seeking
::Issue
{
112 if let IssueClassification
::Bad(issue
) = result
{
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
] {
125 if todo_idx
== TO_DO_CHARS
.len() {
126 return Seeking
::Number
{
128 issue_type
: IssueType
::Todo
,
129 missing_number
: if let ReportTactic
::Unnumbered
= self.report_todo
{
135 part
: NumberPart
::OpenParen
,
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.
143 if fixme_idx
== FIX_ME_CHARS
.len() {
144 return Seeking
::Number
{
146 issue_type
: IssueType
::Fixme
,
147 missing_number
: if let ReportTactic
::Unnumbered
= self.report_fixme
{
153 part
: NumberPart
::OpenParen
,
173 mut part
: NumberPart
,
174 ) -> IssueClassification
{
175 if !issue
.missing_number
|| c
== '
\n'
{
176 return IssueClassification
::Bad(issue
);
178 return if let NumberPart
::CloseParen
= part
{
179 IssueClassification
::Good
181 IssueClassification
::Bad(issue
)
186 NumberPart
::OpenParen
=> {
188 return IssueClassification
::Bad(issue
);
190 part
= NumberPart
::Pound
;
193 NumberPart
::Pound
=> {
195 part
= NumberPart
::Number
;
198 NumberPart
::Number
=> {
199 if c
>= '
0'
&& c
<= '
9'
{
200 part
= NumberPart
::CloseParen
;
202 return IssueClassification
::Bad(issue
);
205 NumberPart
::CloseParen
=> {}
208 self.state
= Seeking
::Number { part, issue }
;
210 IssueClassification
::None
215 fn find_unnumbered_issue() {
216 fn check_fail(text
: &str, failing_pos
: usize) {
217 let mut seeker
= BadIssueSeeker
::new(ReportTactic
::Unnumbered
, ReportTactic
::Unnumbered
);
220 text
.find(|c
| seeker
.inspect(c
).is_some())
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()));
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);
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())
249 assert
!(is_bad_issue(
250 "TODO(@maintainer, #1222, hello)\n",
251 ReportTactic
::Always
,
255 assert
!(!is_bad_issue(
258 ReportTactic
::Always
,
261 assert
!(!is_bad_issue(
262 "Todo: mixed case\n",
264 ReportTactic
::Always
,
267 assert
!(is_bad_issue(
268 "This is a FIXME(#1)\n",
270 ReportTactic
::Always
,
273 assert
!(is_bad_issue(
274 "This is a FixMe(#1) mixed case\n",
276 ReportTactic
::Always
,
279 assert
!(!is_bad_issue(
281 ReportTactic
::Always
,
288 let mut seeker
= BadIssueSeeker
::new(ReportTactic
::Always
, ReportTactic
::Never
);
289 let expected
= Some(Issue
{
290 issue_type
: IssueType
::Todo
,
291 missing_number
: false,
296 "TODO(#100): more awesomeness"
298 .map(|c
| seeker
.inspect(c
))
299 .find(Option
::is_some
)
303 let mut seeker
= BadIssueSeeker
::new(ReportTactic
::Never
, ReportTactic
::Unnumbered
);
304 let expected
= Some(Issue
{
305 issue_type
: IssueType
::Fixme
,
306 missing_number
: true,
311 "Test. FIXME: bad, bad, not good"
313 .map(|c
| seeker
.inspect(c
))
314 .find(Option
::is_some
)