Skip to main content

candela/
warnings.rs

1//! Compile warnings: findings that do not stop a build.
2//!
3//! The error funnel is fatal on the first error, so it has one slot. A warning
4//! leaves the compile running, so any number of them can be raised, and they
5//! travel on a channel of their own. [`collect_warnings`] gathers the warnings
6//! raised while it runs, the way `collect_diagnostic` gathers an error; with no
7//! collector active a warning is printed to stderr as a report, which is what
8//! `candela check` and `candela build` show.
9//!
10//! This lives on the compiler side. The VM raises no warnings, so candela-vm
11//! carries none of it.
12
13use crate::Diagnostic;
14use crate::rt::Source;
15use ariadne::Color;
16use ariadne::Label;
17use ariadne::Report;
18use ariadne::ReportKind;
19use candela_vm::captured_output;
20use std::cell::RefCell;
21
22thread_local! {
23    /// The warnings raised under the innermost [`collect_warnings`], or `None`
24    /// when no collector is active.
25    static WARNINGS: RefCell<Option<Vec<Diagnostic>>> = const { RefCell::new(None) };
26}
27
28/// Puts the enclosing collector's warnings back when a nested one ends, even
29/// when it ends by unwinding out of a compile error.
30struct Restore(Option<Vec<Diagnostic>>);
31
32impl Drop for Restore {
33    fn drop(&mut self) {
34        let previous = self.0.take();
35        WARNINGS.with(|w| *w.borrow_mut() = previous);
36    }
37}
38
39/// Runs `f` and returns what it produced together with every warning raised
40/// while it ran, in the order they were raised. Nothing is printed.
41///
42/// Collectors nest: an inner one takes the warnings raised inside it, and the
43/// outer one sees none of them.
44pub fn collect_warnings<R>(f: impl FnOnce() -> R) -> (R, Vec<Diagnostic>) {
45    let previous = WARNINGS.with(|w| w.borrow_mut().replace(Vec::new()));
46    let restore = Restore(previous);
47    let result = f();
48    let warnings = WARNINGS.with(|w| w.borrow_mut().take()).unwrap_or_default();
49    drop(restore);
50    (result, warnings)
51}
52
53/// Raises `warning`. Under [`collect_warnings`] it is recorded; otherwise it
54/// is printed to stderr as a report titled `title`, labelled at its span in
55/// the source it names.
56pub fn emit_warning(sources: &[Source], title: &str, warning: Diagnostic) {
57    let unrecorded = WARNINGS.with(|w| match w.borrow_mut().as_mut() {
58        Some(warnings) => {
59            warnings.push(warning);
60            None
61        }
62        None => Some(warning),
63    });
64    let Some(warning) = unrecorded else {
65        return;
66    };
67    let contents = sources
68        .iter()
69        .find(|source| source.filename == warning.filename)
70        .map_or("", |source| source.contents.as_str());
71    let filename = warning.filename.as_str();
72    let report = Report::build(ReportKind::Warning, (filename, warning.span.clone()))
73        .with_message(title)
74        .with_label(
75            Label::new((filename, warning.span.clone()))
76                .with_message(warning.message.as_str())
77                .with_color(Color::Yellow),
78        )
79        .finish();
80    // A stderr that refuses the report costs the report, not the build.
81    let _ = report.write(
82        (filename, ariadne::Source::from(contents)),
83        &mut captured_output::stderr(),
84    );
85}
86
87/// Warns about each `catch "kind"` in the program's own files whose kind
88/// nothing can raise: it is not a kind a built-in error carries, and no string
89/// literal anywhere in the program, a `throw` or otherwise, spells it. Such a
90/// clause never runs, which is what a typo in a kind looks like.
91///
92/// The check reads literals only. A kind built at run time is not followed, so
93/// a program that raises one and catches it by a name it never writes as a
94/// literal is warned about.
95pub fn warn_unraisable_catch_kinds(sources: &[Source], std_files: &[bool]) {
96    let scanned: Vec<_> = sources
97        .iter()
98        .map(|source| crate::parser::catch_kinds_and_strings(&source.contents))
99        .collect();
100    for (file, (kinds, _)) in scanned.iter().enumerate() {
101        if std_files.get(file).copied().unwrap_or(false) {
102            continue;
103        }
104        for (kind, span) in kinds {
105            let raisable = candela_vm::errors::BUILTIN_KINDS.contains(&kind.as_str())
106                || scanned
107                    .iter()
108                    .any(|(_, strings)| strings.iter().any(|s| s == kind));
109            if raisable {
110                continue;
111            }
112            emit_warning(
113                sources,
114                "Catch for a kind nothing raises",
115                Diagnostic {
116                    filename: sources[file].filename.to_string(),
117                    span: (span.start as usize)..(span.end as usize),
118                    message: format!(
119                        "No built-in error has the kind {kind:?} and nothing in the program throws it, so this catch never runs. Check the spelling"
120                    ),
121                    code: String::from("unraisable_catch_kind"),
122                },
123            );
124        }
125    }
126}