@samitouri / QOS-React-2 / commits / 18f7481ebf

[rust] General-purpose diagnostic type and helpers

This PR adapts the `Diagnostic` type and helpers from Relay Compiler to Forget. The main changes are: * Removing some fields it doesn't seem we'll use for a while, if ever (like machine-readable arbitrary key/value data) * Switching from Relay Compiler's `Location` type to our SourceRange type * Using the severity enum previously established in the `forget_build_hir` crate, with Todo/Unsupported/InvalidSyntax/InvalidReact/Invariant variants * Adding support for translating our `Diagnostic` into a `miette::Diagnostic` so we can use miette's pretty printing With the new Diagnostic type in place i updated the existing build_hir code to use it and confirmed that the errors are now even nicer (when we attach extra data to annotate labels): <img width="860" alt="Screenshot 2023-07-14 at 3 10 00 PM" src="https://github.com/facebook/react-forget/assets/6425824/9d29425a-938b-4872-b999-aa174a3c329a"> This addresses (or brings us closer to addressing) many of your comments on the last diagnostics PR, @poteto!

Joe Savona committed Jul 14, 2023 at 15:21 UTC 18f7481ebf6db8103889c3ae051c41c2067355d5
12 files changed +393 -133
compiler/forget/Cargo.lock
+10
@@ -451,10 +451,20 @@ name = "forget_build_hir"
451 version = "0.1.0"
452 dependencies = [
453 "bumpalo",
454 + "forget_diagnostics",
455 "forget_estree",
456 "forget_hir",
457 "indexmap 2.0.0",
458 + "thiserror",
459 +]
460 +
461 +[[package]]
462 +name = "forget_diagnostics"
463 +version = "0.1.0"
464 +dependencies = [
465 + "forget_estree",
466 "miette 5.9.0",
467 + "static_assertions",
468 "thiserror",
469 ]
470
compiler/forget/Cargo.toml
+1
@@ -14,6 +14,7 @@ repository = "https://github.com/facebook/react-forget"
14 [workspace.dependencies]
15 # workspace crates
16 forget_build_hir = { path = "crates/forget_build_hir" }
17 +forget_diagnostics = { path = "crates/forget_diagnostics" }
18 forget_estree = { path = "crates/forget_estree" }
19 forget_estree_codegen = { path = "crates/forget_estree_codegen" }
20 forget_estree_swc = { path = "crates/forget_estree_swc" }
compiler/forget/crates/forget_build_hir/Cargo.toml
+1 -1
@@ -17,5 +17,5 @@ forget_hir = { workspace = true }
17 forget_estree = { workspace = true}
18 indexmap = { workspace = true }
19 bumpalo = { workspace = true }
20 -miette = { workspace = true }
20 +forget_diagnostics = { workspace = true }
21 thiserror = { workspace = true }
compiler/forget/crates/forget_build_hir/src/build.rs
+25 -27
@@ -1,6 +1,7 @@
1 use std::collections::HashSet;
2
3 use bumpalo::{boxed::Box, collections::String};
4 +use forget_diagnostics::{Diagnostic, DiagnosticSeverity};
5 use forget_estree::{
6 AssignmentTarget, BinaryExpression, BlockStatement, Expression, ForInit, ForStatement,
7 Function, FunctionExpression, IfStatement, JsValue, Literal, Pattern, Statement,
@@ -15,8 +16,7 @@ use forget_hir::{
16 use crate::{
17 builder::{Binding, Builder, LoopScope},
18 context::get_context_identifiers,
18 - error::DiagnosticError,
19 - BuildDiagnostic, ErrorSeverity,
19 + error::BuildHIRError,
20 };
21
22 /// Converts a React function in ESTree format into HIR. Returns the HIR
@@ -28,7 +28,7 @@ use crate::{
28 pub fn build<'a>(
29 env: &'a Environment<'a>,
30 fun: Function,
31 -) -> Result<Box<'a, forget_hir::Function<'a>>, BuildDiagnostic> {
31 +) -> Result<Box<'a, forget_hir::Function<'a>>, Diagnostic> {
32 let mut builder = Builder::new(env);
33
34 match fun.body {
@@ -39,9 +39,8 @@ pub fn build<'a>(
39 lower_expression(env, &mut builder, body)?;
40 }
41 None => {
42 - return Err(BuildDiagnostic::new(
43 - DiagnosticError::EmptyFunction,
44 - ErrorSeverity::InvalidSyntax,
42 + return Err(Diagnostic::invalid_syntax(
43 + BuildHIRError::EmptyFunction,
44 fun.range,
45 ));
46 }
@@ -96,7 +95,7 @@ fn lower_block_statement<'a>(
95 env: &'a Environment<'a>,
96 builder: &mut Builder<'a>,
97 stmt: BlockStatement,
99 -) -> Result<(), BuildDiagnostic> {
98 +) -> Result<(), Diagnostic> {
99 for stmt in stmt.body {
100 lower_statement(env, builder, stmt, None)?;
101 }
@@ -110,7 +109,7 @@ fn lower_statement<'a>(
109 builder: &mut Builder<'a>,
110 stmt: Statement,
111 label: Option<String<'a>>,
113 -) -> Result<(), BuildDiagnostic> {
112 +) -> Result<(), Diagnostic> {
113 match stmt {
114 Statement::BlockStatement(stmt) => {
115 lower_block_statement(env, builder, *stmt)?;
@@ -160,9 +159,8 @@ fn lower_statement<'a>(
159 VariableDeclarationKind::Const => InstructionKind::Const,
160 VariableDeclarationKind::Let => InstructionKind::Let,
161 VariableDeclarationKind::Var => {
163 - return Err(BuildDiagnostic::new(
164 - DiagnosticError::VariableDeclarationKindIsVar,
165 - ErrorSeverity::Unsupported,
162 + return Err(Diagnostic::unsupported(
163 + BuildHIRError::VariableDeclarationKindIsVar,
164 stmt.range,
165 ));
166 }
@@ -184,9 +182,8 @@ fn lower_statement<'a>(
182 let identifier = match binding {
183 Binding::Local(identifier) => identifier,
184 _ => {
187 - return Err(BuildDiagnostic::new(
188 - DiagnosticError::VariableDeclarationBindingIsNonLocal,
189 - ErrorSeverity::Invariant,
185 + return Err(Diagnostic::invariant(
186 + BuildHIRError::VariableDeclarationBindingIsNonLocal,
187 id.range,
188 ));
189 }
@@ -269,9 +266,8 @@ fn lower_statement<'a>(
266 kind: GotoKind::Break,
267 }))
268 } else {
272 - Err(BuildDiagnostic::new(
273 - DiagnosticError::ForStatementIsMissingInitializer,
274 - ErrorSeverity::Todo,
269 + Err(Diagnostic::todo(
270 + BuildHIRError::ForStatementIsMissingInitializer,
271 None,
272 ))
273 }
@@ -325,9 +321,8 @@ fn lower_statement<'a>(
321 });
322 builder.terminate_with_fallthrough(terminal, fallthrough_block);
323 } else {
328 - return Err(BuildDiagnostic::new(
329 - DiagnosticError::ForStatementIsMissingTest,
330 - ErrorSeverity::Todo,
324 + return Err(Diagnostic::todo(
325 + BuildHIRError::ForStatementIsMissingTest,
326 stmt.range,
327 ));
328 }
@@ -345,7 +340,7 @@ fn lower_expression<'a>(
340 env: &'a Environment<'a>,
341 builder: &mut Builder<'a>,
342 expr: Expression,
348 -) -> Result<InstrIx, BuildDiagnostic> {
343 +) -> Result<InstrIx, Diagnostic> {
344 let value = match expr {
345 Expression::Identifier(expr) => {
346 // TODO: handle unbound variables
@@ -438,7 +433,7 @@ fn lower_function<'a>(
433 env: &'a Environment<'a>,
434 builder: &mut Builder<'a>,
435 expr: FunctionExpression,
441 -) -> Result<forget_hir::FunctionExpression<'a>, BuildDiagnostic> {
436 +) -> Result<forget_hir::FunctionExpression<'a>, Diagnostic> {
437 let FunctionExpression { function, .. } = expr;
438 println!("get_context_identifiers() ...");
439 let context_identifiers = get_context_identifiers(env, &function);
@@ -474,7 +469,7 @@ fn lower_assignment<'a>(
469 kind: InstructionKind,
470 lvalue: AssignmentTarget,
471 value: InstrIx,
477 -) -> Result<InstrIx, BuildDiagnostic> {
472 +) -> Result<InstrIx, Diagnostic> {
473 Ok(match lvalue {
474 AssignmentTarget::Pattern(lvalue) => match lvalue {
475 Pattern::Identifier(lvalue) => {
@@ -498,12 +493,15 @@ fn lower_identifier_for_assignment<'a>(
493 builder: &mut Builder<'a>,
494 _kind: InstructionKind,
495 identifier: forget_estree::Identifier,
501 -) -> Result<IdentifierOperand<'a>, BuildDiagnostic> {
496 +) -> Result<IdentifierOperand<'a>, Diagnostic> {
497 let binding = builder.resolve_identifier(&identifier)?;
498 match binding {
504 - Binding::Module(..) | Binding::Global => Err(BuildDiagnostic::new(
505 - DiagnosticError::ReassignedGlobal,
506 - ErrorSeverity::InvalidReact,
499 + Binding::Module(..) | Binding::Global => Err(Diagnostic::invalid_react(
500 + BuildHIRError::ReassignedGlobal,
501 + identifier.range,
502 + )
503 + .annotate(
504 + format!("Cannot reassign `{}`", &identifier.name),
505 identifier.range,
506 )),
507 Binding::Local(id) => Ok(IdentifierOperand {
compiler/forget/crates/forget_build_hir/src/builder.rs
+23 -30
@@ -2,6 +2,7 @@ use bumpalo::{
2 boxed::Box,
3 collections::{String, Vec},
4 };
5 +use forget_diagnostics::{invariant, Diagnostic, DiagnosticSeverity};
6 use std::{cell::RefCell, collections::HashSet, rc::Rc};
7
8 use forget_hir::{
@@ -10,7 +11,7 @@ use forget_hir::{
11 };
12 use indexmap::IndexMap;
13
13 -use crate::{invariant, BuildDiagnostic, DiagnosticError, ErrorSeverity};
14 +use crate::BuildHIRError;
15
16 /// Helper struct used when converting from ESTree to HIR. Includes:
17 /// - Variable resolution
@@ -112,7 +113,7 @@ impl<'a> Builder<'a> {
113 ///
114 /// TODO: refine the type, only invariants should be possible here,
115 /// not other types of errors
115 - pub(crate) fn build(self) -> Result<HIR<'a>, BuildDiagnostic> {
116 + pub(crate) fn build(self) -> Result<HIR<'a>, Diagnostic> {
117 let mut hir = HIR {
118 entry: self.entry,
119 blocks: self.completed,
@@ -176,9 +177,9 @@ impl<'a> Builder<'a> {
177 }
178 }
179
179 - pub(crate) fn enter<F>(&mut self, kind: BlockKind, f: F) -> Result<BlockId, BuildDiagnostic>
180 + pub(crate) fn enter<F>(&mut self, kind: BlockKind, f: F) -> Result<BlockId, Diagnostic>
181 where
181 - F: FnOnce(&mut Self) -> Result<TerminalValue<'a>, BuildDiagnostic>,
182 + F: FnOnce(&mut Self) -> Result<TerminalValue<'a>, Diagnostic>,
183 {
184 let wip = self.reserve(kind);
185 let id = wip.id;
@@ -186,9 +187,9 @@ impl<'a> Builder<'a> {
187 Ok(id)
188 }
189
189 - fn enter_reserved<F>(&mut self, wip: WipBlock<'a>, f: F) -> Result<(), BuildDiagnostic>
190 + fn enter_reserved<F>(&mut self, wip: WipBlock<'a>, f: F) -> Result<(), Diagnostic>
191 where
191 - F: FnOnce(&mut Self) -> Result<TerminalValue<'a>, BuildDiagnostic>,
192 + F: FnOnce(&mut Self) -> Result<TerminalValue<'a>, Diagnostic>,
193 {
194 let current = std::mem::replace(&mut self.wip, wip);
195
@@ -226,9 +227,9 @@ impl<'a> Builder<'a> {
227 &mut self,
228 scope: LoopScope<'a>,
229 f: F,
229 - ) -> Result<TerminalValue<'a>, BuildDiagnostic>
230 + ) -> Result<TerminalValue<'a>, Diagnostic>
231 where
231 - F: FnOnce(&mut Self) -> Result<TerminalValue<'a>, BuildDiagnostic>,
232 + F: FnOnce(&mut Self) -> Result<TerminalValue<'a>, Diagnostic>,
233 {
234 self.scopes.push(ControlFlowScope::Loop(scope.clone()));
235 let terminal = f(self);
@@ -260,7 +261,7 @@ impl<'a> Builder<'a> {
261 pub(crate) fn resolve_break(
262 &self,
263 label: Option<&forget_estree::Identifier>,
263 - ) -> Result<BlockId, BuildDiagnostic> {
264 + ) -> Result<BlockId, Diagnostic> {
265 for scope in self.scopes.iter().rev() {
266 match (label, scope.label()) {
267 // If this is an unlabeled break, return the most recent break target
@@ -273,9 +274,8 @@ impl<'a> Builder<'a> {
274 _ => continue,
275 }
276 }
276 - Err(BuildDiagnostic::new(
277 - DiagnosticError::UnresolvedBreakTarget,
278 - ErrorSeverity::InvalidSyntax,
277 + Err(Diagnostic::invalid_syntax(
278 + BuildHIRError::UnresolvedBreakTarget,
279 None,
280 ))
281 }
@@ -286,7 +286,7 @@ impl<'a> Builder<'a> {
286 pub(crate) fn resolve_continue(
287 &self,
288 label: Option<&forget_estree::Identifier>,
289 - ) -> Result<BlockId, BuildDiagnostic> {
289 + ) -> Result<BlockId, Diagnostic> {
290 for scope in self.scopes.iter().rev() {
291 match scope {
292 ControlFlowScope::Loop(scope) => {
@@ -307,9 +307,8 @@ impl<'a> Builder<'a> {
307 match (label, scope.label()) {
308 (Some(label), Some(scope_label)) if label.name.as_str() == scope_label => {
309 // Error, the continue referred to a label that is not a loop
310 - return Err(BuildDiagnostic::new(
311 - DiagnosticError::ContinueTargetIsNotALoop,
312 - ErrorSeverity::InvalidSyntax,
310 + return Err(Diagnostic::invalid_syntax(
311 + BuildHIRError::ContinueTargetIsNotALoop,
312 None,
313 ));
314 }
@@ -318,9 +317,8 @@ impl<'a> Builder<'a> {
317 }
318 }
319 }
321 - Err(BuildDiagnostic::new(
322 - DiagnosticError::UnresolvedContinueTarget,
323 - ErrorSeverity::InvalidSyntax,
320 + Err(Diagnostic::invalid_syntax(
321 + BuildHIRError::UnresolvedContinueTarget,
322 None,
323 ))
324 }
@@ -328,7 +326,7 @@ impl<'a> Builder<'a> {
326 pub(crate) fn resolve_identifier(
327 &mut self,
328 identifier: &forget_estree::Identifier,
331 - ) -> Result<Binding<'a>, BuildDiagnostic> {
329 + ) -> Result<Binding<'a>, Diagnostic> {
330 match &identifier.binding {
331 Some(binding) => Ok(match binding {
332 forget_estree::Binding::Global => Binding::Global,
@@ -341,16 +339,15 @@ impl<'a> Builder<'a> {
339 .resolve_binding_identifier(&identifier.name, *id),
340 ),
341 }),
344 - _ => Err(BuildDiagnostic::new(
345 - DiagnosticError::UnknownIdentifier,
346 - ErrorSeverity::Invariant,
342 + _ => Err(Diagnostic::invariant(
343 + BuildHIRError::UnknownIdentifier,
344 identifier.range.clone(),
345 )),
346 }
347 }
348 }
349
353 -pub fn initialize_hir<'a>(hir: &mut HIR<'a>) -> Result<(), BuildDiagnostic> {
350 +pub fn initialize_hir<'a>(hir: &mut HIR<'a>) -> Result<(), Diagnostic> {
351 reverse_postorder_blocks(hir);
352 remove_unreachable_for_updates(hir);
353 remove_unreachable_fallthroughs(hir);
@@ -465,18 +462,14 @@ pub fn remove_unreachable_do_while_statements<'a>(hir: &mut HIR<'a>) {
462
463 /// Updates the instruction ids for all instructions and blocks
464 /// Relies on the blocks being in reverse postorder to ensure that id ordering is correct
468 -pub fn mark_instruction_ids<'a>(hir: &mut HIR<'a>) -> Result<(), BuildDiagnostic> {
465 +pub fn mark_instruction_ids<'a>(hir: &mut HIR<'a>) -> Result<(), Diagnostic> {
466 let mut id_gen = InstructionIdGenerator::new();
467 let mut visited = HashSet::<(usize, usize)>::new();
468 for (ii, block) in hir.blocks.values_mut().enumerate() {
469 let block_id = block.id;
470 for (jj, instr_ix) in block.instructions.iter_mut().enumerate() {
471 invariant(visited.insert((ii, jj)), || {
475 - BuildDiagnostic::new(
476 - DiagnosticError::BlockVisitedTwice { block: block_id },
477 - ErrorSeverity::Invariant,
478 - None,
479 - )
472 + Diagnostic::invariant(BuildHIRError::BlockVisitedTwice { block: block_id }, None)
473 })?;
474 let instr = &mut hir.instructions[usize::from(*instr_ix)];
475 instr.id = id_gen.next();
compiler/forget/crates/forget_build_hir/src/error.rs
+2 -73
@@ -1,34 +1,9 @@
1 -use forget_estree::SourceRange;
1 use forget_hir::BlockId;
3 -use miette::{ByteOffset, Diagnostic, SourceSpan};
2 use thiserror::Error;
3
6 -#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Error)]
7 -pub enum ErrorSeverity {
8 - /// A feature that is intended to work but not yet implemented
9 - #[error("Not implemented")]
10 - Todo,
11 -
12 - /// Syntax that is valid but inentionally not supported
13 - #[error("Unsupported")]
14 - Unsupported,
15 -
16 - /// Invalid syntax
17 - #[error("Invalid JavaScript")]
18 - InvalidSyntax,
19 -
20 - /// Valid syntax, but invalid React
21 - #[error("Invalid React")]
22 - InvalidReact,
23 -
24 - /// Internal compiler error (ICE)
25 - #[error("Internal error")]
26 - Invariant,
27 -}
28 -
4 /// Errors which can occur during HIR construction
30 -#[derive(Error, Diagnostic, Debug)]
31 -pub enum DiagnosticError {
5 +#[derive(Error, Debug)]
6 +pub enum BuildHIRError {
7 /// ErrorSeverity::Unsupported
8 #[error(
9 "Variable declarations must be `let` or `const`, `var` declarations are not supported"
@@ -81,49 +56,3 @@ pub enum DiagnosticError {
56 #[error("Expected function to have a body")]
57 EmptyFunction,
58 }
84 -
85 -#[derive(Error, Diagnostic, Debug)]
86 -#[error("{error}")]
87 -pub struct BuildDiagnostic {
88 - /// The actual error
89 - pub error: DiagnosticError,
90 -
91 - /// Error severity
92 - pub severity: ErrorSeverity,
93 -
94 - /// Source of the error
95 - #[label]
96 - pub range: Option<SourceSpan>,
97 -}
98 -
99 -impl BuildDiagnostic {
100 - pub fn new(
101 - error: DiagnosticError,
102 - severity: ErrorSeverity,
103 - range: Option<SourceRange>,
104 - ) -> Self {
105 - Self {
106 - error,
107 - severity,
108 - range: range.map(|range| {
109 - SourceSpan::new(
110 - ByteOffset::from(range.start as usize - 1).into(),
111 - ByteOffset::from((u32::from(range.end) - range.start) as usize).into(),
112 - )
113 - }),
114 - }
115 - }
116 -}
117 -
118 -/// Returns Ok(()) if the condition is true, otherwise returns Err()
119 -/// with the diagnostic produced by the provided callback
120 -pub fn invariant<F>(cond: bool, f: F) -> Result<(), BuildDiagnostic>
121 -where
122 - F: Fn() -> BuildDiagnostic,
123 -{
124 - if cond {
125 - Ok(())
126 - } else {
127 - Err(f())
128 - }
129 -}
compiler/forget/crates/forget_diagnostics/Cargo.toml new
+23
@@ -0,0 +1,23 @@
1 +[package]
2 +name = "forget_diagnostics"
3 +version = "0.1.0"
4 +publish = false
5 +authors.workspace = true
6 +description.workspace = true
7 +edition.workspace = true
8 +homepage.workspace = true
9 +keywords.workspace = true
10 +license.workspace = true
11 +repository.workspace = true
12 +
13 +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
14 +
15 +[dependencies]
16 +# TODO: extract SourceRange into a separate crate so that
17 +# we don't depend on full estree here
18 +forget_estree = { workspace = true }
19 +# TODO: consider extracting a separate forget_miette crate which does
20 +# the translation from forget_diagnostics::Diagnostic to miette::Diagnostic
21 +miette = { workspace = true }
22 +thiserror = { workspace = true }
23 +static_assertions = { workspace = true }
\ No newline at end of file
compiler/forget/crates/forget_diagnostics/README.md new
+10
@@ -0,0 +1,10 @@
1 +# forget_diagnostics
2 +
3 +Types for representing compiler diagnostics. Includes a general-purpose representation
4 +of diagnostics with related information which can be converted into `miette::Diagnostic` to exploit miette's pretty printing of errors.
5 +
6 +Unlike miette, lsp_types, and other diagnostic libraries, the error severities match
7 +Forget's semantics. The intent is that a given Forget diagnostic may be displayed as
8 +an error, warning, or not displayed at all depending on the context in which the
9 +compiler is being used. For example, an ESLint plugin powered by Forget may ignore
10 +InvalidSyntax diagnostics, whereas the regular compiler may report them as errors.
compiler/forget/crates/forget_diagnostics/src/diagnostic.rs new
+280
@@ -0,0 +1,280 @@
1 +use std::{
2 + error::Error,
3 + fmt::{Debug, Display, Write},
4 +};
5 +
6 +use forget_estree::SourceRange;
7 +use miette::{ByteOffset, SourceSpan};
8 +use static_assertions::assert_impl_all;
9 +use thiserror::Error;
10 +
11 +pub type Diagnostics = Vec<Diagnostic>;
12 +pub type DiagnosticsResult<T> = Result<T, Diagnostics>;
13 +
14 +#[derive(Debug)]
15 +pub struct WithDiagnostics<T> {
16 + pub item: T,
17 + pub diagnostics: Vec<Diagnostic>,
18 +}
19 +
20 +impl<T> From<WithDiagnostics<T>> for Result<T, Diagnostics> {
21 + fn from(s: WithDiagnostics<T>) -> Result<T, Diagnostics> {
22 + if s.diagnostics.is_empty() {
23 + Ok(s.item)
24 + } else {
25 + Err(s.diagnostics)
26 + }
27 + }
28 +}
29 +
30 +pub fn diagnostics_result<T>(result: T, diagnostics: Diagnostics) -> DiagnosticsResult<T> {
31 + if diagnostics.is_empty() {
32 + Ok(result)
33 + } else {
34 + Err(diagnostics)
35 + }
36 +}
37 +
38 +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Error)]
39 +pub enum DiagnosticSeverity {
40 + /// A feature that is intended to work but not yet implemented
41 + #[error("Not implemented")]
42 + Todo,
43 +
44 + /// Syntax that is valid but intentionally not supported
45 + #[error("Unsupported")]
46 + Unsupported,
47 +
48 + /// Invalid syntax
49 + #[error("Invalid JavaScript")]
50 + InvalidSyntax,
51 +
52 + /// Valid syntax, but invalid React
53 + #[error("Invalid React")]
54 + InvalidReact,
55 +
56 + /// Internal compiler error (ICE)
57 + #[error("Internal error")]
58 + Invariant,
59 +}
60 +
61 +/// A diagnostic message as a result of validating some code. This struct is
62 +/// modeled after the LSP Diagnostic type:
63 +/// https://microsoft.github.io/language-server-protocol/specification#diagnostic
64 +///
65 +/// Changes from LSP:
66 +/// - `location` is different from LSP in that it's a file + span instead of
67 +/// just a span.
68 +/// - Unused fields are omitted.
69 +/// - Severity is a custom enum that represents Forget-specific categories of error.
70 +/// The translation to an LSP error/warning/etc depends on compiler settings and
71 +/// invocation context.
72 +#[derive(Debug)]
73 +pub struct Diagnostic(Box<DiagnosticData>);
74 +
75 +impl Diagnostic {
76 + fn with_severity<T: 'static + DiagnosticDisplay>(
77 + severity: DiagnosticSeverity,
78 + message: T,
79 + range: Option<SourceRange>,
80 + ) -> Self {
81 + Self(Box::new(DiagnosticData {
82 + message: Box::new(message),
83 + span: range.map(source_span_from_range),
84 + related_information: Vec::new(),
85 + severity,
86 + data: Vec::new(),
87 + }))
88 + }
89 +
90 + /// Creates a new Todo Diagnostic.
91 + /// Additional locations can be added with the `.annotate()` function.
92 + pub fn todo<T: 'static + DiagnosticDisplay>(message: T, range: Option<SourceRange>) -> Self {
93 + Diagnostic::with_severity(DiagnosticSeverity::Todo, message, range)
94 + }
95 +
96 + /// Creates a new Unsupported Diagnostic.
97 + /// Additional locations can be added with the `.annotate()` function.
98 + pub fn unsupported<T: 'static + DiagnosticDisplay>(
99 + message: T,
100 + range: Option<SourceRange>,
101 + ) -> Self {
102 + Diagnostic::with_severity(DiagnosticSeverity::Unsupported, message, range)
103 + }
104 +
105 + /// Creates a new InvalidSyntax Diagnostic.
106 + /// Additional locations can be added with the `.annotate()` function.
107 + pub fn invalid_syntax<T: 'static + DiagnosticDisplay>(
108 + message: T,
109 + range: Option<SourceRange>,
110 + ) -> Self {
111 + Diagnostic::with_severity(DiagnosticSeverity::InvalidSyntax, message, range)
112 + }
113 +
114 + /// Creates a new InvalidReact Diagnostic.
115 + /// Additional locations can be added with the `.annotate()` function.
116 + pub fn invalid_react<T: 'static + DiagnosticDisplay>(
117 + message: T,
118 + range: Option<SourceRange>,
119 + ) -> Self {
120 + Diagnostic::with_severity(DiagnosticSeverity::InvalidReact, message, range)
121 + }
122 +
123 + /// Creates a new InvalidReact Diagnostic.
124 + /// Additional locations can be added with the `.annotate()` function.
125 + pub fn invariant<T: 'static + DiagnosticDisplay>(
126 + message: T,
127 + range: Option<SourceRange>,
128 + ) -> Self {
129 + Diagnostic::with_severity(DiagnosticSeverity::Invariant, message, range)
130 + }
131 +
132 + /// Annotates this error with an additional location and associated message.
133 + pub fn annotate<T: 'static + DiagnosticDisplay>(
134 + mut self,
135 + message: T,
136 + range: Option<SourceRange>,
137 + ) -> Self {
138 + self.0
139 + .related_information
140 + .push(DiagnosticRelatedInformation {
141 + message: Box::new(message),
142 + span: range.map(source_span_from_range),
143 + });
144 + self
145 + }
146 +
147 + pub fn message(&self) -> &impl DiagnosticDisplay {
148 + &self.0.message
149 + }
150 +
151 + pub fn span(&self) -> Option<SourceSpan> {
152 + self.0.span
153 + }
154 +
155 + pub fn get_data(&self) -> &[impl DiagnosticDisplay] {
156 + &self.0.data
157 + }
158 +
159 + pub fn severity(&self) -> DiagnosticSeverity {
160 + self.0.severity
161 + }
162 +
163 + pub fn related_information(&self) -> &[DiagnosticRelatedInformation] {
164 + &self.0.related_information
165 + }
166 +
167 + pub fn print_without_source(&self) -> String {
168 + let mut result = String::new();
169 + writeln!(
170 + result,
171 + "{message}:{span:?}",
172 + message = &self.0.message,
173 + span = self.0.span
174 + )
175 + .unwrap();
176 + if !self.0.related_information.is_empty() {
177 + for (ix, related) in self.0.related_information.iter().enumerate() {
178 + writeln!(
179 + result,
180 + "[related {ix}] {message}:{span:?}",
181 + ix = ix + 1,
182 + message = related.message,
183 + span = related.span
184 + )
185 + .unwrap();
186 + }
187 + };
188 + result
189 + }
190 +}
191 +
192 +impl Display for Diagnostic {
193 + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
194 + write!(f, "{}", self.0.message.to_string())
195 + }
196 +}
197 +
198 +impl Error for Diagnostic {}
199 +
200 +impl miette::Diagnostic for Diagnostic {
201 + fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
202 + Some(Box::new(self.0.message.to_string()))
203 + }
204 +
205 + fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
206 + let related_items = &self.0.related_information;
207 + let mut spans: Vec<miette::LabeledSpan> = Vec::new();
208 + for related in related_items {
209 + if let Some(span) = related.span {
210 + spans.push(miette::LabeledSpan::new_with_span(
211 + Some(related.message.to_string()),
212 + span,
213 + ))
214 + }
215 + }
216 + if spans.is_empty() {
217 + if let Some(span) = self.0.span {
218 + spans.push(miette::LabeledSpan::new_with_span(
219 + Some(self.0.message.to_string()),
220 + span,
221 + ))
222 + }
223 + }
224 + Some(Box::new(spans.into_iter()))
225 + }
226 +}
227 +
228 +// Ensure Diagnostic is thread-safe
229 +assert_impl_all!(Diagnostic: Send, Sync);
230 +
231 +#[derive(Debug)]
232 +struct DiagnosticData {
233 + /// Human readable error message.
234 + message: Box<dyn DiagnosticDisplay>,
235 +
236 + /// The primary location of this diagnostic.
237 + span: Option<SourceSpan>,
238 +
239 + /// Related diagnostic information, such as other definitions in the case of
240 + /// a duplicate definition error.
241 + related_information: Vec<DiagnosticRelatedInformation>,
242 +
243 + severity: DiagnosticSeverity,
244 +
245 + /// A list with data that can be passed to the code actions
246 + /// `data` is used in the LSP protocol:
247 + /// @see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#diagnostic
248 + data: Vec<Box<dyn DiagnosticDisplay>>,
249 +}
250 +
251 +/// Secondary locations attached to a diagnostic.
252 +#[derive(Debug)]
253 +pub struct DiagnosticRelatedInformation {
254 + /// The message of this related diagnostic information.
255 + pub message: Box<dyn DiagnosticDisplay>,
256 +
257 + /// The location of this related diagnostic information.
258 + pub span: Option<SourceSpan>,
259 +}
260 +
261 +/// Trait for diagnostic messages to allow structs that capture
262 +/// some data and can lazily convert it to a message.
263 +pub trait DiagnosticDisplay: Debug + Display + Send + Sync {}
264 +
265 +/// Automatically implement the trait if constraints are met, so that
266 +/// implementors don't need to.
267 +impl<T> DiagnosticDisplay for T where T: Debug + Display + Send + Sync {}
268 +
269 +impl From<Diagnostic> for Diagnostics {
270 + fn from(diagnostic: Diagnostic) -> Self {
271 + vec![diagnostic]
272 + }
273 +}
274 +
275 +fn source_span_from_range(range: SourceRange) -> SourceSpan {
276 + SourceSpan::new(
277 + ByteOffset::from(range.start as usize - 1).into(),
278 + ByteOffset::from((u32::from(range.end) - range.start) as usize).into(),
279 + )
280 +}
compiler/forget/crates/forget_diagnostics/src/lib.rs new
+16
@@ -0,0 +1,16 @@
1 +mod diagnostic;
2 +
3 +pub use diagnostic::*;
4 +
5 +/// Returns Ok(()) if the condition is true, otherwise returns Err()
6 +/// with the diagnostic produced by the provided callback
7 +pub fn invariant<F>(cond: bool, f: F) -> Result<(), Diagnostic>
8 +where
9 + F: Fn() -> Diagnostic,
10 +{
11 + if cond {
12 + Ok(())
13 + } else {
14 + Err(f())
15 + }
16 +}
compiler/forget/crates/forget_estree/src/range.rs
+1 -1
@@ -2,7 +2,7 @@ use std::num::NonZeroU32;
2
3 use serde::{Deserialize, Serialize};
4
5 -#[derive(Serialize, Deserialize, Clone, Debug)]
5 +#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, PartialOrd, Hash)]
6 pub struct SourceRange {
7 pub start: u32,
8 pub end: NonZeroU32,
compiler/forget/crates/forget_hir/src/terminal.rs
+1 -1
@@ -78,7 +78,7 @@ impl<'a> TerminalValue<'a> {
78
79 #[derive(Debug)]
80 pub struct UnsupportedTerminal<'a> {
81 - phantom: std::marker::PhantomData<&'a ()>
81 + phantom: std::marker::PhantomData<&'a ()>,
82 }
83
84 #[derive(Debug)]