diff --git a/crates/typst-cli/src/compile.rs b/crates/typst-cli/src/compile.rs index 9dfa4d41a..f68193191 100644 --- a/crates/typst-cli/src/compile.rs +++ b/crates/typst-cli/src/compile.rs @@ -4,9 +4,9 @@ use std::path::Path; use codespan_reporting::diagnostic::{Diagnostic, Label}; use codespan_reporting::term::{self, termcolor}; use termcolor::{ColorChoice, StandardStream}; -use typst::diag::{bail, SourceError, StrResult}; +use typst::diag::{bail, Severity, SourceDiagnostic, StrResult}; use typst::doc::Document; -use typst::eval::eco_format; +use typst::eval::{eco_format, Tracer}; use typst::geom::Color; use typst::syntax::{FileId, Source}; use typst::World; @@ -46,9 +46,13 @@ pub fn compile_once( world.reset(); world.source(world.main()).map_err(|err| err.to_string())?; - let result = typst::compile(world); + let mut tracer = Tracer::default(); + + let result = typst::compile(world, &mut tracer); let duration = start.elapsed(); + let warnings = tracer.warnings(); + match result { // Export the PDF / PNG. Ok(document) => { @@ -56,9 +60,16 @@ pub fn compile_once( tracing::info!("Compilation succeeded in {duration:?}"); if watching { - Status::Success(duration).print(command).unwrap(); + if warnings.is_empty() { + Status::Success(duration).print(command).unwrap(); + } else { + Status::PartialSuccess(duration).print(command).unwrap(); + } } + print_diagnostics(world, &[], &warnings, command.diagnostic_format) + .map_err(|_| "failed to print diagnostics")?; + if let Some(open) = command.open.take() { open_file(open.as_deref(), &command.output())?; } @@ -73,7 +84,7 @@ pub fn compile_once( Status::Error.print(command).unwrap(); } - print_diagnostics(world, *errors, command.diagnostic_format) + print_diagnostics(world, &errors, &warnings, command.diagnostic_format) .map_err(|_| "failed to print diagnostics")?; } } @@ -143,7 +154,8 @@ fn open_file(open: Option<&str>, path: &Path) -> StrResult<()> { /// Print diagnostic messages to the terminal. fn print_diagnostics( world: &SystemWorld, - errors: Vec, + errors: &[SourceDiagnostic], + warnings: &[SourceDiagnostic], diagnostic_format: DiagnosticFormat, ) -> Result<(), codespan_reporting::files::Error> { let mut w = match diagnostic_format { @@ -156,23 +168,28 @@ fn print_diagnostics( config.display_style = term::DisplayStyle::Short; } - for error in errors { - // The main diagnostic. - let diag = Diagnostic::error() - .with_message(error.message) - .with_notes( - error - .hints - .iter() - .map(|e| (eco_format!("hint: {e}")).into()) - .collect(), - ) - .with_labels(vec![Label::primary(error.span.id(), world.range(error.span))]); + for diagnostic in warnings.iter().chain(errors.iter()) { + let diag = match diagnostic.severity { + Severity::Error => Diagnostic::error(), + Severity::Warning => Diagnostic::warning(), + } + .with_message(diagnostic.message.clone()) + .with_notes( + diagnostic + .hints + .iter() + .map(|e| (eco_format!("hint: {e}")).into()) + .collect(), + ) + .with_labels(vec![Label::primary( + diagnostic.span.id(), + world.range(diagnostic.span), + )]); term::emit(&mut w, &config, world, &diag)?; // Stacktrace-like helper diagnostics. - for point in error.trace { + for point in &diagnostic.trace { let message = point.v.to_string(); let help = Diagnostic::help().with_message(message).with_labels(vec![ Label::primary(point.span.id(), world.range(point.span)), diff --git a/crates/typst-cli/src/watch.rs b/crates/typst-cli/src/watch.rs index cf9c05bae..759d27ec7 100644 --- a/crates/typst-cli/src/watch.rs +++ b/crates/typst-cli/src/watch.rs @@ -138,6 +138,7 @@ fn is_event_relevant(event: ¬ify::Event, output: &Path) -> bool { pub enum Status { Compiling, Success(std::time::Duration), + PartialSuccess(std::time::Duration), Error, } @@ -176,6 +177,9 @@ impl Status { match self { Self::Compiling => "compiling ...".into(), Self::Success(duration) => format!("compiled successfully in {duration:.2?}"), + Self::PartialSuccess(duration) => { + format!("compiled with warnings in {duration:.2?}") + } Self::Error => "compiled with errors".into(), } } @@ -184,6 +188,7 @@ impl Status { let styles = term::Styles::default(); match self { Self::Error => styles.header_error, + Self::PartialSuccess(_) => styles.header_warning, _ => styles.header_note, } } diff --git a/crates/typst-docs/src/html.rs b/crates/typst-docs/src/html.rs index ed49f9fef..b021d4a79 100644 --- a/crates/typst-docs/src/html.rs +++ b/crates/typst-docs/src/html.rs @@ -4,7 +4,7 @@ use comemo::Prehashed; use pulldown_cmark as md; use typed_arena::Arena; use typst::diag::FileResult; -use typst::eval::Datetime; +use typst::eval::{Datetime, Tracer}; use typst::font::{Font, FontBook}; use typst::geom::{Point, Size}; use typst::syntax::{FileId, Source}; @@ -428,7 +428,9 @@ fn code_block(resolver: &dyn Resolver, lang: &str, text: &str) -> Html { let id = FileId::new(None, Path::new("/main.typ")); let source = Source::new(id, compile); let world = DocWorld(source); - let mut frames = match typst::compile(&world) { + let mut tracer = Tracer::default(); + + let mut frames = match typst::compile(&world, &mut tracer) { Ok(doc) => doc.pages, Err(err) => { let msg = &err[0].message; diff --git a/crates/typst/src/diag.rs b/crates/typst/src/diag.rs index 85fb10a20..1cc4a0450 100644 --- a/crates/typst/src/diag.rs +++ b/crates/typst/src/diag.rs @@ -33,7 +33,7 @@ macro_rules! __bail { }; ($span:expr, $fmt:literal $(, $arg:expr)* $(,)?) => { - return Err(Box::new(vec![$crate::diag::SourceError::new( + return Err(Box::new(vec![$crate::diag::SourceDiagnostic::error( $span, $crate::diag::eco_format!($fmt, $($arg),*), )])) @@ -43,7 +43,7 @@ macro_rules! __bail { #[doc(inline)] pub use crate::__bail as bail; -/// Construct an [`EcoString`] or [`SourceError`]. +/// Construct an [`EcoString`] or [`SourceDiagnostic`] with severity `Error`. #[macro_export] #[doc(hidden)] macro_rules! __error { @@ -52,7 +52,19 @@ macro_rules! __error { }; ($span:expr, $fmt:literal $(, $arg:expr)* $(,)?) => { - $crate::diag::SourceError::new( + $crate::diag::SourceDiagnostic::error( + $span, + $crate::diag::eco_format!($fmt, $($arg),*), + ) + }; +} + +/// Construct a [`SourceDiagnostic`] with severity `Warning`. +#[macro_export] +#[doc(hidden)] +macro_rules! __warning { + ($span:expr, $fmt:literal $(, $arg:expr)* $(,)?) => { + $crate::diag::SourceDiagnostic::warning( $span, $crate::diag::eco_format!($fmt, $($arg),*), ) @@ -61,33 +73,47 @@ macro_rules! __error { #[doc(inline)] pub use crate::__error as error; +#[doc(inline)] +pub use crate::__warning as warning; #[doc(hidden)] pub use ecow::{eco_format, EcoString}; /// A result that can carry multiple source errors. -pub type SourceResult = Result>>; +pub type SourceResult = Result>>; -/// An error in a source file. +/// An error or warning in a source file. /// /// The contained spans will only be detached if any of the input source files /// were detached. #[derive(Debug, Clone, Eq, PartialEq, Hash)] -pub struct SourceError { - /// The span of the erroneous node in the source code. +pub struct SourceDiagnostic { + /// Whether the diagnostic is an error or a warning. + pub severity: Severity, + /// The span of the relevant node in the source code. pub span: Span, /// A diagnostic message describing the problem. pub message: EcoString, - /// The trace of function calls leading to the error. + /// The trace of function calls leading to the problem. pub trace: Vec>, - /// Additonal hints to the user, indicating how this error could be avoided + /// Additonal hints to the user, indicating how this problem could be avoided /// or worked around. pub hints: Vec, } -impl SourceError { +/// The severity of a [`SourceDiagnostic`]. +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] +pub enum Severity { + /// A fatal error. + Error, + /// A non-fatal warning. + Warning, +} + +impl SourceDiagnostic { /// Create a new, bare error. - pub fn new(span: Span, message: impl Into) -> Self { + pub fn error(span: Span, message: impl Into) -> Self { Self { + severity: Severity::Error, span, trace: vec![], message: message.into(), @@ -95,16 +121,34 @@ impl SourceError { } } - /// Adds user-facing hints to the error. + /// Create a new, bare warning. + pub fn warning(span: Span, message: impl Into) -> Self { + Self { + severity: Severity::Warning, + span, + trace: vec![], + message: message.into(), + hints: vec![], + } + } + + /// Adds a single hint to the diagnostic. + pub fn with_hint(mut self, hint: EcoString) -> Self { + self.hints.push(hint); + self + } + + /// Adds user-facing hints to the diagnostic. pub fn with_hints(mut self, hints: impl IntoIterator) -> Self { self.hints.extend(hints); self } } -impl From for SourceError { +impl From for SourceDiagnostic { fn from(error: SyntaxError) -> Self { Self { + severity: Severity::Error, span: error.span, message: error.message, trace: vec![], @@ -113,7 +157,7 @@ impl From for SourceError { } } -/// A part of an error's [trace](SourceError::trace). +/// A part of a diagnostic's [trace](SourceDiagnostic::trace). #[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] pub enum Tracepoint { /// A function call. @@ -194,7 +238,7 @@ where S: Into, { fn at(self, span: Span) -> SourceResult { - self.map_err(|message| Box::new(vec![SourceError::new(span, message)])) + self.map_err(|message| Box::new(vec![SourceDiagnostic::error(span, message)])) } } @@ -214,7 +258,9 @@ pub struct HintedString { impl At for Result { fn at(self, span: Span) -> SourceResult { self.map_err(|diags| { - Box::new(vec![SourceError::new(span, diags.message).with_hints(diags.hints)]) + Box::new(vec![ + SourceDiagnostic::error(span, diags.message).with_hints(diags.hints) + ]) }) } } diff --git a/crates/typst/src/eval/mod.rs b/crates/typst/src/eval/mod.rs index f72eeaa4f..b4048e1bd 100644 --- a/crates/typst/src/eval/mod.rs +++ b/crates/typst/src/eval/mod.rs @@ -24,6 +24,7 @@ mod none; pub mod ops; mod scope; mod symbol; +mod tracer; #[doc(hidden)] pub use { @@ -53,6 +54,7 @@ pub use self::none::NoneValue; pub use self::scope::{Scope, Scopes}; pub use self::str::{format_str, Regex, Str}; pub use self::symbol::Symbol; +pub use self::tracer::Tracer; pub use self::value::{Dynamic, Type, Value}; use std::collections::HashSet; @@ -66,7 +68,8 @@ use unicode_segmentation::UnicodeSegmentation; use self::func::{CapturesVisitor, Closure}; use crate::diag::{ - bail, error, At, FileError, SourceError, SourceResult, StrResult, Trace, Tracepoint, + bail, error, warning, At, FileError, SourceDiagnostic, SourceResult, StrResult, + Trace, Tracepoint, }; use crate::model::{ Content, DelayedErrors, Introspector, Label, Locator, Recipe, ShowableSelector, @@ -292,7 +295,7 @@ pub enum FlowEvent { impl FlowEvent { /// Return an error stating that this control flow is forbidden. - pub fn forbidden(&self) -> SourceError { + pub fn forbidden(&self) -> SourceDiagnostic { match *self { Self::Break(span) => { error!(span, "cannot break outside of loop") @@ -351,47 +354,6 @@ impl<'a> Route<'a> { } } -/// Traces which values existed for an expression at a span. -#[derive(Default, Clone)] -pub struct Tracer { - span: Option, - values: Vec, -} - -impl Tracer { - /// The maximum number of traced items. - pub const MAX: usize = 10; - - /// Create a new tracer, possibly with a span under inspection. - pub fn new(span: Option) -> Self { - Self { span, values: vec![] } - } - - /// Get the traced values. - pub fn finish(self) -> Vec { - self.values - } -} - -#[comemo::track] -impl Tracer { - /// The traced span if it is part of the given source file. - fn span(&self, id: FileId) -> Option { - if self.span.map(Span::id) == Some(id) { - self.span - } else { - None - } - } - - /// Trace a value for the span. - fn trace(&mut self, v: Value) { - if self.values.len() < Self::MAX { - self.values.push(v); - } - } -} - /// Evaluate an expression. pub(super) trait Eval { /// The output of evaluating the expression. @@ -616,7 +578,18 @@ impl Eval for ast::Strong { #[tracing::instrument(name = "Strong::eval", skip_all)] fn eval(&self, vm: &mut Vm) -> SourceResult { - Ok((vm.items.strong)(self.body().eval(vm)?)) + let body = self.body(); + if body.exprs().next().is_none() { + vm.vt + .tracer + .warn(warning!(self.span(), "no text within stars").with_hint( + EcoString::from( + "using multiple consecutive stars (e.g. **) has no additional effect", + ), + )); + } + + Ok((vm.items.strong)(body.eval(vm)?)) } } diff --git a/crates/typst/src/eval/tracer.rs b/crates/typst/src/eval/tracer.rs new file mode 100644 index 000000000..0be6c1896 --- /dev/null +++ b/crates/typst/src/eval/tracer.rs @@ -0,0 +1,70 @@ +use std::collections::HashSet; + +use ecow::{eco_vec, EcoVec}; + +use super::Value; +use crate::diag::SourceDiagnostic; +use crate::syntax::{FileId, Span}; +use crate::util::hash128; + +/// Traces warnings and which values existed for an expression at a span. +#[derive(Default, Clone)] +pub struct Tracer { + span: Option, + values: EcoVec, + warnings: EcoVec, + warnings_set: HashSet, +} + +impl Tracer { + /// The maximum number of traced items. + pub const MAX: usize = 10; + + /// Create a new tracer, possibly with a span under inspection. + pub fn new(span: Option) -> Self { + Self { + span, + values: eco_vec![], + warnings: eco_vec![], + warnings_set: HashSet::new(), + } + } + + /// Get the traced values. + pub fn values(self) -> EcoVec { + self.values + } + + /// Get the stored warnings. + pub fn warnings(self) -> EcoVec { + self.warnings + } +} + +#[comemo::track] +impl Tracer { + /// The traced span if it is part of the given source file. + pub fn span(&self, id: FileId) -> Option { + if self.span.map(Span::id) == Some(id) { + self.span + } else { + None + } + } + + /// Trace a value for the span. + pub fn trace(&mut self, v: Value) { + if self.values.len() < Self::MAX { + self.values.push(v); + } + } + + /// Add a warning. + pub fn warn(&mut self, warning: SourceDiagnostic) { + // Check if warning is a duplicate. + let hash = hash128(&(&warning.span, &warning.message)); + if self.warnings_set.insert(hash) { + self.warnings.push(warning); + } + } +} diff --git a/crates/typst/src/ide/analyze.rs b/crates/typst/src/ide/analyze.rs index dad466c18..c143720af 100644 --- a/crates/typst/src/ide/analyze.rs +++ b/crates/typst/src/ide/analyze.rs @@ -1,5 +1,5 @@ use comemo::Track; -use ecow::EcoString; +use ecow::{eco_vec, EcoString, EcoVec}; use crate::doc::Frame; use crate::eval::{eval, Module, Route, Tracer, Value}; @@ -8,18 +8,18 @@ use crate::syntax::{ast, LinkedNode, Source, SyntaxKind}; use crate::World; /// Try to determine a set of possible values for an expression. -pub fn analyze_expr(world: &(dyn World + 'static), node: &LinkedNode) -> Vec { +pub fn analyze_expr(world: &dyn World, node: &LinkedNode) -> EcoVec { match node.cast::() { - Some(ast::Expr::None(_)) => vec![Value::None], - Some(ast::Expr::Auto(_)) => vec![Value::Auto], - Some(ast::Expr::Bool(v)) => vec![Value::Bool(v.get())], - Some(ast::Expr::Int(v)) => vec![Value::Int(v.get())], - Some(ast::Expr::Float(v)) => vec![Value::Float(v.get())], - Some(ast::Expr::Numeric(v)) => vec![Value::numeric(v.get())], - Some(ast::Expr::Str(v)) => vec![Value::Str(v.get().into())], + Some(ast::Expr::None(_)) => eco_vec![Value::None], + Some(ast::Expr::Auto(_)) => eco_vec![Value::Auto], + Some(ast::Expr::Bool(v)) => eco_vec![Value::Bool(v.get())], + Some(ast::Expr::Int(v)) => eco_vec![Value::Int(v.get())], + Some(ast::Expr::Float(v)) => eco_vec![Value::Float(v.get())], + Some(ast::Expr::Numeric(v)) => eco_vec![Value::numeric(v.get())], + Some(ast::Expr::Str(v)) => eco_vec![Value::Str(v.get().into())], Some(ast::Expr::FieldAccess(access)) => { - let Some(child) = node.children().next() else { return vec![] }; + let Some(child) = node.children().next() else { return eco_vec![] }; analyze_expr(world, &child) .into_iter() .filter_map(|target| target.field(&access.field()).ok()) @@ -33,36 +33,17 @@ pub fn analyze_expr(world: &(dyn World + 'static), node: &LinkedNode) -> Vec vec![], + _ => eco_vec![], } } /// Try to load a module from the current source file. -pub fn analyze_import( - world: &(dyn World + 'static), - source: &Source, - path: &str, -) -> Option { +pub fn analyze_import(world: &dyn World, source: &Source, path: &str) -> Option { let route = Route::default(); let mut tracer = Tracer::default(); let id = source.id().join(path).ok()?; @@ -77,7 +58,7 @@ pub fn analyze_import( /// - A split offset: All labels before this offset belong to nodes, all after /// belong to a bibliography. pub fn analyze_labels( - world: &(dyn World + 'static), + world: &dyn World, frames: &[Frame], ) -> (Vec<(Label, Option)>, usize) { let mut output = vec![]; diff --git a/crates/typst/src/lib.rs b/crates/typst/src/lib.rs index 861cb853f..3365bbaae 100644 --- a/crates/typst/src/lib.rs +++ b/crates/typst/src/lib.rs @@ -66,10 +66,9 @@ use crate::syntax::{FileId, PackageSpec, Source, Span}; use crate::util::Bytes; /// Compile a source file into a fully layouted document. -#[tracing::instrument(skip(world))] -pub fn compile(world: &dyn World) -> SourceResult { +#[tracing::instrument(skip_all)] +pub fn compile(world: &dyn World, tracer: &mut Tracer) -> SourceResult { let route = Route::default(); - let mut tracer = Tracer::default(); // Call `track` just once to keep comemo's ID stable. let world = world.track(); @@ -83,7 +82,7 @@ pub fn compile(world: &dyn World) -> SourceResult { &world.main(), )?; - // Typeset the module's contents. + // Typeset it. model::typeset(world, tracer, &module.content()) } diff --git a/crates/typst/src/model/mod.rs b/crates/typst/src/model/mod.rs index ee9402361..cab1f71f5 100644 --- a/crates/typst/src/model/mod.rs +++ b/crates/typst/src/model/mod.rs @@ -28,7 +28,7 @@ use std::mem::ManuallyDrop; use comemo::{Track, Tracked, TrackedMut, Validate}; -use crate::diag::{SourceError, SourceResult}; +use crate::diag::{SourceDiagnostic, SourceResult}; use crate::doc::Document; use crate::eval::Tracer; use crate::World; @@ -137,12 +137,12 @@ impl Vt<'_> { /// Holds delayed errors. #[derive(Default, Clone)] -pub struct DelayedErrors(Vec); +pub struct DelayedErrors(Vec); #[comemo::track] impl DelayedErrors { /// Push a delayed error. - fn push(&mut self, error: SourceError) { + fn push(&mut self, error: SourceDiagnostic) { self.0.push(error); } } diff --git a/tests/src/benches.rs b/tests/src/benches.rs index 8e70ffd73..524fda19b 100644 --- a/tests/src/benches.rs +++ b/tests/src/benches.rs @@ -1,7 +1,7 @@ use comemo::{Prehashed, Track, Tracked}; use iai::{black_box, main, Iai}; use typst::diag::FileResult; -use typst::eval::{Datetime, Library}; +use typst::eval::{Datetime, Library, Tracer}; use typst::font::{Font, FontBook}; use typst::geom::Color; use typst::syntax::{FileId, Source}; @@ -83,12 +83,14 @@ fn bench_typeset(iai: &mut Iai) { fn bench_compile(iai: &mut Iai) { let world = BenchWorld::new(); - iai.run(|| typst::compile(&world)); + let mut tracer = Tracer::default(); + iai.run(|| typst::compile(&world, &mut tracer)); } fn bench_render(iai: &mut Iai) { let world = BenchWorld::new(); - let document = typst::compile(&world).unwrap(); + let mut tracer = Tracer::default(); + let document = typst::compile(&world, &mut tracer).unwrap(); iai.run(|| typst::export::render(&document.pages[0], 1.0, Color::WHITE)) } diff --git a/tests/src/tests.rs b/tests/src/tests.rs index ee9ae9f95..a8cf25f73 100644 --- a/tests/src/tests.rs +++ b/tests/src/tests.rs @@ -20,9 +20,9 @@ use tiny_skia as sk; use unscanny::Scanner; use walkdir::WalkDir; -use typst::diag::{bail, FileError, FileResult, StrResult}; +use typst::diag::{bail, FileError, FileResult, Severity, StrResult}; use typst::doc::{Document, Frame, FrameItem, Meta}; -use typst::eval::{eco_format, func, Datetime, Library, NoneValue, Value}; +use typst::eval::{eco_format, func, Datetime, Library, NoneValue, Tracer, Value}; use typst::font::{Font, FontBook}; use typst::geom::{Abs, Color, RgbaColor, Smart}; use typst::syntax::{FileId, Source, Span, SyntaxNode}; @@ -514,51 +514,63 @@ fn test_part( let world = (world as &dyn World).track(); let route = typst::eval::Route::default(); let mut tracer = typst::eval::Tracer::default(); + let module = typst::eval::eval(world, route.track(), tracer.track_mut(), &source).unwrap(); writeln!(output, "Model:\n{:#?}\n", module.content()).unwrap(); } - let (mut frames, errors) = match typst::compile(world) { - Ok(document) => (document.pages, vec![]), - Err(errors) => (vec![], *errors), + let mut tracer = Tracer::default(); + + let (mut frames, diagnostics) = match typst::compile(world, &mut tracer) { + Ok(document) => (document.pages, tracer.warnings()), + Err(errors) => { + let mut warnings = tracer.warnings(); + warnings.extend(*errors); + (vec![], warnings) + } }; - // Don't retain frames if we don't wanna compare with reference images. + // Don't retain frames if we don't want to compare with reference images. if !compare_ref { frames.clear(); } - // Map errors to range and message format, discard traces and errors from + // Map diagnostics to range and message format, discard traces and errors from // other files, collect hints. // // This has one caveat: due to the format of the expected hints, we can not - // verify if a hint belongs to a error or not. That should be irrelevant + // verify if a hint belongs to a diagnostic or not. That should be irrelevant // however, as the line of the hint is still verified. - let actual_errors_and_hints: HashSet = errors + let actual_diagnostics: HashSet = diagnostics .into_iter() - .inspect(|error| assert!(!error.span.is_detached())) - .filter(|error| error.span.id() == source.id()) - .flat_map(|error| { - let range = world.range(error.span); - let output_error = - UserOutput::Error(range.clone(), error.message.replace('\\', "/")); - let hints = error + .inspect(|diagnostic| assert!(!diagnostic.span.is_detached())) + .filter(|diagnostic| diagnostic.span.id() == source.id()) + .flat_map(|diagnostic| { + let range = world.range(diagnostic.span); + let message = diagnostic.message.replace('\\', "/"); + let output = match diagnostic.severity { + Severity::Error => UserOutput::Error(range.clone(), message), + Severity::Warning => UserOutput::Warning(range.clone(), message), + }; + + let hints = diagnostic .hints .iter() .filter(|_| validate_hints) // No unexpected hints should be verified if disabled. .map(|hint| UserOutput::Hint(range.clone(), hint.to_string())); - iter::once(output_error).chain(hints).collect::>() + + iter::once(output).chain(hints).collect::>() }) .collect(); // Basically symmetric_difference, but we need to know where an item is coming from. - let mut unexpected_outputs = actual_errors_and_hints + let mut unexpected_outputs = actual_diagnostics .difference(&metadata.invariants) .collect::>(); let mut missing_outputs = metadata .invariants - .difference(&actual_errors_and_hints) + .difference(&actual_diagnostics) .collect::>(); unexpected_outputs.sort_by_key(|&o| o.start()); @@ -592,6 +604,7 @@ fn print_user_output( ) { let (range, message) = match &user_output { UserOutput::Error(r, m) => (r, m), + UserOutput::Warning(r, m) => (r, m), UserOutput::Hint(r, m) => (r, m), }; @@ -601,6 +614,7 @@ fn print_user_output( let end_col = 1 + source.byte_to_column(range.end).unwrap(); let kind = match user_output { UserOutput::Error(_, _) => "Error", + UserOutput::Warning(_, _) => "Warning", UserOutput::Hint(_, _) => "Hint", }; writeln!(output, "{kind}: {start_line}:{start_col}-{end_line}:{end_col}: {message}") @@ -620,6 +634,7 @@ struct TestPartMetadata { #[derive(PartialEq, Eq, Debug, Hash)] enum UserOutput { Error(Range, String), + Warning(Range, String), Hint(Range, String), } @@ -627,6 +642,7 @@ impl UserOutput { fn start(&self) -> usize { match self { UserOutput::Error(r, _) => r.start, + UserOutput::Warning(r, _) => r.start, UserOutput::Hint(r, _) => r.start, } } @@ -635,6 +651,10 @@ impl UserOutput { UserOutput::Error(range, message) } + fn warning(range: Range, message: String) -> UserOutput { + UserOutput::Warning(range, message) + } + fn hint(range: Range, message: String) -> UserOutput { UserOutput::Hint(range, message) } @@ -666,12 +686,18 @@ fn parse_part_metadata(source: &Source) -> TestPartMetadata { }; let error_factory: fn(Range, String) -> UserOutput = UserOutput::error; + let warning_factory: fn(Range, String) -> UserOutput = UserOutput::warning; let hint_factory: fn(Range, String) -> UserOutput = UserOutput::hint; let error_metadata = get_metadata(line, "Error").map(|s| (s, error_factory)); + let get_warning_metadata = + || get_metadata(line, "Warning").map(|s| (s, warning_factory)); let get_hint_metadata = || get_metadata(line, "Hint").map(|s| (s, hint_factory)); - if let Some((expectation, factory)) = error_metadata.or_else(get_hint_metadata) { + if let Some((expectation, factory)) = error_metadata + .or_else(get_warning_metadata) + .or_else(get_hint_metadata) + { let mut s = Scanner::new(expectation); let start = pos(&mut s); let end = if s.eat_if('-') { pos(&mut s) } else { start }; diff --git a/tests/typ/lint/markup.typ b/tests/typ/lint/markup.typ new file mode 100644 index 000000000..1cb446589 --- /dev/null +++ b/tests/typ/lint/markup.typ @@ -0,0 +1,13 @@ +/// Test markup lints. +// Ref: false + +--- +// Warning: 1-3 no text within stars +// Hint: 1-3 using multiple consecutive stars (e.g. **) has no additional effect +** +--- +// Warning: 1-3 no text within stars +// Hint: 1-3 using multiple consecutive stars (e.g. **) has no additional effect +// Warning: 11-13 no text within stars +// Hint: 11-13 using multiple consecutive stars (e.g. **) has no additional effect +**not bold**