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