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