Skip to main content

nu_protocol/errors/
report_error.rs

1//! This module manages the step of turning error types into printed error messages
2//!
3//! Relies on the `miette` crate for pretty layout
4use std::hash::{DefaultHasher, Hash, Hasher};
5use std::io::Write;
6use std::sync::atomic::{AtomicBool, Ordering};
7
8use crate::{
9    CompileError, Config, ErrorStyle, ParseError, ParseWarning, ShellError, ShellWarning,
10    ShortReportHandler,
11    engine::{EngineState, Stack, StateWorkingSet},
12};
13use miette::{
14    LabeledSpan, MietteHandlerOpts, NarratableReportHandler, ReportHandler, RgbColors, Severity,
15    SourceCode,
16};
17use serde::{Deserialize, Serialize};
18use thiserror::Error;
19
20/// While doing in-process testing of Nushell, the reports may spam the console output.
21///
22/// This value allows suppressing these outputs to not see them during test execution.
23pub static SUPPRESS_REPORTING: AtomicBool = AtomicBool::new(false);
24
25/// This error exists so that we can defer SourceCode handling. It simply
26/// forwards most methods, except for `.source_code()`, which we provide.
27#[derive(Error)]
28#[error("{diagnostic}")]
29struct CliError<'src> {
30    stack: Option<&'src Stack>,
31    diagnostic: &'src dyn miette::Diagnostic,
32    working_set: &'src StateWorkingSet<'src>,
33    // error code to use if `diagnostic` doesn't provide one
34    default_code: Option<&'static str>,
35}
36
37impl<'src> CliError<'src> {
38    pub fn new(
39        stack: Option<&'src Stack>,
40        diagnostic: &'src dyn miette::Diagnostic,
41        working_set: &'src StateWorkingSet<'src>,
42        default_code: Option<&'static str>,
43    ) -> Self {
44        CliError {
45            stack,
46            diagnostic,
47            working_set,
48            default_code,
49        }
50    }
51}
52
53/// A bloom-filter like structure to store the hashes of warnings,
54/// without actually permanently storing the entire warning in memory.
55/// May rarely result in warnings incorrectly being unreported upon hash collision.
56#[derive(Default, derive_more::Debug)]
57#[debug("ReportLog([...])")]
58pub struct ReportLog(Vec<u64>);
59
60/// How a warning/error should be reported
61#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
62pub enum ReportMode {
63    FirstUse,
64    EveryUse,
65}
66
67/// For warnings/errors which have a ReportMode that dictates when they are reported
68pub trait Reportable {
69    fn report_mode(&self) -> ReportMode;
70}
71
72/// Returns true if this warning should be reported
73fn should_show_reportable<R>(engine_state: &EngineState, reportable: &R) -> bool
74where
75    R: Reportable + Hash,
76{
77    match reportable.report_mode() {
78        ReportMode::EveryUse => true,
79        ReportMode::FirstUse => {
80            let mut hasher = DefaultHasher::new();
81            reportable.hash(&mut hasher);
82            let hash = hasher.finish();
83
84            let mut report_log = engine_state
85                .report_log
86                .lock()
87                .expect("report log lock is poisoned");
88
89            match report_log.0.contains(&hash) {
90                true => false,
91                false => {
92                    report_log.0.push(hash);
93                    true
94                }
95            }
96        }
97    }
98}
99
100pub fn format_cli_error(
101    stack: Option<&Stack>,
102    working_set: &StateWorkingSet,
103    error: &dyn miette::Diagnostic,
104    default_code: Option<&'static str>,
105) -> String {
106    format!(
107        "Error: {:?}",
108        CliError::new(stack, error, working_set, default_code)
109    )
110}
111
112pub fn report_shell_error(stack: Option<&Stack>, engine_state: &EngineState, error: &ShellError) {
113    if get_config(stack, engine_state)
114        .display_errors
115        .should_show(error)
116    {
117        let working_set = StateWorkingSet::new(engine_state);
118        report_error(stack, &working_set, error, "nu::shell::error")
119    }
120}
121
122pub fn report_shell_warning(
123    stack: Option<&Stack>,
124    engine_state: &EngineState,
125    warning: &ShellWarning,
126) {
127    if should_show_reportable(engine_state, warning) {
128        report_warning(
129            stack,
130            &StateWorkingSet::new(engine_state),
131            warning,
132            "nu::shell::warning",
133        );
134    }
135}
136
137pub fn report_parse_error(
138    stack: Option<&Stack>,
139    working_set: &StateWorkingSet,
140    error: &ParseError,
141) {
142    report_error(stack, working_set, error, "nu::parser::error");
143}
144
145pub fn report_parse_warning(
146    stack: Option<&Stack>,
147    working_set: &StateWorkingSet,
148    warning: &ParseWarning,
149) {
150    if should_show_reportable(working_set.permanent(), warning) {
151        report_warning(stack, working_set, warning, "nu::parser::warning");
152    }
153}
154
155pub fn report_compile_error(
156    stack: Option<&Stack>,
157    working_set: &StateWorkingSet,
158    error: &CompileError,
159) {
160    report_error(stack, working_set, error, "nu::compile::error");
161}
162
163pub fn report_experimental_option_warning(
164    stack: Option<&Stack>,
165    working_set: &StateWorkingSet,
166    warning: &dyn miette::Diagnostic,
167) {
168    report_warning(
169        stack,
170        working_set,
171        warning,
172        "nu::experimental_option::warning",
173    );
174}
175
176fn report_error(
177    stack: Option<&Stack>,
178    working_set: &StateWorkingSet,
179    error: &dyn miette::Diagnostic,
180    default_code: &'static str,
181) {
182    let report = format!(
183        "Error: {:?}",
184        CliError::new(stack, error, working_set, Some(default_code))
185    );
186
187    if !SUPPRESS_REPORTING.load(Ordering::Relaxed) {
188        // Avoid eprintln! since it panics on broken stderr, which double-panics
189        // through miette's panic hook and aborts.
190        if writeln!(std::io::stderr(), "{report}").is_err() {
191            let _ = writeln!(std::io::stdout(), "{report}");
192        }
193    }
194
195    // reset vt processing, aka ansi because illbehaved externals can break it
196    #[cfg(windows)]
197    {
198        let _ = nu_utils::enable_vt_processing();
199    }
200}
201
202fn report_warning(
203    stack: Option<&Stack>,
204    working_set: &StateWorkingSet,
205    warning: &dyn miette::Diagnostic,
206    default_code: &'static str,
207) {
208    let report = format!(
209        "Warning: {:?}",
210        CliError::new(stack, warning, working_set, Some(default_code))
211    );
212
213    if !SUPPRESS_REPORTING.load(Ordering::Relaxed)
214        && writeln!(std::io::stderr(), "{report}").is_err()
215    {
216        let _ = writeln!(std::io::stdout(), "{report}");
217    }
218
219    // reset vt processing, aka ansi because illbehaved externals can break it
220    #[cfg(windows)]
221    {
222        let _ = nu_utils::enable_vt_processing();
223    }
224}
225
226fn get_config<'a>(stack: Option<&'a Stack>, engine_state: &'a EngineState) -> &'a Config {
227    stack
228        .and_then(|s| s.config.as_deref())
229        .unwrap_or(engine_state.get_config())
230}
231
232impl std::fmt::Debug for CliError<'_> {
233    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
234        let engine_state = self.working_set.permanent();
235        let config = get_config(self.stack, engine_state);
236
237        let ansi_support = config.use_ansi_coloring.get(engine_state);
238
239        let error_style = config.error_style;
240
241        let error_lines = config.error_lines;
242
243        let miette_handler: Box<dyn ReportHandler> = match error_style {
244            ErrorStyle::Short => Box::new(ShortReportHandler::new()),
245            ErrorStyle::Plain => Box::new(NarratableReportHandler::new()),
246            style => {
247                let handler = MietteHandlerOpts::new()
248                    // For better support of terminal themes use the ANSI coloring
249                    .rgb_colors(RgbColors::Never)
250                    // If ansi support is disabled in the config disable the eye-candy
251                    .color(ansi_support)
252                    .unicode(ansi_support)
253                    .terminal_links(ansi_support)
254                    .context_lines(error_lines as usize)
255                    .with_cause_chain();
256                match style {
257                    ErrorStyle::Nested => Box::new(handler.show_related_errors_as_nested().build()),
258                    _ => Box::new(handler.build()),
259                }
260            }
261        };
262
263        // Ignore error to prevent format! panics. This can happen if span points at some
264        // inaccessible location, for example by calling `report_error()` with wrong working set.
265        let _ = miette_handler.debug(self, f);
266
267        Ok(())
268    }
269}
270
271impl miette::Diagnostic for CliError<'_> {
272    fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
273        self.diagnostic.code().or_else(|| {
274            self.default_code
275                .map(|code| Box::new(code) as Box<dyn std::fmt::Display>)
276        })
277    }
278
279    fn severity(&self) -> Option<Severity> {
280        self.diagnostic.severity()
281    }
282
283    fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
284        self.diagnostic.help()
285    }
286
287    fn url<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
288        self.diagnostic.url()
289    }
290
291    fn labels<'a>(&'a self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + 'a>> {
292        self.diagnostic.labels()
293    }
294
295    // Finally, we redirect the source_code method to our own source.
296    fn source_code(&self) -> Option<&dyn SourceCode> {
297        if let Some(source_code) = self.diagnostic.source_code() {
298            Some(source_code)
299        } else {
300            Some(&self.working_set)
301        }
302    }
303
304    fn related<'a>(&'a self) -> Option<Box<dyn Iterator<Item = &'a dyn miette::Diagnostic> + 'a>> {
305        self.diagnostic.related()
306    }
307
308    fn diagnostic_source(&self) -> Option<&dyn miette::Diagnostic> {
309        self.diagnostic.diagnostic_source()
310    }
311}