use std::collections::{HashMap, HashSet};
pub fn parse_ignore_comment(line: &str) -> Option<Vec<&str>> {
line.trim()
.strip_prefix('#')
.map(str::trim)
.and_then(|s| s.strip_prefix("nu-lint-ignore:"))
.map(|rules_part| {
rules_part
.split(',')
.map(str::trim)
.filter(|s| !s.is_empty())
.collect()
})
}
pub struct IgnoreIndex {
ignored_lines: HashMap<usize, HashSet<String>>,
line_offsets: Vec<usize>,
}
impl IgnoreIndex {
pub fn new(source: &str) -> Self {
let lines: Vec<&str> = source.lines().collect();
let mut ignored_lines = HashMap::new();
let mut line_offsets = vec![0];
for (pos, ch) in source.char_indices() {
if ch == '\n' {
line_offsets.push(pos + 1);
}
}
for (line_num, line) in lines.iter().enumerate() {
if let Some(rules) = parse_ignore_comment(line) {
let rule_set: HashSet<String> = rules.iter().map(|&s| String::from(s)).collect();
let target = find_target_line(&lines, line_num + 1);
ignored_lines
.entry(target)
.or_insert_with(HashSet::new)
.extend(rule_set);
} else if let Some(comment_start) = line.find('#') {
let comment_part = &line[comment_start..];
if let Some(rules) = parse_ignore_comment(comment_part) {
let rule_set: HashSet<String> =
rules.iter().map(|&s| String::from(s)).collect();
ignored_lines
.entry(line_num)
.or_insert_with(HashSet::new)
.extend(rule_set);
}
}
}
Self {
ignored_lines,
line_offsets,
}
}
pub fn should_ignore(&self, byte_offset: usize, rule_id: &str) -> bool {
let line = self.offset_to_line(byte_offset);
self.ignored_lines
.get(&line)
.is_some_and(|rules| rules.contains(rule_id))
}
fn offset_to_line(&self, offset: usize) -> usize {
self.line_offsets
.partition_point(|&start| start <= offset)
.saturating_sub(1)
}
}
fn find_target_line(lines: &[&str], start: usize) -> usize {
lines
.iter()
.enumerate()
.skip(start)
.find(|(_, line)| {
let trimmed = line.trim();
!trimmed.is_empty() && !trimmed.starts_with('@')
})
.map_or(start, |(i, _)| i)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_single_rule() {
assert_eq!(
parse_ignore_comment("# nu-lint-ignore: my_rule"),
Some(vec!["my_rule"])
);
}
#[test]
fn parse_multiple_rules() {
assert_eq!(
parse_ignore_comment("# nu-lint-ignore: rule_a, rule_b"),
Some(vec!["rule_a", "rule_b"])
);
}
#[test]
fn parse_regular_comment_returns_none() {
assert_eq!(parse_ignore_comment("# regular comment"), None);
}
#[test]
fn ignore_matching_rule() {
let source = "# nu-lint-ignore: my_rule\nlet x = 1";
let index = IgnoreIndex::new(source);
assert!(index.should_ignore(26, "my_rule"));
}
#[test]
fn dont_ignore_other_rule() {
let source = "# nu-lint-ignore: my_rule\nlet x = 1";
let index = IgnoreIndex::new(source);
assert!(!index.should_ignore(26, "other_rule"));
}
#[test]
fn dont_ignore_without_comment() {
let source = "let x = 1";
let index = IgnoreIndex::new(source);
assert!(!index.should_ignore(0, "my_rule"));
}
#[test]
fn ignore_with_attributes() {
let source = "# nu-lint-ignore: my_rule\n@search-terms 'test'\ndef my-cmd [] {}";
let index = IgnoreIndex::new(source);
let def_offset = source.find("def").unwrap();
assert!(index.should_ignore(def_offset, "my_rule"));
}
#[test]
fn ignore_with_multiple_attributes() {
let source =
"# nu-lint-ignore: my_rule\n@category 'test'\n@search-terms 'a'\ndef my-cmd [] {}";
let index = IgnoreIndex::new(source);
let def_offset = source.find("def").unwrap();
assert!(index.should_ignore(def_offset, "my_rule"));
}
#[test]
fn ignore_inline_comment() {
let source = "let x = 1 # nu-lint-ignore: my_rule";
let index = IgnoreIndex::new(source);
assert!(index.should_ignore(0, "my_rule"));
assert!(index.should_ignore(4, "my_rule"));
}
#[test]
fn ignore_inline_with_multiple_rules() {
let source = "let x = 1 # nu-lint-ignore: rule_a, rule_b";
let index = IgnoreIndex::new(source);
assert!(index.should_ignore(0, "rule_a"));
assert!(index.should_ignore(0, "rule_b"));
assert!(!index.should_ignore(0, "rule_c"));
}
}