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