use std::collections::BTreeSet;
use std::fmt;
use std::sync::OnceLock;
use serde::{Deserialize, Serialize};
use crate::checker::Checker;
use crate::getter::Getter;
use crate::metric_set::Metric;
use crate::node::{Ancestors, Node};
use crate::traits::ParserTrait;
#[must_use]
pub fn threshold_metric_for_name(name: &str) -> Option<Metric> {
let family = name.split_once('.').map_or(name, |(prefix, _)| prefix);
if family == "tokens" {
return None;
}
family.parse().ok()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SuppressionPolicy {
Honor,
Ignore,
}
impl SuppressionPolicy {
#[must_use]
pub const fn from_no_suppress(no_suppress: bool) -> Self {
if no_suppress {
Self::Ignore
} else {
Self::Honor
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", tag = "kind", content = "metrics")]
pub enum SuppressionScope {
All,
Some(BTreeSet<Metric>),
}
impl Default for SuppressionScope {
fn default() -> Self {
Self::Some(BTreeSet::new())
}
}
impl SuppressionScope {
#[must_use]
pub fn is_all(&self) -> bool {
matches!(self, Self::All)
}
#[must_use]
pub fn is_empty(&self) -> bool {
matches!(self, Self::Some(s) if s.is_empty())
}
#[must_use]
pub fn covers(&self, metric: Metric) -> bool {
match self {
Self::All => true,
Self::Some(s) => s.contains(&metric),
}
}
pub fn merge(&mut self, other: &SuppressionScope) {
match (&mut *self, other) {
(Self::All, _) => {}
(slot, Self::All) => *slot = Self::All,
(Self::Some(a), Self::Some(b)) => a.extend(b.iter().copied()),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum SuppressionKind {
Function,
File,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum SuppressionSource {
Native,
Lizard,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Suppression {
pub(crate) kind: SuppressionKind,
pub(crate) scope: SuppressionScope,
pub(crate) source: SuppressionSource,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct MarkerScan {
pub(crate) suppression: Option<Suppression>,
pub(crate) diagnostics: Vec<SuppressionError>,
}
impl MarkerScan {
fn not_a_marker() -> Self {
Self {
suppression: None,
diagnostics: Vec::new(),
}
}
fn rejected(error: SuppressionError) -> Self {
Self {
suppression: None,
diagnostics: vec![error],
}
}
fn directive(suppression: Suppression) -> Self {
Self {
suppression: Some(suppression),
diagnostics: Vec::new(),
}
}
fn partial(suppression: Suppression, diagnostics: Vec<SuppressionError>) -> Self {
Self {
suppression: Some(suppression),
diagnostics,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum SuppressionError {
UnknownVerb(String),
UnknownMetric(String),
NonSuppressibleMetric(String),
MalformedBody(String),
ElidedDiagnostics(usize),
}
const MAX_MARKER_DIAGNOSTICS: usize = 8;
fn suppressible_metric_hint() -> &'static str {
static HINT: OnceLock<String> = OnceLock::new();
HINT.get_or_init(|| {
let mut names: Vec<String> = Metric::suppressible()
.map(|metric| metric.to_string())
.collect();
names.sort_unstable();
names.join(", ")
})
}
impl fmt::Display for SuppressionError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::UnknownVerb(v) => write!(
f,
"unknown bca directive verb '{v}'; expected `suppress` or `suppress-file`"
),
Self::UnknownMetric(m) => {
write!(
f,
"unknown metric '{m}' in bca suppression marker; known metrics: {}",
suppressible_metric_hint()
)
}
Self::NonSuppressibleMetric(m) => {
write!(f, "metric '{m}' has no threshold and cannot be suppressed")
}
Self::ElidedDiagnostics(n) => write!(
f,
"… and {n} more unusable metric name(s) in this bca suppression marker"
),
Self::MalformedBody(body) => {
write!(
f,
"malformed bca suppression marker body '{body}'; expected \
`bca: suppress` / `bca: suppress-file` with nothing after \
the verb, or `bca: suppress(<metrics>)`, which may carry a \
rationale (`bca: suppress(cognitive, cyclomatic) — \
reason`); to keep a reason here, name the metrics or move \
the reason to the line above"
)
}
}
}
}
impl std::error::Error for SuppressionError {}
pub(crate) fn parse_marker(comment_text: &str) -> MarkerScan {
if !comment_text.contains("bca:") && !comment_text.contains("lizard") {
return MarkerScan::not_a_marker();
}
let trimmed = strip_block_delims(comment_text.trim()).trim();
let no_opener = trimmed
.trim_start_matches(|c: char| {
c == '/' || c == '*' || c == '!' || c == ';' || c == '-' || c.is_whitespace()
})
.trim_end_matches(|c: char| c == '*' || c == '/' || c.is_whitespace())
.trim();
let lizard_candidate = if no_opener.starts_with("#l") {
no_opener
} else if let Some(rest) = no_opener.strip_prefix('#') {
rest.trim_start()
} else {
no_opener
};
if let Some(suppression) = parse_lizard(lizard_candidate) {
return MarkerScan::directive(suppression);
}
let body = no_opener
.trim_start_matches(|c: char| c == '#' || c.is_whitespace())
.trim();
parse_native(body)
}
fn strip_block_delims(s: &str) -> &str {
let s = s.strip_prefix("/*").unwrap_or(s);
s.strip_suffix("*/").unwrap_or(s)
}
fn parse_lizard(trimmed: &str) -> Option<Suppression> {
let s = trimmed.strip_prefix('#')?.trim_start();
let s = s.strip_prefix("lizard")?;
let rest = s.trim();
if rest == "forgives" {
return Some(Suppression {
kind: SuppressionKind::Function,
scope: SuppressionScope::All,
source: SuppressionSource::Lizard,
});
}
if rest == "forgive global" {
return Some(Suppression {
kind: SuppressionKind::File,
scope: SuppressionScope::All,
source: SuppressionSource::Lizard,
});
}
None
}
fn parse_native(body: &str) -> MarkerScan {
let Some(rest) = body.strip_prefix("bca:") else {
return MarkerScan::not_a_marker();
};
let rest = rest.trim_start();
if rest.is_empty() {
return MarkerScan::not_a_marker();
}
let malformed = || MarkerScan::rejected(SuppressionError::MalformedBody(body.to_owned()));
let verb_end = rest
.find(|c: char| !(c.is_ascii_alphabetic() || c == '-'))
.unwrap_or(rest.len());
let (verb, after_verb) = rest.split_at(verb_end);
let kind = match verb {
"suppress" => SuppressionKind::Function,
"suppress-file" => SuppressionKind::File,
"" => return malformed(),
other => return MarkerScan::rejected(SuppressionError::UnknownVerb(other.to_owned())),
};
let after_verb = after_verb.trim_start();
let (scope, diagnostics) = if after_verb.is_empty() {
(SuppressionScope::All, Vec::new())
} else if let Some(list) = after_verb.strip_prefix('(') {
let Some(close) = list.find(')') else {
return malformed();
};
let (metrics, diagnostics) = parse_metric_list(&list[..close]);
(SuppressionScope::Some(metrics), diagnostics)
} else {
return malformed();
};
MarkerScan::partial(
Suppression {
kind,
scope,
source: SuppressionSource::Native,
},
diagnostics,
)
}
fn parse_metric_list(inside: &str) -> (BTreeSet<Metric>, Vec<SuppressionError>) {
let mut set = BTreeSet::new();
let mut diagnostics = Vec::new();
let mut reported: BTreeSet<&str> = BTreeSet::new();
let mut unusable = 0_usize;
for token in inside.split(',') {
let name = token.trim();
if name.is_empty() {
continue;
}
let unusable_name = match name.parse::<Metric>() {
Ok(Metric::Tokens) => SuppressionError::NonSuppressibleMetric(name.to_owned()),
Ok(metric) => {
set.insert(metric);
continue;
}
Err(_) => SuppressionError::UnknownMetric(name.to_owned()),
};
if reported.insert(name) {
unusable += 1;
if diagnostics.len() < MAX_MARKER_DIAGNOSTICS {
diagnostics.push(unusable_name);
}
}
}
let elided = unusable.saturating_sub(MAX_MARKER_DIAGNOSTICS);
if elided > 0 {
diagnostics.push(SuppressionError::ElidedDiagnostics(elided));
}
(set, diagnostics)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum SuppressionTarget {
Function,
File,
}
impl From<SuppressionKind> for SuppressionTarget {
fn from(kind: SuppressionKind) -> Self {
match kind {
SuppressionKind::Function => Self::Function,
SuppressionKind::File => Self::File,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum SuppressionDialect {
Native,
Lizard,
}
impl From<SuppressionSource> for SuppressionDialect {
fn from(source: SuppressionSource) -> Self {
match source {
SuppressionSource::Native => Self::Native,
SuppressionSource::Lizard => Self::Lizard,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct SuppressionMarker {
pub line: usize,
pub target: SuppressionTarget,
pub scope: SuppressionScope,
pub dialect: SuppressionDialect,
pub function: Option<String>,
}
#[must_use]
pub(crate) fn suppression_markers<T: ParserTrait>(parser: &T) -> Vec<SuppressionMarker> {
let code = parser.code();
let mut markers = Vec::new();
let mut chain: Vec<Node<'_>> = Vec::new();
let root = parser.root();
let mut stack: Vec<(Node<'_>, Option<&str>, usize)> = vec![(root, None, 0)];
let mut cursor = root.cursor();
while let Some((node, enclosing, depth)) = stack.pop() {
chain.truncate(depth);
if let Some(marker) = marker_at::<T>(&node, code, enclosing) {
markers.push(marker);
}
let ancestors = Ancestors::checked(&chain, &node);
let child_enclosing = if T::Checker::is_func_with_code(&node, code, ancestors) {
T::Getter::get_func_name(&node, code, ancestors).or(enclosing)
} else {
enclosing
};
chain.push(node);
stack.extend(
node.children_with(&mut cursor)
.map(|child| (child, child_enclosing, depth + 1)),
);
}
markers.sort_by_key(|m| m.line);
markers
}
fn marker_at<T: ParserTrait>(
node: &Node<'_>,
code: &[u8],
enclosing: Option<&str>,
) -> Option<SuppressionMarker> {
if !T::Checker::is_comment(node) {
return None;
}
let suppression = parse_marker(node.utf8_text(code)?).suppression?;
let function = match suppression.kind {
SuppressionKind::Function => enclosing.map(str::to_owned),
SuppressionKind::File => None,
};
Some(SuppressionMarker {
line: node.start_row() + 1,
target: suppression.kind.into(),
scope: suppression.scope,
dialect: suppression.source.into(),
function,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[track_caller]
fn marker(text: &str) -> Suppression {
let scan = parse_marker(text);
assert!(
scan.diagnostics.is_empty(),
"expected a clean parse of {text:?}; got {:?}",
scan.diagnostics,
);
scan.suppression
.unwrap_or_else(|| panic!("expected {text:?} to parse as a marker"))
}
fn scan_diagnostics(text: &str) -> Vec<SuppressionError> {
parse_marker(text).diagnostics
}
fn is_not_a_marker(text: &str) -> bool {
let scan = parse_marker(text);
scan.suppression.is_none() && scan.diagnostics.is_empty()
}
#[track_caller]
fn sole_diagnostic(text: &str) -> SuppressionError {
let mut diagnostics = scan_diagnostics(text);
assert_eq!(
diagnostics.len(),
1,
"expected exactly one diagnostic for {text:?}; got {diagnostics:?}",
);
diagnostics.remove(0)
}
#[track_caller]
fn voiding_diagnostic(text: &str) -> SuppressionError {
let scan = parse_marker(text);
assert!(
scan.suppression.is_none(),
"expected {text:?} to yield no directive; got {:?}",
scan.suppression,
);
sole_diagnostic(text)
}
#[test]
fn native_bare_suppress_covers_all_for_function() {
let s = marker("// bca: suppress");
assert_eq!(s.kind, SuppressionKind::Function);
assert_eq!(s.source, SuppressionSource::Native);
assert!(matches!(s.scope, SuppressionScope::All));
}
#[test]
fn native_suppress_with_metric_list() {
let s = marker("// bca: suppress(cyclomatic, cognitive)");
assert_eq!(s.kind, SuppressionKind::Function);
let SuppressionScope::Some(metrics) = s.scope else {
panic!("expected Some(...)");
};
assert!(metrics.contains(&Metric::Cyclomatic));
assert!(metrics.contains(&Metric::Cognitive));
assert_eq!(metrics.len(), 2);
}
#[test]
fn native_mixed_valid_and_unknown_metric_keeps_the_valid_half() {
let scan = parse_marker("// bca: suppress(cyclomatic, no_such_metric)");
let Some(Suppression {
scope: SuppressionScope::Some(metrics),
..
}) = &scan.suppression
else {
panic!(
"expected an explicit metric set; got {:?}",
scan.suppression
);
};
assert_eq!(
metrics.iter().copied().collect::<Vec<_>>(),
vec![Metric::Cyclomatic],
"the recognized half of the list must still suppress",
);
assert!(
matches!(
scan.diagnostics.as_slice(),
[SuppressionError::UnknownMetric(name)] if name == "no_such_metric",
),
"the unrecognized half must still be reported; got {:?}",
scan.diagnostics,
);
}
#[test]
fn native_suppress_file_bare() {
let s = marker("# bca: suppress-file");
assert_eq!(s.kind, SuppressionKind::File);
assert!(matches!(s.scope, SuppressionScope::All));
}
#[test]
fn native_suppress_file_with_metric_list() {
let s = marker("/* bca: suppress-file(halstead, loc) */");
assert_eq!(s.kind, SuppressionKind::File);
let SuppressionScope::Some(metrics) = s.scope else {
panic!("expected Some(...)");
};
assert!(metrics.contains(&Metric::Halstead));
assert!(metrics.contains(&Metric::Loc));
}
#[test]
fn native_unknown_metric_errors() {
let err = sole_diagnostic("// bca: suppress(no_such_metric)");
assert!(matches!(err, SuppressionError::UnknownMetric(_)));
let rendered = err.to_string();
assert!(rendered.contains("no_such_metric"));
assert!(rendered.contains("cyclomatic"));
assert!(
!rendered.contains("tokens"),
"hint must omit the non-suppressible `tokens`; got: {rendered}",
);
let mut expected: Vec<String> = Metric::suppressible()
.map(|metric| metric.to_string())
.collect();
expected.sort_unstable();
assert!(
rendered.ends_with(&format!("known metrics: {}", expected.join(", "))),
"hint must list exactly the suppressible metrics from \
`Metric::suppressible()`, alphabetised; got: {rendered}",
);
}
#[test]
fn native_tokens_is_not_suppressible() {
let err = sole_diagnostic("// bca: suppress(tokens)");
assert!(
matches!(&err, SuppressionError::NonSuppressibleMetric(m) if m == "tokens"),
"expected NonSuppressibleMetric(\"tokens\"); got: {err:?}",
);
let rendered = err.to_string();
assert!(rendered.contains("tokens"));
assert!(
rendered.contains("no threshold"),
"message must explain why tokens cannot be suppressed; got: {rendered}",
);
}
#[test]
fn native_unknown_verb_errors() {
let err = voiding_diagnostic("// bca: disable");
assert!(matches!(err, SuppressionError::UnknownVerb(_)));
let rendered = err.to_string();
assert!(
rendered.contains("`suppress`"),
"expected message to name the bare `suppress` verb; got: {rendered}"
);
assert!(
rendered.contains("`suppress-file`"),
"expected message to name the `suppress-file` verb; got: {rendered}"
);
}
#[test]
fn legacy_allow_verb_is_unknown() {
let err = voiding_diagnostic("// bca: allow");
assert!(matches!(err, SuppressionError::UnknownVerb(v) if v == "allow"));
let err = voiding_diagnostic("// bca: allow-file");
assert!(matches!(err, SuppressionError::UnknownVerb(v) if v == "allow-file"));
let err = voiding_diagnostic("// bca: allow(cyclomatic)");
assert!(matches!(err, SuppressionError::UnknownVerb(v) if v == "allow"));
}
#[test]
fn native_malformed_body_errors() {
assert!(matches!(
voiding_diagnostic("// bca: suppress(cyclomatic"),
SuppressionError::MalformedBody(_)
));
assert!(matches!(
voiding_diagnostic("// bca: suppress garbage"),
SuppressionError::MalformedBody(_)
));
}
#[test]
fn malformed_body_message_names_the_accepted_shapes() {
let rendered = voiding_diagnostic("// bca: suppress - see #123").to_string();
assert!(
rendered.contains("bca: suppress - see #123"),
"message must echo the offending body; got: {rendered}",
);
assert!(
rendered.contains("`bca: suppress(<metrics>)`"),
"message must name the metric-list shape; got: {rendered}",
);
assert!(
rendered.contains("rationale"),
"message must point at the rationale form; got: {rendered}",
);
assert!(
rendered.contains("name the metrics"),
"message must tell the author to name the metrics; got: {rendered}",
);
assert!(
rendered.contains("line above"),
"message must offer the move-the-reason-up escape; got: {rendered}",
);
}
#[test]
fn native_bare_colon_is_not_a_marker() {
let scan = parse_marker("// bca:");
assert_eq!(scan.suppression, None);
assert!(scan.diagnostics.is_empty());
}
#[test]
fn empty_metric_list_is_noop_not_error() {
let s = marker("// bca: suppress()");
assert!(s.scope.is_empty());
assert!(!s.scope.covers(Metric::Cyclomatic));
}
#[test]
fn trailing_rationale_after_metric_list_is_accepted() {
for text in [
"// bca: suppress(nargs) \u{2014} threaded context, not a god-function",
"// bca: suppress(nargs) \u{2013} threaded context",
"// bca: suppress(nargs) - threaded context",
"// bca: suppress(nargs): threaded context",
"// bca: suppress(nargs) // threaded context",
"// bca: suppress(nargs) threaded context",
"/* bca: suppress(nargs) \u{2014} threaded context */",
] {
let s = marker(text);
assert_eq!(s.kind, SuppressionKind::Function, "for {text:?}");
assert!(
matches!(&s.scope, SuppressionScope::Some(m)
if m.iter().copied().eq([Metric::Nargs])),
"rationale must not disturb the metric list; {text:?} gave {:?}",
s.scope,
);
}
}
#[test]
fn a_bare_verb_takes_no_trailing_text_whatever_the_separator() {
for text in [
"// bca: suppress \u{2014} irreducible dispatch",
"// bca: suppress \u{2013} irreducible dispatch",
"// bca: suppress - we removed this marker, see #123",
"// bca: suppress: not applicable to this function",
"// bca: suppress // generated shim",
"// bca: suppress /some/path",
"// bca: suppress markers are honoured here",
"# bca: suppress-file # generated",
"// bca: suppress-file generated file",
] {
let scan = parse_marker(text);
assert_eq!(
scan.suppression, None,
"a bare verb plus trailing text must not suppress; {text:?}",
);
assert!(
matches!(
scan.diagnostics.as_slice(),
[SuppressionError::MalformedBody(_)]
),
"{text:?} must warn that the marker is inert; got {:?}",
scan.diagnostics,
);
}
assert!(matches!(
marker("// bca: suppress").scope,
SuppressionScope::All
));
assert!(matches!(
marker("// bca: suppress(nargs) \u{2014} threaded context").scope,
SuppressionScope::Some(_)
));
}
#[test]
fn unusable_names_are_deduplicated_and_capped_per_marker() {
let repeated = ["nope"; 500].join(",");
let scan = parse_marker(&format!("// bca: suppress({repeated})"));
assert_eq!(
scan.diagnostics,
vec![SuppressionError::UnknownMetric("nope".to_owned())],
"500 copies of one name must cost exactly one diagnostic",
);
let overflow = 5;
let distinct: Vec<String> = (0..MAX_MARKER_DIAGNOSTICS + overflow)
.map(|i| format!("nope{i}"))
.collect();
let scan = parse_marker(&format!("// bca: suppress({})", distinct.join(",")));
assert_eq!(
scan.diagnostics.len(),
MAX_MARKER_DIAGNOSTICS + 1,
"expected {MAX_MARKER_DIAGNOSTICS} names plus one tail; got {:?}",
scan.diagnostics,
);
assert_eq!(
scan.diagnostics.last(),
Some(&SuppressionError::ElidedDiagnostics(overflow)),
"the elided count must survive the cap; got {:?}",
scan.diagnostics,
);
let tail = scan
.diagnostics
.last()
.expect("the cap always appends a tail")
.to_string();
assert!(
tail.contains(&overflow.to_string()),
"the rendered tail must name how many were elided; got {tail:?}",
);
assert!(
tail.contains("more unusable metric name"),
"the rendered tail must say what was elided; got {tail:?}",
);
let scan = parse_marker(&format!(
"// bca: suppress(cognitive,{})",
distinct.join(",")
));
let Some(Suppression {
scope: SuppressionScope::Some(metrics),
..
}) = &scan.suppression
else {
panic!(
"expected an explicit metric set; got {:?}",
scan.suppression
);
};
assert!(
metrics.contains(&Metric::Cognitive),
"capping diagnostics must not narrow the suppression; got {metrics:?}",
);
}
#[test]
fn rationale_may_contain_parentheses_and_marker_syntax() {
let s = marker("// bca: suppress(nargs) — mirrors suppress(abc) in do_thing(x)");
assert!(
matches!(&s.scope, SuppressionScope::Some(m)
if m.iter().copied().eq([Metric::Nargs])),
"got {:?}",
s.scope,
);
}
#[test]
fn rationale_survives_a_flawed_metric_list() {
let scan = parse_marker("// bca: suppress(cognitive, exit) — hand-rolled state machine");
assert!(
matches!(&scan.suppression, Some(s)
if matches!(&s.scope, SuppressionScope::Some(m)
if m.iter().copied().eq([Metric::Cognitive]))),
"got {:?}",
scan.suppression,
);
assert!(
matches!(
scan.diagnostics.as_slice(),
[SuppressionError::UnknownMetric(name)] if name == "exit",
),
"`exit` is the documented `nexits` typo and must still be \
reported; got {:?}",
scan.diagnostics,
);
}
#[test]
fn whitespace_only_rationale_is_not_a_diagnostic() {
let s = marker("// bca: suppress(nargs) \t ");
assert!(matches!(&s.scope, SuppressionScope::Some(m) if m.len() == 1));
}
#[test]
fn lizard_function_marker() {
let s = marker("// #lizard forgives");
assert_eq!(s.kind, SuppressionKind::Function);
assert_eq!(s.source, SuppressionSource::Lizard);
assert!(matches!(s.scope, SuppressionScope::All));
}
#[test]
fn lizard_file_marker() {
let s = marker("# #lizard forgive global");
assert_eq!(s.kind, SuppressionKind::File);
assert_eq!(s.source, SuppressionSource::Lizard);
}
#[test]
fn lizard_unknown_phrase_is_not_a_marker() {
assert!(is_not_a_marker("// #lizard skip"));
}
#[test]
fn plain_comment_is_not_a_marker() {
assert!(is_not_a_marker("// just a comment"));
assert!(is_not_a_marker("/* TODO: fix later */"));
}
#[test]
fn fast_bail_skips_sigil_free_comments() {
assert!(is_not_a_marker("// Copyright (c) 2026 Some Corp."));
assert!(is_not_a_marker("/* SPDX-License-Identifier: MIT */"));
assert!(is_not_a_marker("// authors: jane lizard, john doe"));
}
#[test]
fn marker_grammar_is_case_sensitive() {
assert!(is_not_a_marker("// Bca: suppress"));
assert!(is_not_a_marker("/* BCA: suppress */"));
assert!(is_not_a_marker("# #Lizard forgives"));
assert!(is_not_a_marker("// #Lizard forgives"));
}
#[test]
fn scope_merge_all_absorbs() {
let mut a = SuppressionScope::Some(BTreeSet::from([Metric::Loc]));
a.merge(&SuppressionScope::All);
assert!(a.is_all());
let mut b = SuppressionScope::All;
b.merge(&SuppressionScope::Some(BTreeSet::from([Metric::Loc])));
assert!(b.is_all());
}
#[test]
fn scope_merge_some_unions() {
let mut a = SuppressionScope::Some(BTreeSet::from([Metric::Loc]));
a.merge(&SuppressionScope::Some(BTreeSet::from([Metric::Cognitive])));
assert!(a.covers(Metric::Loc));
assert!(a.covers(Metric::Cognitive));
assert!(!a.covers(Metric::Cyclomatic));
}
#[test]
fn scope_covers_respects_all_vs_some() {
assert!(SuppressionScope::All.covers(Metric::Cyclomatic));
let some = SuppressionScope::Some(BTreeSet::from([Metric::Loc]));
assert!(some.covers(Metric::Loc));
assert!(!some.covers(Metric::Cyclomatic));
}
#[test]
fn scope_serialization_uses_canonical_names_and_stable_order() {
let scope = SuppressionScope::Some(BTreeSet::from([
Metric::Wmc,
Metric::Nexits,
Metric::Nargs,
Metric::Cognitive,
]));
let json = serde_json::to_string(&scope).unwrap();
assert_eq!(
json,
r#"{"kind":"some","metrics":["cognitive","nargs","nexits","wmc"]}"#,
);
let back: SuppressionScope = serde_json::from_str(&json).unwrap();
assert_eq!(back, scope);
}
#[test]
fn for_threshold_name_maps_dotted_subnames_to_families() {
assert_eq!(
threshold_metric_for_name("cyclomatic"),
Some(Metric::Cyclomatic)
);
assert_eq!(
threshold_metric_for_name("cyclomatic.modified"),
Some(Metric::Cyclomatic)
);
assert_eq!(
threshold_metric_for_name("halstead.volume"),
Some(Metric::Halstead)
);
assert_eq!(threshold_metric_for_name("loc.lloc"), Some(Metric::Loc));
}
#[test]
fn for_threshold_name_resolves_nexits_canonically() {
assert_eq!(threshold_metric_for_name("nexits"), Some(Metric::Nexits));
}
#[test]
fn for_threshold_name_returns_none_for_unknown() {
assert_eq!(threshold_metric_for_name("tokens"), None);
assert_eq!(threshold_metric_for_name("no_such_metric"), None);
}
#[test]
fn default_scope_is_empty() {
let d = SuppressionScope::default();
assert!(d.is_empty());
assert!(!d.is_all());
}
#[test]
fn inner_doc_comments_recognized() {
let line = marker("//! bca: suppress");
assert_eq!(line.kind, SuppressionKind::Function);
assert!(matches!(line.scope, SuppressionScope::All));
let block = marker("/*! bca: suppress */");
assert_eq!(block.kind, SuppressionKind::Function);
assert!(matches!(block.scope, SuppressionScope::All));
}
use crate::{CppParser, ElixirParser, PythonParser, RustParser};
use std::path::PathBuf;
fn rust_markers(src: &str) -> Vec<SuppressionMarker> {
let parser = RustParser::new(src.as_bytes().to_vec(), &PathBuf::from("t.rs"), None);
suppression_markers(&parser)
}
#[test]
fn collector_function_scoped_native_marker_attributes_enclosing_fn() {
let src = "fn do_thing() {\n // bca: suppress\n let x = 1;\n}\n";
let markers = rust_markers(src);
assert_eq!(markers.len(), 1);
let m = &markers[0];
assert_eq!(m.line, 2);
assert_eq!(m.target, SuppressionTarget::Function);
assert_eq!(m.dialect, SuppressionDialect::Native);
assert!(matches!(m.scope, SuppressionScope::All));
assert_eq!(m.function.as_deref(), Some("do_thing"));
}
#[test]
fn collector_metric_list_scope_is_preserved() {
let src = "fn f() {\n // bca: suppress(cyclomatic, cognitive)\n}\n";
let markers = rust_markers(src);
assert_eq!(markers.len(), 1);
let SuppressionScope::Some(metrics) = &markers[0].scope else {
panic!("expected an explicit metric set");
};
assert!(metrics.contains(&Metric::Cyclomatic));
assert!(metrics.contains(&Metric::Cognitive));
assert_eq!(metrics.len(), 2);
}
#[test]
fn collector_file_scoped_marker_has_no_enclosing_fn() {
let src = "fn f() {\n // bca: suppress-file\n}\n";
let markers = rust_markers(src);
assert_eq!(markers.len(), 1);
assert_eq!(markers[0].target, SuppressionTarget::File);
assert_eq!(markers[0].function, None);
}
#[test]
fn collector_nested_fn_attributes_innermost() {
let src = "fn outer() {\n fn inner() {\n // bca: suppress\n }\n}\n";
let markers = rust_markers(src);
assert_eq!(markers.len(), 1);
assert_eq!(markers[0].function.as_deref(), Some("inner"));
}
#[test]
fn collector_marker_outside_any_fn_has_no_enclosing_fn() {
let src = "// bca: suppress\nfn f() {}\n";
let markers = rust_markers(src);
assert_eq!(markers.len(), 1);
assert_eq!(markers[0].target, SuppressionTarget::Function);
assert_eq!(markers[0].function, None);
}
#[test]
fn collector_recognizes_lizard_dialect() {
let src = "fn f() {\n // #lizard forgives\n}\n";
let markers = rust_markers(src);
assert_eq!(markers.len(), 1);
assert_eq!(markers[0].dialect, SuppressionDialect::Lizard);
assert_eq!(markers[0].function.as_deref(), Some("f"));
}
#[test]
fn collector_markers_sorted_by_line() {
let src = "fn a() {\n // bca: suppress\n}\nfn b() {\n // bca: suppress\n}\n";
let markers = rust_markers(src);
assert_eq!(markers.len(), 2);
assert!(markers[0].line < markers[1].line);
assert_eq!(markers[0].function.as_deref(), Some("a"));
assert_eq!(markers[1].function.as_deref(), Some("b"));
}
#[test]
fn collector_python_hash_marker() {
let src = "def helper():\n # bca: suppress\n pass\n";
let parser = PythonParser::new(src.as_bytes().to_vec(), &PathBuf::from("t.py"), None);
let markers = suppression_markers(&parser);
assert_eq!(markers.len(), 1);
assert_eq!(markers[0].target, SuppressionTarget::Function);
assert_eq!(markers[0].function.as_deref(), Some("helper"));
}
#[test]
fn collector_cpp_attributes_enclosing_function() {
let src = "int compute(int a) {\n // bca: suppress\n return a;\n}\n";
let parser = CppParser::new(src.as_bytes().to_vec(), &PathBuf::from("t.cpp"), None);
let markers = suppression_markers(&parser);
assert_eq!(markers.len(), 1);
assert_eq!(markers[0].target, SuppressionTarget::Function);
assert_eq!(markers[0].function.as_deref(), Some("compute"));
}
#[test]
fn collector_elixir_requires_code_aware_func_predicate() {
let src =
"defmodule M do\n def parse_long do\n # bca: suppress\n x = 1\n end\nend\n";
let parser = ElixirParser::new(src.as_bytes().to_vec(), &PathBuf::from("t.ex"), None);
let markers = suppression_markers(&parser);
assert_eq!(markers.len(), 1);
assert_eq!(markers[0].target, SuppressionTarget::Function);
assert_eq!(markers[0].function.as_deref(), Some("parse_long"));
}
#[test]
fn collector_empty_source_yields_no_markers() {
assert!(rust_markers("").is_empty());
assert!(rust_markers("fn f() {}\n").is_empty());
}
#[test]
fn collector_skips_comments_that_are_not_valid_markers() {
let src = "// an ordinary comment\n\
fn f() {\n\
\x20 // bca: suppress garbage\n\
\x20 // bca: disable(cognitive)\n\
\x20 // bca: suppress(cognitive)\n\
}\n";
let markers = rust_markers(src);
assert_eq!(
markers.len(),
1,
"only the well-formed marker is collected, got {markers:?}"
);
assert_eq!(markers[0].line, 5);
assert_eq!(markers[0].function.as_deref(), Some("f"));
assert!(
rust_markers("// bca: disable\n// not a marker at all\n").is_empty(),
"a rejected marker must not be collected with a fallback scope"
);
}
#[test]
fn rationale_marker_at_eof_without_trailing_newline() {
let space = crate::test_support::space_verbatim(
crate::LANG::Rust,
b"fn f(a: u8, b: u8) -> u8 { a + b }\n\
// bca: suppress-file(nargs) \xe2\x80\x94 two is plenty",
crate::MetricsOptions::default(),
);
assert!(
space.suppressed.covers(Metric::Nargs),
"file-scoped marker at EOF must attach; got {:?}",
space.suppressed,
);
}
#[test]
fn rationale_marker_survives_crlf_line_endings() {
let space = crate::test_support::space_verbatim(
crate::LANG::Rust,
"fn f(a: u8, b: u8) -> u8 {\r\n\
// bca: suppress(nargs) \u{2014} two is plenty\r\n\
a + b\r\n}\r\n"
.as_bytes(),
crate::MetricsOptions::default(),
);
let f = space
.spaces
.iter()
.find(|s| s.name.as_deref() == Some("f"))
.expect("function space f");
assert!(
f.suppressed.covers(Metric::Nargs),
"CRLF marker must attach; got {:?}",
f.suppressed,
);
}
#[test]
fn collector_lists_a_marker_whose_list_was_partly_unusable() {
let src = "fn f() {\n // bca: suppress(cognitive, exit) — state machine\n}\n";
let markers = rust_markers(src);
assert_eq!(markers.len(), 1, "got {markers:?}");
assert!(
matches!(&markers[0].scope, SuppressionScope::Some(m)
if m.iter().copied().eq([Metric::Cognitive])),
"got {:?}",
markers[0].scope,
);
}
}