]>
Commit | Line | Data |
---|---|---|
1a4d82fc | 1 | use self::WhichLine::*; |
223e47cc | 2 | |
54a0048b | 3 | use std::fmt; |
c34b1796 | 4 | use std::fs::File; |
c34b1796 | 5 | use std::io::prelude::*; |
94b46f34 | 6 | use std::io::BufReader; |
c34b1796 | 7 | use std::path::Path; |
54a0048b | 8 | use std::str::FromStr; |
e8be2606 | 9 | use std::sync::OnceLock; |
54a0048b | 10 | |
60c5eb7d | 11 | use regex::Regex; |
3dfed10e | 12 | use tracing::*; |
48663c56 | 13 | |
c0240ec0 | 14 | #[derive(Copy, Clone, Debug, PartialEq)] |
54a0048b SL |
15 | pub enum ErrorKind { |
16 | Help, | |
17 | Error, | |
18 | Note, | |
19 | Suggestion, | |
20 | Warning, | |
21 | } | |
22 | ||
23 | impl FromStr for ErrorKind { | |
24 | type Err = (); | |
25 | fn from_str(s: &str) -> Result<Self, Self::Err> { | |
a7813a04 XL |
26 | let s = s.to_uppercase(); |
27 | let part0: &str = s.split(':').next().unwrap(); | |
28 | match part0 { | |
54a0048b SL |
29 | "HELP" => Ok(ErrorKind::Help), |
30 | "ERROR" => Ok(ErrorKind::Error), | |
31 | "NOTE" => Ok(ErrorKind::Note), | |
32 | "SUGGESTION" => Ok(ErrorKind::Suggestion), | |
94b46f34 | 33 | "WARN" | "WARNING" => Ok(ErrorKind::Warning), |
54a0048b SL |
34 | _ => Err(()), |
35 | } | |
36 | } | |
37 | } | |
38 | ||
39 | impl fmt::Display for ErrorKind { | |
9fa01778 | 40 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
54a0048b | 41 | match *self { |
abe05a73 | 42 | ErrorKind::Help => write!(f, "help message"), |
54a0048b SL |
43 | ErrorKind::Error => write!(f, "error"), |
44 | ErrorKind::Note => write!(f, "note"), | |
45 | ErrorKind::Suggestion => write!(f, "suggestion"), | |
46 | ErrorKind::Warning => write!(f, "warning"), | |
47 | } | |
48 | } | |
49 | } | |
223e47cc | 50 | |
a7813a04 XL |
51 | #[derive(Debug)] |
52 | pub struct Error { | |
54a0048b | 53 | pub line_num: usize, |
0731742a | 54 | /// What kind of message we expect (e.g., warning, error, suggestion). |
54a0048b SL |
55 | /// `None` if not specified or unknown message kind. |
56 | pub kind: Option<ErrorKind>, | |
1a4d82fc JJ |
57 | pub msg: String, |
58 | } | |
223e47cc | 59 | |
31ef2f64 FG |
60 | impl Error { |
61 | pub fn render_for_expected(&self) -> String { | |
62 | use colored::Colorize; | |
63 | format!( | |
64 | "{: <10}line {: >3}: {}", | |
65 | self.kind.map(|kind| kind.to_string()).unwrap_or_default().to_uppercase(), | |
66 | self.line_num, | |
67 | self.msg.cyan(), | |
68 | ) | |
69 | } | |
70 | } | |
71 | ||
85aaf69f | 72 | #[derive(PartialEq, Debug)] |
5bcae85e SL |
73 | enum WhichLine { |
74 | ThisLine, | |
75 | FollowPrevious(usize), | |
76 | AdjustBackward(usize), | |
77 | } | |
85aaf69f | 78 | |
1a4d82fc JJ |
79 | /// Looks for either "//~| KIND MESSAGE" or "//~^^... KIND MESSAGE" |
80 | /// The former is a "follow" that inherits its target from the preceding line; | |
81 | /// the latter is an "adjusts" that goes that many lines up. | |
82 | /// | |
83 | /// Goal is to enable tests both like: //~^^^ ERROR go up three | |
84 | /// and also //~^ ERROR message one for the preceding line, and | |
85 | /// //~| ERROR message two for that same line. | |
54a0048b | 86 | /// |
c620b35d FG |
87 | /// If revision is not None, then we look |
88 | /// for `//[X]~` instead, where `X` is the current revision. | |
89 | pub fn load_errors(testfile: &Path, revision: Option<&str>) -> Vec<Error> { | |
c34b1796 | 90 | let rdr = BufReader::new(File::open(testfile).unwrap()); |
223e47cc | 91 | |
1a4d82fc JJ |
92 | // `last_nonfollow_error` tracks the most recently seen |
93 | // line with an error template that did not use the | |
94 | // follow-syntax, "//~| ...". | |
95 | // | |
96 | // (pnkfelix could not find an easy way to compose Iterator::scan | |
97 | // and Iterator::filter_map to pass along this information into | |
98 | // `parse_expected`. So instead I am storing that state here and | |
99 | // updating it in the map callback below.) | |
100 | let mut last_nonfollow_error = None; | |
223e47cc | 101 | |
54a0048b | 102 | rdr.lines() |
5bcae85e SL |
103 | .enumerate() |
104 | .filter_map(|(line_num, line)| { | |
c620b35d | 105 | parse_expected(last_nonfollow_error, line_num + 1, &line.unwrap(), revision).map( |
94b46f34 | 106 | |(which, error)| { |
5bcae85e SL |
107 | match which { |
108 | FollowPrevious(_) => {} | |
109 | _ => last_nonfollow_error = Some(error.line_num), | |
110 | } | |
60c5eb7d | 111 | |
5bcae85e | 112 | error |
94b46f34 XL |
113 | }, |
114 | ) | |
5bcae85e SL |
115 | }) |
116 | .collect() | |
1a4d82fc | 117 | } |
223e47cc | 118 | |
94b46f34 XL |
119 | fn parse_expected( |
120 | last_nonfollow_error: Option<usize>, | |
121 | line_num: usize, | |
122 | line: &str, | |
c620b35d | 123 | test_revision: Option<&str>, |
94b46f34 | 124 | ) -> Option<(WhichLine, Error)> { |
60c5eb7d XL |
125 | // Matches comments like: |
126 | // //~ | |
127 | // //~| | |
128 | // //~^ | |
129 | // //~^^^^^ | |
c620b35d FG |
130 | // //[rev1]~ |
131 | // //[rev1,rev2]~^^ | |
e8be2606 | 132 | static RE: OnceLock<Regex> = OnceLock::new(); |
60c5eb7d | 133 | |
e8be2606 FG |
134 | let captures = RE |
135 | .get_or_init(|| Regex::new(r"//(?:\[(?P<revs>[\w\-,]+)])?~(?P<adjust>\||\^*)").unwrap()) | |
136 | .captures(line)?; | |
60c5eb7d | 137 | |
c620b35d FG |
138 | match (test_revision, captures.name("revs")) { |
139 | // Only error messages that contain our revision between the square brackets apply to us. | |
140 | (Some(test_revision), Some(revision_filters)) => { | |
141 | if !revision_filters.as_str().split(',').any(|r| r == test_revision) { | |
142 | return None; | |
143 | } | |
144 | } | |
60c5eb7d XL |
145 | |
146 | (None, Some(_)) => panic!("Only tests with revisions should use `//[X]~`"), | |
147 | ||
148 | // If an error has no list of revisions, it applies to all revisions. | |
149 | (Some(_), None) | (None, None) => {} | |
150 | } | |
151 | ||
152 | let (follow, adjusts) = match &captures["adjust"] { | |
153 | "|" => (true, 0), | |
154 | circumflexes => (false, circumflexes.len()), | |
85aaf69f | 155 | }; |
60c5eb7d XL |
156 | |
157 | // Get the part of the comment after the sigil (e.g. `~^^` or ~|). | |
158 | let whole_match = captures.get(0).unwrap(); | |
159 | let (_, mut msg) = line.split_at(whole_match.end()); | |
160 | ||
dfeec247 | 161 | let first_word = msg.split_whitespace().next().expect("Encountered unexpected empty comment"); |
60c5eb7d XL |
162 | |
163 | // If we find `//~ ERROR foo` or something like that, skip the first word. | |
164 | let kind = first_word.parse::<ErrorKind>().ok(); | |
3dfed10e | 165 | if kind.is_some() { |
60c5eb7d | 166 | msg = &msg.trim_start().split_at(first_word.len()).1; |
a7813a04 | 167 | } |
60c5eb7d | 168 | |
a7813a04 | 169 | let msg = msg.trim().to_owned(); |
223e47cc | 170 | |
54a0048b | 171 | let (which, line_num) = if follow { |
041b39d2 | 172 | assert_eq!(adjusts, 0, "use either //~| or //~^, not both."); |
94b46f34 XL |
173 | let line_num = last_nonfollow_error.expect( |
174 | "encountered //~| without \ | |
175 | preceding //~^ line.", | |
176 | ); | |
54a0048b | 177 | (FollowPrevious(line_num), line_num) |
85aaf69f | 178 | } else { |
dfeec247 | 179 | let which = if adjusts > 0 { AdjustBackward(adjusts) } else { ThisLine }; |
54a0048b SL |
180 | let line_num = line_num - adjusts; |
181 | (which, line_num) | |
85aaf69f | 182 | }; |
223e47cc | 183 | |
94b46f34 XL |
184 | debug!( |
185 | "line={} tag={:?} which={:?} kind={:?} msg={:?}", | |
dfeec247 XL |
186 | line_num, |
187 | whole_match.as_str(), | |
94b46f34 | 188 | which, |
dfeec247 XL |
189 | kind, |
190 | msg | |
191 | ); | |
192 | Some((which, Error { line_num, kind, msg })) | |
223e47cc | 193 | } |
e8be2606 FG |
194 | |
195 | #[cfg(test)] | |
196 | mod tests; |