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}