summaryrefslogtreecommitdiffstats
path: root/src/tools/tidy/src/error_codes_check.rs
blob: 610e322e12963e63854e0b3ba08b5300762abacd (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
//! Checks that all error codes have at least one test to prevent having error
//! codes that are silently not thrown by the compiler anymore.

use crate::walk::{filter_dirs, walk};
use std::collections::{HashMap, HashSet};
use std::ffi::OsStr;
use std::fs::read_to_string;
use std::path::Path;

use regex::Regex;

// A few of those error codes can't be tested but all the others can and *should* be tested!
const EXEMPTED_FROM_TEST: &[&str] = &[
    "E0313", "E0377", "E0461", "E0462", "E0465", "E0476", "E0490", "E0514", "E0519", "E0523",
    "E0554", "E0640", "E0717", "E0729", "E0789",
];

// Some error codes don't have any tests apparently...
const IGNORE_EXPLANATION_CHECK: &[&str] = &["E0464", "E0570", "E0601", "E0602", "E0729"];

// If the file path contains any of these, we don't want to try to extract error codes from it.
//
// We need to declare each path in the windows version (with backslash).
const PATHS_TO_IGNORE_FOR_EXTRACTION: &[&str] =
    &["src/test/", "src\\test\\", "src/doc/", "src\\doc\\", "src/tools/", "src\\tools\\"];

#[derive(Default, Debug)]
struct ErrorCodeStatus {
    has_test: bool,
    has_explanation: bool,
    is_used: bool,
}

fn check_error_code_explanation(
    f: &str,
    error_codes: &mut HashMap<String, ErrorCodeStatus>,
    err_code: String,
) -> bool {
    let mut invalid_compile_fail_format = false;
    let mut found_error_code = false;

    for line in f.lines() {
        let s = line.trim();
        if s.starts_with("```") {
            if s.contains("compile_fail") && s.contains('E') {
                if !found_error_code {
                    error_codes.get_mut(&err_code).map(|x| x.has_test = true);
                    found_error_code = true;
                }
            } else if s.contains("compile-fail") {
                invalid_compile_fail_format = true;
            }
        } else if s.starts_with("#### Note: this error code is no longer emitted by the compiler") {
            if !found_error_code {
                error_codes.get_mut(&err_code).map(|x| x.has_test = true);
                found_error_code = true;
            }
        }
    }
    invalid_compile_fail_format
}

fn check_if_error_code_is_test_in_explanation(f: &str, err_code: &str) -> bool {
    let mut ignore_found = false;

    for line in f.lines() {
        let s = line.trim();
        if s.starts_with("#### Note: this error code is no longer emitted by the compiler") {
            return true;
        }
        if s.starts_with("```") {
            if s.contains("compile_fail") && s.contains(err_code) {
                return true;
            } else if s.contains("ignore") {
                // It's very likely that we can't actually make it fail compilation...
                ignore_found = true;
            }
        }
    }
    ignore_found
}

macro_rules! some_or_continue {
    ($e:expr) => {
        match $e {
            Some(e) => e,
            None => continue,
        }
    };
}

fn extract_error_codes(
    f: &str,
    error_codes: &mut HashMap<String, ErrorCodeStatus>,
    path: &Path,
    errors: &mut Vec<String>,
) {
    let mut reached_no_explanation = false;

    for line in f.lines() {
        let s = line.trim();
        if !reached_no_explanation && s.starts_with('E') && s.contains("include_str!(\"") {
            let err_code = s
                .split_once(':')
                .expect(
                    format!(
                        "Expected a line with the format `E0xxx: include_str!(\"..\")`, but got {} \
                         without a `:` delimiter",
                        s,
                    )
                    .as_str(),
                )
                .0
                .to_owned();
            error_codes.entry(err_code.clone()).or_default().has_explanation = true;

            // Now we extract the tests from the markdown file!
            let md_file_name = match s.split_once("include_str!(\"") {
                None => continue,
                Some((_, md)) => match md.split_once("\")") {
                    None => continue,
                    Some((file_name, _)) => file_name,
                },
            };
            let path = some_or_continue!(path.parent())
                .join(md_file_name)
                .canonicalize()
                .expect("failed to canonicalize error explanation file path");
            match read_to_string(&path) {
                Ok(content) => {
                    let has_test = check_if_error_code_is_test_in_explanation(&content, &err_code);
                    if !has_test && !IGNORE_EXPLANATION_CHECK.contains(&err_code.as_str()) {
                        errors.push(format!(
                            "`{}` doesn't use its own error code in compile_fail example",
                            path.display(),
                        ));
                    } else if has_test && IGNORE_EXPLANATION_CHECK.contains(&err_code.as_str()) {
                        errors.push(format!(
                            "`{}` has a compile_fail example with its own error code, it shouldn't \
                             be listed in IGNORE_EXPLANATION_CHECK!",
                            path.display(),
                        ));
                    }
                    if check_error_code_explanation(&content, error_codes, err_code) {
                        errors.push(format!(
                            "`{}` uses invalid tag `compile-fail` instead of `compile_fail`",
                            path.display(),
                        ));
                    }
                }
                Err(e) => {
                    eprintln!("Couldn't read `{}`: {}", path.display(), e);
                }
            }
        } else if reached_no_explanation && s.starts_with('E') {
            let err_code = match s.split_once(',') {
                None => s,
                Some((err_code, _)) => err_code,
            }
            .to_string();
            if !error_codes.contains_key(&err_code) {
                // this check should *never* fail!
                error_codes.insert(err_code, ErrorCodeStatus::default());
            }
        } else if s == ";" {
            reached_no_explanation = true;
        }
    }
}

fn extract_error_codes_from_tests(f: &str, error_codes: &mut HashMap<String, ErrorCodeStatus>) {
    for line in f.lines() {
        let s = line.trim();
        if s.starts_with("error[E") || s.starts_with("warning[E") {
            let err_code = match s.split_once(']') {
                None => continue,
                Some((err_code, _)) => match err_code.split_once('[') {
                    None => continue,
                    Some((_, err_code)) => err_code,
                },
            };
            error_codes.entry(err_code.to_owned()).or_default().has_test = true;
        }
    }
}

fn extract_error_codes_from_source(
    f: &str,
    error_codes: &mut HashMap<String, ErrorCodeStatus>,
    regex: &Regex,
) {
    for line in f.lines() {
        if line.trim_start().starts_with("//") {
            continue;
        }
        for cap in regex.captures_iter(line) {
            if let Some(error_code) = cap.get(1) {
                error_codes.entry(error_code.as_str().to_owned()).or_default().is_used = true;
            }
        }
    }
}

pub fn check(paths: &[&Path], bad: &mut bool) {
    let mut errors = Vec::new();
    let mut found_explanations = 0;
    let mut found_tests = 0;
    let mut error_codes: HashMap<String, ErrorCodeStatus> = HashMap::new();
    let mut explanations: HashSet<String> = HashSet::new();
    // We want error codes which match the following cases:
    //
    // * foo(a, E0111, a)
    // * foo(a, E0111)
    // * foo(E0111, a)
    // * #[error = "E0111"]
    let regex = Regex::new(r#"[(,"\s](E\d{4})[,)"]"#).unwrap();

    println!("Checking which error codes lack tests...");

    for path in paths {
        walk(path, &mut filter_dirs, &mut |entry, contents| {
            let file_name = entry.file_name();
            let entry_path = entry.path();

            if file_name == "error_codes.rs" {
                extract_error_codes(contents, &mut error_codes, entry.path(), &mut errors);
                found_explanations += 1;
            } else if entry_path.extension() == Some(OsStr::new("stderr")) {
                extract_error_codes_from_tests(contents, &mut error_codes);
                found_tests += 1;
            } else if entry_path.extension() == Some(OsStr::new("rs")) {
                let path = entry.path().to_string_lossy();
                if PATHS_TO_IGNORE_FOR_EXTRACTION.iter().all(|c| !path.contains(c)) {
                    extract_error_codes_from_source(contents, &mut error_codes, &regex);
                }
            } else if entry_path
                .parent()
                .and_then(|p| p.file_name())
                .map(|p| p == "error_codes")
                .unwrap_or(false)
                && entry_path.extension() == Some(OsStr::new("md"))
            {
                explanations.insert(file_name.to_str().unwrap().replace(".md", ""));
            }
        });
    }
    if found_explanations == 0 {
        eprintln!("No error code explanation was tested!");
        *bad = true;
    }
    if found_tests == 0 {
        eprintln!("No error code was found in compilation errors!");
        *bad = true;
    }
    if explanations.is_empty() {
        eprintln!("No error code explanation was found!");
        *bad = true;
    }
    if errors.is_empty() {
        println!("Found {} error codes", error_codes.len());

        for (err_code, error_status) in &error_codes {
            if !error_status.has_test && !EXEMPTED_FROM_TEST.contains(&err_code.as_str()) {
                errors.push(format!("Error code {err_code} needs to have at least one UI test!"));
            } else if error_status.has_test && EXEMPTED_FROM_TEST.contains(&err_code.as_str()) {
                errors.push(format!(
                    "Error code {} has a UI test, it shouldn't be listed into EXEMPTED_FROM_TEST!",
                    err_code
                ));
            }
            if !error_status.is_used && !error_status.has_explanation {
                errors.push(format!(
                    "Error code {} isn't used and doesn't have an error explanation, it should be \
                     commented in error_codes.rs file",
                    err_code
                ));
            }
        }
    }
    if errors.is_empty() {
        // Checking if local constants need to be cleaned.
        for err_code in EXEMPTED_FROM_TEST {
            match error_codes.get(err_code.to_owned()) {
                Some(status) => {
                    if status.has_test {
                        errors.push(format!(
                            "{} error code has a test and therefore should be \
                            removed from the `EXEMPTED_FROM_TEST` constant",
                            err_code
                        ));
                    }
                }
                None => errors.push(format!(
                    "{} error code isn't used anymore and therefore should be removed \
                        from `EXEMPTED_FROM_TEST` constant",
                    err_code
                )),
            }
        }
    }
    if errors.is_empty() {
        for explanation in explanations {
            if !error_codes.contains_key(&explanation) {
                errors.push(format!(
                    "{} error code explanation should be listed in `error_codes.rs`",
                    explanation
                ));
            }
        }
    }
    errors.sort();
    for err in &errors {
        eprintln!("{err}");
    }
    println!("Found {} error(s) in error codes", errors.len());
    if !errors.is_empty() {
        *bad = true;
    }
    println!("Done!");
}