summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--crates/typst-syntax/src/ast.rs62
-rw-r--r--crates/typst-syntax/src/highlight.rs4
-rw-r--r--crates/typst-syntax/src/kind.rs16
-rw-r--r--crates/typst-syntax/src/lexer.rs230
-rw-r--r--crates/typst-syntax/src/node.rs59
-rw-r--r--crates/typst-syntax/src/parser.rs68
-rw-r--r--crates/typst-syntax/src/set.rs1
-rw-r--r--crates/typst/src/diag.rs186
-rw-r--r--crates/typst/src/engine.rs87
-rw-r--r--crates/typst/src/eval/call.rs7
-rw-r--r--crates/typst/src/eval/import.rs6
-rw-r--r--crates/typst/src/eval/markup.rs8
-rw-r--r--crates/typst/src/foundations/styles.rs4
-rw-r--r--crates/typst/src/introspection/counter.rs8
-rw-r--r--crates/typst/src/introspection/locate.rs4
-rw-r--r--crates/typst/src/introspection/query.rs4
-rw-r--r--crates/typst/src/introspection/state.rs8
-rw-r--r--crates/typst/src/layout/measure.rs4
-rw-r--r--crates/typst/src/lib.rs21
-rw-r--r--crates/typst/src/text/mod.rs3
-rw-r--r--tests/suite/syntax/annotation.typ185
21 files changed, 863 insertions, 112 deletions
diff --git a/crates/typst-syntax/src/ast.rs b/crates/typst-syntax/src/ast.rs
index 1cd9cd42..c66c6b2f 100644
--- a/crates/typst-syntax/src/ast.rs
+++ b/crates/typst-syntax/src/ast.rs
@@ -80,6 +80,68 @@ impl<'a> Markup<'a> {
}
}
+node! {
+ AnnotationName
+}
+
+impl<'a> AnnotationName<'a> {
+ /// Get the annotation name.
+ pub fn get(self) -> &'a EcoString {
+ self.0.text()
+ }
+
+ /// Get the annotation name as a string slice.
+ pub fn as_str(self) -> &'a str {
+ self.get()
+ }
+}
+
+node! {
+ /// An annotation: `// @allow("warning")`.
+ Annotation
+}
+
+impl<'a> Annotation<'a> {
+ /// The name of the annotation, e.g. `allow`.
+ pub fn name(self) -> AnnotationName<'a> {
+ self.0.cast_first_match().unwrap_or_default()
+ }
+
+ /// The annotation's arguments, which are always either identifiers or
+ /// strings.
+ pub fn arguments(self) -> impl DoubleEndedIterator<Item = AnnotationArg<'a>> {
+ self.0.children().filter_map(AnnotationArg::from_untyped)
+ }
+}
+
+/// An annotation argument, which always corresponds to a simple string, which,
+/// however, may be abbreviated and specified as a valid identifier directly.
+pub enum AnnotationArg<'a> {
+ /// An identifier specified directly, without quotes.
+ Ident(Ident<'a>),
+ /// A string specified with double quotes.
+ Str(Str<'a>),
+}
+
+impl<'a> AnnotationArg<'a> {
+ /// Casts an untyped node to an annotation argument, if possible.
+ pub fn from_untyped(node: &'a SyntaxNode) -> Option<Self> {
+ match node.kind() {
+ SyntaxKind::Ident => node.cast().map(Self::Ident),
+ SyntaxKind::Str => node.cast().map(Self::Str),
+ _ => Option::None,
+ }
+ }
+
+ /// Gets the text specified for this argument.
+ pub fn get(self) -> EcoString {
+ match self {
+ Self::Ident(ident) => ident.get().clone(),
+ Self::Str(str) => str.get(),
+ }
+ }
+}
+
/// An expression in markup, math or code.
#[derive(Debug, Copy, Clone, Hash)]
pub enum Expr<'a> {
diff --git a/crates/typst-syntax/src/highlight.rs b/crates/typst-syntax/src/highlight.rs
index 0c1f3d5f..d825e240 100644
--- a/crates/typst-syntax/src/highlight.rs
+++ b/crates/typst-syntax/src/highlight.rs
@@ -286,6 +286,10 @@ pub fn highlight(node: &LinkedNode) -> Option<Tag> {
SyntaxKind::Destructuring => None,
SyntaxKind::DestructAssignment => None,
+ SyntaxKind::Annotation => None,
+ SyntaxKind::AnnotationMarker => None,
+ SyntaxKind::AnnotationName => None,
+
SyntaxKind::LineComment => Some(Tag::Comment),
SyntaxKind::BlockComment => Some(Tag::Comment),
SyntaxKind::Error => Some(Tag::Error),
diff --git a/crates/typst-syntax/src/kind.rs b/crates/typst-syntax/src/kind.rs
index 7505dbc6..9597f2a2 100644
--- a/crates/typst-syntax/src/kind.rs
+++ b/crates/typst-syntax/src/kind.rs
@@ -13,6 +13,8 @@ pub enum SyntaxKind {
LineComment,
/// A block comment: `/* ... */`.
BlockComment,
+ /// An annotation: `// @allow("warning")`.
+ Annotation,
/// The contents of a file or content block.
Markup,
@@ -278,6 +280,11 @@ pub enum SyntaxKind {
Destructuring,
/// A destructuring assignment expression: `(x, y) = (1, 2)`.
DestructAssignment,
+
+ /// An annotation's marker: `// @`.
+ AnnotationMarker,
+ /// An annotation's name: `allow`.
+ AnnotationName,
}
impl SyntaxKind {
@@ -355,7 +362,11 @@ impl SyntaxKind {
pub fn is_trivia(self) -> bool {
matches!(
self,
- Self::LineComment | Self::BlockComment | Self::Space | Self::Parbreak
+ Self::LineComment
+ | Self::BlockComment
+ | Self::Space
+ | Self::Parbreak
+ | Self::Annotation
)
}
@@ -371,6 +382,7 @@ impl SyntaxKind {
Self::Error => "syntax error",
Self::LineComment => "line comment",
Self::BlockComment => "block comment",
+ Self::Annotation => "annotation",
Self::Markup => "markup",
Self::Text => "text",
Self::Space => "space",
@@ -498,6 +510,8 @@ impl SyntaxKind {
Self::FuncReturn => "`return` expression",
Self::Destructuring => "destructuring pattern",
Self::DestructAssignment => "destructuring assignment expression",
+ Self::AnnotationMarker => "annotation marker",
+ Self::AnnotationName => "annotation name",
}
}
}
diff --git a/crates/typst-syntax/src/lexer.rs b/crates/typst-syntax/src/lexer.rs
index 993af080..ecbf0ea6 100644
--- a/crates/typst-syntax/src/lexer.rs
+++ b/crates/typst-syntax/src/lexer.rs
@@ -4,7 +4,7 @@ use unicode_script::{Script, UnicodeScript};
use unicode_segmentation::UnicodeSegmentation;
use unscanny::Scanner;
-use crate::{SyntaxError, SyntaxKind};
+use crate::{SyntaxError, SyntaxKind, SyntaxNode};
/// Splits up a string of source code into tokens.
#[derive(Clone)]
@@ -75,7 +75,7 @@ impl<'s> Lexer<'s> {
}
/// Take out the last error, if any.
- pub fn take_error(&mut self) -> Option<SyntaxError> {
+ fn take_error(&mut self) -> Option<SyntaxError> {
self.error.take()
}
}
@@ -97,23 +97,31 @@ impl Lexer<'_> {
/// Shared methods with all [`LexMode`].
impl Lexer<'_> {
- /// Proceed to the next token and return its [`SyntaxKind`]. Note the
- /// token could be a [trivia](SyntaxKind::is_trivia).
- pub fn next(&mut self) -> SyntaxKind {
+ /// Proceed to the next token and return a [`SyntaxNode`] containing it.
+ ///
+ /// Note the token could be a [trivia](SyntaxKind::is_trivia). Also, the
+ /// syntax node returned might not always be a leaf, but could actually
+ /// come with a subtree (could be an inner node). This happens when it is
+ /// preferred to perform parsing at the character level instead of at the
+ /// token level, as seen, for example, in [`annotation`](Lexer::annotation).
+ pub fn next(&mut self) -> SyntaxNode {
if self.mode == LexMode::Raw {
let Some((kind, end)) = self.raw.pop() else {
- return SyntaxKind::End;
+ return SyntaxNode::end();
};
+ let start = self.s.cursor();
self.s.jump(end);
- return kind;
+ return self.emit_token(kind, start);
}
self.newline = false;
self.error = None;
let start = self.s.cursor();
- match self.s.eat() {
+ let token = match self.s.eat() {
Some(c) if is_space(c, self.mode) => self.whitespace(start, c),
- Some('/') if self.s.eat_if('/') => self.line_comment(),
+ Some('/') if self.s.eat_if('/') => {
+ return self.line_comment_or_annotation(start);
+ }
Some('/') if self.s.eat_if('*') => self.block_comment(),
Some('*') if self.s.eat_if('/') => {
let kind = self.error("unexpected end of block comment");
@@ -123,7 +131,6 @@ impl Lexer<'_> {
);
kind
}
-
Some(c) => match self.mode {
LexMode::Markup => self.markup(start, c),
LexMode::Math => self.math(start, c),
@@ -132,6 +139,33 @@ impl Lexer<'_> {
},
None => SyntaxKind::End,
+ };
+
+ self.emit_token(token, start)
+ }
+
+ /// Constructs an error node with the given message.
+ ///
+ /// The node's inner text is taken from the given start position up to and
+ /// including the current cursor position.
+ fn emit_error(&self, message: impl Into<EcoString>, start: usize) -> SyntaxNode {
+ let text = self.s.from(start);
+ SyntaxNode::error(SyntaxError::new(message), text)
+ }
+
+ /// Converts a token into a syntax node based on its kind. Produces an
+ /// error node if there are errors.
+ ///
+ /// The node's inner text is taken from the given start position up to and
+ /// including the current cursor position.
+ fn emit_token(&mut self, kind: SyntaxKind, start: usize) -> SyntaxNode {
+ let text = self.s.from(start);
+ if kind == SyntaxKind::End {
+ SyntaxNode::end()
+ } else if let Some(error) = self.take_error() {
+ SyntaxNode::error(error, text)
+ } else {
+ SyntaxNode::leaf(kind, text)
}
}
@@ -151,9 +185,17 @@ impl Lexer<'_> {
}
}
- fn line_comment(&mut self) -> SyntaxKind {
+ /// Parses an annotation if the line comment has the form
+ /// `// @something`
+ ///
+ /// Otherwise, parses a regular line comment.
+ fn line_comment_or_annotation(&mut self, start: usize) -> SyntaxNode {
+ self.s.eat_while(is_inline_whitespace);
+ if self.s.eat_if('@') {
+ return self.annotation(start);
+ }
self.s.eat_until(is_newline);
- SyntaxKind::LineComment
+ self.emit_token(SyntaxKind::LineComment, start)
}
fn block_comment(&mut self) -> SyntaxKind {
@@ -182,6 +224,139 @@ impl Lexer<'_> {
}
}
+/// Annotation lexing and auxiliary methods.
+impl Lexer<'_> {
+ /// Lexes and parses an annotation into a complete syntax subtree.
+ ///
+ /// The lexer is fully responsible for the annotation, as it is simpler to
+ /// parse them at the character level, given they follow a very simple
+ /// and rigid structure, in the form
+ /// `// @annotation-name("string argument1", "string argument2")`
+ /// with optional whitespaces and comments between arguments.
+ fn annotation(&mut self, start: usize) -> SyntaxNode {
+ // Start by lexing the marker.
+ let marker = self.emit_token(SyntaxKind::AnnotationMarker, start);
+ let mut subtree = vec![marker];
+
+ let current_start = self.s.cursor();
+
+ // Ignore initial non-newline whitespaces.
+ if !self.s.eat_while(is_inline_whitespace).is_empty() {
+ subtree.push(self.emit_token(SyntaxKind::Space, current_start));
+ }
+
+ // Lex the annotation name.
+ let current_start = self.s.cursor();
+ if !self.s.eat_if(is_id_start) {
+ self.s.eat_until(is_newline);
+ subtree.push(self.emit_error("expected identifier", current_start));
+
+ // Return a single error node until the end of the annotation.
+ return SyntaxNode::inner(SyntaxKind::Annotation, subtree);
+ }
+
+ let name = self.annotation_name(current_start);
+ subtree.push(self.emit_token(name, current_start));
+
+ // Optional left parenthesis before annotation arguments.
+ let current_start = self.s.cursor();
+ let has_opening_paren = self.s.eat_if('(');
+
+ if has_opening_paren {
+ subtree.push(self.emit_token(SyntaxKind::LeftParen, current_start));
+ }
+
+ // Annotation arguments:
+ // Keep reading until we find a right parenthesis (if we got a left
+ // parenthesis) or newline. We have to check the newline before eating
+ // (through '.peek()') to ensure it is not considered part of the
+ // annotation.
+ let mut current_start = self.s.cursor();
+ let mut found_closing_paren = false;
+ while !self.s.at(is_newline) {
+ let token = match self.s.eat() {
+ Some(c) if c.is_whitespace() => {
+ self.s.eat_while(is_inline_whitespace);
+ SyntaxKind::Space
+ }
+ Some(_) if found_closing_paren => {
+ // After we finished specifying arguments, there must only
+ // be whitespaces until the line ends.
+ self.s.eat_until(char::is_whitespace);
+ self.error("expected end of annotation")
+ }
+ Some(c) if is_id_start(c) => {
+ self.s.eat_while(is_id_continue);
+ SyntaxKind::Ident
+ }
+ Some('"') => self.annotation_string(),
+ Some(')') if has_opening_paren => {
+ found_closing_paren = true;
+ SyntaxKind::RightParen
+ }
+ Some(c) => self.error(eco_format!(
+ "the character '{c}' is not valid in an annotation"
+ )),
+ None => break,
+ };
+
+ let node = self.emit_token(token, current_start);
+ subtree.push(node);
+
+ current_start = self.s.cursor();
+ }
+
+ // Right parenthesis (covered above)
+ if has_opening_paren && !found_closing_paren {
+ subtree.push(self.emit_error("expected closing paren", self.s.cursor()));
+ }
+
+ SyntaxNode::inner(SyntaxKind::Annotation, subtree)
+ }
+
+ /// Lexes an annotation name.
+ ///
+ /// An annotation name is an identifier within a specific subset of allowed
+ /// identifiers. Currently, `allow` is the only valid annotation name.
+ fn annotation_name(&mut self, start: usize) -> SyntaxKind {
+ self.s.eat_while(is_id_continue);
+ let ident = self.s.from(start);
+
+ if ident == "allow" {
+ SyntaxKind::AnnotationName
+ } else {
+ let error = self.error(eco_format!("invalid annotation name"));
+ self.hint("must be 'allow'");
+ error
+ }
+ }
+
+ /// Lexes a string in an annotation.
+ ///
+ /// Currently, such strings only allow a very restricted set of characters.
+ /// These restrictions may be lifted in the future.
+ fn annotation_string(&mut self) -> SyntaxKind {
+ // TODO: Allow more characters in annotations' strings, perhaps allowing
+ // newlines somehow.
+ // Could perhaps use one // per line so we can break an annotation into
+ // multiple lines in a sensible way.
+ let start = self.s.cursor();
+ self.s.eat_while(|c| !is_newline(c) && c != '"');
+
+ let content = self.s.from(start);
+ if !self.s.eat_if('"') {
+ return self.error("unclosed string");
+ }
+
+ if let Some(c) = content.chars().find(|c| !is_valid_in_annotation_string(*c)) {
+ return self
+ .error(eco_format!("invalid character '{c}' in an annotation's string"));
+ }
+
+ SyntaxKind::Str
+ }
+}
+
/// Markup.
impl Lexer<'_> {
fn markup(&mut self, start: usize, c: char) -> SyntaxKind {
@@ -805,6 +980,13 @@ fn is_space(character: char, mode: LexMode) -> bool {
}
}
+/// Whether a character is a whitespace but not interpreted as a newline by
+/// Typst.
+#[inline]
+pub fn is_inline_whitespace(character: char) -> bool {
+ character.is_whitespace() && !is_newline(character)
+}
+
/// Whether a character is interpreted as a newline by Typst.
#[inline]
pub fn is_newline(character: char) -> bool {
@@ -892,6 +1074,24 @@ fn count_newlines(text: &str) -> usize {
newlines
}
+/// Count newlines in text. Only counts up to 2 newlines.
+pub(crate) fn count_capped_newlines(text: &str) -> u8 {
+ let mut newlines = 0;
+ let mut s = Scanner::new(text);
+ while let Some(c) = s.eat() {
+ if is_newline(c) {
+ if c == '\r' {
+ s.eat_if('\n');
+ }
+ newlines += 1;
+ if newlines == 2 {
+ break;
+ }
+ }
+ }
+ newlines
+}
+
/// Whether a string is a valid Typst identifier.
///
/// In addition to what is specified in the [Unicode Standard][uax31], we allow:
@@ -937,6 +1137,12 @@ fn is_valid_in_label_literal(c: char) -> bool {
is_id_continue(c) || matches!(c, ':' | '.')
}
+/// Whether a character can be part of a string in an annotation.
+#[inline]
+fn is_valid_in_annotation_string(c: char) -> bool {
+ is_id_continue(c) || c == '@' || c == '/'
+}
+
/// Returns true if this string is valid in a label literal.
pub fn is_valid_label_literal_id(id: &str) -> bool {
!id.is_empty() && id.chars().all(is_valid_in_label_literal)
diff --git a/crates/typst-syntax/src/node.rs b/crates/typst-syntax/src/node.rs
index bc378e66..9a55ca5c 100644
--- a/crates/typst-syntax/src/node.rs
+++ b/crates/typst-syntax/src/node.rs
@@ -39,6 +39,11 @@ impl SyntaxNode {
Self(Repr::Error(Arc::new(ErrorNode::new(error, text))))
}
+ /// Create a new end node. It is only used to terminate the token stream.
+ pub const fn end() -> Self {
+ Self::placeholder(SyntaxKind::End)
+ }
+
/// Create a dummy node of the given kind.
///
/// Panics if `kind` is `SyntaxKind::Error`.
@@ -148,6 +153,28 @@ impl SyntaxNode {
}
}
+ /// The amount of newlines in this node or its descendants, capped at 2,
+ /// that is, a return value of 2 means that the total amount of newlines
+ /// may be 2 or larger.
+ pub fn capped_newlines(&self) -> u8 {
+ match &self.0 {
+ Repr::Leaf(_) | Repr::Error(_) => {
+ crate::lexer::count_capped_newlines(self.text())
+ }
+ Repr::Inner(inner) => {
+ let mut newlines = 0;
+ for child in &inner.children {
+ newlines += child.capped_newlines();
+ if newlines >= 2 {
+ break;
+ }
+ }
+
+ newlines.min(2)
+ }
+ }
+ }
+
/// The error messages for this node and its descendants.
pub fn errors(&self) -> Vec<SyntaxError> {
if !self.erroneous() {
@@ -773,13 +800,17 @@ impl<'a> LinkedNode<'a> {
self.parent.as_deref()
}
- /// Get the first previous non-trivia sibling node.
- pub fn prev_sibling(&self) -> Option<Self> {
+ fn prev_sibling_inner(&self) -> Option<Self> {
let parent = self.parent()?;
let index = self.index.checked_sub(1)?;
let node = parent.node.children().nth(index)?;
let offset = self.offset - node.len();
- let prev = Self { node, parent: self.parent.clone(), index, offset };
+ Some(Self { node, parent: self.parent.clone(), index, offset })
+ }
+
+ /// Get the first previous non-trivia sibling node.
+ pub fn prev_sibling(&self) -> Option<Self> {
+ let prev = self.prev_sibling_inner()?;
if prev.kind().is_trivia() {
prev.prev_sibling()
} else {
@@ -787,6 +818,28 @@ impl<'a> LinkedNode<'a> {
}
}
+ /// Get the first sibling annotation node at the line above this node.
+ /// This is done by moving backwards, checking for annotations, until we hit
+ /// a second newline (that is, we only check, at most, the line before this
+ /// node).
+ pub fn prev_attached_annotation(&self) -> Option<Self> {
+ let mut cursor = self.prev_sibling_inner()?;
+ let mut newlines = cursor.capped_newlines();
+ while newlines < 2 {
+ if cursor.kind() == SyntaxKind::Annotation {
+ return Some(cursor);
+ }
+
+ cursor = cursor.prev_sibling_inner()?;
+ newlines += cursor.capped_newlines();
+ }
+
+ // Annotations are attached if they're in the previous line.
+ // If we counted at least two newlines, no annotations are attached to
+ // this node.
+ None
+ }
+
/// Get the next non-trivia sibling node.
pub fn next_sibling(&self) -> Option<Self> {
let parent = self.parent()?;
diff --git a/crates/typst-syntax/src/parser.rs b/crates/typst-syntax/src/parser.rs
index ff01bacf..62904fc7 100644
--- a/crates/typst-syntax/src/parser.rs
+++ b/crates/typst-syntax/src/parser.rs
@@ -108,7 +108,8 @@ fn markup_expr(p: &mut Parser, at_start: &mut bool) {
SyntaxKind::Space
| SyntaxKind::Parbreak
| SyntaxKind::LineComment
- | SyntaxKind::BlockComment => {
+ | SyntaxKind::BlockComment
+ | SyntaxKind::Annotation => {
p.eat();
return;
}
@@ -202,7 +203,8 @@ fn heading(p: &mut Parser) {
whitespace_line(p);
markup(p, false, usize::MAX, |p| {
p.at_set(END)
- && (!p.at(SyntaxKind::Space) || p.lexer.clone().next() == SyntaxKind::Label)
+ && (!p.at(SyntaxKind::Space)
+ || p.lexer.clone().next().kind() == SyntaxKind::Label)
});
p.wrap(m, SyntaxKind::Heading);
}
@@ -304,7 +306,7 @@ fn math_expr_prec(p: &mut Parser, min_prec: usize, stop: SyntaxKind) {
let start = copy.cursor();
let next = copy.next();
let end = copy.cursor();
- matches!(next, SyntaxKind::MathIdent | SyntaxKind::Text)
+ matches!(next.kind(), SyntaxKind::MathIdent | SyntaxKind::Text)
&& is_ident(&p.text[start..end])
} {
p.convert(SyntaxKind::Dot);
@@ -696,8 +698,8 @@ fn code_expr_prec(p: &mut Parser, atomic: bool, min_prec: usize) {
continue;
}
- let at_field_or_method =
- p.directly_at(SyntaxKind::Dot) && p.lexer.clone().next() == SyntaxKind::Ident;
+ let at_field_or_method = p.directly_at(SyntaxKind::Dot)
+ && p.lexer.clone().next().kind() == SyntaxKind::Ident;
if atomic && !at_field_or_method {
break;
@@ -1497,7 +1499,7 @@ struct Parser<'s> {
lexer: Lexer<'s>,
prev_end: usize,
current_start: usize,
- current: SyntaxKind,
+ current: SyntaxNode,
balanced: bool,
nodes: Vec<SyntaxNode>,
modes: Vec<LexMode>,
@@ -1525,7 +1527,7 @@ struct Checkpoint<'s> {
lexer: Lexer<'s>,
prev_end: usize,
current_start: usize,
- current: SyntaxKind,
+ current: SyntaxNode,
nodes: usize,
}
@@ -1558,7 +1560,7 @@ impl<'s> Parser<'s> {
}
fn current(&self) -> SyntaxKind {
- self.current
+ self.current.kind()
}
fn current_start(&self) -> usize {
@@ -1574,11 +1576,11 @@ impl<'s> Parser<'s> {
}
fn at(&self, kind: SyntaxKind) -> bool {
- self.current == kind
+ self.current.kind() == kind
}
fn at_set(&self, set: SyntaxSet) -> bool {
- set.contains(self.current)
+ set.contains(self.current.kind())
}
fn end(&self) -> bool {
@@ -1586,20 +1588,18 @@ impl<'s> Parser<'s> {
}
fn directly_at(&self, kind: SyntaxKind) -> bool {
- self.current == kind && self.prev_end == self.current_start
+ self.current.kind() == kind && self.prev_end == self.current_start
}
fn eat(&mut self) {
- self.save();
- self.lex();
+ self.save_and_lex();
self.skip();
}
#[track_caller]
fn eat_and_get(&mut self) -> &mut SyntaxNode {
let offset = self.nodes.len();
- self.save();
- self.lex();
+ self.save_and_lex();
self.skip();
&mut self.nodes[offset]
}
@@ -1627,12 +1627,12 @@ impl<'s> Parser<'s> {
#[track_caller]
fn assert(&mut self, kind: SyntaxKind) {
- assert_eq!(self.current, kind);
+ assert_eq!(self.current(), kind);
self.eat();
}
fn convert(&mut self, kind: SyntaxKind) {
- self.current = kind;
+ self.current.convert_to_kind(kind);
self.eat();
}
@@ -1721,7 +1721,7 @@ impl<'s> Parser<'s> {
lexer: self.lexer.clone(),
prev_end: self.prev_end,
current_start: self.current_start,
- current: self.current,
+ current: self.current.clone(),
nodes: self.nodes.len(),
}
}
@@ -1736,9 +1736,8 @@ impl<'s> Parser<'s> {
fn skip(&mut self) {
if self.lexer.mode() != LexMode::Markup {
- while self.current.is_trivia() {
- self.save();
- self.lex();
+ while self.current().is_trivia() {
+ self.save_and_lex();
}
}
}
@@ -1754,26 +1753,25 @@ impl<'s> Parser<'s> {
}
}
- fn save(&mut self) {
- let text = self.current_text();
- if self.at(SyntaxKind::Error) {
- let error = self.lexer.take_error().unwrap();
- self.nodes.push(SyntaxNode::error(error, text));
- } else {
- self.nodes.push(SyntaxNode::leaf(self.current, text));
- }
+ fn save_and_lex(&mut self) {
+ // Replace 'current' with a placeholder node until we lex.
+ let current = std::mem::replace(&mut self.current, SyntaxNode::end());
- if self.lexer.mode() == LexMode::Markup || !self.current.is_trivia() {
+ if self.lexer.mode() == LexMode::Markup || !current.kind().is_trivia() {
self.prev_end = self.current_end();
}
+
+ self.nodes.push(current);
+
+ self.lex();
}
fn next_non_trivia(lexer: &mut Lexer<'s>) -> SyntaxKind {
loop {
let next = lexer.next();
// Loop is terminatable, because SyntaxKind::End is not a trivia.
- if !next.is_trivia() {
- break next;
+ if !next.kind().is_trivia() {
+ break next.kind();
}
}
}
@@ -1795,7 +1793,7 @@ impl<'s> Parser<'s> {
None => false,
}
{
- self.current = SyntaxKind::End;
+ self.current = SyntaxNode::end();
}
}
}
@@ -1806,7 +1804,7 @@ impl<'s> Parser<'s> {
let at = self.at(kind);
if at {
self.eat();
- } else if kind == SyntaxKind::Ident && self.current.is_keyword() {
+ } else if kind == SyntaxKind::Ident && self.current().is_keyword() {
self.trim_errors();
self.eat_and_get().expected(kind.name());
} else {
@@ -1852,7 +1850,7 @@ impl<'s> Parser<'s> {
/// unexpected.
fn unexpected(&mut self) {
self.trim_errors();
- self.balanced &= !self.current.is_grouping();
+ self.balanced &= !self.current().is_grouping();
self.eat_and_get().unexpected();
}
diff --git a/crates/typst-syntax/src/set.rs b/crates/typst-syntax/src/set.rs
index 39e64651..55f71038 100644
--- a/crates/typst-syntax/src/set.rs
+++ b/crates/typst-syntax/src/set.rs
@@ -54,6 +54,7 @@ pub const MARKUP_EXPR: SyntaxSet = SyntaxSet::new()
.add(SyntaxKind::Parbreak)
.add(SyntaxKind::LineComment)
.add(SyntaxKind::BlockComment)
+ .add(SyntaxKind::Annotation)
.add(SyntaxKind::Text)
.add(SyntaxKind::Linebreak)
.add(SyntaxKind::Escape)
diff --git a/crates/typst/src/diag.rs b/crates/typst/src/diag.rs
index ad3f3e4a..2e4437c7 100644
--- a/crates/typst/src/diag.rs
+++ b/crates/typst/src/diag.rs
@@ -1,5 +1,6 @@
//! Diagnostics.
+use std::collections::HashSet;
use std::fmt::{self, Display, Formatter};
use std::io;
use std::path::{Path, PathBuf};
@@ -8,9 +9,10 @@ use std::string::FromUtf8Error;
use comemo::Tracked;
use ecow::{eco_vec, EcoVec};
+use typst_utils::PicoStr;
use crate::syntax::package::{PackageSpec, PackageVersion};
-use crate::syntax::{Span, Spanned, SyntaxError};
+use crate::syntax::{ast, Span, Spanned, SyntaxError};
use crate::{World, WorldExt};
/// Early-return with a [`StrResult`] or [`SourceResult`].
@@ -115,12 +117,14 @@ macro_rules! __error {
macro_rules! __warning {
(
$span:expr,
- $fmt:literal $(, $arg:expr)*
+ id: $id:expr,
+ message: $fmt:literal $(, $arg:expr)*
$(; hint: $hint:literal $(, $hint_arg:expr)*)*
$(,)?
) => {
$crate::diag::SourceDiagnostic::warning(
$span,
+ $id,
$crate::diag::eco_format!($fmt, $($arg),*),
) $(.with_hint($crate::diag::eco_format!($hint, $($hint_arg),*)))*
};
@@ -157,6 +161,10 @@ pub struct SourceDiagnostic {
pub severity: Severity,
/// The span of the relevant node in the source code.
pub span: Span,
+ /// The identifier for this diagnostic.
+ ///
+ /// Currently, this field is only used by warnings.
+ pub identifier: Option<PicoStr>,
/// A diagnostic message describing the problem.
pub message: EcoString,
/// The trace of function calls leading to the problem.
@@ -181,6 +189,7 @@ impl SourceDiagnostic {
Self {
severity: Severity::Error,
span,
+ identifier: None,
trace: eco_vec![],
message: message.into(),
hints: eco_vec![],
@@ -188,10 +197,15 @@ impl SourceDiagnostic {
}
/// Create a new, bare warning.
- pub fn warning(span: Span, message: impl Into<EcoString>) -> Self {
+ pub fn warning(
+ span: Span,
+ identifier: impl Into<PicoStr>,
+ message: impl Into<EcoString>,
+ ) -> Self {
Self {
severity: Severity::Warning,
span,
+ identifier: Some(identifier.into()),
trace: eco_vec![],
message: message.into(),
hints: eco_vec![],
@@ -220,6 +234,7 @@ impl From<SyntaxError> for SourceDiagnostic {
fn from(error: SyntaxError) -> Self {
Self {
severity: Severity::Error,
+ identifier: None,
span: error.span,
message: error.message,
trace: eco_vec![],
@@ -258,36 +273,48 @@ impl Display for Tracepoint {
}
}
-/// Enrich a [`SourceResult`] with a tracepoint.
-pub trait Trace<T> {
- /// Add the tracepoint to all errors that lie outside the `span`.
+/// Enrich diagnostics with a tracepoint.
+pub trait Trace {
+ /// Add the tracepoint to all diagnostics that lie outside the `span`.
fn trace<F>(self, world: Tracked<dyn World + '_>, make_point: F, span: Span) -> Self
where
F: Fn() -> Tracepoint;
}
-impl<T> Trace<T> for SourceResult<T> {
- fn trace<F>(self, world: Tracked<dyn World + '_>, make_point: F, span: Span) -> Self
+impl Trace for EcoVec<SourceDiagnostic> {
+ fn trace<F>(
+ mut self,
+ world: Tracked<dyn World + '_>,
+ make_point: F,
+ span: Span,
+ ) -> Self
where
F: Fn() -> Tracepoint,
{
- self.map_err(|mut errors| {
- let Some(trace_range) = world.range(span) else { return errors };
- for error in errors.make_mut().iter_mut() {
- // Skip traces that surround the error.
- if let Some(error_range) = world.range(error.span) {
- if error.span.id() == span.id()
- && trace_range.start <= error_range.start
- && trace_range.end >= error_range.end
- {
- continue;
- }
+ let Some(trace_range) = world.range(span) else { return self };
+ for error in self.make_mut().iter_mut() {
+ // Skip traces that surround the error.
+ if let Some(error_range) = world.range(error.span) {
+ if error.span.id() == span.id()
+ && trace_range.start <= error_range.start
+ && trace_range.end >= error_range.end
+ {
+ continue;
}
-
- error.trace.push(Spanned::new(make_point(), span));
}
- errors
- })
+
+ error.trace.push(Spanned::new(make_point(), span));
+ }
+ self
+ }
+}
+
+impl<T> Trace for SourceResult<T> {
+ fn trace<F>(self, world: Tracked<dyn World + '_>, make_point: F, span: Span) -> Self
+ where
+ F: Fn() -> Tracepoint,
+ {
+ self.map_err(|errors| errors.trace(world, make_point, span))
}
}
@@ -410,6 +437,119 @@ impl<T> Hint<T> for HintedStrResult<T> {
}
}
+/// Deduplicate errors based on their spans and messages.
+pub fn deduplicate_errors(
+ mut errors: EcoVec<SourceDiagnostic>,
+) -> EcoVec<SourceDiagnostic> {
+ let mut unique = HashSet::new();
+ errors.retain(|error| {
+ debug_assert!(error.severity == Severity::Error);
+ let hash = crate::utils::hash128(&(&error.span, &error.message));
+ unique.insert(hash)
+ });
+ errors
+}
+
+/// Apply warning suppression, deduplication, and return the remaining
+/// warnings.
+///
+/// We deduplicate warnings which are identical modulo tracepoints.
+/// This is so we can attempt to suppress each tracepoint separately,
+/// without having one suppression discard all other attempts of raising
+/// this same warning through a different set of tracepoints.
+///
+/// For example, calling the same function twice but suppressing a warning
+/// on the first call shouldn't suppress on the second, so each set of
+/// tracepoints for a particular warning matters. If at least one instance
+/// of a warning isn't suppressed, the warning will be returned, and the
+/// remaining duplicates are discarded.
+pub fn deduplicate_and_suppress_warnings(
+ warnings: &mut EcoVec<SourceDiagnostic>,
+ world: &dyn World,
+) {
+ let mut unsuppressed = HashSet::<u128>::default();
+
+ // Only retain warnings which weren't locally suppressed where they
+ // were emitted or at any of their tracepoints.
+ warnings.retain(|diag| {
+ debug_assert!(diag.severity == Severity::Warning);
+ let hash = crate::utils::hash128(&(&diag.span, &diag.identifier, &diag.message));
+ if unsuppressed.contains(&hash) {
+ // This warning - with the same span, identifier and message -
+ // was already raised and not suppressed before, with a
+ // different set of tracepoints. Therefore, we should not raise
+ // it again, and checking for suppression is unnecessary.
+ return false;
+ }
+
+ let Some(identifier) = &diag.identifier else {
+ // Can't suppress without an identifier. Therefore, retain the
+ // warning. It is not a duplicate due to the check above.
+ unsuppressed.insert(hash);
+ return true;
+ };
+
+ let identifier = identifier.resolve();
+ let should_raise = !is_warning_suppressed(diag.span, world, identifier)
+ && !diag.trace.iter().any(|tracepoint| {
+ is_warning_suppressed(tracepoint.span, world, identifier)
+ });
+
+ // If this warning wasn't suppressed, any further duplicates (with
+ // different tracepoints) should be removed.
+ should_raise && unsuppressed.insert(hash)
+ });
+}
+
+/// Checks if a given warning is suppressed given one span it has a tracepoint
+/// in. If one of the ancestors of the node where the warning occurred has a
+/// warning suppression annotation sibling right before it suppressing this
+/// particular warning, the warning is considered suppressed.
+fn is_warning_suppressed(span: Span, world: &dyn World, warning: &str) -> bool {
+ // Don't suppress detached warnings.
+ let Some(source) = span.id().and_then(|file| world.source(file).ok()) else {
+ return false;
+ };
+
+ let search_root = source.find(span);
+ let mut searched_node = search_root.as_ref();
+
+ // Walk the parent nodes to check for a warning suppression in the
+ // previous line.
+ while let Some(node) = searched_node {
+ let mut searched_annotation = node.prev_attached_annotation();
+ while let Some(sibling) = searched_annotation {
+ let annotation = sibling.cast::<ast::Annotation>().unwrap();
+ if check_annotation_suppresses_warning(annotation, warning) {
+ return true;
+ }
+ searched_annotation = sibling.prev_attached_annotation();
+ }
+ searched_node = node.parent();
+ }
+
+ false
+}
+
+/// Checks if an 'allow' annotation would cause a warning with a particular
+/// identifier to be suppressed.
+fn check_annotation_suppresses_warning(
+ annotation: ast::Annotation,
+ warning: &str,
+) -> bool {
+ if annotation.name().as_str() != "allow" {
+ return false;
+ }
+
+ for argument in annotation.arguments() {
+ if warning == argument.get() {
+ return true;
+ }
+ }
+
+ false
+}
+
/// A result type with a file-related error.
pub type FileResult<T> = Result<T, FileError>;
diff --git a/crates/typst/src/engine.rs b/crates/typst/src/engine.rs
index 2e2525b2..d58a5ac5 100644
--- a/crates/typst/src/engine.rs
+++ b/crates/typst/src/engine.rs
@@ -7,7 +7,7 @@ use comemo::{Track, Tracked, TrackedMut, Validate};
use ecow::EcoVec;
use rayon::iter::{IndexedParallelIterator, IntoParallelIterator, ParallelIterator};
-use crate::diag::{SourceDiagnostic, SourceResult};
+use crate::diag::{self, SourceDiagnostic, SourceResult, Trace, Tracepoint};
use crate::foundations::{Styles, Value};
use crate::introspection::Introspector;
use crate::syntax::{FileId, Span};
@@ -79,12 +79,53 @@ impl Engine<'_> {
// Apply the subsinks to the outer sink.
for (_, sink) in &mut pairs {
- let sink = std::mem::take(sink);
- self.sink.extend(sink.delayed, sink.warnings, sink.values);
+ Sink::extend_tracked(&mut self.sink, std::mem::take(sink));
}
pairs.into_iter().map(|(output, _)| output)
}
+
+ /// Executes some code with a tracepoint for diagnostics.
+ ///
+ /// The tracepoint is added to any diagnostics returned by the function,
+ /// as well as any warnings emitted by it.
+ pub fn tracepoint<F, M, T>(
+ &mut self,
+ make_point: M,
+ span: Span,
+ f: F,
+ ) -> SourceResult<T>
+ where
+ F: FnOnce(&mut Engine) -> SourceResult<T>,
+ M: Copy + Fn() -> Tracepoint,
+ {
+ let Engine { world, introspector, traced, ref route, .. } = *self;
+
+ // Create a temporary sink to accumulate all warnings produced by
+ // this call (either directly or due to a nested call). Later, we
+ // add a tracepoint to those warnings, indicating this call was
+ // part of the call stack that led to the warning being raised,
+ // thus allowing suppression of the warning through this call.
+ let mut sink = Sink::new();
+ let mut engine = Engine {
+ world,
+ introspector,
+ traced,
+ sink: sink.track_mut(),
+ route: route.clone(),
+ };
+
+ // Trace errors and warnings on the sink immediately.
+ let call_result = f(&mut engine).trace(world, make_point, span);
+ sink.warnings = std::mem::take(&mut sink.warnings).trace(world, make_point, span);
+
+ // Push the accumulated warnings and other fields back to the
+ // original sink after we have modified them. This is needed so the
+ // warnings are properly returned by compilation later.
+ Sink::extend_tracked(&mut self.sink, sink);
+
+ call_result
+ }
}
/// May hold a span that is currently under inspection.
@@ -146,20 +187,32 @@ impl Sink {
Self::default()
}
+ /// Extend the destination sink with the data from the source sink.
+ ///
+ /// This calls a tracked function on the destination unless the source
+ /// is fully empty (which is usually the case).
+ pub fn extend_tracked(destination: &mut TrackedMut<'_, Self>, source: Sink) {
+ let Sink { delayed, warnings, values, .. } = source;
+ if !delayed.is_empty() || !warnings.is_empty() || !values.is_empty() {
+ destination.extend(delayed, warnings, values);
+ }
+ }
+
/// Get the stored delayed errors.
pub fn delayed(&mut self) -> EcoVec<SourceDiagnostic> {
std::mem::take(&mut self.delayed)
}
- /// Get the stored warnings.
- pub fn warnings(self) -> EcoVec<SourceDiagnostic> {
- self.warnings
- }
-
/// Get the values for the traced span.
pub fn values(self) -> EcoVec<(Value, Option<Styles>)> {
self.values
}
+
+ /// Deduplicates and suppresses the stored warnings before returning them.
+ pub fn finish_warnings(mut self, world: &dyn World) -> EcoVec<SourceDiagnostic> {
+ diag::deduplicate_and_suppress_warnings(&mut self.warnings, world);
+ self.warnings
+ }
}
#[comemo::track]
@@ -172,7 +225,19 @@ impl Sink {
/// Add a warning.
pub fn warn(&mut self, warning: SourceDiagnostic) {
// Check if warning is a duplicate.
- let hash = crate::utils::hash128(&(&warning.span, &warning.message));
+ //
+ // Identical warnings with differing tracepoints are considered
+ // separate because suppressing the warning through one tracepoint
+ // shouldn't suppress it through the others. Later, during warning
+ // suppression (when calling `suppress_and_deduplicate_warnings`), we
+ // deduplicate without considering tracepoints, such that, if at least
+ // one duplicate wasn't suppressed, it is raised.
+ let hash = crate::utils::hash128(&(
+ &warning.span,
+ &warning.identifier,
+ &warning.message,
+ &warning.trace,
+ ));
if self.warnings_set.insert(hash) {
self.warnings.push(warning);
}
@@ -186,6 +251,10 @@ impl Sink {
}
/// Extend from another sink.
+ ///
+ /// Using `Sink::extend_tracked` is preferable as it avoids a call to this
+ /// function if all arguments are empty, thus avoiding an unnecessary
+ /// tracked call in most cases.
fn extend(
&mut self,
delayed: EcoVec<SourceDiagnostic>,
diff --git a/crates/typst/src/eval/call.rs b/crates/typst/src/eval/call.rs
index ee4c4787..5ce7ebe4 100644
--- a/crates/typst/src/eval/call.rs
+++ b/crates/typst/src/eval/call.rs
@@ -58,8 +58,11 @@ impl Eval for ast::FuncCall<'_> {
let point = || Tracepoint::Call(func.name().map(Into::into));
let f = || {
- func.call(&mut vm.engine, vm.context, args)
- .trace(vm.world(), point, span)
+ // Add this function call as a tracepoint to the returned
+ // diagnostics. This allows visualizing which calls led to them,
+ // but also allows suppressing warnings at each tracepoint.
+ vm.engine
+ .tracepoint(point, span, |engine| func.call(engine, vm.context, args))
};
// Stacker is broken on WASM.
diff --git a/crates/typst/src/eval/import.rs b/crates/typst/src/eval/import.rs
index 68187a96..fc0de6c5 100644
--- a/crates/typst/src/eval/import.rs
+++ b/crates/typst/src/eval/import.rs
@@ -37,7 +37,8 @@ impl Eval for ast::ModuleImport<'_> {
// Warn on `import x as x`
vm.engine.sink.warn(warning!(
new_name.span(),
- "unnecessary import rename to same name",
+ id: "unnecessary-import-renaming",
+ message: "unnecessary import rename to same name",
));
}
}
@@ -112,7 +113,8 @@ impl Eval for ast::ModuleImport<'_> {
{
vm.engine.sink.warn(warning!(
renamed_item.new_name().span(),
- "unnecessary import rename to same name",
+ id: "unnecessary-import-renaming",
+ message: "unnecessary import rename to same name",
));
}
}
diff --git a/crates/typst/src/eval/markup.rs b/crates/typst/src/eval/markup.rs
index b44c9789..d9ef2886 100644
--- a/crates/typst/src/eval/markup.rs
+++ b/crates/typst/src/eval/markup.rs
@@ -136,7 +136,9 @@ impl Eval for ast::Strong<'_> {
vm.engine
.sink
.warn(warning!(
- self.span(), "no text within stars";
+ self.span(),
+ id: "unnecessary-stars",
+ message: "no text within stars";
hint: "using multiple consecutive stars (e.g. **) has no additional effect",
));
}
@@ -154,7 +156,9 @@ impl Eval for ast::Emph<'_> {
vm.engine
.sink
.warn(warning!(
- self.span(), "no text within underscores";
+ self.span(),
+ id: "unnecessary-underscores",
+ message: "no text within underscores";
hint: "using multiple consecutive underscores (e.g. __) has no additional effect"
));
}
diff --git a/crates/typst/src/foundations/styles.rs b/crates/typst/src/foundations/styles.rs
index 48009c8c..db870d62 100644
--- a/crates/typst/src/foundations/styles.rs
+++ b/crates/typst/src/foundations/styles.rs
@@ -46,7 +46,9 @@ pub fn style(
func: Func,
) -> Content {
engine.sink.warn(warning!(
- span, "`style` is deprecated";
+ span,
+ id: "deprecated",
+ message: "`style` is deprecated";
hint: "use a `context` expression instead"
));
diff --git a/crates/typst/src/introspection/counter.rs b/crates/typst/src/introspection/counter.rs
index 2aefb68e..9829d42e 100644
--- a/crates/typst/src/introspection/counter.rs
+++ b/crates/typst/src/introspection/counter.rs
@@ -465,7 +465,9 @@ impl Counter {
self.display_impl(engine, loc, numbering, both, context.styles().ok())
} else {
engine.sink.warn(warning!(
- span, "`counter.display` without context is deprecated";
+ span,
+ id: "deprecated",
+ message: "`counter.display` without context is deprecated";
hint: "use it in a `context` expression instead"
));
@@ -523,7 +525,9 @@ impl Counter {
context.location().at(span)?;
} else {
engine.sink.warn(warning!(
- span, "calling `counter.final` with a location is deprecated";
+ span,
+ id: "deprecated",
+ message: "calling `counter.final` with a location is deprecated";
hint: "try removing the location argument"
));
}
diff --git a/crates/typst/src/introspection/locate.rs b/crates/typst/src/introspection/locate.rs
index 8991ae9b..15e51ce1 100644
--- a/crates/typst/src/introspection/locate.rs
+++ b/crates/typst/src/introspection/locate.rs
@@ -58,7 +58,9 @@ pub fn locate(
}
LocateInput::Func(func) => {
engine.sink.warn(warning!(
- span, "`locate` with callback function is deprecated";
+ span,
+ id: "deprecated",
+ message: "`locate` with callback function is deprecated";
hint: "use a `context` expression instead"
));
diff --git a/crates/typst/src/introspection/query.rs b/crates/typst/src/introspection/query.rs
index 07f761a8..79991990 100644
--- a/crates/typst/src/introspection/query.rs
+++ b/crates/typst/src/introspection/query.rs
@@ -161,7 +161,9 @@ pub fn query(
context.introspect()?;
} else {
engine.sink.warn(warning!(
- span, "calling `query` with a location is deprecated";
+ span,
+ id: "deprecated",
+ message: "calling `query` with a location is deprecated";
hint: "try removing the location argument"
));
}
diff --git a/crates/typst/src/introspection/state.rs b/crates/typst/src/introspection/state.rs
index a88ed526..a1b87199 100644
--- a/crates/typst/src/introspection/state.rs
+++ b/crates/typst/src/introspection/state.rs
@@ -335,7 +335,9 @@ impl State {
context.location().at(span)?;
} else {
engine.sink.warn(warning!(
- span, "calling `state.final` with a location is deprecated";
+ span,
+ id: "deprecated",
+ message: "calling `state.final` with a location is deprecated";
hint: "try removing the location argument"
));
}
@@ -382,7 +384,9 @@ impl State {
func: Option<Func>,
) -> Content {
engine.sink.warn(warning!(
- span, "`state.display` is deprecated";
+ span,
+ id: "deprecated",
+ message: "`state.display` is deprecated";
hint: "use `state.get` in a `context` expression instead"
));
diff --git a/crates/typst/src/layout/measure.rs b/crates/typst/src/layout/measure.rs
index cb08e591..8a4c1694 100644
--- a/crates/typst/src/layout/measure.rs
+++ b/crates/typst/src/layout/measure.rs
@@ -84,7 +84,9 @@ pub fn measure(
let styles = match &styles {
Some(styles) => {
engine.sink.warn(warning!(
- span, "calling `measure` with a styles argument is deprecated";
+ span,
+ id: "deprecated",
+ message: "calling `measure` with a styles argument is deprecated";
hint: "try removing the styles argument"
));
StyleChain::new(styles)
diff --git a/crates/typst/src/lib.rs b/crates/typst/src/lib.rs
index cfcfd757..1fe18c05 100644
--- a/crates/typst/src/lib.rs
+++ b/crates/typst/src/lib.rs
@@ -56,7 +56,6 @@ pub use typst_syntax as syntax;
#[doc(inline)]
pub use typst_utils as utils;
-use std::collections::HashSet;
use std::ops::{Deref, Range};
use comemo::{Track, Tracked, Validate};
@@ -87,8 +86,10 @@ use crate::visualize::Color;
pub fn compile(world: &dyn World) -> Warned<SourceResult<Document>> {
let mut sink = Sink::new();
let output = compile_inner(world.track(), Traced::default().track(), &mut sink)
- .map_err(deduplicate);
- Warned { output, warnings: sink.warnings() }
+ .map_err(diag::deduplicate_errors);
+
+ let warnings = sink.finish_warnings(world);
+ Warned { output, warnings }
}
/// Compiles sources and returns all values and styles observed at the given
@@ -160,7 +161,9 @@ fn compile_inner(
if iter >= 5 {
sink.warn(warning!(
- Span::detached(), "layout did not converge within 5 attempts";
+ Span::detached(),
+ id: "non-converging-layout",
+ message: "layout did not converge within 5 attempts";
hint: "check if any states or queries are updating themselves"
));
break;
@@ -176,16 +179,6 @@ fn compile_inner(
Ok(document)
}
-/// Deduplicate diagnostics.
-fn deduplicate(mut diags: EcoVec<SourceDiagnostic>) -> EcoVec<SourceDiagnostic> {
- let mut unique = HashSet::new();
- diags.retain(|diag| {
- let hash = crate::utils::hash128(&(&diag.span, &diag.message));
- unique.insert(hash)
- });
- diags
-}
-
/// The environment in which typesetting occurs.
///
/// All loading functions (`main`, `source`, `file`, `font`) should perform
diff --git a/crates/typst/src/text/mod.rs b/crates/typst/src/text/mod.rs
index 76ea26c1..23442259 100644
--- a/crates/typst/src/text/mod.rs
+++ b/crates/typst/src/text/mod.rs
@@ -134,7 +134,8 @@ pub struct TextElem {
if !book.contains_family(family.as_str()) {
engine.sink.warn(warning!(
font_list.span,
- "unknown font family: {}",
+ id: "unknown-font-families",
+ message: "unknown font family: {}",
family.as_str(),
));
}
diff --git a/tests/suite/syntax/annotation.typ b/tests/suite/syntax/annotation.typ
new file mode 100644
index 00000000..930ef7e2
--- /dev/null
+++ b/tests/suite/syntax/annotation.typ
@@ -0,0 +1,185 @@
+// Test annotations.
+
+--- basic-annotations ---
+
+// @allow
+// @allow identifier
+// @allow "quoted"
+// @allow("parenthesized")
+
+// @allow unnecessary-stars
+#h(0em)
+
+#let _ = {
+ // @allow unnecessary-stars
+ h(0em)
+}
+
+#let _ = $
+ // @allow unnecessary-stars
+ h(#0em)
+$
+
+--- annotation-comments ---
+// Error: 2:17-2:18 the character '/' is not valid in an annotation
+// Error: 2:18-2:19 the character '/' is not valid in an annotation
+
+// @allow "abc" // comment
+
+// Error: 2:17-2:18 the character '/' is not valid in an annotation
+// Error: 2:18-2:19 the character '*' is not valid in an annotation
+// Error: 2:28-2:29 the character '*' is not valid in an annotation
+// Error: 2:29-2:30 the character '/' is not valid in an annotation
+
+// @allow "abc" /* comment */
+
+// Error: 2:17-2:18 the character '/' is not valid in an annotation
+// Error: 2:18-2:19 the character '*' is not valid in an annotation
+// Error: 2:28-2:29 the character '*' is not valid in an annotation
+// Error: 2:29-2:30 the character '/' is not valid in an annotation
+
+// @allow "abc" /* comment */ "abc"
+
+--- annotation-strings ---
+
+// @allow("@some/thing-there123")
+
+--- unknown-annotation ---
+// Error: 2:5-2:13 invalid annotation name
+// Hint: 2:5-2:13 must be 'allow'
+
+// @whatever A
+
+--- invalid-annotation-syntax ---
+// Error: 2:11-2:12 the character '*' is not valid in an annotation
+
+// @allow *
+
+// Error: 2:11-2:12 the character '5' is not valid in an annotation
+
+// @allow 5
+
+// Error: 2:5-2:19 expected identifier
+
+// @555!**INVALID!
+
+// Error: 2:10-2:11 the character ')' is not valid in an annotation
+// Error: 2:11-2:13 unclosed string
+
+// @allow)")
+
+// Error: 2:11-2:15 unclosed string
+// Error: 2:15 expected closing paren
+
+// @allow("abc
+
+// Error: 2:16-2:19 expected end of annotation
+
+// @allow(abc) abc
+
+// Error: 2:18-2:19 the character ',' is not valid in an annotation
+
+// @allow(abc abc, "abc")
+
+--- invalid-annotation-strings ---
+
+// Error: 2:11-2:16 invalid character ' ' in an annotation's string
+
+// @allow "a b"
+
+// Error: 2:11-2:19 invalid character '|' in an annotation's string
+
+// @allow "aaaaa|"
+
+// TODO: Why does this print / instead of \?
+// Error: 2:11-2:19 invalid character '/' in an annotation's string
+
+// @allow "aaaaa\"
+
+--- invalid-annotation-in-annotation ---
+// Error: 2:17-2:18 the character '/' is not valid in an annotation
+// Error: 2:18-2:19 the character '/' is not valid in an annotation
+// Error: 2:20-2:21 the character '@' is not valid in an annotation
+// Error: 2:26-2:27 the character '(' is not valid in an annotation
+// Error: 2:32-2:33 the character ')' is not valid in an annotation
+
+// @allow "aaa" // @allow("bbb")
+
+--- allow-suppresses-warns-below ---
+
+// @allow unnecessary-stars
+#[**]
+
+// @allow unnecessary-stars
+#{
+ {
+ [**]
+ }
+}
+
+/**/ // @allow unnecessary-stars
+#[**]
+
+// @allow unnecessary-stars
+**
+
+--- allow-suppresses-warn-with-tracepoint ---
+#let f() = {
+ text(font: "Unbeknownst")[]
+}
+
+#let g() = {
+ f()
+}
+
+// @allow unknown-font-families
+#g()
+
+--- allow-suppresses-line-below-but-not-same-line ---
+// Warning: 3-5 no text within stars
+// Hint: 3-5 using multiple consecutive stars (e.g. **) has no additional effect
+#[**] // @allow unnecessary-stars
+#[**]
+
+--- allow-before-parbreak-doesnt-suppress-warn ---
+// Warning: 4:3-4:5 no text within stars
+// Hint: 4:3-4:5 using multiple consecutive stars (e.g. **) has no additional effect
+
+// @allow unnecessary-stars
+
+#[**]
+
+--- allow-before-empty-code-line-doesnt-suppress-warn ---
+// Warning: 4:4-4:6 no text within stars
+// Hint: 4:4-4:6 using multiple consecutive stars (e.g. **) has no additional effect
+#{
+ // @allow unnecessary-stars
+
+ [**]
+}
+
+--- unattached-allow-doesnt-suppress-warn ---
+
+// Warning: 1-3 no text within stars
+// Hint: 1-3 using multiple consecutive stars (e.g. **) has no additional effect
+**
+
+// @allow unnecessary-stars
+#h(0em)
+// Warning: 3-5 no text within stars
+// Hint: 3-5 using multiple consecutive stars (e.g. **) has no additional effect
+#[**]
+
+--- allow-doesnt-suppress-warn-in-nested-context ---
+// Warning: 2:14-2:27 unknown font family: unbeknownst
+#let f() = context {
+ text(font: "Unbeknownst")[]
+}
+
+// @allow unknown-font-families
+#f()
+
+// @allow unknown-font-families
+#context {
+ text(font: "Unbeknownst")[]
+}