Skip to content

Commit cc324ab

Browse files
committed
ruff_db: add new Diagnostic type
... with supporting types. This is meant to give us a base to work with in terms of our new diagnostic data model. I expect the representations to be tweaked over time, but I think this is a decent start. I would also like to add doctest examples, but I think it's better if we wait until an initial version of the renderer is done for that.
1 parent 80be0a0 commit cc324ab

File tree

1 file changed

+287
-0
lines changed
  • crates/ruff_db/src/diagnostic

1 file changed

+287
-0
lines changed

crates/ruff_db/src/diagnostic/mod.rs

Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,293 @@ use crate::files::File;
1414
// the APIs in this module.
1515
mod old;
1616

17+
/// A collection of information that can be rendered into a diagnostic.
18+
///
19+
/// A diagnostic is a collection of information gathered by a tool intended
20+
/// for presentation to an end user, and which describes a group of related
21+
/// characteristics in the inputs given to the tool. Typically, but not always,
22+
/// a characteristic is a deficiency. An example of a characteristic that is
23+
/// _not_ a deficiency is the `reveal_type` diagnostic for our type checker.
24+
#[derive(Debug, Clone, Eq, PartialEq)]
25+
pub struct Diagnostic {
26+
/// The actual diagnostic.
27+
///
28+
/// We box the diagnostic since it is somewhat big.
29+
inner: Box<DiagnosticInner>,
30+
}
31+
32+
impl Diagnostic {
33+
/// Create a new diagnostic with the given identifier, severity and
34+
/// message.
35+
///
36+
/// The identifier should be something that uniquely identifies the _type_
37+
/// of diagnostic being reported. It should be usable as a reference point
38+
/// for humans communicating about diagnostic categories. It will also
39+
/// appear in the output when this diagnostic is rendered.
40+
///
41+
/// The severity should describe the assumed level of importance to an end
42+
/// user.
43+
///
44+
/// The message is meant to be read by end users. The primary message
45+
/// is meant to be a single terse description (usually a short phrase)
46+
/// describing the group of related characteristics that the diagnostic
47+
/// describes. Stated differently, if only one thing from a diagnostic can
48+
/// be shown to an end user in a particular context, it is the primary
49+
/// message.
50+
pub fn new<'a>(
51+
id: DiagnosticId,
52+
severity: Severity,
53+
message: impl std::fmt::Display + 'a,
54+
) -> Diagnostic {
55+
let message = message.to_string().into_boxed_str();
56+
let inner = Box::new(DiagnosticInner {
57+
id,
58+
severity,
59+
message,
60+
annotations: vec![],
61+
subs: vec![],
62+
#[cfg(debug_assertions)]
63+
printed: false,
64+
});
65+
Diagnostic { inner }
66+
}
67+
68+
/// Add an annotation to this diagnostic.
69+
///
70+
/// Annotations for a diagnostic are optional, but if any are added,
71+
/// callers should strive to make at least one of them primary. That is, it
72+
/// should be constructed via [`Annotation::primary`]. A diagnostic with no
73+
/// primary annotations is allowed, but its rendering may be sub-optimal.
74+
pub fn annotate(&mut self, ann: Annotation) {
75+
self.inner.annotations.push(ann);
76+
}
77+
78+
/// Adds an "info" sub-diagnostic with the given message.
79+
///
80+
/// If callers want to add an "info" sub-diagnostic with annotations, then
81+
/// create a [`SubDiagnostic`] manually and use [`Diagnostic::sub`] to
82+
/// attach it to a parent diagnostic.
83+
///
84+
/// An "info" diagnostic is useful when contextualizing or otherwise
85+
/// helpful information can be added to help end users understand the
86+
/// main diagnostic message better. For example, if a the main diagnostic
87+
/// message is about a function call being invalid, a useful "info"
88+
/// sub-diagnostic could show the function definition (or only the relevant
89+
/// parts of it).
90+
pub fn info<'a>(&mut self, message: impl std::fmt::Display + 'a) {
91+
self.sub(SubDiagnostic::new(Severity::Info, message));
92+
}
93+
94+
/// Adds a "sub" diagnostic to this diagnostic.
95+
///
96+
/// This is useful when a sub diagnostic has its own annotations attached
97+
/// to it. For the simpler case of a sub-diagnostic with only a message,
98+
/// using a method like [`Diagnostic::info`] may be more convenient.
99+
pub fn sub(&mut self, sub: SubDiagnostic) {
100+
self.inner.subs.push(sub);
101+
}
102+
}
103+
104+
#[derive(Debug, Clone, Eq, PartialEq)]
105+
struct DiagnosticInner {
106+
id: DiagnosticId,
107+
severity: Severity,
108+
message: Box<str>,
109+
annotations: Vec<Annotation>,
110+
subs: Vec<SubDiagnostic>,
111+
/// This will make the `Drop` impl panic if a `Diagnostic` hasn't
112+
/// been printed to stderr. This is usually a bug, so we want it to
113+
/// be loud. But only when `debug_assertions` is enabled.
114+
#[cfg(debug_assertions)]
115+
printed: bool,
116+
}
117+
118+
impl Drop for DiagnosticInner {
119+
fn drop(&mut self) {
120+
#[cfg(debug_assertions)]
121+
{
122+
if self.printed || std::thread::panicking() {
123+
return;
124+
}
125+
panic!(
126+
"diagnostic `{id}` with severity `{severity:?}` and message `{message}` \
127+
did not get printed to stderr before being dropped",
128+
id = self.id,
129+
severity = self.severity,
130+
message = self.message,
131+
);
132+
}
133+
}
134+
}
135+
136+
/// A collection of information subservient to a diagnostic.
137+
///
138+
/// A sub-diagnostic is always rendered after the parent diagnostic it is
139+
/// attached to. A parent diagnostic may have many sub-diagnostics, and it is
140+
/// guaranteed that they will not interleave with one another in rendering.
141+
///
142+
/// Currently, the order in which sub-diagnostics are rendered relative to one
143+
/// another (for a single parent diagnostic) is the order in which they were
144+
/// attached to the diagnostic.
145+
#[derive(Debug, Clone, Eq, PartialEq)]
146+
pub struct SubDiagnostic {
147+
/// Like with `Diagnostic`, we box the `SubDiagnostic` to make it
148+
/// pointer-sized.
149+
inner: Box<SubDiagnosticInner>,
150+
}
151+
152+
impl SubDiagnostic {
153+
/// Create a new sub-diagnostic with the given severity and message.
154+
///
155+
/// The severity should describe the assumed level of importance to an end
156+
/// user.
157+
///
158+
/// The message is meant to be read by end users. The primary message
159+
/// is meant to be a single terse description (usually a short phrase)
160+
/// describing the group of related characteristics that the sub-diagnostic
161+
/// describes. Stated differently, if only one thing from a diagnostic can
162+
/// be shown to an end user in a particular context, it is the primary
163+
/// message.
164+
pub fn new<'a>(severity: Severity, message: impl std::fmt::Display + 'a) -> SubDiagnostic {
165+
let message = message.to_string().into_boxed_str();
166+
let inner = Box::new(SubDiagnosticInner {
167+
severity,
168+
message,
169+
annotations: vec![],
170+
#[cfg(debug_assertions)]
171+
printed: false,
172+
});
173+
SubDiagnostic { inner }
174+
}
175+
176+
/// Add an annotation to this sub-diagnostic.
177+
///
178+
/// Annotations for a sub-diagnostic, like for a diagnostic, are optional.
179+
/// If any are added, callers should strive to make at least one of them
180+
/// primary. That is, it should be constructed via [`Annotation::primary`].
181+
/// A diagnostic with no primary annotations is allowed, but its rendering
182+
/// may be sub-optimal.
183+
///
184+
/// Note that it is expected to be somewhat more common for sub-diagnostics
185+
/// to have no annotations (e.g., a simple note) than for a diagnostic to
186+
/// have no annotations.
187+
pub fn annotate(&mut self, ann: Annotation) {
188+
self.inner.annotations.push(ann);
189+
}
190+
}
191+
192+
#[derive(Debug, Clone, Eq, PartialEq)]
193+
struct SubDiagnosticInner {
194+
severity: Severity,
195+
message: Box<str>,
196+
annotations: Vec<Annotation>,
197+
/// This will make the `Drop` impl panic if a `SubDiagnostic` hasn't
198+
/// been printed to stderr. This is usually a bug, so we want it to
199+
/// be loud. But only when `debug_assertions` is enabled.
200+
#[cfg(debug_assertions)]
201+
printed: bool,
202+
}
203+
204+
impl Drop for SubDiagnosticInner {
205+
fn drop(&mut self) {
206+
#[cfg(debug_assertions)]
207+
{
208+
if self.printed || std::thread::panicking() {
209+
return;
210+
}
211+
panic!(
212+
"sub-diagnostic with severity `{severity:?}` and message `{message}` \
213+
did not get printed to stderr before being dropped",
214+
severity = self.severity,
215+
message = self.message,
216+
);
217+
}
218+
}
219+
}
220+
221+
/// A pointer to a subsequence in the end user's input.
222+
///
223+
/// Also known as an annotation, the pointer can optionally contain a short
224+
/// message, typically describing in general terms what is being pointed to.
225+
///
226+
/// An annotation is either primary or secondary, depending on whether it was
227+
/// constructed via [`Annotation::primary`] or [`Annotation::secondary`].
228+
/// Semantically, a primary annotation is meant to point to the "locus" of a
229+
/// diagnostic. Visually, the difference between a primary and a secondary
230+
/// annotation is usually just a different form of highlighting on the
231+
/// corresponding span.
232+
///
233+
/// # Advice
234+
///
235+
/// The span on an annotation should be as _specific_ as possible. For example,
236+
/// if there is a problem with a function call because one of its arguments has
237+
/// an invalid type, then the span should point to the specific argument and
238+
/// not to the entire function call.
239+
///
240+
/// Messages attached to annotations should also be as brief and specific as
241+
/// possible. Long messages could negative impact the quality of rendering.
242+
#[derive(Debug, Clone, Eq, PartialEq)]
243+
pub struct Annotation {
244+
/// The span of this annotation, corresponding to some subsequence of the
245+
/// user's input that we want to highlight.
246+
span: Span,
247+
/// An optional message associated with this annotation's span.
248+
///
249+
/// When present, rendering will include this message in the output and
250+
/// draw a line between the highlighted span and the message.
251+
message: Option<Box<str>>,
252+
/// Whether this annotation is "primary" or not. When it isn't primary, an
253+
/// annotation is said to be "secondary."
254+
is_primary: bool,
255+
}
256+
257+
impl Annotation {
258+
/// Create a "primary" annotation.
259+
///
260+
/// A primary annotation is meant to highlight the "locus" of a diagnostic.
261+
/// That is, it should point to something in the end user's input that is
262+
/// the subject or "point" of a diagnostic.
263+
///
264+
/// A diagnostic may have many primary annotations. A diagnostic may not
265+
/// have any annotations, but if it does, at least one _ought_ to be
266+
/// primary.
267+
pub fn primary(span: Span) -> Annotation {
268+
Annotation {
269+
span,
270+
message: None,
271+
is_primary: true,
272+
}
273+
}
274+
275+
/// Create a "secondary" annotation.
276+
///
277+
/// A secondary annotation is meant to highlight relevant context for a
278+
/// diagnostic, but not to point to the "locus" of the diagnostic.
279+
///
280+
/// A diagnostic with only secondary annotations is usually not sensible,
281+
/// but it is allowed and will produce a reasonable rendering.
282+
pub fn secondary(span: Span) -> Annotation {
283+
Annotation {
284+
span,
285+
message: None,
286+
is_primary: false,
287+
}
288+
}
289+
290+
/// Attach a message to this annotation.
291+
///
292+
/// An annotation without a message will still have a presence in
293+
/// rendering. In particular, it will highlight the span association with
294+
/// this annotation in some way.
295+
///
296+
/// When a message is attached to an annotation, then it will be associated
297+
/// with the highlighted span in some way during rendering.
298+
pub fn message<'a>(self, message: impl std::fmt::Display + 'a) -> Annotation {
299+
let message = Some(message.to_string().into_boxed_str());
300+
Annotation { message, ..self }
301+
}
302+
}
303+
17304
/// A string identifier for a lint rule.
18305
///
19306
/// This string is used in command line and configuration interfaces. The name should always

0 commit comments

Comments
 (0)