compiletest/
errors.rs

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