use std::collections::{HashMap, HashSet};
use rowan::NodeOrToken;
use crate::syntax::{SyntaxKind, SyntaxNode, SyntaxToken};
#[derive(Debug, Clone, Default)]
pub struct SuppressionMap {
file_rules: HashSet<String>,
file_all: bool,
node_skips: HashMap<String, Vec<(usize, usize)>>,
}
impl SuppressionMap {
pub fn build(root: &SyntaxNode) -> Self {
let mut map = Self::default();
for element in root.descendants_with_tokens() {
if let NodeOrToken::Token(token) = element
&& token.kind() == SyntaxKind::COMMENT
{
classify_comment(&token, &mut map);
}
}
map
}
pub fn is_suppressed(&self, rule: &str, start: usize, end: usize) -> bool {
if self.file_all {
return true;
}
if self.file_rules.contains(rule) {
return true;
}
if let Some(ranges) = self.node_skips.get(rule) {
return ranges.iter().any(|(rs, re)| *rs <= start && end <= *re);
}
false
}
}
fn classify_comment(token: &SyntaxToken, map: &mut SuppressionMap) {
let body = match token.text().strip_prefix('%') {
Some(rest) => rest.trim_start(),
None => return,
};
if let Some(rest) = body.strip_prefix("badness-ignore-file") {
let rest = rest.trim_start();
if rest.starts_with(':') {
map.file_all = true;
} else if let Some(rule) = parse_rule(rest) {
map.file_rules.insert(rule);
}
return;
}
if let Some(rest) = body.strip_prefix("badness-ignore")
&& let Some(rule) = parse_rule(rest.trim_start())
&& let Some(target) = next_meaningful_sibling(token)
{
map.node_skips.entry(rule).or_default().push(target);
}
}
fn parse_rule(rest: &str) -> Option<String> {
let trimmed = rest.trim_start();
let end = trimmed
.find(|c: char| c == ':' || c.is_whitespace())
.unwrap_or(trimmed.len());
if end == 0 {
return None;
}
Some(trimmed[..end].to_string())
}
fn next_meaningful_sibling(token: &SyntaxToken) -> Option<(usize, usize)> {
let mut current = token.clone();
loop {
let parent = current.parent()?;
if let Some(range) = first_meaningful_after(&parent, &NodeOrToken::Token(current.clone())) {
return Some(range);
}
let grand = parent.parent()?;
if let Some(range) = first_meaningful_after(&grand, &NodeOrToken::Node(parent.clone())) {
return Some(range);
}
current = grand.first_token()?;
if grand == parent {
return None;
}
}
}
fn first_meaningful_after(
parent: &SyntaxNode,
after: &NodeOrToken<SyntaxNode, SyntaxToken>,
) -> Option<(usize, usize)> {
let mut past = false;
for element in parent.children_with_tokens() {
if !past {
if &element == after {
past = true;
}
continue;
}
match &element {
NodeOrToken::Token(t)
if matches!(
t.kind(),
SyntaxKind::WHITESPACE | SyntaxKind::NEWLINE | SyntaxKind::COMMENT
) => {}
_ => {
let range = element.text_range();
return Some((usize::from(range.start()), usize::from(range.end())));
}
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parser::parse;
fn map_of(src: &str) -> SuppressionMap {
SuppressionMap::build(&SyntaxNode::new_root(parse(src).green))
}
#[test]
fn file_all_suppresses_everything() {
let m = map_of("% badness-ignore-file: noisy\n\\bf\n");
assert!(m.is_suppressed("anything", 0, 1));
}
#[test]
fn file_rule_suppresses_only_that_rule() {
let m = map_of("% badness-ignore-file deprecated-command: legacy\n\\bf\n");
assert!(m.is_suppressed("deprecated-command", 0, 1));
assert!(!m.is_suppressed("duplicate-label", 0, 1));
}
#[test]
fn non_directive_comment_is_inert() {
let m = map_of("% just a note\n\\bf\n");
assert!(!m.is_suppressed("deprecated-command", 0, 1));
assert!(!m.file_all);
}
}