diff --git a/crates/typst-syntax/src/ast.rs b/crates/typst-syntax/src/ast.rs index 85c4ca47f..8380ae1c6 100644 --- a/crates/typst-syntax/src/ast.rs +++ b/crates/typst-syntax/src/ast.rs @@ -81,15 +81,31 @@ impl<'a> Markup<'a> { } node! { - /// A comment: `// something`. - LineComment + /// A decorator: `/! allow("warning")`. + Decorator } -impl<'a> LineComment<'a> { - /// The comment's contents, excluding the initial '//' marker. - pub fn content(self) -> &'a str { - let text = self.0.text(); - text.strip_prefix("//").unwrap_or(text) +impl<'a> Decorator<'a> { + /// The name of the decorator, e.g. `allow`. + pub fn name(self) -> Ident<'a> { + self.0.cast_first_match().unwrap_or_default() + } + + /// The decorator's arguments. + pub fn arguments(self) -> impl DoubleEndedIterator> { + let mut found_non_ident = false; + self.0 + .children() + .filter(move |node| { + // Skip the name (first identifier). + if node.is::() { + return found_non_ident; + } else if !node.kind().is_trivia() { + found_non_ident = true; + } + true + }) + .filter_map(Expr::from_untyped) } } diff --git a/crates/typst-syntax/src/node.rs b/crates/typst-syntax/src/node.rs index 08cb346c4..c784353e4 100644 --- a/crates/typst-syntax/src/node.rs +++ b/crates/typst-syntax/src/node.rs @@ -6,7 +6,7 @@ use std::sync::Arc; use ecow::{eco_format, eco_vec, EcoString, EcoVec}; use crate::ast::AstNode; -use crate::{FileId, Span, SyntaxKind}; +use crate::{is_newline, FileId, Span, SyntaxKind}; /// A node in the untyped syntax tree. #[derive(Clone, Eq, PartialEq, Hash)] @@ -148,6 +148,21 @@ impl SyntaxNode { } } + /// The amount of newlines in this node or its descendants, + /// capped at 256, that is, a return value of 256 means that the total + /// amount of newlines may be 256 or larger. + pub fn newlines(&self) -> u8 { + match &self.0 { + Repr::Leaf(_) | Repr::Error(_) => self + .text() + .chars() + .filter(|c| is_newline(*c)) + .take(u8::MAX as usize) + .count() as u8, + Repr::Inner(inner) => inner.newlines, + } + } + /// The error messages for this node and its descendants. pub fn errors(&self) -> Vec { if !self.erroneous() { @@ -381,6 +396,11 @@ struct InnerNode { descendants: usize, /// Whether this node or any of its children are erroneous. erroneous: bool, + /// The (capped) amount of newlines in this node's descendants. + /// This is solely used to tell whether this node contains 0, 1, 2 or more + /// newlines. As such, this number is capped at 256, even though there may + /// be more newlines inside this node. + newlines: u8, /// The upper bound of this node's numbering range. upper: u64, /// This node's children, losslessly make up this node. @@ -407,6 +427,7 @@ impl InnerNode { kind, len, span: Span::detached(), + newlines: 0, descendants, erroneous, upper: 0, @@ -791,36 +812,40 @@ impl<'a> LinkedNode<'a> { } } - /// Get the first sibling comment node at the line above this node. + /// Get the first sibling decorator node at the line above this node. /// This is done by moving backwards until the rightmost newline, and then - /// checking for comments before the previous newline. - pub fn prev_attached_comment(&self) -> Option { - if self.kind() == SyntaxKind::Parbreak - || self.kind() == SyntaxKind::Space && self.text().contains('\n') - { - // We hit a newline, so let's check for comments before the next - // newline. - let mut sibling_before_newline = self.prev_sibling_inner()?; - while sibling_before_newline.kind().is_trivia() - && !matches!( - sibling_before_newline.kind(), - SyntaxKind::Space | SyntaxKind::LineComment | SyntaxKind::Parbreak - ) - { - sibling_before_newline = sibling_before_newline.prev_sibling_inner()?; + /// checking for decorators before the previous newline. + pub fn prev_attached_decorator(&self) -> Option { + let mut cursor = self.prev_sibling_inner()?; + let mut newlines = cursor.newlines(); + let mut at_previous_line = false; + while newlines < 2 { + if at_previous_line { + if cursor.kind() == SyntaxKind::Decorator { + // Decorators are attached if they're in a consecutive + // previous line. + return Some(cursor); + } } - if sibling_before_newline.kind() == SyntaxKind::LineComment { - // Found a comment on the previous line - Some(sibling_before_newline) - } else { - // No comments on the previous line - None + + if newlines == 1 { + if at_previous_line { + // No decorators at the previous line. + // Don't move onto the line before the previous, since then + // the decorator wouldn't be attached. + return None; + } else { + at_previous_line = true; + } } - } else { - self.prev_sibling_inner() - .as_ref() - .and_then(Self::prev_attached_comment) + + cursor = cursor.prev_sibling_inner()?; + newlines = cursor.newlines(); } + + // Found a parbreak or something else with two or more newlines. + // Can't have an attached decorator there. + return None; } /// Get the next non-trivia sibling node. diff --git a/crates/typst/src/engine.rs b/crates/typst/src/engine.rs index 93e737043..06d43f544 100644 --- a/crates/typst/src/engine.rs +++ b/crates/typst/src/engine.rs @@ -351,13 +351,13 @@ fn check_warning_suppressed( // Walk the parent nodes to check for a warning suppression in the // previous line. while let Some(node) = searched_node { - if let Some(sibling) = node.prev_attached_comment() { - if let Some(comment) = sibling.cast::() { - if matches!(parse_warning_suppression(comment.content()), Some(suppressed) if identifier.name() == suppressed) - { - return true; - } + let mut searched_decorator = node.prev_attached_decorator(); + while let Some(sibling) = searched_decorator { + let decorator = sibling.cast::().unwrap(); + if check_decorator_suppresses_warning(decorator, identifier) { + return true; } + searched_decorator = sibling.prev_attached_decorator(); } searched_node = node.parent(); } @@ -365,20 +365,24 @@ fn check_warning_suppressed( false } -// TODO: replace this ad-hoc solution -// Expects a comment '//! allow("identifier") -fn parse_warning_suppression(comment: &str) -> Option<&str> { - const ALLOW_SEGMENT: &str = "! allow(\""; - if !comment.starts_with(ALLOW_SEGMENT) { - return None; - } - let after_allow = comment.get(ALLOW_SEGMENT.len()..)?.trim(); - let (suppressed_identifier, rest) = after_allow.split_once('"')?; - if rest.trim() != ")" { - return None; +fn check_decorator_suppresses_warning( + decorator: ast::Decorator, + warning: &diag::Identifier, +) -> bool { + if decorator.name().as_str() != "allow" { + return false; } - Some(suppressed_identifier) + for argument in decorator.arguments() { + let ast::Expr::Str(str) = argument else { + continue; + }; + if warning.name() == str.get() { + return true; + } + } + + false } /// The route the engine took during compilation. This is used to detect