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