diff --git a/crates/typst-syntax/src/ast.rs b/crates/typst-syntax/src/ast.rs index 1cd9cd429..85c4ca47f 100644 --- a/crates/typst-syntax/src/ast.rs +++ b/crates/typst-syntax/src/ast.rs @@ -80,6 +80,19 @@ impl<'a> Markup<'a> { } } +node! { + /// A comment: `// something`. + LineComment +} + +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) + } +} + /// An expression in markup, math or code. #[derive(Debug, Copy, Clone, Hash)] pub enum Expr<'a> { diff --git a/crates/typst-syntax/src/node.rs b/crates/typst-syntax/src/node.rs index bc378e667..08cb346c4 100644 --- a/crates/typst-syntax/src/node.rs +++ b/crates/typst-syntax/src/node.rs @@ -773,13 +773,17 @@ impl<'a> LinkedNode<'a> { self.parent.as_deref() } - /// Get the first previous non-trivia sibling node. - pub fn prev_sibling(&self) -> Option { + fn prev_sibling_inner(&self) -> Option { 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 { + let prev = self.prev_sibling_inner()?; if prev.kind().is_trivia() { prev.prev_sibling() } else { @@ -787,6 +791,38 @@ impl<'a> LinkedNode<'a> { } } + /// Get the first sibling comment 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()?; + } + 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 + } + } else { + self.prev_sibling_inner() + .as_ref() + .and_then(Self::prev_attached_comment) + } + } + /// Get the next non-trivia sibling node. pub fn next_sibling(&self) -> Option { let parent = self.parent()?; diff --git a/crates/typst/src/engine.rs b/crates/typst/src/engine.rs index 2e2525b20..fed0e8dee 100644 --- a/crates/typst/src/engine.rs +++ b/crates/typst/src/engine.rs @@ -7,10 +7,10 @@ use comemo::{Track, Tracked, TrackedMut, Validate}; use ecow::EcoVec; use rayon::iter::{IndexedParallelIterator, IntoParallelIterator, ParallelIterator}; -use crate::diag::{SourceDiagnostic, SourceResult}; +use crate::diag::{Severity, SourceDiagnostic, SourceResult}; use crate::foundations::{Styles, Value}; use crate::introspection::Introspector; -use crate::syntax::{FileId, Span}; +use crate::syntax::{ast, FileId, Span}; use crate::World; /// Holds all data needed during compilation. @@ -160,6 +160,19 @@ impl Sink { pub fn values(self) -> EcoVec<(Value, Option)> { self.values } + + /// Apply warning suppression. + pub fn suppress_warnings(&mut self, world: &dyn World) { + self.warnings.retain(|diag| { + // Only retain warnings which weren't locally suppressed where they + // were emitted or at any of their tracepoints. + diag.severity != Severity::Warning + || (!check_warning_suppressed(diag.span, world, &diag.message) + && !diag.trace.iter().any(|tracepoint| { + check_warning_suppressed(tracepoint.span, world, &diag.message) + })) + }); + } } #[comemo::track] @@ -202,6 +215,41 @@ impl Sink { } } +/// 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 decorator sibling right before it suppressing this +/// particular warning, the warning is considered suppressed. +fn check_warning_suppressed( + span: Span, + world: &dyn World, + _message: &ecow::EcoString, +) -> bool { + let Some(file) = span.id() else { + // Don't suppress detached warnings. + return false; + }; + + // The source must exist if a warning occurred in the file, + // or has a tracepoint in the file. + let source = world.source(file).unwrap(); + // The span must point to this source file, so we unwrap. + let mut node = &source.find(span).unwrap(); + + // Walk the parent nodes to check for a warning suppression. + while let Some(parent) = node.parent() { + if let Some(sibling) = parent.prev_attached_comment() { + if let Some(comment) = sibling.cast::() { + let _text = comment.content(); + // Find comment, return true if this warning was suppressed + todo!(); + } + } + node = parent; + } + + false +} + /// The route the engine took during compilation. This is used to detect /// cyclic imports and excessive nesting. pub struct Route<'a> { diff --git a/crates/typst/src/lib.rs b/crates/typst/src/lib.rs index e934ea37e..e38abacc2 100644 --- a/crates/typst/src/lib.rs +++ b/crates/typst/src/lib.rs @@ -86,6 +86,8 @@ pub fn compile(world: &dyn World) -> Warned> { let mut sink = Sink::new(); let output = compile_inner(world.track(), Traced::default().track(), &mut sink) .map_err(deduplicate); + + sink.suppress_warnings(world); Warned { output, warnings: sink.warnings() } }