diff options
| -rw-r--r-- | crates/typst-syntax/src/ast.rs | 62 | ||||
| -rw-r--r-- | crates/typst-syntax/src/highlight.rs | 4 | ||||
| -rw-r--r-- | crates/typst-syntax/src/kind.rs | 16 | ||||
| -rw-r--r-- | crates/typst-syntax/src/lexer.rs | 230 | ||||
| -rw-r--r-- | crates/typst-syntax/src/node.rs | 59 | ||||
| -rw-r--r-- | crates/typst-syntax/src/parser.rs | 68 | ||||
| -rw-r--r-- | crates/typst-syntax/src/set.rs | 1 | ||||
| -rw-r--r-- | crates/typst/src/diag.rs | 186 | ||||
| -rw-r--r-- | crates/typst/src/engine.rs | 87 | ||||
| -rw-r--r-- | crates/typst/src/eval/call.rs | 7 | ||||
| -rw-r--r-- | crates/typst/src/eval/import.rs | 6 | ||||
| -rw-r--r-- | crates/typst/src/eval/markup.rs | 8 | ||||
| -rw-r--r-- | crates/typst/src/foundations/styles.rs | 4 | ||||
| -rw-r--r-- | crates/typst/src/introspection/counter.rs | 8 | ||||
| -rw-r--r-- | crates/typst/src/introspection/locate.rs | 4 | ||||
| -rw-r--r-- | crates/typst/src/introspection/query.rs | 4 | ||||
| -rw-r--r-- | crates/typst/src/introspection/state.rs | 8 | ||||
| -rw-r--r-- | crates/typst/src/layout/measure.rs | 4 | ||||
| -rw-r--r-- | crates/typst/src/lib.rs | 21 | ||||
| -rw-r--r-- | crates/typst/src/text/mod.rs | 3 | ||||
| -rw-r--r-- | tests/suite/syntax/annotation.typ | 185 |
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")[] +} |
