use std::collections::BTreeMap;
use rowan::TextRange;
use crate::{Diagnostic, DiagnosticCode, FileId};
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Suppressions {
pub disable_all: bool,
pub disable_file: bool,
pub file_codes: Vec<DiagnosticCode>,
pub malformed: Vec<MalformedDirective>,
pub line_directives: BTreeMap<u32, LineDirective>,
pub allow_scopes: Vec<AllowScope>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AllowScope {
pub range: TextRange,
pub codes: Vec<DiagnosticCode>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MalformedDirective {
pub range: TextRange,
pub text: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LineDirective {
pub kind: DirectiveKind,
pub codes: Option<Vec<DiagnosticCode>>,
pub range: TextRange,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DirectiveKind {
Disable,
Expect,
}
fn looks_like_directive_attempt(line: &str, comment: &str) -> bool {
if !line.trim_start().starts_with("//") {
return false;
}
let first = comment.split_whitespace().next().unwrap_or("");
first.starts_with("brink-disable") || first.starts_with("brink-expect")
}
#[must_use]
#[expect(
clippy::cast_possible_truncation,
reason = "source file line indices and byte offsets fit in u32 for any reasonable file"
)]
pub fn parse_suppressions(source: &str) -> Suppressions {
let mut result = Suppressions::default();
let mut byte_offset: u32 = 0;
for (line_idx, line) in source.lines().enumerate() {
let line_byte_start = byte_offset;
byte_offset += line.len() as u32;
let rest = &source[byte_offset as usize..];
if rest.starts_with("\r\n") {
byte_offset += 2;
} else if rest.starts_with('\n') || rest.starts_with('\r') {
byte_offset += 1;
}
let Some(comment_pos) = line.find("//") else {
continue;
};
let comment = line[comment_pos + 2..].trim();
let comment_byte_start = line_byte_start + comment_pos as u32;
let comment_byte_end = line_byte_start + line.len() as u32;
let comment_range = TextRange::new(comment_byte_start.into(), comment_byte_end.into());
let reportable = looks_like_directive_attempt(line, comment);
if comment == "brink-disable-all" {
result.disable_all = true;
} else if comment == "brink-disable-file-all" {
result.disable_file = true;
} else if let Some(codes_str) = comment.strip_prefix("brink-disable-file ") {
let codes: Vec<DiagnosticCode> = codes_str
.split_whitespace()
.filter_map(DiagnosticCode::from_str_code)
.collect();
if codes.is_empty() {
if reportable {
result.malformed.push(MalformedDirective {
range: comment_range,
text: comment.to_owned(),
});
}
} else {
result.file_codes.extend(codes);
}
} else if comment == "brink-disable-file" {
if reportable {
result.malformed.push(MalformedDirective {
range: comment_range,
text: comment.to_owned(),
});
}
} else if comment == "brink-disable" || comment == "brink-expect" {
let kind = if comment == "brink-expect" {
DirectiveKind::Expect
} else {
DirectiveKind::Disable
};
let target_line = (line_idx + 1) as u32;
result.line_directives.insert(
target_line,
LineDirective {
kind,
codes: None,
range: comment_range,
},
);
} else if let Some(codes_str) = comment
.strip_prefix("brink-disable ")
.or_else(|| comment.strip_prefix("brink-expect "))
{
let kind = if comment.starts_with("brink-expect") {
DirectiveKind::Expect
} else {
DirectiveKind::Disable
};
let codes: Vec<DiagnosticCode> = codes_str
.split_whitespace()
.filter_map(DiagnosticCode::from_str_code)
.collect();
if codes.is_empty() {
if reportable {
result.malformed.push(MalformedDirective {
range: comment_range,
text: comment.to_owned(),
});
}
} else {
let target_line = (line_idx + 1) as u32;
result.line_directives.insert(
target_line,
LineDirective {
kind,
codes: Some(codes),
range: comment_range,
},
);
}
} else if reportable {
result.malformed.push(MalformedDirective {
range: comment_range,
text: comment.to_owned(),
});
}
}
result
}
#[expect(
clippy::cast_possible_truncation,
reason = "source file byte offsets and line counts fit in u32 for any reasonable file"
)]
pub fn apply_suppressions(
file_id: FileId,
source: &str,
diagnostics: Vec<Diagnostic>,
suppressions: &Suppressions,
) -> Vec<Diagnostic> {
if suppressions.disable_file {
return Vec::new();
}
let line_starts: Vec<u32> = std::iter::once(0)
.chain(source.bytes().enumerate().filter_map(|(i, b)| {
if b == b'\n' {
Some((i + 1) as u32)
} else {
None
}
}))
.collect();
let mut expect_satisfied: BTreeMap<u32, bool> = BTreeMap::new();
for (&target_line, directive) in &suppressions.line_directives {
if directive.kind == DirectiveKind::Expect {
expect_satisfied.insert(target_line, false);
}
}
let mut result = Vec::with_capacity(diagnostics.len());
for diag in diagnostics {
if suppressions.file_codes.contains(&diag.code) {
continue;
}
let byte_offset: u32 = diag.range.start().into();
let line = line_starts
.partition_point(|&start| start <= byte_offset)
.saturating_sub(1) as u32;
if let Some(directive) = suppressions.line_directives.get(&line) {
let matches = match &directive.codes {
None => true,
Some(codes) => codes.contains(&diag.code),
};
if matches {
if directive.kind == DirectiveKind::Expect {
expect_satisfied.insert(line, true);
}
continue; }
}
if suppressions
.allow_scopes
.iter()
.any(|s| s.range.contains(diag.range.start()) && s.codes.contains(&diag.code))
{
continue;
}
result.push(diag);
}
for directive in &suppressions.malformed {
result.push(Diagnostic {
file: file_id,
range: directive.range,
message: format!(
"`// {}` is not a directive this compiler understands, so it suppresses \
nothing — did you mean `// brink-disable-file <CODE>…`, \
`// brink-disable-file-all`, or `// brink-disable <CODE>…`?",
directive.text
),
code: DiagnosticCode::E192,
});
}
for (&target_line, satisfied) in &expect_satisfied {
if !satisfied && let Some(directive) = suppressions.line_directives.get(&target_line) {
let message = match &directive.codes {
None => "expected a diagnostic on the next line, but none was produced".to_string(),
Some(codes) => {
let code_strs: Vec<&str> = codes.iter().map(|c| c.as_str()).collect();
format!(
"expected diagnostic {} on the next line, but it was not produced",
code_strs.join(", ")
)
}
};
result.push(Diagnostic {
file: file_id,
range: directive.range,
message,
code: DiagnosticCode::E036,
});
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_disable_all() {
let src = "// brink-disable-all\nHello\n";
let sup = parse_suppressions(src);
assert!(sup.disable_all);
assert!(!sup.disable_file);
assert!(sup.line_directives.is_empty());
}
#[test]
fn the_file_blanket_is_spelled_disable_file_all() {
let sup = parse_suppressions("// brink-disable-file-all\nHello\n");
assert!(!sup.disable_all);
assert!(sup.disable_file);
assert!(sup.malformed.is_empty(), "{:?}", sup.malformed);
}
#[test]
fn a_bare_disable_file_is_malformed_rather_than_a_silent_blanket() {
let sup = parse_suppressions("// brink-disable-file\nHello\n");
assert!(!sup.disable_file);
assert!(sup.file_codes.is_empty());
assert_eq!(sup.malformed.len(), 1, "{:?}", sup.malformed);
assert_eq!(sup.malformed[0].text, "brink-disable-file");
}
#[test]
fn a_file_directive_names_codes_whitespace_separated() {
let sup = parse_suppressions("// brink-disable-file E027 E028\nHello\n");
assert_eq!(
sup.file_codes,
vec![DiagnosticCode::E027, DiagnosticCode::E028]
);
assert!(
!sup.disable_file,
"naming codes must not silence everything"
);
assert!(sup.malformed.is_empty(), "{:?}", sup.malformed);
}
#[test]
fn a_file_directive_naming_only_unknown_codes_is_malformed() {
let sup = parse_suppressions("// brink-disable-file XXXX\nHello\n");
assert!(sup.file_codes.is_empty());
assert_eq!(sup.malformed.len(), 1, "{:?}", sup.malformed);
}
#[test]
fn a_misspelled_directive_in_the_disable_family_is_recorded() {
let sup = parse_suppressions("// brink-disable-fil E027\nHello\n");
assert_eq!(sup.malformed.len(), 1, "{:?}", sup.malformed);
assert_eq!(sup.malformed[0].text, "brink-disable-fil E027");
}
#[test]
fn prose_that_merely_mentions_a_brink_name_is_not_a_directive() {
let sup = parse_suppressions(
"// brink-environment`), so it now sits alongside a project's own\nHello\n",
);
assert!(sup.malformed.is_empty(), "{:?}", sup.malformed);
}
#[test]
fn an_ordinary_comment_is_not_a_directive() {
let sup = parse_suppressions("// just a note about brink\nHello\n");
assert!(sup.malformed.is_empty(), "{:?}", sup.malformed);
}
#[test]
fn a_mid_line_comment_is_never_reported() {
let sup = parse_suppressions("Text with // brink-disable-file in it\nHello\n");
assert!(sup.malformed.is_empty(), "{:?}", sup.malformed);
}
#[test]
fn a_badly_transposed_name_is_not_caught_and_that_is_deliberate() {
let sup = parse_suppressions("// brink-disabel-file E027\nHello\n");
assert!(sup.malformed.is_empty(), "{:?}", sup.malformed);
}
#[test]
fn parse_blanket_disable_next_line() {
let src = "// brink-disable\nHello\n";
let sup = parse_suppressions(src);
assert_eq!(sup.line_directives.len(), 1);
let dir = sup.line_directives.get(&1);
assert!(dir.is_some());
let dir = dir.unwrap();
assert_eq!(dir.kind, DirectiveKind::Disable);
assert!(dir.codes.is_none());
}
#[test]
fn parse_specific_disable() {
let src = "// brink-disable E027 E028\nHello\n";
let sup = parse_suppressions(src);
let dir = sup.line_directives.get(&1).unwrap();
assert_eq!(dir.kind, DirectiveKind::Disable);
let codes = dir.codes.as_ref().unwrap();
assert_eq!(codes, &[DiagnosticCode::E027, DiagnosticCode::E028]);
}
#[test]
fn parse_expect_blanket() {
let src = "// brink-expect\nHello\n";
let sup = parse_suppressions(src);
let dir = sup.line_directives.get(&1).unwrap();
assert_eq!(dir.kind, DirectiveKind::Expect);
assert!(dir.codes.is_none());
}
#[test]
fn parse_expect_specific() {
let src = "// brink-expect E025\nHello\n";
let sup = parse_suppressions(src);
let dir = sup.line_directives.get(&1).unwrap();
assert_eq!(dir.kind, DirectiveKind::Expect);
let codes = dir.codes.as_ref().unwrap();
assert_eq!(codes, &[DiagnosticCode::E025]);
}
#[test]
fn parse_ignores_invalid_codes() {
let src = "// brink-disable XXXX E027\nHello\n";
let sup = parse_suppressions(src);
let dir = sup.line_directives.get(&1).unwrap();
let codes = dir.codes.as_ref().unwrap();
assert_eq!(codes, &[DiagnosticCode::E027]);
}
#[test]
fn parse_all_invalid_codes_produces_no_directive() {
let src = "// brink-disable XXXX YYYY\nHello\n";
let sup = parse_suppressions(src);
assert!(sup.line_directives.is_empty());
}
#[test]
fn apply_disable_file_removes_all() {
let file_id = FileId(0);
let source = "// brink-disable-file\nHello\n";
let sup = Suppressions {
disable_file: true,
..Suppressions::default()
};
let diags = vec![Diagnostic {
file: file_id,
range: TextRange::new(22.into(), 27.into()),
message: "test".to_string(),
code: DiagnosticCode::E025,
}];
let result = apply_suppressions(file_id, source, diags, &sup);
assert!(result.is_empty());
}
#[test]
fn apply_blanket_disable_suppresses() {
let file_id = FileId(0);
let source = "// brink-disable\nHello\n";
let sup = parse_suppressions(source);
let diags = vec![Diagnostic {
file: file_id,
range: TextRange::new(17.into(), 22.into()), message: "test".to_string(),
code: DiagnosticCode::E025,
}];
let result = apply_suppressions(file_id, source, diags, &sup);
assert!(result.is_empty());
}
#[test]
fn apply_specific_disable_only_matches_code() {
let file_id = FileId(0);
let source = "// brink-disable E027\nHello\n";
let sup = parse_suppressions(source);
let diags = vec![
Diagnostic {
file: file_id,
range: TextRange::new(22.into(), 27.into()),
message: "ambiguous".to_string(),
code: DiagnosticCode::E027,
},
Diagnostic {
file: file_id,
range: TextRange::new(22.into(), 27.into()),
message: "unresolved".to_string(),
code: DiagnosticCode::E025,
},
];
let result = apply_suppressions(file_id, source, diags, &sup);
assert_eq!(result.len(), 1);
assert_eq!(result[0].code, DiagnosticCode::E025);
}
#[test]
fn apply_expect_satisfied() {
let file_id = FileId(0);
let source = "// brink-expect E025\nHello\n";
let sup = parse_suppressions(source);
let diags = vec![Diagnostic {
file: file_id,
range: TextRange::new(21.into(), 26.into()),
message: "unresolved".to_string(),
code: DiagnosticCode::E025,
}];
let result = apply_suppressions(file_id, source, diags, &sup);
assert!(result.is_empty()); }
#[test]
fn apply_expect_unsatisfied_emits_e036() {
let file_id = FileId(0);
let source = "// brink-expect E025\nHello\n";
let sup = parse_suppressions(source);
let diags = vec![]; let result = apply_suppressions(file_id, source, diags, &sup);
assert_eq!(result.len(), 1);
assert_eq!(result[0].code, DiagnosticCode::E036);
assert!(result[0].message.contains("E025"));
}
#[test]
fn apply_blanket_expect_unsatisfied() {
let file_id = FileId(0);
let source = "// brink-expect\nHello\n";
let sup = parse_suppressions(source);
let diags = vec![];
let result = apply_suppressions(file_id, source, diags, &sup);
assert_eq!(result.len(), 1);
assert_eq!(result[0].code, DiagnosticCode::E036);
assert!(result[0].message.contains("expected a diagnostic"));
}
#[test]
fn diagnostic_on_wrong_line_not_suppressed() {
let file_id = FileId(0);
let source = "// brink-disable\nOk line\nBad line\n";
let sup = parse_suppressions(source);
let diags = vec![Diagnostic {
file: file_id,
range: TextRange::new(25.into(), 33.into()),
message: "test".to_string(),
code: DiagnosticCode::E025,
}];
let result = apply_suppressions(file_id, source, diags, &sup);
assert_eq!(result.len(), 1);
}
fn diag_at(code: DiagnosticCode, start: u32, end: u32) -> Diagnostic {
Diagnostic {
file: FileId(0),
range: TextRange::new(start.into(), end.into()),
message: "test".to_string(),
code,
}
}
fn with_scope(codes: Vec<DiagnosticCode>, start: u32, end: u32) -> Suppressions {
Suppressions {
allow_scopes: vec![AllowScope {
range: TextRange::new(start.into(), end.into()),
codes,
}],
..Suppressions::default()
}
}
#[test]
fn allow_scope_suppresses_a_matching_code_inside_it() {
let sup = with_scope(vec![DiagnosticCode::E014], 10, 40);
let result = apply_suppressions(
FileId(0),
"",
vec![diag_at(DiagnosticCode::E014, 20, 25)],
&sup,
);
assert!(result.is_empty(), "{result:?}");
}
#[test]
fn allow_scope_leaves_an_unnamed_code_alone() {
let sup = with_scope(vec![DiagnosticCode::E014], 10, 40);
let result = apply_suppressions(
FileId(0),
"",
vec![diag_at(DiagnosticCode::E035, 20, 25)],
&sup,
);
assert_eq!(result.len(), 1, "{result:?}");
}
#[test]
fn allow_scope_does_not_leak_past_the_declaration() {
let sup = with_scope(vec![DiagnosticCode::E014], 10, 40);
let result = apply_suppressions(
FileId(0),
"",
vec![
diag_at(DiagnosticCode::E014, 5, 9),
diag_at(DiagnosticCode::E014, 40, 45),
],
&sup,
);
assert_eq!(result.len(), 2, "{result:?}");
}
#[test]
fn allow_scope_matches_on_the_diagnostic_start_offset() {
let sup = with_scope(vec![DiagnosticCode::E014], 10, 40);
let result = apply_suppressions(
FileId(0),
"",
vec![diag_at(DiagnosticCode::E014, 38, 60)],
&sup,
);
assert!(result.is_empty(), "{result:?}");
}
#[test]
fn allow_scope_does_not_satisfy_a_brink_expect() {
let source = "// brink-expect E014\nHello\n";
let mut sup = parse_suppressions(source);
sup.allow_scopes = vec![AllowScope {
range: TextRange::new(0.into(), 100.into()),
codes: vec![DiagnosticCode::E014],
}];
let diags = vec![Diagnostic {
file: FileId(0),
range: TextRange::new(21.into(), 26.into()), message: "test".to_string(),
code: DiagnosticCode::E014,
}];
let result = apply_suppressions(FileId(0), source, diags, &sup);
assert!(result.is_empty(), "{result:?}");
}
#[test]
fn disable_file_still_wins_over_everything() {
let sup = Suppressions {
disable_file: true,
..with_scope(vec![DiagnosticCode::E014], 10, 40)
};
let result = apply_suppressions(
FileId(0),
"",
vec![diag_at(DiagnosticCode::E035, 20, 25)],
&sup,
);
assert!(result.is_empty(), "{result:?}");
}
}