From 632d8250e5b01c32390c59bc785d44ba88e1b63e Mon Sep 17 00:00:00 2001 From: Elijah Zupancic Date: Tue, 2 Jun 2026 21:00:35 -0700 Subject: [PATCH 1/7] refactor(py): collapse lang_to_name onto LANG::get_name lang_to_name was a 22-arm flat match (cyclomatic 22) carrying a suppress(cyclomatic) marker, yet 19 of its arms returned exactly what the upstream LANG::get_name() already returns. Delegate the common case to other.get_name() and keep only the three arms that genuinely deviate: - Cpp: get_name() -> "c/c++", not a usable lookup token -> "cpp" - Csharp: get_name() -> "c#" -> "csharp" - Tsx: get_name() -> "typescript", collides with Typescript -> "tsx" Behaviour is identical for all 22 variants (verified arm-by-arm against src/langs.rs display names). Cyclomatic drops 22 -> 4 as a side effect, so the suppress(cyclomatic) marker is removed and the file-header note about it dropped; the suppress-file(halstead, nargs) marker stays. The doc comment is rewritten to state the real rule (CLI display name plus three identifier/collision overrides) instead of the inaccurate "lowercased variant name" framing. Adds tests: pin Cpp -> "cpp" and Csharp -> "csharp" (the two overrides no other test fixed; test-via-revert-able), plus a parity test asserting lang_to_name(lang) == lang.get_name() for every public variant outside the three overrides, making the delegation contract explicit and surfacing any future upstream display rename. Fixes #500 --- big-code-analysis-py/src/language.rs | 102 +++++++++++++-------------- 1 file changed, 51 insertions(+), 51 deletions(-) diff --git a/big-code-analysis-py/src/language.rs b/big-code-analysis-py/src/language.rs index 4721f51d..122df2d5 100644 --- a/big-code-analysis-py/src/language.rs +++ b/big-code-analysis-py/src/language.rs @@ -1,7 +1,6 @@ // bca: suppress-file(halstead, nargs) // Language-name / enum mapping helpers; file-level halstead and summed -// nargs are many-fn aggregation artifacts. (`lang_to_name`'s cyclomatic — a -// flat `match` over every LANG variant — is suppressed per-function below.) +// nargs are many-fn aggregation artifacts. //! Language detection helpers exposed to Python. //! @@ -23,61 +22,31 @@ use crate::analysis::AnalysisError; /// Returns the Python-facing language identifier for `lang`. /// -/// The upstream `LANG::get_name` returns a *display* name shared -/// across variants — both `LANG::Tsx` and `LANG::Typescript` report -/// `"typescript"`, both `LANG::Mozjs` and `LANG::Javascript` report -/// `"javascript"` — which makes it ambiguous as a *lookup* key when -/// two variants would round-trip differently through -/// `parse_language_name`. The Python bindings disambiguate by -/// preferring the upstream display name when only one variant in a -/// display group is actually reachable (no helper-variant collision), -/// and falling back to the lowercase Rust variant name otherwise: +/// The rule is: use the upstream CLI display name +/// ([`LANG::get_name`]), except for three tokens that are unusable as +/// `parse_language_name` lookup identifiers and are overridden here: /// -/// - `Mozjs` is exposed as `"javascript"` — matches the CLI's -/// `"language"` field on every `.js` / `.jsm` / `.mjs` / `.jsx` -/// file. `LANG::Javascript` exists as a placeholder for a future -/// strict-ECMAScript dispatch but has no registered extensions -/// and is filtered out by [`public_languages`], so the shared -/// `"javascript"` name is unambiguous from the Python API. -/// - `Tsx` and `Typescript` get distinct variant names (`"tsx"`, -/// `"typescript"`) because both are reachable (TSX via `.tsx` -/// files, TypeScript via `.ts`) and the CLI display collision -/// would lose information at the API boundary. -/// - `Csharp` is exposed as `"csharp"`. -/// - All other variants use their variant name lowercased -/// (`Rust` → `"rust"`, `Java` → `"java"`, …). +/// - `Cpp`: `get_name()` returns `"c/c++"`, which is not a valid +/// `language=` lookup token; the facade exposes `"cpp"`. +/// - `Csharp`: `get_name()` returns `"c#"`; the facade exposes +/// `"csharp"`. +/// - `Tsx`: `get_name()` returns `"typescript"`, colliding with +/// `Typescript`'s display name; the facade exposes `"tsx"` so the +/// two TypeScript variants stay distinct lookup keys (TSX via +/// `.tsx`, TypeScript via `.ts`). +/// +/// Every other variant delegates to `get_name()` unchanged. Notably +/// `Mozjs` and `Javascript` both report `"javascript"` upstream: +/// `Mozjs` is the reachable `.js` / `.jsm` / `.mjs` / `.jsx` variant, +/// while `Javascript` has no registered extensions and is filtered +/// out by [`public_languages`], so the shared name is unambiguous +/// from the Python API. pub(crate) fn lang_to_name(lang: LANG) -> &'static str { - // bca: suppress(cyclomatic) - // Flat `match lang { … }` over every LANG variant — cyclomatic is - // arm count, not branching logic. match lang { - LANG::Bash => "bash", - LANG::Ccomment => "ccomment", LANG::Cpp => "cpp", LANG::Csharp => "csharp", - LANG::Elixir => "elixir", - LANG::Go => "go", - LANG::Groovy => "groovy", - LANG::Irules => "irules", - LANG::Java => "java", - // `Javascript` has no extensions and is filtered out of - // `public_languages`, so this arm is never reached through - // the Python API — but the match must stay exhaustive, and - // grouping the two `"javascript"` variants together documents - // the intentional CLI-name alias and keeps clippy - // (`match_same_arms`) quiet. - LANG::Javascript | LANG::Mozjs => "javascript", - LANG::Kotlin => "kotlin", - LANG::Lua => "lua", - LANG::Perl => "perl", - LANG::Php => "php", - LANG::Preproc => "preproc", - LANG::Python => "python", - LANG::Ruby => "ruby", - LANG::Rust => "rust", - LANG::Tcl => "tcl", LANG::Tsx => "tsx", - LANG::Typescript => "typescript", + other => other.get_name(), } } @@ -416,4 +385,35 @@ mod tests { assert!(parse_language_name("ccomment").is_none()); assert!(parse_language_name("preproc").is_none()); } + + #[test] + fn lang_to_name_overrides_cpp_and_csharp() { + // Pin the two overrides that no other test fixes. Upstream + // `get_name()` returns "c/c++" / "c#", neither of which is a + // usable `parse_language_name` lookup token. Test-via-revert: + // deleting the matching override arm makes `get_name()`'s value + // leak through and fails these assertions. + assert_eq!(lang_to_name(LANG::Cpp), "cpp"); + assert_eq!(lang_to_name(LANG::Csharp), "csharp"); + } + + #[test] + fn lang_to_name_delegates_to_get_name_outside_overrides() { + // Parity contract: every public variant EXCEPT the three + // identifier/collision overrides exposes exactly the upstream + // CLI display name. This makes the delegation explicit and + // turns an accidental upstream display rename into a test + // failure rather than a silent Python-API drift. + const OVERRIDES: [LANG; 3] = [LANG::Cpp, LANG::Csharp, LANG::Tsx]; + for lang in public_languages() { + if OVERRIDES.contains(&lang) { + continue; + } + assert_eq!( + lang_to_name(lang), + lang.get_name(), + "non-override variant {lang:?} must mirror get_name()" + ); + } + } } From 5b6782ccd73da37b6ac8e49f17087b82a9ad6159 Mon Sep 17 00:00:00 2001 From: Elijah Zupancic Date: Tue, 2 Jun 2026 21:05:58 -0700 Subject: [PATCH 2/7] test(py): guard lang_to_name parity test against vacuous pass Post-review remediation for #500. The get_name() parity loop asserted nothing if public_languages() ever returned only the overrides (or empty); add a checked-count guard so an over-filtered language set fails loudly instead of passing green. Also reword the Tsx doc bullet to state the override is what makes Tsx/Typescript distinct, rather than implying they were already distinct after delegation. --- big-code-analysis-py/src/language.rs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/big-code-analysis-py/src/language.rs b/big-code-analysis-py/src/language.rs index 122df2d5..bf02fad6 100644 --- a/big-code-analysis-py/src/language.rs +++ b/big-code-analysis-py/src/language.rs @@ -30,10 +30,10 @@ use crate::analysis::AnalysisError; /// `language=` lookup token; the facade exposes `"cpp"`. /// - `Csharp`: `get_name()` returns `"c#"`; the facade exposes /// `"csharp"`. -/// - `Tsx`: `get_name()` returns `"typescript"`, colliding with -/// `Typescript`'s display name; the facade exposes `"tsx"` so the -/// two TypeScript variants stay distinct lookup keys (TSX via -/// `.tsx`, TypeScript via `.ts`). +/// - `Tsx`: `get_name()` returns `"typescript"` for *both* `Tsx` and +/// `Typescript`, so without an override the two would collapse to +/// one lookup key. The override exposes `"tsx"`, making them +/// distinct (TSX via `.tsx`, TypeScript via `.ts`). /// /// Every other variant delegates to `get_name()` unchanged. Notably /// `Mozjs` and `Javascript` both report `"javascript"` upstream: @@ -405,6 +405,7 @@ mod tests { // turns an accidental upstream display rename into a test // failure rather than a silent Python-API drift. const OVERRIDES: [LANG; 3] = [LANG::Cpp, LANG::Csharp, LANG::Tsx]; + let mut checked = 0; for lang in public_languages() { if OVERRIDES.contains(&lang) { continue; @@ -414,6 +415,14 @@ mod tests { lang.get_name(), "non-override variant {lang:?} must mirror get_name()" ); + checked += 1; } + // Guard against a vacuous pass: if `public_languages()` ever + // shrank to nothing (or to only the overrides), the loop above + // would assert nothing yet still report green. + assert!( + checked > 0, + "parity loop exercised no non-override variants" + ); } } From cfc6e04ca06e83e91b7c7e3653a3ed13ad4baf5c Mon Sep 17 00:00:00 2001 From: Elijah Zupancic Date: Tue, 2 Jun 2026 21:26:05 -0700 Subject: [PATCH 3/7] feat(report): honor bca: suppress markers in aggregated reports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `bca report markdown|html` listed raw metric values and ignored `bca: suppress` / `bca: suppress-file` markers, so the published hotspots report re-surfaced every silenced function — contradicting `bca check` and the SARIF emitter. The report now HONORS markers BY DEFAULT, per metric: a function is omitted from a metric's hotspot table when that metric is suppressed for it. `extract_summaries` folds the top-level Unit's `suppress-file` scope into each function's own scope (file ∪ function), mirroring `ThresholdSet::evaluate_with_policy`; each hotspot table filters on its `MetricKind` via `SuppressionScope::covers`, including the nexits→exit alias. `--no-suppress` (and `[report] no_suppress = true` in the manifest) opts back into the raw audit view. Advisory roll-ups (Actionable Summary, CC-stats note) intentionally keep counting raw measurements; only the per-metric hotspot tables filter. Widens `SuppressionScope::merge` from `pub(crate)` to `pub` (additive) so report consumers can fold scopes. Tests: markdown + HTML cover per-metric function-scope drop, file-scope suppress-all drop, --no-suppress inclusion, nexits→exit aliasing, and the extract_summaries file∪function merge; manifest tests cover the [report] known-key and merge_report OR semantics. Fixes #501 --- big-code-analysis-book/src/commands/report.md | 28 ++ .../src/commands/suppression.md | 16 +- big-code-analysis-cli/src/commands.rs | 29 +- big-code-analysis-cli/src/html_report.rs | 166 +++++++++-- big-code-analysis-cli/src/lib.rs | 8 + big-code-analysis-cli/src/manifest.rs | 33 ++- big-code-analysis-cli/src/manifest_tests.rs | 50 ++++ big-code-analysis-cli/src/markdown_report.rs | 265 ++++++++++++++++-- .../src/markdown_report/sections.rs | 106 +++++-- .../src/markdown_report/sections_tests.rs | 1 + man/bca-report.1 | 5 +- src/suppression.rs | 5 +- 12 files changed, 625 insertions(+), 87 deletions(-) diff --git a/big-code-analysis-book/src/commands/report.md b/big-code-analysis-book/src/commands/report.md index 0811e9c3..69eca68d 100644 --- a/big-code-analysis-book/src/commands/report.md +++ b/big-code-analysis-book/src/commands/report.md @@ -37,8 +37,36 @@ bca --paths /path/to/project report markdown --output report.md | --- | --- | --- | | `--top N` | 20 | Maximum entries per hotspot table. | | `--strip-prefix PATH` | *(empty)* | Prefix removed from file paths. | +| `--no-suppress` | *(off)* | Include functions silenced by in-source suppression markers (raw audit view). | | `-o, --output FILE` | *(stdout)* | Output file. Parent directory must exist. | +## Suppression markers + +By default, `bca report markdown|html` **honours** in-source suppression +markers — the same `// bca: suppress`, `// bca: suppress-file`, and +`#lizard forgives` comments that [`bca check`](check.md) and the SARIF +emitter respect (see [Suppression](suppression.md)). A function is +omitted from a metric's hotspot table when that metric is suppressed for +it, so the published report agrees with the threshold gate instead of +re-surfacing every silenced offender. + +Suppression is per-metric: a `// bca: suppress(cyclomatic)` marker drops +the function from the Cyclomatic table only — it still appears in the +Cognitive, Halstead, and other tables. A bare `// bca: suppress` (or +`// bca: suppress-file`) covers every metric. + +Pass `--no-suppress` for the raw audit view that lists every offender +regardless of markers. The setting can also be pinned in the +[`bca.toml` manifest](check.md): + +```toml +[report] +no_suppress = true +``` + +The CLI flag wins; a bare `--no-suppress` can force the audit view on, +but the manifest never forces it off. + ## Examples Show only the five worst hotspots per section: diff --git a/big-code-analysis-book/src/commands/suppression.md b/big-code-analysis-book/src/commands/suppression.md index 4cb1b282..3c476269 100644 --- a/big-code-analysis-book/src/commands/suppression.md +++ b/big-code-analysis-book/src/commands/suppression.md @@ -4,9 +4,12 @@ In-source suppression markers silence threshold violations without editing the offending function or excluding the file from the walk. Drop a marker in any comment in the source file and `bca check` treats the covered metrics as if they were within limits for that -scope. Metric computation is unaffected — raw `bca metrics` / -`bca report` output still reports every number. Suppression is a -threshold-check concern only. +scope. Metric computation is unaffected — raw `bca metrics` output +still reports every number. Suppression is a measurement-display +concern: `bca check` drops the covered violations from the gate, and +`bca report markdown|html` omits the covered functions from the +matching hotspot tables by default (pass `bca report --no-suppress` +for the raw audit view — see [report](report.md)). Markers exist for the cases editing the code is not an option: generated-style legacy modules awaiting rewrite, accepted exceptions @@ -179,9 +182,10 @@ offender list: bca --paths src/ check --no-suppress ``` -The flag has no effect on metric values themselves: raw -`bca metrics` / `bca report` output already ignores markers, since -suppression is a threshold-check concern only. +The flag has no effect on metric values themselves: raw `bca metrics` +output always reports every number. `bca report markdown|html` honours +markers in its hotspot tables by default and accepts its own +[`--no-suppress`](report.md) flag for the same raw audit view. ## Surfacing suppressed debt (`--report-suppressed`) diff --git a/big-code-analysis-cli/src/commands.rs b/big-code-analysis-cli/src/commands.rs index 85c968e8..2f88e553 100644 --- a/big-code-analysis-cli/src/commands.rs +++ b/big-code-analysis-cli/src/commands.rs @@ -1422,7 +1422,9 @@ pub fn run() { Command::Functions => run_command_functions(cli.globals, preproc), Command::Metrics(args) => run_command_metrics(cli.globals, args, preproc), Command::Ops(args) => run_command_ops(cli.globals, args, preproc), - Command::Report(args) => run_command_report(cli.globals, args, preproc), + Command::Report(args) => { + run_command_report(cli.globals, args, manifest.as_ref(), preproc); + } Command::Find(args) => run_command_find(cli.globals, args, preproc), Command::Count(args) => run_command_count(cli.globals, args, preproc), Command::StripComments(args) => run_command_strip_comments(cli.globals, args, preproc), @@ -1563,7 +1565,16 @@ fn run_command_ops( run_walk(globals, cfg); } -fn run_command_report(globals: GlobalOpts, args: ReportArgs, preproc: Option>) { +fn run_command_report( + globals: GlobalOpts, + mut args: ReportArgs, + manifest: Option<&Manifest>, + preproc: Option>, +) { + if let Some(m) = manifest { + m.merge_report(&mut args); + } + let policy = SuppressionPolicy::from_no_suppress(args.no_suppress); if let Some(ref output) = args.output { if output.exists() && output.is_dir() { die("--output must be a file path for `report`"); @@ -1590,8 +1601,8 @@ fn run_command_report(globals: GlobalOpts, args: ReportArgs, preproc: Option = rx.into_iter().collect(); let report = match args.format { - ReportFormat::Markdown => generate_report(&summaries, args.top as usize), - ReportFormat::Html => generate_html_report(&summaries, args.top as usize), + ReportFormat::Markdown => generate_report(&summaries, args.top as usize, policy), + ReportFormat::Html => generate_html_report(&summaries, args.top as usize, policy), }; if let Some(ref output_path) = args.output { std::fs::write(output_path, &report) @@ -1739,6 +1750,16 @@ nargs = 7 nexits = 5 abc = 50 wmc = 60 + +# Aggregated-report options for `bca report markdown|html` (#501). By +# default the report honours in-source suppression markers +# (`bca: suppress`, `bca: suppress-file`, `#lizard forgives`) and omits a +# function from a metric's hotspot table when that metric is suppressed +# for it — matching `bca check` and the SARIF emitter. Uncomment to opt +# into the raw audit view that lists every offender (equivalent to +# `bca report --no-suppress`). +# [report] +# no_suppress = true "; /// Canonical contents of a freshly-scaffolded `.bcaignore`. The diff --git a/big-code-analysis-cli/src/html_report.rs b/big-code-analysis-cli/src/html_report.rs index 41e99cf7..ed033d1c 100644 --- a/big-code-analysis-cli/src/html_report.rs +++ b/big-code-analysis-cli/src/html_report.rs @@ -38,7 +38,7 @@ use std::borrow::Cow; use std::collections::BTreeMap; use std::fmt::Write; -use big_code_analysis::SpaceKind; +use big_code_analysis::{MetricKind, SpaceKind, SuppressionPolicy}; use crate::format_util::MetricScalar; use crate::markdown_report::{ @@ -319,6 +319,12 @@ struct HotspotSpec { metric: fn(&FunctionSummary) -> f64, dir: SortDir, columns: &'static [HotspotColumn], + /// Which metric family this table ranks. When the report honors + /// suppression markers (the default), an entry is dropped if its + /// effective scope covers this kind — the same per-table filtering + /// the Markdown report applies (issue #501). `--no-suppress` + /// (`SuppressionPolicy::Ignore`) bypasses it. + metric_kind: MetricKind, } // Column descriptors shared verbatim across multiple hotspot specs. @@ -365,6 +371,7 @@ const MI_LOWEST_HOTSPOT: HotspotSpec = HotspotSpec { keep: |s| s.mi_visual_studio > 0.0, metric: |s| s.mi_visual_studio, dir: SortDir::Asc, + metric_kind: MetricKind::Mi, columns: &[ COL_FILE, HotspotColumn { @@ -381,6 +388,7 @@ const CC_HOTSPOT: HotspotSpec = HotspotSpec { keep: |s| s.cyclomatic > 0.0, metric: |s| s.cyclomatic, dir: SortDir::Desc, + metric_kind: MetricKind::Cyclomatic, columns: &[ COL_FUNCTION, COL_FILE, @@ -396,6 +404,7 @@ const COGNITIVE_HOTSPOT: HotspotSpec = HotspotSpec { keep: |s| s.cognitive > 0.0, metric: |s| s.cognitive, dir: SortDir::Desc, + metric_kind: MetricKind::Cognitive, columns: &[ COL_FUNCTION, COL_FILE, @@ -411,6 +420,7 @@ const HALSTEAD_EFFORT_HOTSPOT: HotspotSpec = HotspotSpec { keep: |s| s.halstead_effort > 0.0, metric: |s| s.halstead_effort, dir: SortDir::Desc, + metric_kind: MetricKind::Halstead, columns: &[ COL_FUNCTION, COL_FILE, @@ -438,6 +448,7 @@ const LARGEST_BY_SLOC_HOTSPOT: HotspotSpec = HotspotSpec { keep: |s| s.sloc > 0, metric: |s| s.sloc as f64, dir: SortDir::Desc, + metric_kind: MetricKind::Loc, columns: &[ COL_FUNCTION, COL_FILE, @@ -453,6 +464,7 @@ const MANY_PARAMS_HOTSPOT: HotspotSpec = HotspotSpec { keep: |s| s.nargs > 3, metric: |s| s.nargs as f64, dir: SortDir::Desc, + metric_kind: MetricKind::Nargs, columns: &[ COL_FUNCTION, COL_FILE, @@ -475,6 +487,7 @@ const WMC_HOTSPOT: HotspotSpec = HotspotSpec { keep: |s| is_class_like(s.kind) && s.wmc > 0.0, metric: |s| s.wmc, dir: SortDir::Desc, + metric_kind: MetricKind::Wmc, columns: &[ HotspotColumn { header: "Class", @@ -508,10 +521,15 @@ const WMC_HOTSPOT: HotspotSpec = HotspotSpec { ], }; +// The exit-points table maps to `MetricKind::Exit` — the suppression +// vocabulary's spelling of the threshold engine's `nexits` (matching +// `MetricKind::for_threshold_name`'s `nexits → exit` alias), so a +// `bca: suppress(exit)` marker silences it. const NEXITS_HOTSPOT: HotspotSpec = HotspotSpec { keep: |s| s.nexits > 0, metric: |s| s.nexits as f64, dir: SortDir::Desc, + metric_kind: MetricKind::Exit, columns: &[ COL_FUNCTION, COL_FILE, @@ -531,6 +549,7 @@ const ABC_HOTSPOT: HotspotSpec = HotspotSpec { keep: |s| s.abc > 0.0, metric: |s| s.abc, dir: SortDir::Desc, + metric_kind: MetricKind::Abc, columns: &[ COL_FUNCTION, COL_FILE, @@ -617,9 +636,13 @@ fn emit_hotspot( base: &[&FunctionSummary], top_n: usize, spec: &HotspotSpec, + policy: SuppressionPolicy, ) -> bool { - let mut entries: Vec<&FunctionSummary> = - base.iter().copied().filter(|s| (spec.keep)(s)).collect(); + let mut entries: Vec<&FunctionSummary> = base + .iter() + .copied() + .filter(|s| (spec.keep)(s) && !s.is_hidden_for(spec.metric_kind, policy)) + .collect(); if entries.is_empty() { return false; } @@ -838,7 +861,11 @@ fn write_html_tail(out: &mut String) { /// Produce a self-contained HTML quality-metrics report from the /// collected summaries. `top_n` controls how many entries appear in /// each hotspot table. -pub(crate) fn generate_html_report(summaries: &[FunctionSummary], top_n: usize) -> String { +pub(crate) fn generate_html_report( + summaries: &[FunctionSummary], + top_n: usize, + policy: SuppressionPolicy, +) -> String { // Each summary contributes at most one row across all hotspot // tables (sections × top_n is bounded), but the per-language // overview table plus the inline CSS/JS already costs a few KB of @@ -854,7 +881,7 @@ pub(crate) fn generate_html_report(summaries: &[FunctionSummary], top_n: usize) if !by_lang.is_empty() { write_overview_table(&mut out, &by_lang); for (&lang_name, lang_summaries) in &by_lang { - write_language_section(&mut out, lang_name, lang_summaries, top_n); + write_language_section(&mut out, lang_name, lang_summaries, top_n, policy); } } write_html_tail(&mut out); @@ -1001,7 +1028,12 @@ impl CyclomaticStats { /// stats are computed first so the note can be gated on the table /// actually rendering — an empty `funcs` slice yields no table and no /// misleading `Average CC: 0.0` line. -fn emit_cc_hotspot_with_stats(out: &mut String, funcs: &[&FunctionSummary], top_n: usize) { +fn emit_cc_hotspot_with_stats( + out: &mut String, + funcs: &[&FunctionSummary], + top_n: usize, + policy: SuppressionPolicy, +) { let stats = CyclomaticStats::from_funcs(funcs); if emit_hotspot( out, @@ -1009,6 +1041,7 @@ fn emit_cc_hotspot_with_stats(out: &mut String, funcs: &[&FunctionSummary], top_ funcs, top_n, &CC_HOTSPOT, + policy, ) { let _ = writeln!( out, @@ -1109,6 +1142,7 @@ fn write_language_section( lang_name: &str, entries: &[&FunctionSummary], top_n: usize, + policy: SuppressionPolicy, ) { write_language_header(out, lang_name); let (units, funcs) = partition_by_kind(entries); @@ -1120,14 +1154,16 @@ fn write_language_section( &units, top_n, &MI_LOWEST_HOTSPOT, + policy, ); - emit_cc_hotspot_with_stats(out, &funcs, top_n); + emit_cc_hotspot_with_stats(out, &funcs, top_n, policy); emit_hotspot( out, "Cognitive Complexity Hotspots", &funcs, top_n, &COGNITIVE_HOTSPOT, + policy, ); emit_hotspot( out, @@ -1135,6 +1171,7 @@ fn write_language_section( &funcs, top_n, &HALSTEAD_EFFORT_HOTSPOT, + policy, ); emit_hotspot( out, @@ -1142,6 +1179,7 @@ fn write_language_section( &funcs, top_n, &LARGEST_BY_SLOC_HOTSPOT, + policy, ); emit_hotspot( out, @@ -1149,6 +1187,7 @@ fn write_language_section( &funcs, top_n, &MANY_PARAMS_HOTSPOT, + policy, ); write_actionable_summary(out, &funcs); // WMC sources `entries` (all kinds), not `funcs`: class-likes are @@ -1159,6 +1198,7 @@ fn write_language_section( entries, top_n, &WMC_HOTSPOT, + policy, ); emit_hotspot( out, @@ -1166,8 +1206,16 @@ fn write_language_section( &funcs, top_n, &NEXITS_HOTSPOT, + policy, + ); + emit_hotspot( + out, + "ABC Magnitude Hotspots", + &funcs, + top_n, + &ABC_HOTSPOT, + policy, ); - emit_hotspot(out, "ABC Magnitude Hotspots", &funcs, top_n, &ABC_HOTSPOT); let _ = out.write_str("\n"); } @@ -1205,6 +1253,7 @@ mod tests { name: name.to_string(), kind, language, + suppressed: big_code_analysis::SuppressionScope::default(), start_line: 1, end_line: 10, sloc: 20, @@ -1271,7 +1320,7 @@ mod tests { #[test] fn empty_summaries_emit_no_tables() { - let out = generate_html_report(&[], 20); + let out = generate_html_report(&[], 20, SuppressionPolicy::Honor); assert!(out.contains("

Code Quality Metrics Summary

")); assert!(!out.contains("10,000<") && out.contains(">1,500,000<"), "expected thousands-formatted cells in output" @@ -1327,7 +1376,7 @@ mod tests { #[test] fn single_language_well_formed() { - let out = generate_html_report(&rust_fixture(), 20); + let out = generate_html_report(&rust_fixture(), 20, SuppressionPolicy::Honor); assert!(out.contains("

Rust

")); assert!(out.contains("class=\"hotspot\"")); assert_html_well_formed(&out); @@ -1335,7 +1384,7 @@ mod tests { #[test] fn two_language_well_formed_and_alphabetical() { - let out = generate_html_report(&two_lang_fixture(), 20); + let out = generate_html_report(&two_lang_fixture(), 20, SuppressionPolicy::Honor); assert!(out.contains("

Python

")); assert!(out.contains("

Rust

")); let py = out.find("

Python

").expect("python heading"); @@ -1353,7 +1402,7 @@ mod tests { summaries[1].name = "".to_string(); summaries[1].file = "a&b\"c'd".to_string(); - let out = generate_html_report(&summaries, 20); + let out = generate_html_report(&summaries, 20, SuppressionPolicy::Honor); assert!( !out.contains("