mirror of
https://github.com/typst/typst
synced 2025-05-21 20:45:27 +08:00
Add infrastructure for compiler warnings (#1731)
This commit is contained in:
parent
8a57395ee4
commit
b37c1e2731
@ -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)),
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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)
|
||||
])
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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)?))
|
||||
}
|
||||
}
|
||||
|
||||
|
70
crates/typst/src/eval/tracer.rs
Normal file
70
crates/typst/src/eval/tracer.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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![];
|
||||
|
@ -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())
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
|
||||
|
@ -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
13
tests/typ/lint/markup.typ
Normal 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**
|
Loading…
x
Reference in New Issue
Block a user