compiletest/
errors.rs

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