diff --git a/crates/oxc_diagnostics/src/lib.rs b/crates/oxc_diagnostics/src/lib.rs index 08a666e91bd6c..103751896f63b 100644 --- a/crates/oxc_diagnostics/src/lib.rs +++ b/crates/oxc_diagnostics/src/lib.rs @@ -6,7 +6,10 @@ mod graphical_theme; mod reporter; mod service; -use std::path::PathBuf; +use std::{ + fmt::{self, Display}, + path::PathBuf, +}; pub use miette; pub use thiserror; @@ -20,12 +23,11 @@ pub use crate::{ pub type Error = miette::Error; pub type Severity = miette::Severity; pub type Report = miette::Report; -pub type OxcDiagnostic = miette::MietteDiagnostic; -pub type Result = std::result::Result; +pub type Result = std::result::Result; -use miette::Diagnostic; pub use miette::LabeledSpan; +use miette::{Diagnostic, SourceCode}; use thiserror::Error; #[derive(Debug, Error, Diagnostic)] @@ -37,3 +39,79 @@ pub struct MinifiedFileError(pub PathBuf); #[error("Failed to open file {0:?} with error \"{1}\"")] #[diagnostic(help("Failed to open file {0:?} with error \"{1}\""))] pub struct FailedToOpenFileError(pub PathBuf, pub std::io::Error); + +// [miette::MietteDiagnostic] is 128 bytes. This is a version with less fields (72 bytes). +#[derive(Debug, Clone)] +pub struct OxcDiagnostic { + pub message: String, + pub labels: Option>, + pub help: Option, +} + +impl fmt::Display for OxcDiagnostic { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", &self.message) + } +} + +impl std::error::Error for OxcDiagnostic {} + +impl Diagnostic for OxcDiagnostic { + fn help<'a>(&'a self) -> Option> { + self.help.as_ref().map(Box::new).map(|c| c as Box) + } + + fn labels(&self) -> Option + '_>> { + self.labels + .as_ref() + .map(|ls| ls.iter().cloned()) + .map(Box::new) + .map(|b| b as Box>) + } +} + +impl OxcDiagnostic { + #[must_use] + pub fn new>(message: T) -> Self { + Self { message: message.into(), labels: None, help: None } + } + + #[must_use] + pub fn with_help>(mut self, help: T) -> Self { + self.help = Some(help.into()); + self + } + + #[must_use] + pub fn with_label>(mut self, label: T) -> Self { + self.labels = Some(vec![label.into()]); + self + } + + #[must_use] + pub fn with_labels>(mut self, labels: T) -> Self { + self.labels = Some(labels.into_iter().collect()); + self + } + + #[must_use] + pub fn and_label>(mut self, label: T) -> Self { + let mut labels = self.labels.unwrap_or_default(); + labels.push(label.into()); + self.labels = Some(labels); + self + } + + #[must_use] + pub fn and_labels>(mut self, labels: T) -> Self { + let mut all_labels = self.labels.unwrap_or_default(); + all_labels.extend(labels); + self.labels = Some(all_labels); + self + } + + #[must_use] + pub fn with_source_code(self, code: T) -> Error { + Error::from(self).with_source_code(code) + } +} diff --git a/crates/oxc_language_server/src/linter.rs b/crates/oxc_language_server/src/linter.rs index d187a41c332ff..12afc3eae5ad3 100644 --- a/crates/oxc_language_server/src/linter.rs +++ b/crates/oxc_language_server/src/linter.rs @@ -279,7 +279,10 @@ impl IsolatedLintHandler { let reports = ret .errors .into_iter() - .map(|diagnostic| ErrorReport { error: diagnostic, fixed_content: None }) + .map(|diagnostic| ErrorReport { + error: Error::from(diagnostic), + fixed_content: None, + }) .collect(); return Some(Self::wrap_diagnostics(path, &original_source_text, reports, start)); }; diff --git a/crates/oxc_linter/examples/linter.rs b/crates/oxc_linter/examples/linter.rs index d24565865c4b8..417137e2cb160 100644 --- a/crates/oxc_linter/examples/linter.rs +++ b/crates/oxc_linter/examples/linter.rs @@ -4,10 +4,7 @@ use std::{env, path::Path}; use oxc_allocator::Allocator; use oxc_ast::AstKind; -use oxc_diagnostics::{ - miette::{self, Diagnostic}, - thiserror::Error, -}; +use oxc_diagnostics::{LabeledSpan, OxcDiagnostic}; use oxc_parser::Parser; use oxc_semantic::SemanticBuilder; use oxc_span::{SourceType, Span}; @@ -35,18 +32,18 @@ fn main() -> std::io::Result<()> { let semantic_ret = SemanticBuilder::new(&source_text, source_type).with_trivias(ret.trivias).build(program); - let mut errors: Vec = vec![]; + let mut errors: Vec = vec![]; for node in semantic_ret.semantic.nodes().iter() { match node.kind() { AstKind::DebuggerStatement(stmt) => { - errors.push(NoDebugger(stmt.span).into()); + errors.push(no_debugger(stmt.span)); } AstKind::ArrayPattern(array) if array.elements.is_empty() => { - errors.push(NoEmptyPattern("array", array.span).into()); + errors.push(no_empty_pattern("array", array.span)); } AstKind::ObjectPattern(object) if object.properties.is_empty() => { - errors.push(NoEmptyPattern("object", object.span).into()); + errors.push(no_empty_pattern("object", object.span)); } _ => {} } @@ -61,7 +58,7 @@ fn main() -> std::io::Result<()> { Ok(()) } -fn print_errors(source_text: &str, errors: Vec) { +fn print_errors(source_text: &str, errors: Vec) { for error in errors { let error = error.with_source_code(source_text.to_string()); println!("{error:?}"); @@ -75,10 +72,9 @@ fn print_errors(source_text: &str, errors: Vec) { // 1 │ debugger; // · ───────── // ╰──── -#[derive(Debug, Error, Diagnostic)] -#[error("`debugger` statement is not allowed")] -#[diagnostic(severity(warning))] -struct NoDebugger(#[label] pub Span); +fn no_debugger(span0: Span) -> OxcDiagnostic { + OxcDiagnostic::new("`debugger` statement is not allowed").with_labels([span0.into()]) +} // This prints: // @@ -88,7 +84,8 @@ struct NoDebugger(#[label] pub Span); // · ─┬ // · ╰── Empty object binding pattern // ╰──── -#[derive(Debug, Error, Diagnostic)] -#[error("empty destructuring pattern is not allowed")] -#[diagnostic(severity(warning))] -struct NoEmptyPattern(&'static str, #[label("Empty {0} binding pattern")] pub Span); +fn no_empty_pattern(s0: &str, span1: Span) -> OxcDiagnostic { + OxcDiagnostic::new("empty destructuring pattern is not allowed").with_labels([ + LabeledSpan::new_with_span(Some(format!("Empty {s0} binding pattern")), span1), + ]) +} diff --git a/crates/oxc_linter/src/service.rs b/crates/oxc_linter/src/service.rs index 44e537b99a5bc..02cf94bcb5358 100644 --- a/crates/oxc_linter/src/service.rs +++ b/crates/oxc_linter/src/service.rs @@ -256,7 +256,11 @@ impl Runtime { .parse(); if !ret.errors.is_empty() { - return ret.errors.into_iter().map(|err| Message::new(err, None)).collect(); + return ret + .errors + .into_iter() + .map(|err| Message::new(Error::from(err), None)) + .collect(); }; let program = allocator.alloc(ret.program); diff --git a/crates/oxc_parser/src/cursor.rs b/crates/oxc_parser/src/cursor.rs index 134d863e9cd31..af18ec6a5c976 100644 --- a/crates/oxc_parser/src/cursor.rs +++ b/crates/oxc_parser/src/cursor.rs @@ -158,7 +158,7 @@ impl<'a> ParserImpl<'a> { pub(crate) fn asi(&mut self) -> Result<()> { if !self.can_insert_semicolon() { let span = Span::new(self.prev_token_end, self.cur_token().start); - return Err(diagnostics::auto_semicolon_insertion(span).into()); + return Err(diagnostics::auto_semicolon_insertion(span)); } if self.at(Kind::Semicolon) { self.advance(Kind::Semicolon); @@ -178,9 +178,7 @@ impl<'a> ParserImpl<'a> { pub(crate) fn expect_without_advance(&mut self, kind: Kind) -> Result<()> { if !self.at(kind) { let range = self.cur_token().span(); - return Err( - diagnostics::expect_token(kind.to_str(), self.cur_kind().to_str(), range).into() - ); + return Err(diagnostics::expect_token(kind.to_str(), self.cur_kind().to_str(), range)); } Ok(()) } diff --git a/crates/oxc_parser/src/js/expression.rs b/crates/oxc_parser/src/js/expression.rs index 67ebbe586bd28..a178f0c2e6c70 100644 --- a/crates/oxc_parser/src/js/expression.rs +++ b/crates/oxc_parser/src/js/expression.rs @@ -214,7 +214,7 @@ impl<'a> ParserImpl<'a> { let paren_span = self.end_span(span); if expressions.is_empty() { - return Err(diagnostics::empty_parenthesized_expression(paren_span).into()); + return Err(diagnostics::empty_parenthesized_expression(paren_span)); } // ParenthesizedExpression is from acorn --preserveParens diff --git a/crates/oxc_parser/src/js/grammar.rs b/crates/oxc_parser/src/js/grammar.rs index ca0482339fca5..8129095f2be05 100644 --- a/crates/oxc_parser/src/js/grammar.rs +++ b/crates/oxc_parser/src/js/grammar.rs @@ -43,7 +43,7 @@ impl<'a> CoverGrammar<'a, Expression<'a>> for SimpleAssignmentTarget<'a> { let span = expr.span; match expr.unbox().expression { Expression::ObjectExpression(_) | Expression::ArrayExpression(_) => { - Err(diagnostics::invalid_assignment(span).into()) + Err(diagnostics::invalid_assignment(span)) } expr => SimpleAssignmentTarget::cover(expr, p), } @@ -59,7 +59,7 @@ impl<'a> CoverGrammar<'a, Expression<'a>> for SimpleAssignmentTarget<'a> { Expression::TSInstantiationExpression(expr) => { Ok(SimpleAssignmentTarget::TSInstantiationExpression(expr)) } - expr => Err(diagnostics::invalid_assignment(expr.span()).into()), + expr => Err(diagnostics::invalid_assignment(expr.span())), } } } @@ -87,7 +87,7 @@ impl<'a> CoverGrammar<'a, ArrayExpression<'a>> for ArrayAssignmentTarget<'a> { p.error(diagnostics::binding_rest_element_trailing_comma(span)); } } else { - return Err(diagnostics::spread_last_element(elem.span).into()); + return Err(diagnostics::spread_last_element(elem.span)); } } ArrayExpressionElement::Elision(_) => elements.push(None), @@ -143,7 +143,7 @@ impl<'a> CoverGrammar<'a, ObjectExpression<'a>> for ObjectAssignmentTarget<'a> { target: AssignmentTarget::cover(spread.unbox().argument, p)?, }); } else { - return Err(diagnostics::spread_last_element(spread.span).into()); + return Err(diagnostics::spread_last_element(spread.span)); } } } diff --git a/crates/oxc_parser/src/js/list.rs b/crates/oxc_parser/src/js/list.rs index 47618648bd48c..e52a1d6e52016 100644 --- a/crates/oxc_parser/src/js/list.rs +++ b/crates/oxc_parser/src/js/list.rs @@ -1,11 +1,7 @@ use oxc_allocator::Vec; use oxc_ast::ast::*; -use oxc_diagnostics::{ - miette::{self, Diagnostic}, - thiserror::{self, Error}, - Result, -}; -use oxc_span::{Atom, CompactStr, GetSpan, Span}; +use oxc_diagnostics::{LabeledSpan, OxcDiagnostic, Result}; +use oxc_span::{Atom, GetSpan, Span}; use rustc_hash::FxHashMap; use crate::{ @@ -15,14 +11,12 @@ use crate::{ ParserImpl, }; -#[derive(Debug, Error, Diagnostic)] -#[error("Identifier `{0}` has already been declared")] -#[diagnostic()] -struct Redeclaration( - pub CompactStr, - #[label("`{0}` has already been declared here")] pub Span, - #[label("It can not be redeclared here")] pub Span, -); +pub fn redeclaration(x0: &str, span1: Span, span2: Span) -> OxcDiagnostic { + OxcDiagnostic::new(format!("Identifier `{x0}` has already been declared")).with_labels([ + LabeledSpan::new_with_span(Some(format!("`{x0}` has already been declared here")), span1), + LabeledSpan::new_with_span(Some("It can not be redeclared here".to_string()), span2), + ]) +} /// ObjectExpression.properties pub struct ObjectExpressionProperties<'a> { @@ -321,7 +315,7 @@ impl<'a> SeparatedList<'a> for AssertEntries<'a> { }; if let Some(old_span) = self.keys.get(&key.as_atom()) { - p.error(Redeclaration(key.as_atom().into_compact_str(), *old_span, key.span())); + p.error(redeclaration(key.as_atom().as_str(), *old_span, key.span())); } else { self.keys.insert(key.as_atom(), key.span()); } diff --git a/crates/oxc_parser/src/jsx/mod.rs b/crates/oxc_parser/src/jsx/mod.rs index 1fda0e396d613..3c15514e7f66b 100644 --- a/crates/oxc_parser/src/jsx/mod.rs +++ b/crates/oxc_parser/src/jsx/mod.rs @@ -262,9 +262,7 @@ impl<'a> ParserImpl<'a> { self.ctx = Context::default().and_await(ctx.has_await()); let expr = self.parse_expression(); if let Ok(Expression::SequenceExpression(seq)) = &expr { - return Err( - diagnostics::jsx_expressions_may_not_use_the_comma_operator(seq.span).into() - ); + return Err(diagnostics::jsx_expressions_may_not_use_the_comma_operator(seq.span)); } self.ctx = ctx; expr diff --git a/crates/oxc_parser/src/lexer/mod.rs b/crates/oxc_parser/src/lexer/mod.rs index 84859f20cb089..ee493c45b7d4f 100644 --- a/crates/oxc_parser/src/lexer/mod.rs +++ b/crates/oxc_parser/src/lexer/mod.rs @@ -32,7 +32,7 @@ use std::collections::VecDeque; use oxc_allocator::Allocator; use oxc_ast::ast::RegExpFlags; -use oxc_diagnostics::Error; +use oxc_diagnostics::OxcDiagnostic; use oxc_span::{SourceType, Span}; use self::{ @@ -80,7 +80,7 @@ pub struct Lexer<'a> { token: Token, - pub(crate) errors: Vec, + pub(crate) errors: Vec, lookahead: VecDeque>, @@ -223,8 +223,8 @@ impl<'a> Lexer<'a> { } // ---------- Private Methods ---------- // - fn error>(&mut self, error: T) { - self.errors.push(error.into()); + fn error(&mut self, error: OxcDiagnostic) { + self.errors.push(error); } /// Get the length offset from the source, in UTF-8 bytes diff --git a/crates/oxc_parser/src/lib.rs b/crates/oxc_parser/src/lib.rs index c1ed6b76114a1..b6c156e49f983 100644 --- a/crates/oxc_parser/src/lib.rs +++ b/crates/oxc_parser/src/lib.rs @@ -84,7 +84,7 @@ pub use crate::lexer::Kind; // re-export for codegen use context::{Context, StatementContext}; use oxc_allocator::Allocator; use oxc_ast::{ast::Program, AstBuilder, Trivias}; -use oxc_diagnostics::{Error, Result}; +use oxc_diagnostics::{OxcDiagnostic, Result}; use oxc_span::{ModuleKind, SourceType, Span}; use crate::{ @@ -113,7 +113,7 @@ pub const MAX_LEN: usize = if std::mem::size_of::() >= 8 { /// When `errors.len() > 0`, then program may or may not be empty due to error recovery. pub struct ParserReturn<'a> { pub program: Program<'a>, - pub errors: Vec, + pub errors: Vec, pub trivias: Trivias, pub panicked: bool, } @@ -245,7 +245,7 @@ struct ParserImpl<'a> { /// All syntax errors from parser and lexer /// Note: favor adding to `Diagnostics` instead of raising Err - errors: Vec, + errors: Vec, /// The current parsing token token: Token, @@ -361,21 +361,21 @@ impl<'a> ParserImpl<'a> { /// Check for Flow declaration if the file cannot be parsed. /// The declaration must be [on the first line before any code](https://flow.org/en/docs/usage/#toc-prepare-your-code-for-flow) - fn flow_error(&self) -> Option { + fn flow_error(&self) -> Option { if self.source_type.is_javascript() && (self.source_text.starts_with("// @flow") || self.source_text.starts_with("/* @flow */")) { - return Some(diagnostics::flow(Span::new(0, 8)).into()); + return Some(diagnostics::flow(Span::new(0, 8))); } None } /// Check if source length exceeds MAX_LEN, if the file cannot be parsed. /// Original parsing error is not real - `Lexer::new` substituted "\0" as the source text. - fn overlong_error(&self) -> Option { + fn overlong_error(&self) -> Option { if self.source_text.len() > MAX_LEN { - return Some(diagnostics::overlong_source().into()); + return Some(diagnostics::overlong_source()); } None } @@ -383,7 +383,7 @@ impl<'a> ParserImpl<'a> { /// Return error info at current token /// # Panics /// * The lexer did not push a diagnostic when `Kind::Undetermined` is returned - fn unexpected(&mut self) -> Error { + fn unexpected(&mut self) -> OxcDiagnostic { // The lexer should have reported a more meaningful diagnostic // when it is a undetermined kind. if self.cur_kind() == Kind::Undetermined { @@ -391,12 +391,12 @@ impl<'a> ParserImpl<'a> { return error; } } - diagnostics::unexpected_token(self.cur_token().span()).into() + diagnostics::unexpected_token(self.cur_token().span()) } /// Push a Syntax Error - fn error>(&mut self, error: T) { - self.errors.push(error.into()); + fn error(&mut self, error: OxcDiagnostic) { + self.errors.push(error); } fn ts_enabled(&self) -> bool { diff --git a/crates/oxc_wasm/src/lib.rs b/crates/oxc_wasm/src/lib.rs index 3c996d4056c36..5931022f85e45 100644 --- a/crates/oxc_wasm/src/lib.rs +++ b/crates/oxc_wasm/src/lib.rs @@ -172,7 +172,7 @@ impl Oxc { .parse(); self.comments = self.map_comments(&ret.trivias); - self.save_diagnostics(ret.errors); + self.save_diagnostics(ret.errors.into_iter().map(Error::from).collect::>()); self.ir = format!("{:#?}", ret.program.body).into(); diff --git a/napi/parser/src/lib.rs b/napi/parser/src/lib.rs index 64794a1d1d698..a20d2ad4fa358 100644 --- a/napi/parser/src/lib.rs +++ b/napi/parser/src/lib.rs @@ -7,7 +7,7 @@ use napi_derive::napi; use oxc_allocator::Allocator; pub use oxc_ast::ast::Program; use oxc_ast::CommentKind; -use oxc_diagnostics::miette::NamedSource; +use oxc_diagnostics::{miette::NamedSource, Error}; use oxc_parser::{Parser, ParserReturn}; use oxc_span::SourceType; @@ -103,7 +103,7 @@ pub fn parse_sync(source_text: String, options: Option) -> ParseR let source = Arc::new(NamedSource::new(file_name, source_text.to_string())); ret.errors .into_iter() - .map(|diagnostic| diagnostic.with_source_code(Arc::clone(&source))) + .map(|diagnostic| Error::from(diagnostic).with_source_code(Arc::clone(&source))) .map(|error| format!("{error:?}")) .collect() }; diff --git a/tasks/coverage/src/suite.rs b/tasks/coverage/src/suite.rs index f844bf907339f..50b220ffc9764 100644 --- a/tasks/coverage/src/suite.rs +++ b/tasks/coverage/src/suite.rs @@ -18,7 +18,7 @@ use walkdir::WalkDir; use oxc_allocator::Allocator; use oxc_ast::Trivias; -use oxc_diagnostics::{miette::NamedSource, GraphicalReportHandler, GraphicalTheme}; +use oxc_diagnostics::{miette::NamedSource, Error, GraphicalReportHandler, GraphicalTheme}; use oxc_parser::Parser; use oxc_semantic::SemanticBuilder; use oxc_span::{SourceType, Span}; @@ -334,7 +334,12 @@ pub trait Case: Sized + Sync + Send + UnwindSafe { if let Some(res) = self.check_semantic(&semantic_ret.semantic) { return res; } - let errors = parser_ret.errors.into_iter().chain(semantic_ret.errors).collect::>(); + let errors = parser_ret + .errors + .into_iter() + .map(Error::from) + .chain(semantic_ret.errors) + .collect::>(); let result = if errors.is_empty() { Ok(String::new()) diff --git a/wasm/parser/src/lib.rs b/wasm/parser/src/lib.rs index 531133adf3292..8103d14d816ce 100644 --- a/wasm/parser/src/lib.rs +++ b/wasm/parser/src/lib.rs @@ -82,13 +82,14 @@ pub fn parse_sync( ret.errors .iter() .flat_map(|error| { - let Some(labels) = error.labels() else { return vec![] }; + let Some(labels) = &error.labels else { return vec![] }; labels + .iter() .map(|label| { Diagnostic { start: label.offset(), end: label.offset() + label.len(), - severity: format!("{:?}", error.severity().unwrap_or_default()), + severity: "Error".to_string(), message: format!("{error}"), } .serialize(&serializer)