Add infrastructure for compiler warnings (#1731)

This commit is contained in:
lolstork 2023-07-19 12:52:47 +02:00 committed by GitHub
parent 8a57395ee4
commit b37c1e2731
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 279 additions and 145 deletions

View file

@ -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<SourceError>,
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)),

View file

@ -138,6 +138,7 @@ fn is_event_relevant(event: &notify::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,
}
}

View file

@ -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;

View file

@ -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<T> = Result<T, Box<Vec<SourceError>>>;
pub type SourceResult<T> = Result<T, Box<Vec<SourceDiagnostic>>>;
/// 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<Spanned<Tracepoint>>,
/// 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<EcoString>,
}
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<EcoString>) -> Self {
pub fn error(span: Span, message: impl Into<EcoString>) -> 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<EcoString>) -> 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<Item = EcoString>) -> Self {
self.hints.extend(hints);
self
}
}
impl From<SyntaxError> for SourceError {
impl From<SyntaxError> 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<SyntaxError> 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<EcoString>,
{
fn at(self, span: Span) -> SourceResult<T> {
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<T> At<T> for Result<T, HintedString> {
fn at(self, span: Span) -> SourceResult<T> {
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)
])
})
}
}

View file

@ -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<Span>,
values: Vec<Value>,
}
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<Span>) -> Self {
Self { span, values: vec![] }
}
/// Get the traced values.
pub fn finish(self) -> Vec<Value> {
self.values
}
}
#[comemo::track]
impl Tracer {
/// The traced span if it is part of the given source file.
fn span(&self, id: FileId) -> Option<Span> {
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<Self::Output> {
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)?))
}
}

View file

@ -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<Span>,
values: EcoVec<Value>,
warnings: EcoVec<SourceDiagnostic>,
warnings_set: HashSet<u128>,
}
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<Span>) -> Self {
Self {
span,
values: eco_vec![],
warnings: eco_vec![],
warnings_set: HashSet::new(),
}
}
/// Get the traced values.
pub fn values(self) -> EcoVec<Value> {
self.values
}
/// Get the stored warnings.
pub fn warnings(self) -> EcoVec<SourceDiagnostic> {
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<Span> {
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);
}
}
}

View file

@ -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<Value> {
pub fn analyze_expr(world: &dyn World, node: &LinkedNode) -> EcoVec<Value> {
match node.cast::<ast::Expr>() {
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<Val
}
}
let route = Route::default();
let mut tracer = Tracer::new(Some(node.span()));
typst::eval::eval(
world.track(),
route.track(),
tracer.track_mut(),
&world.main(),
)
.and_then(|module| {
typst::model::typeset(
world.track(),
tracer.track_mut(),
&module.content(),
)
})
.ok();
tracer.finish()
crate::compile(world, &mut tracer).ok();
tracer.values()
}
_ => 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<Module> {
pub fn analyze_import(world: &dyn World, source: &Source, path: &str) -> Option<Module> {
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<EcoString>)>, usize) {
let mut output = vec![];

View file

@ -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<Document> {
#[tracing::instrument(skip_all)]
pub fn compile(world: &dyn World, tracer: &mut Tracer) -> SourceResult<Document> {
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<Document> {
&world.main(),
)?;
// Typeset the module's contents.
// Typeset it.
model::typeset(world, tracer, &module.content())
}

View file

@ -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<SourceError>);
pub struct DelayedErrors(Vec<SourceDiagnostic>);
#[comemo::track]
impl DelayedErrors {
/// Push a delayed error.
fn push(&mut self, error: SourceError) {
fn push(&mut self, error: SourceDiagnostic) {
self.0.push(error);
}
}

View file

@ -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))
}

View file

@ -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<UserOutput> = errors
let actual_diagnostics: HashSet<UserOutput> = 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::<Vec<_>>()
iter::once(output).chain(hints).collect::<Vec<_>>()
})
.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::<Vec<_>>();
let mut missing_outputs = metadata
.invariants
.difference(&actual_errors_and_hints)
.difference(&actual_diagnostics)
.collect::<Vec<_>>();
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<usize>, String),
Warning(Range<usize>, String),
Hint(Range<usize>, 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<usize>, message: String) -> UserOutput {
UserOutput::Warning(range, message)
}
fn hint(range: Range<usize>, message: String) -> UserOutput {
UserOutput::Hint(range, message)
}
@ -666,12 +686,18 @@ fn parse_part_metadata(source: &Source) -> TestPartMetadata {
};
let error_factory: fn(Range<usize>, String) -> UserOutput = UserOutput::error;
let warning_factory: fn(Range<usize>, String) -> UserOutput = UserOutput::warning;
let hint_factory: fn(Range<usize>, 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 };

13
tests/typ/lint/markup.typ Normal file
View file

@ -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**