starlark_syntax/
diagnostic.rs

1/*
2 * Copyright 2019 The Starlark in Rust Authors.
3 * Copyright (c) Facebook, Inc. and its affiliates.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 *     https://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18use std::error::Error as StdError;
19use std::fmt;
20
21use crate::call_stack::CallStack;
22use crate::codemap::CodeMap;
23use crate::codemap::FileSpan;
24use crate::codemap::Span;
25use crate::span_display::span_display;
26
27/// A value of type `T`, together with some diagnostic information.
28///
29/// Most code in starlark should be using `starlark::Error` as the error type. However, some code
30/// may want to have strongly typed errors, while still being able to have diagnostics.
31/// `WithDiagnostic<MyErrorType>` is the tool for that. `WithDiagnostic` is always one word in size,
32/// and so can be used as an error type in performance sensitive code.
33///
34/// `WithDiagnostic` is `pub`, but only within the starlark crates, it's not a part of the API.
35///
36/// Returning a `WithDiagnostic` value guarantees that a diagnostic is actually present, ie the
37/// diagnostic is not optional.
38pub struct WithDiagnostic<T>(Box<WithDiagnosticInner<T>>);
39
40struct WithDiagnosticInner<T> {
41    t: T,
42    diagnostic: Diagnostic,
43}
44
45impl<T> WithDiagnostic<T> {
46    pub fn new_spanned(t: T, span: Span, codemap: &CodeMap) -> Self {
47        Self(Box::new(WithDiagnosticInner {
48            t,
49            diagnostic: Diagnostic {
50                span: Some(codemap.file_span(span)),
51                call_stack: CallStack::default(),
52            },
53        }))
54    }
55
56    /// The contract of this type is normally that it actually contains diagnostic information.
57    /// However, `starlark::Error` doesn't guarantee that, but it'd be convenient to use this type
58    /// for it anyway. So we make an exception. Don't use this function for anything else.
59    pub(crate) fn new_empty(t: T) -> Self {
60        Self(Box::new(WithDiagnosticInner {
61            t,
62            diagnostic: Diagnostic::default(),
63        }))
64    }
65
66    pub fn inner(&self) -> &T {
67        &self.0.t
68    }
69
70    pub fn into_inner(self) -> T {
71        self.0.t
72    }
73
74    pub fn map<U>(self, f: impl FnOnce(T) -> U) -> WithDiagnostic<U> {
75        WithDiagnostic(Box::new(WithDiagnosticInner {
76            t: f(self.0.t),
77            diagnostic: self.0.diagnostic,
78        }))
79    }
80
81    pub fn span(&self) -> Option<&FileSpan> {
82        self.0.diagnostic.span.as_ref()
83    }
84
85    pub fn call_stack(&self) -> &CallStack {
86        &self.0.diagnostic.call_stack
87    }
88
89    /// Set the span, unless it's already been set.
90    pub fn set_span(&mut self, span: Span, codemap: &CodeMap) {
91        if self.0.diagnostic.span.is_none() {
92            self.0.diagnostic.span = Some(codemap.file_span(span));
93        }
94    }
95
96    /// Set the `call_stack` field, unless it's already been set.
97    pub fn set_call_stack(&mut self, call_stack: impl FnOnce() -> CallStack) {
98        if self.0.diagnostic.call_stack.is_empty() {
99            self.0.diagnostic.call_stack = call_stack();
100        }
101    }
102}
103
104impl<T: StdError> fmt::Display for WithDiagnostic<T> {
105    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
106        // Not showing the context trace without `{:#}` or `{:?}` is the same thing that anyhow does
107        let with_context = f.alternate() && self.0.t.source().is_some();
108        diagnostic_display(self, false, f, with_context)
109    }
110}
111
112impl<T: StdError> fmt::Debug for WithDiagnostic<T> {
113    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
114        diagnostic_display(self, false, f, /* with_context */ true)
115    }
116}
117
118impl<T: Into<crate::Error>> From<WithDiagnostic<T>> for crate::Error {
119    fn from(e: WithDiagnostic<T>) -> Self {
120        let diagnostic = e.0.diagnostic;
121        let mut e: crate::Error = e.0.t.into();
122        e.0.0.diagnostic = diagnostic;
123        e
124    }
125}
126
127/// A description of where in starlark execution the error happened.
128#[derive(Debug, Default)]
129struct Diagnostic {
130    /// Location where the error originated.
131    span: Option<FileSpan>,
132
133    /// Call stack where the error originated.
134    call_stack: CallStack,
135}
136
137impl Diagnostic {
138    /// Gets annotated snippets for a [`Diagnostic`].
139    fn get_display_list<'a>(
140        &'a self,
141        annotation_label: &'a str,
142        color: bool,
143    ) -> impl fmt::Display + 'a {
144        span_display(
145            self.span.as_ref().map(|s| s.as_ref()),
146            annotation_label,
147            color,
148        )
149    }
150}
151
152/////////////////////////////////////////////////////////////////////
153// DISPLAY RELATED UTILITIES
154// Since formatting these types is difficult, we reuse the Rust compiler
155// variants by doing a conversion using annotate-snippets
156// (https://github.com/rust-lang/annotate-snippets-rs)
157
158pub(crate) fn diagnostic_display<T: fmt::Debug + fmt::Display>(
159    d: &WithDiagnostic<T>,
160    color: bool,
161    f: &mut dyn fmt::Write,
162    with_context: bool,
163) -> fmt::Result {
164    write!(f, "{}", d.call_stack())?;
165    let annotation_label = format!("{}", d.inner());
166    // I set color to false here to make the comparison easier with tests (coloring
167    // adds in pretty strange unicode chars).
168    let display_list = d.0.diagnostic.get_display_list(&annotation_label, color);
169    writeln!(f, "{}", display_list)?;
170    // Print out the `Caused by:` trace (if exists) and rust backtrace (if enabled).
171    // The trace printed comes from an [`anyhow::Error`] that is not a [`Diagnostic`].
172    if with_context {
173        writeln!(f, "\n\n{:?}", d.inner())?;
174    }
175
176    Ok(())
177}