//! `--format github-summary`: GitHub Actions job-summary markdown, written
//! by workflows as `fallow ... --format github-summary >> "$GITHUB_STEP_SUMMARY"`.
//!
//! Sections, ordering, and truncation caps were first ported from the bundled
//! action's jq renderers (`action/jq/summary-{check,dupes,health,audit,
//! security,fix,combined}.jq`). This renderer is now the source of truth for
//! fallow 3.4.2 and later. The action keeps the jq files as frozen legacy
//! renderers for older binaries, so new issue kinds go here and not into the
//! jq files (`summary-fix.jq` still serves `fix` on every version). Deviations
//! from the jq layer: em dashes in
//! the jq templates render as plain hyphens (repo style rule), and the
//! combined view's dupes file links read `GH_REPO` / `GITHUB_REPOSITORY` and
//! `PR_HEAD_SHA` / `GITHUB_SHA` (the jq layer read only the action-set
//! `GH_REPO` / `PR_HEAD_SHA`), with the link path prefix coming from the
//! path-rebase resolution instead of the action-set `PREFIX` env var.
//!
//! Like the annotations renderer, this is value-driven over the `--format
//! json` envelope, which keeps `fallow report --from` output byte-identical
//! to the direct format run.
use std::fmt::Write as _;
use std::path::Path;
use std::process::ExitCode;
use fallow_output::{markdown_code_span, markdown_table_code_span, markdown_table_text};
use fallow_types::output_dead_code::caveat_labels_for_tokens;
use serde_json::Value;
use super::github::{PathRebase, arr, b, fmt_num, num, resolve_render_options, s, u};
use super::github_annotations::EnvelopeKind;
use crate::report::sink::outln;
const DEAD_CODE_DOCS: &str = "https://docs.fallow.tools/explanations/dead-code";
const HEALTH_DOCS: &str = "https://docs.fallow.tools/explanations/health";
const DUPES_DOCS: &str = "https://docs.fallow.tools/explanations/duplication";
const SUPPRESSION_DOCS: &str = "https://docs.fallow.tools/configuration/suppression";
/// Environment-derived context for the combined view's dupes file links.
#[derive(Debug, Default, Clone)]
pub struct LinkContext {
/// Repo-root path prefix for link targets (empty or `dir/` with a
/// trailing slash).
pub prefix: String,
/// `owner/repo`; links render as plain code when empty.
pub repo: String,
/// Head commit SHA; links render as plain code when empty.
pub sha: String,
}
impl LinkContext {
/// Resolve from the workflow environment plus the path-rebase offset.
#[must_use]
pub(crate) fn from_env(rebase: &PathRebase) -> Self {
let env = |primary: &str, fallback: &str| {
std::env::var(primary)
.or_else(|_| std::env::var(fallback))
.unwrap_or_default()
};
let prefix = match rebase {
PathRebase::None => String::new(),
PathRebase::Prefix(prefix) => format!("{prefix}/"),
};
Self {
prefix,
repo: env("GH_REPO", "GITHUB_REPOSITORY"),
sha: env("PR_HEAD_SHA", "GITHUB_SHA"),
}
}
}
/// Render and print the job-summary markdown for one envelope.
pub(crate) fn print_summary(kind: EnvelopeKind, envelope: &Value, root: &Path) -> ExitCode {
let options = resolve_render_options(root);
let links = LinkContext::from_env(&options.rebase);
outln!("{}", render_summary(kind, envelope, &links));
// Appended rather than folded into each per-kind renderer: the verdict is
// one fact about the run, not a section of the report, and every kind
// reports it the same way.
if let Some(line) = crate::report::gate_outcome_text::summary_line(envelope) {
outln!("");
outln!("{line}");
}
// Same reasoning, one fact further: whether the run did what it was asked
// belongs beside what it concluded, not inside a per-kind section.
if let Some(line) = crate::report::request_outcome_text::summary_line(envelope) {
outln!("");
outln!("{line}");
}
ExitCode::SUCCESS
}
/// Render the fix envelope's job summary directly from `fallow fix`. The fix
/// envelope has no `kind` field; `fallow report --from` reaches the same
/// renderer via [`EnvelopeKind::Fix`] (resolved by field detection), while the
/// live `fallow fix` command calls this entry point.
pub(crate) fn print_fix_summary(envelope: &Value) -> ExitCode {
outln!("{}", render_fix_summary(envelope));
ExitCode::SUCCESS
}
/// Pure renderer, dispatching on the envelope family.
#[must_use]
pub fn render_summary(kind: EnvelopeKind, envelope: &Value, links: &LinkContext) -> String {
match kind {
EnvelopeKind::DeadCode => render_check_summary(envelope),
EnvelopeKind::Dupes => render_dupes_summary(envelope),
EnvelopeKind::Health => render_health_summary(envelope),
EnvelopeKind::Audit => render_audit_summary(envelope),
EnvelopeKind::Security => render_security_summary(envelope),
EnvelopeKind::Combined => render_combined_summary(envelope, links),
EnvelopeKind::Fix => render_fix_summary(envelope),
}
}
// ---------------------------------------------------------------------------
// Shared numeric / path helpers (jq `pct`, `signed`, `rel_path`).
// ---------------------------------------------------------------------------
/// jq `pct`: `. * 10 | round / 10`, interpolated without a trailing `.0`.
fn pct(value: f64) -> String {
let rounded = (value * 10.0).round() / 10.0;
fmt_num(&serde_json::json!(rounded))
}
/// jq `signed`: `+x` for positive, plain for negative, `0.0` for zero.
fn signed(value: f64) -> String {
if value > 0.0 {
format!("+{}", pct(value))
} else if value < 0.0 {
pct(value)
} else {
"0.0".to_owned()
}
}
fn opt_f(value: &Value, key: &str) -> Option<f64> {
value.get(key).and_then(Value::as_f64)
}
fn f_or_zero(value: &Value, key: &str) -> f64 {
opt_f(value, key).unwrap_or_default()
}
/// jq `rel_path` (audit/security flavor): shorten only absolute paths to
/// their last three segments.
fn rel_path_absolute_only(path: &str) -> String {
if path.starts_with('/') {
last_three_segments(path)
} else {
path.to_owned()
}
}
/// jq `rel_path` (combined flavor): always shorten to the last three
/// segments.
fn last_three_segments(path: &str) -> String {
let segments: Vec<&str> = path.split('/').collect();
if segments.len() > 3 {
segments[segments.len() - 3..].join("/")
} else {
segments.join("/")
}
}
/// jq `plural(n; word)`.
fn plural_n(n: usize, word: &str) -> String {
let suffix = if n == 1 { "" } else { "s" };
format!("{n} {word}{suffix}")
}
fn str_or<'v>(value: &'v Value, key: &str, default: &'v str) -> &'v str {
value.get(key).and_then(Value::as_str).unwrap_or(default)
}
/// `` `path` `` / `` `path:line` `` with the jq truthiness gate: the `:line`
/// suffix renders whenever `line` is present and non-null (0 included).
fn path_line(item: &Value) -> String {
let path = rel_path_absolute_only(s(item, "path"));
match item.get("line").filter(|line| !line.is_null()) {
Some(line) => markdown_table_code_span(&format!("{path}:{}", fmt_num(line))),
None => markdown_table_code_span(&path),
}
}
/// Escaped code-span table cell for an untrusted envelope string.
fn code_cell(item: &Value, key: &str) -> String {
markdown_table_code_span(s(item, key))
}
/// The italic caveat marker appended inside a dead-code table cell when the
/// verdict behind the row rests on a file this run never fully read. Empty when
/// the finding carries no `reachability_caveats[]`, so a clean run's summary is
/// byte-identical to what it was before this hedge existed.
///
/// The job summary is where a reviewer decides what to delete, so the row that
/// names the finding is the row that has to carry the qualifier. It rides
/// inside an existing cell rather than in a new column: adding a column would
/// change every table's shape, including the rows that carry no caveat.
fn caveat_cell_suffix(item: &Value) -> String {
caveat_labels_for_tokens(arr(item, "reachability_caveats").filter_map(Value::as_str))
.map_or_else(String::new, |labels| {
format!(" *(caveat: {})*", markdown_table_text(&labels))
})
}
/// `` `path:line` `` cell with the audit-flavor path shortening.
fn rel_path_line_cell(item: &Value, path_key: &str) -> String {
markdown_table_code_span(&format!(
"{}:{}",
rel_path_absolute_only(s(item, path_key)),
num(item, "line")
))
}
fn backtick_join(item: &Value, key: &str) -> String {
arr(item, key)
.filter_map(Value::as_str)
.map(markdown_table_code_span)
.collect::<Vec<_>>()
.join(", ")
}
// ---------------------------------------------------------------------------
// Dead-code category table (shared by the check summary and the combined
// code-issues breakdown). Labels, result keys, and docs anchors come from the
// issue registry; a unit test below holds the two equal.
// ---------------------------------------------------------------------------
const DEAD_CODE_CATEGORIES: &[(&str, &str, &str)] = &[
("Unused files", "unused_files", "unused-files"),
("Unused exports", "unused_exports", "unused-exports"),
("Unused types", "unused_types", "unused-types"),
(
"Private type leaks",
"private_type_leaks",
"private-type-leaks",
),
(
"Deprecated exports in use",
"deprecated_exports_in_use",
"deprecated-exports-in-use",
),
(
"Unused dependencies",
"unused_dependencies",
"unused-dependencies",
),
(
"Unused devDependencies",
"unused_dev_dependencies",
"unused-dependencies",
),
(
"Unused optionalDependencies",
"unused_optional_dependencies",
"unused-dependencies",
),
(
"Unused enum members",
"unused_enum_members",
"unused-enum-members",
),
(
"Unused class members",
"unused_class_members",
"unused-class-members",
),
(
"Unused store members",
"unused_store_members",
"unused-store-members",
),
(
"Unresolved imports",
"unresolved_imports",
"unresolved-imports",
),
(
"Unlisted dependencies",
"unlisted_dependencies",
"unlisted-dependencies",
),
(
"Duplicate exports",
"duplicate_exports",
"duplicate-exports",
),
(
"Circular dependencies",
"circular_dependencies",
"circular-dependencies",
),
("Re-export cycles", "re_export_cycles", "re-export-cycles"),
(
"Boundary violations",
"boundary_violations",
"boundary-violations",
),
(
"Boundary coverage",
"boundary_coverage_violations",
"boundary-violations",
),
(
"Boundary calls",
"boundary_call_violations",
"boundary-violations",
),
(
"Policy violations",
"policy_violations",
"policy-violations",
),
(
"Invalid client exports",
"invalid_client_exports",
"invalid-client-exports",
),
(
"Mixed client/server barrels",
"mixed_client_server_barrels",
"mixed-client-server-barrels",
),
(
"Misplaced directives",
"misplaced_directives",
"misplaced-directives",
),
(
"Unused server actions",
"unused_server_actions",
"unused-server-action",
),
("Route collisions", "route_collisions", "route-collisions"),
(
"Dynamic segment conflicts",
"dynamic_segment_name_conflicts",
"dynamic-segment-name-conflicts",
),
(
"Unrendered components",
"unrendered_components",
"unrendered-component",
),
(
"Unused component props",
"unused_component_props",
"unused-component-prop",
),
(
"Unused component emits",
"unused_component_emits",
"unused-component-emit",
),
(
"Unused component inputs",
"unused_component_inputs",
"unused-component-input",
),
(
"Unused component outputs",
"unused_component_outputs",
"unused-component-output",
),
(
"Unused Svelte events",
"unused_svelte_events",
"unused-svelte-event",
),
(
"Unprovided injects",
"unprovided_injects",
"unprovided-inject",
),
(
"Unused load data keys",
"unused_load_data_keys",
"unused-load-data-key",
),
(
"Type-only dependencies",
"type_only_dependencies",
"type-only-dependencies",
),
(
"Test-only dependencies",
"test_only_dependencies",
"test-only-dependencies",
),
(
"Dev dependencies used in production",
"dev_dependencies_in_production",
"dev-dependencies-in-production",
),
(
"Stale suppressions",
"stale_suppressions",
"stale-suppressions",
),
(
"Unused catalog entries",
"unused_catalog_entries",
"unused-catalog-entries",
),
(
"Empty catalog groups",
"empty_catalog_groups",
"empty-catalog-groups",
),
(
"Unresolved catalog references",
"unresolved_catalog_references",
"unresolved-catalog-references",
),
(
"Unused dependency overrides",
"unused_dependency_overrides",
"unused-dependency-overrides",
),
(
"Misconfigured dependency overrides",
"misconfigured_dependency_overrides",
"misconfigured-dependency-overrides",
),
];
fn dead_code_docs(anchor: &str) -> String {
format!("{DEAD_CODE_DOCS}#{anchor}")
}
fn dead_code_category_table(env: &Value) -> String {
DEAD_CODE_CATEGORIES
.iter()
.filter_map(|(name, key, anchor)| {
let n = arr(env, key).count();
(n > 0).then(|| format!("| [{name}]({}) | {n} |", dead_code_docs(anchor)))
})
.collect::<Vec<_>>()
.join("\n")
}
// ---------------------------------------------------------------------------
// summary-check.jq
// ---------------------------------------------------------------------------
struct SectionSpec {
name: &'static str,
key: &'static str,
header: &'static str,
row: fn(&Value) -> String,
}
fn render_check_section(env: &Value, spec: &SectionSpec) -> String {
// The job summary is one flat document, so its rows follow the flat path
// order of the live render. A saved `--group-by` envelope lists its items
// group by group, and `report --from` flattens them in that order. The sort
// gives both renders one order. The serialized item breaks ties, so the
// order is total and does not depend on the input order.
let items = rows_in_path_order(env, spec.key);
let n = items.len();
if n == 0 {
return String::new();
}
let rows = items
.iter()
.take(CHECK_SECTION_ROW_LIMIT)
.map(|item| (spec.row)(item))
.collect::<Vec<_>>()
.join("\n");
let tail = if n > CHECK_SECTION_ROW_LIMIT {
format!(
"\n\n> {} more - run `fallow` locally for the full list",
n - CHECK_SECTION_ROW_LIMIT
)
} else {
String::new()
};
format!(
"\n<details><summary><strong>{} ({n})</strong></summary>\n\n{}{rows}{tail}\n\n</details>\n",
spec.name, spec.header,
)
}
/// Rows a check section renders before it prints the count of the rest.
const CHECK_SECTION_ROW_LIMIT: usize = 25;
/// The items of `key` in (path, line, serialized item) order.
///
/// Path and line are read once per item, not once per comparison. The
/// serialized item only breaks a tie on path and line, and only for the tie
/// groups that reach the rendered rows, because serializing is costly and
/// the rows past [`CHECK_SECTION_ROW_LIMIT`] are only counted.
fn rows_in_path_order<'a>(env: &'a Value, key: &str) -> Vec<&'a Value> {
let mut keyed: Vec<(Option<&str>, Option<u64>, &Value)> = arr(env, key)
.map(|item| {
(
row_sort_path(item),
item.get("line").and_then(Value::as_u64),
item,
)
})
.collect();
keyed.sort_by(|a, b| (a.0, a.1).cmp(&(b.0, b.1)));
let mut start = 0;
for run in keyed.chunk_by_mut(|a, b| (a.0, a.1) == (b.0, b.1)) {
if start >= CHECK_SECTION_ROW_LIMIT {
break;
}
if run.len() > 1 {
run.sort_by_cached_key(|(_, _, item)| item.to_string());
}
start += run.len();
}
keyed.into_iter().map(|(_, _, item)| item).collect()
}
/// The path a dead-code row sorts on: `path`, the source file of a boundary
/// violation, or the first file of a cycle. Rows with none (a duplicate export
/// or an unlisted dependency) sort on the serialized item, which starts with
/// the name.
fn row_sort_path(item: &Value) -> Option<&str> {
item.get("path")
.or_else(|| item.get("from_path"))
.or_else(|| item.get("files").and_then(|files| files.get(0)))
.and_then(Value::as_str)
}
fn check_workspace_context(item: &Value) -> String {
backtick_join(item, "used_in_workspaces")
}
#[expect(
clippy::too_many_lines,
reason = "a flat data table of 14 per-kind row templates ported 1:1 from summary-check.jq; splitting would obscure the correspondence"
)]
fn check_sections_core() -> Vec<SectionSpec> {
vec![
SectionSpec {
name: "Unused files",
key: "unused_files",
header: "Files not reachable from any entry point.\n\n| File |\n|------|\n",
row: |it| format!("| {}{} |", code_cell(it, "path"), caveat_cell_suffix(it)),
},
SectionSpec {
name: "Unused exports",
key: "unused_exports",
header: "Exported symbols with no known consumers.\n\n| File | Line | Export |\n|------|-----:|--------|\n",
row: |it| {
format!(
"| {} | {} | {}{}{} |",
code_cell(it, "path"),
num(it, "line"),
code_cell(it, "export_name"),
if b(it, "is_re_export") {
" *(re-export)*"
} else {
""
},
caveat_cell_suffix(it),
)
},
},
SectionSpec {
name: "Unused types",
key: "unused_types",
header: "Type exports with no known consumers.\n\n| File | Line | Type |\n|------|-----:|------|\n",
row: |it| {
format!(
"| {} | {} | {}{} |",
code_cell(it, "path"),
num(it, "line"),
code_cell(it, "export_name"),
caveat_cell_suffix(it),
)
},
},
SectionSpec {
name: "Private type leaks",
key: "private_type_leaks",
header: "Exported signatures that reference same-file private types.\n\n| File | Line | Export | Private type |\n|------|-----:|--------|--------------|\n",
row: |it| {
format!(
"| {} | {} | {} | {} |",
code_cell(it, "path"),
num(it, "line"),
code_cell(it, "export_name"),
code_cell(it, "type_name"),
)
},
},
SectionSpec {
name: "Deprecated exports in use",
key: "deprecated_exports_in_use",
header: "Exports marked `@deprecated` that still have consumers.\n\n| File | Line | Export | Consumers |\n|------|-----:|--------|----------:|\n",
row: |it| {
format!(
"| {} | {} | {} | {} |",
code_cell(it, "path"),
num(it, "line"),
code_cell(it, "export_name"),
num(it, "consumer_count"),
)
},
},
SectionSpec {
name: "Unused dependencies",
key: "unused_dependencies",
header: "Listed in `dependencies` but never imported by the declaring workspace.\n\n| Package | Imported elsewhere |\n|---------|--------------------|\n",
row: |it| {
format!(
"| {}{} | {} |",
code_cell(it, "package_name"),
caveat_cell_suffix(it),
check_workspace_context(it),
)
},
},
SectionSpec {
name: "Unused devDependencies",
key: "unused_dev_dependencies",
header: "Listed in `devDependencies` but never imported or referenced by the declaring workspace.\n\n| Package | Imported elsewhere |\n|---------|--------------------|\n",
row: |it| {
format!(
"| {}{} | {} |",
code_cell(it, "package_name"),
caveat_cell_suffix(it),
check_workspace_context(it),
)
},
},
SectionSpec {
name: "Unused optionalDependencies",
key: "unused_optional_dependencies",
header: "Listed in `optionalDependencies` but never imported by the declaring workspace.\n\n| Package | Imported elsewhere |\n|---------|--------------------|\n",
row: |it| {
format!(
"| {}{} | {} |",
code_cell(it, "package_name"),
caveat_cell_suffix(it),
check_workspace_context(it),
)
},
},
SectionSpec {
name: "Unused enum members",
key: "unused_enum_members",
header: "Enum members never referenced outside their declaration.\n\n| File | Line | Enum | Member |\n|------|-----:|------|--------|\n",
row: member_row,
},
SectionSpec {
name: "Unused class members",
key: "unused_class_members",
header: "Class methods or properties never referenced outside their class.\n\n| File | Line | Class | Member |\n|------|-----:|-------|--------|\n",
row: member_row,
},
SectionSpec {
name: "Unused store members",
key: "unused_store_members",
header: "Pinia store members (state, getter, action) never accessed by any consumer.\n\n| File | Line | Store | Member |\n|------|-----:|-------|--------|\n",
row: member_row,
},
SectionSpec {
name: "Unresolved imports",
key: "unresolved_imports",
header: "Import paths that could not be resolved - check for missing packages or broken paths.\n\n| File | Line | Import |\n|------|-----:|--------|\n",
row: |it| {
format!(
"| {} | {} | {} |",
code_cell(it, "path"),
num(it, "line"),
code_cell(it, "specifier"),
)
},
},
SectionSpec {
name: "Unlisted dependencies",
key: "unlisted_dependencies",
header: "Packages imported in code but missing from `package.json`.\n\n| Package | Used in |\n|---------|--------|\n",
row: |it| {
let sites: Vec<&Value> = arr(it, "imported_from").collect();
let cell = if sites.is_empty() {
String::new()
} else {
let shown = sites
.iter()
.take(3)
.map(|site| {
markdown_table_code_span(&format!(
"{}:{}",
s(site, "path"),
num(site, "line")
))
})
.collect::<Vec<_>>()
.join(", ");
let more = if sites.len() > 3 {
format!(" *+{} more*", sites.len() - 3)
} else {
String::new()
};
format!("{shown}{more}")
};
format!("| {} | {cell} |", code_cell(it, "package_name"))
},
},
SectionSpec {
name: "Duplicate exports",
key: "duplicate_exports",
header: "Same export name defined in multiple files - barrel re-exports may resolve ambiguously.\n\n| Export | Locations |\n|--------|-----------|\n",
row: |it| {
let locations: Vec<&Value> = arr(it, "locations").collect();
let shown = locations
.iter()
.take(3)
.map(|location| {
markdown_table_code_span(&format!(
"{}:{}",
s(location, "path"),
num(location, "line")
))
})
.collect::<Vec<_>>()
.join(", ");
let more = if locations.len() > 3 {
format!(" *+{} more*", locations.len() - 3)
} else {
String::new()
};
format!("| {} | {shown}{more} |", code_cell(it, "export_name"))
},
},
SectionSpec {
name: "Circular dependencies",
key: "circular_dependencies",
header: "Import cycles that can cause initialization failures and prevent tree-shaking.\n\n| Cycle | Length |\n|-------|-------:|\n",
row: |it| {
let cycle = arr(it, "files")
.filter_map(Value::as_str)
.map(markdown_table_code_span)
.collect::<Vec<_>>()
.join(" \u{2192} ");
format!("| {cycle} | {} |", num(it, "length"))
},
},
]
}
/// Shared by the enum, class, and store member sections. `caveat_cell_suffix`
/// renders nothing for an array the analysis pass does not stamp, so the three
/// sections stay correct without each knowing which set it belongs to, and a
/// future array that starts carrying caveats needs no change here.
fn member_row(it: &Value) -> String {
format!(
"| {} | {} | {} | {}{} |",
code_cell(it, "path"),
num(it, "line"),
code_cell(it, "parent_name"),
code_cell(it, "member_name"),
caveat_cell_suffix(it),
)
}
fn plain_join(item: &Value, key: &str, separator: &str) -> String {
arr(item, key)
.filter_map(Value::as_str)
.collect::<Vec<_>>()
.join(separator)
}
fn path_line_cell(it: &Value) -> String {
markdown_table_code_span(&format!("{}:{}", s(it, "path"), num(it, "line")))
}
#[expect(
clippy::too_many_lines,
reason = "a flat data table of 14 per-kind row templates ported 1:1 from summary-check.jq; splitting would obscure the correspondence"
)]
fn check_sections_architecture() -> Vec<SectionSpec> {
vec![
SectionSpec {
name: "Re-export cycles",
key: "re_export_cycles",
header: "Barrel files that re-export from each other in a loop. Chain propagation through the loop is a no-op, so imports through any member may silently come up empty.\n\n| Cycle | Kind | Members |\n|-------|------|--------:|\n",
row: |it| {
let cycle = arr(it, "files")
.filter_map(Value::as_str)
.map(markdown_table_code_span)
.collect::<Vec<_>>()
.join(" <-> ");
format!(
"| {cycle} | {} | {} |",
markdown_table_text(s(it, "kind")),
arr(it, "files").count()
)
},
},
SectionSpec {
name: "Boundary violations",
key: "boundary_violations",
header: "Imports that cross defined architecture zone boundaries.\n\n| From | To | Zones |\n|------|-----|-------|\n",
row: |it| {
format!(
"| {} | {} | {} \u{2192} {} |",
markdown_table_code_span(&format!(
"{}:{}",
s(it, "from_path"),
num(it, "line")
)),
code_cell(it, "to_path"),
markdown_table_code_span(s(it, "from_zone")),
markdown_table_code_span(s(it, "to_zone")),
)
},
},
SectionSpec {
name: "Boundary coverage",
key: "boundary_coverage_violations",
header: "Files that match no configured architecture boundary zone.\n\n| File |\n|------|\n",
row: |it| format!("| {} |", path_line_cell(it)),
},
SectionSpec {
name: "Boundary calls",
key: "boundary_call_violations",
header: "Calls from zoned files to callees forbidden for that zone.\n\n| File | Callee | Zone | Pattern |\n|------|--------|------|---------|\n",
row: |it| {
format!(
"| {} | {} | {} | {} |",
path_line_cell(it),
code_cell(it, "callee"),
markdown_table_code_span(s(it, "zone")),
code_cell(it, "pattern"),
)
},
},
SectionSpec {
name: "Policy violations",
key: "policy_violations",
header: "Banned calls, imports, and catalogue-derived effects matched by configured rule packs.\n\n| File | Matched | Rule | Severity |\n|------|---------|------|----------|\n",
row: |it| {
format!(
"| {} | {} | {} | {} |",
path_line_cell(it),
code_cell(it, "matched"),
markdown_table_code_span(&format!("{}/{}", s(it, "pack"), s(it, "rule_id"))),
markdown_table_text(s(it, "severity")),
)
},
},
SectionSpec {
name: "Invalid client exports",
key: "invalid_client_exports",
header: "`\"use client\"` files exporting a Next.js server-only / route-config name. Next.js rejects this at build time.\n\n| File | Export | Directive |\n|------|--------|-----------|\n",
row: |it| {
format!(
"| {} | {} | {} |",
path_line_cell(it),
code_cell(it, "export_name"),
markdown_table_code_span(&format!("\"{}\"", s(it, "directive"))),
)
},
},
SectionSpec {
name: "Mixed client/server barrels",
key: "mixed_client_server_barrels",
header: "Barrels re-exporting both a `\"use client\"` module and a server-only module. One import drags the other's directive across the boundary.\n\n| File | Client origin | Server origin |\n|------|---------------|---------------|\n",
row: |it| {
format!(
"| {} | {} | {} |",
path_line_cell(it),
code_cell(it, "client_origin"),
code_cell(it, "server_origin"),
)
},
},
SectionSpec {
name: "Misplaced directives",
key: "misplaced_directives",
header: "`\"use client\"` / `\"use server\"` directives written after a non-directive statement, so the RSC bundler ignores them. Move the directive to the top of the file.\n\n| File | Directive |\n|------|-----------|\n",
row: |it| {
format!(
"| {} | {} |",
path_line_cell(it),
markdown_table_code_span(&format!("\"{}\"", s(it, "directive"))),
)
},
},
SectionSpec {
name: "Unused server actions",
key: "unused_server_actions",
header: "Next.js Server Actions (exports of a `\"use server\"` file) that no project code references. The endpoint stays POST-able, but no code calls it (likely dead).\n\n| File | Action |\n|------|--------|\n",
row: |it| {
format!(
"| {} | {} |",
path_line_cell(it),
code_cell(it, "action_name")
)
},
},
SectionSpec {
name: "Route collisions",
key: "route_collisions",
header: "Next.js App Router route files that resolve to the same URL within one app-root. Next.js fails the build because a URL can have only one owner.\n\n| File | URL |\n|------|-----|\n",
row: |it| format!("| {} | {} |", code_cell(it, "path"), code_cell(it, "url")),
},
SectionSpec {
name: "Dynamic segment conflicts",
key: "dynamic_segment_name_conflicts",
header: "Sibling Next.js dynamic route segments at one position using different slug names. Next.js requires one consistent name per dynamic path.\n\n| File | Position | Segments |\n|------|----------|----------|\n",
row: |it| {
format!(
"| {} | {} | {} |",
code_cell(it, "path"),
code_cell(it, "position"),
markdown_table_code_span(&plain_join(it, "conflicting_segments", ", ")),
)
},
},
]
}
#[expect(
clippy::too_many_lines,
reason = "a flat data table of 14 per-kind row templates ported 1:1 from summary-check.jq; splitting would obscure the correspondence"
)]
fn check_sections_frameworks_and_hygiene() -> Vec<SectionSpec> {
vec![
SectionSpec {
name: "Unrendered components",
key: "unrendered_components",
header: "Vue/Svelte components reachable in the module graph but rendered nowhere: no tag, no dynamic binding, no registration. A barrel re-export keeps them alive even though nothing instantiates them.\n\n| File | Component | Framework |\n|------|-----------|-----------|\n",
row: |it| {
format!(
"| {} | {} | {} |",
path_line_cell(it),
code_cell(it, "component_name"),
markdown_table_text(s(it, "framework")),
)
},
},
SectionSpec {
name: "Unused component props",
key: "unused_component_props",
header: "Vue `defineProps` props referenced nowhere inside their own single-file component (neither script nor template).\n\n| File | Component | Prop |\n|------|-----------|------|\n",
row: |it| component_detail_row(it, "prop_name"),
},
SectionSpec {
name: "Unused component emits",
key: "unused_component_emits",
header: "Vue `defineEmits` events emitted nowhere inside their own single-file component (no matching `emit()` call).\n\n| File | Component | Event |\n|------|-----------|-------|\n",
row: |it| component_detail_row(it, "emit_name"),
},
SectionSpec {
name: "Unused component inputs",
key: "unused_component_inputs",
header: "Angular `@Input()` / signal `input()` declarations read nowhere inside their own component (neither class body nor template).\n\n| File | Component | Input |\n|------|-----------|-------|\n",
row: |it| component_detail_row(it, "input_name"),
},
SectionSpec {
name: "Unused component outputs",
key: "unused_component_outputs",
header: "Angular `@Output()` / signal `output()` declarations emitted nowhere inside their own component (no matching `emit()` call).\n\n| File | Component | Output |\n|------|-----------|--------|\n",
row: |it| component_detail_row(it, "output_name"),
},
SectionSpec {
name: "Unused Svelte events",
key: "unused_svelte_events",
header: "Svelte components dispatching a `createEventDispatcher` event listened to nowhere in the project (cross-file dead-output direction).\n\n| File | Component | Event |\n|------|-----------|-------|\n",
row: |it| component_detail_row(it, "event_name"),
},
SectionSpec {
name: "Unprovided injects",
key: "unprovided_injects",
header: "Vue `inject` / Svelte `getContext` calls for a key that no ancestor `provide` / `setContext` supplies.\n\n| File | Key | Framework |\n|------|-----|-----------|\n",
row: |it| {
format!(
"| {} | {} | {} |",
path_line_cell(it),
code_cell(it, "key_name"),
markdown_table_text(s(it, "framework")),
)
},
},
SectionSpec {
name: "Unused load data keys",
key: "unused_load_data_keys",
header: "SvelteKit `load()` return-object keys read by no consumer (neither the sibling `+page.svelte` nor `$page.data`). The key runs a real server fetch / DB cost per request for data nothing renders.\n\n| File | Route | Key |\n|------|-------|-----|\n",
row: |it| {
format!(
"| {} | {} | {} |",
path_line_cell(it),
code_cell(it, "route_dir"),
code_cell(it, "key_name"),
)
},
},
SectionSpec {
name: "Type-only dependencies",
key: "type_only_dependencies",
header: "Dependencies only used for type imports - consider moving to `devDependencies`.\n\n| Package |\n|---------|\n",
row: package_row,
},
SectionSpec {
name: "Test-only dependencies",
key: "test_only_dependencies",
header: "Production dependencies only imported by test files - consider moving to `devDependencies`.\n\n| Package |\n|---------|\n",
row: package_row,
},
SectionSpec {
name: "Dev dependencies used in production",
key: "dev_dependencies_in_production",
header: "`devDependencies` imported by production code at runtime - consider moving to `dependencies` so a production-only install does not break.\n\n| Package |\n|---------|\n",
row: package_row,
},
SectionSpec {
name: "Stale suppressions",
key: "stale_suppressions",
header: "Suppression comments or JSDoc tags that no longer match any active issue.\n\n| File | Line | Description |\n|------|-----:|-------------|\n",
row: |it| {
format!(
"| {} | {} | {} |",
code_cell(it, "path"),
num(it, "line"),
stale_suppression_description(it),
)
},
},
]
}
fn component_detail_row(it: &Value, detail_key: &str) -> String {
format!(
"| {} | {} | {} |",
path_line_cell(it),
code_cell(it, "component_name"),
code_cell(it, detail_key),
)
}
fn package_row(it: &Value) -> String {
format!("| {} |", code_cell(it, "package_name"))
}
fn stale_suppression_description(it: &Value) -> String {
let origin = it.get("origin").cloned().unwrap_or(Value::Null);
if s(&origin, "type") == "jsdoc_tag" {
return format!(
"`@expected-unused` on {}",
code_cell(&origin, "export_name")
);
}
if origin.get("kind_known").and_then(Value::as_bool) == Some(false) {
return format!("unknown kind {}", code_cell(&origin, "issue_kind"));
}
match origin.get("issue_kind").and_then(Value::as_str) {
Some(kind) => markdown_table_code_span(kind),
None => "blanket".to_owned(),
}
}
fn check_sections_catalog() -> Vec<SectionSpec> {
vec![
SectionSpec {
name: "Unused catalog entries",
key: "unused_catalog_entries",
header: "pnpm catalog entries not referenced by any workspace package.\n\n| Entry | Catalog | Location | Hardcoded consumers |\n|-------|---------|----------|---------------------|\n",
row: |it| {
format!(
"| {} | {} | {} | {} |",
code_cell(it, "entry_name"),
code_cell(it, "catalog_name"),
path_line_cell(it),
backtick_join(it, "hardcoded_consumers"),
)
},
},
SectionSpec {
name: "Empty catalog groups",
key: "empty_catalog_groups",
header: "Named pnpm catalog groups with no entries.\n\n| Catalog | Location |\n|---------|----------|\n",
row: |it| {
format!(
"| {} | {} |",
code_cell(it, "catalog_name"),
path_line_cell(it)
)
},
},
SectionSpec {
name: "Unresolved catalog references",
key: "unresolved_catalog_references",
header: "Workspace `package.json` references to catalogs that do not declare the package. `pnpm install` will fail until each entry is added to its named catalog or the reference is switched.\n\n| Entry | Catalog | Location | Available in |\n|-------|---------|----------|--------------|\n",
row: |it| {
format!(
"| {} | {} | {} | {} |",
code_cell(it, "entry_name"),
code_cell(it, "catalog_name"),
path_line_cell(it),
backtick_join(it, "available_in_catalogs"),
)
},
},
SectionSpec {
name: "Unused dependency overrides",
key: "unused_dependency_overrides",
header: "Package-manager override entries forcing a version no workspace package depends on. Some entries may be intentional pins for transitive CVEs; the hint column flags those.\n\n| Override | Forces | Source | Location | Hint |\n|----------|--------|--------|----------|------|\n",
row: |it| {
format!(
"| {} | {} -> {} | {} | {} | {} |",
code_cell(it, "raw_key"),
code_cell(it, "target_package"),
code_cell(it, "version_range"),
code_cell(it, "source"),
path_line_cell(it),
markdown_table_text(str_or(it, "hint", "")),
)
},
},
SectionSpec {
name: "Misconfigured dependency overrides",
key: "misconfigured_dependency_overrides",
header: "Package-manager override entries with an unparsable key or empty value. The active package manager will reject or ignore these.\n\n| Override | Value | Source | Location | Reason |\n|----------|-------|--------|----------|--------|\n",
row: |it| {
format!(
"| {} | {} | {} | {} | {} |",
markdown_table_code_span(str_or(it, "raw_key", "")),
markdown_table_code_span(str_or(it, "raw_value", "")),
code_cell(it, "source"),
path_line_cell(it),
markdown_table_text(str_or(it, "reason", "unparsable")),
)
},
},
]
}
fn check_tips(env: &Value) -> String {
let fixable = arr(env, "unused_exports").count()
+ arr(env, "unused_dependencies").count()
+ arr(env, "unused_enum_members").count();
let mut tips = String::from("\n\n> [!TIP]\n");
if fixable > 0 {
tips.push_str("> Run `fallow fix --dry-run` to preview safe auto-fixes.\n");
}
if arr(env, "unused_exports").count() > 0 {
let _ = writeln!(
tips,
"> Intentionally public? Add [`/** @public */`]({SUPPRESSION_DOCS}) above exports to preserve them."
);
}
let _ = write!(
tips,
"> Add [`// fallow-ignore-next-line`]({SUPPRESSION_DOCS}) above a line to suppress a specific finding."
);
tips
}
/// Port of `summary-check.jq`.
#[must_use]
fn render_check_summary(env: &Value) -> String {
let elapsed = num(env, "elapsed_ms");
let total_issues = u(env, "total_issues");
if total_issues == 0 {
return format!(
"# Fallow Analysis\n\n> [!NOTE]\n> **No issues found** \u{b7} {elapsed}ms\n\nAll exports are used, all dependencies are declared, and no issues were detected."
);
}
let mut sections = String::new();
for group in [
check_sections_core(),
check_sections_architecture(),
check_sections_frameworks_and_hygiene(),
check_sections_catalog(),
] {
for spec in &group {
sections.push_str(&render_check_section(env, spec));
}
}
let issue_noun = if total_issues == 1 { "issue" } else { "issues" };
format!(
"# Fallow Analysis\n\n> [!WARNING]\n> **{total_issues} {issue_noun}** found \u{b7} {elapsed}ms\n\n| Category | Count |\n|----------|------:|\n{}\n\n---\n{sections}{}",
dead_code_category_table(env),
check_tips(env),
)
}
// ---------------------------------------------------------------------------
// summary-dupes.jq
// ---------------------------------------------------------------------------
fn dupes_family_entry(family: &Value) -> String {
let files: Vec<String> = arr(family, "files")
.filter_map(Value::as_str)
.map(markdown_code_span)
.collect();
let shown = files[..files.len().min(3)].join(", ");
let more = if files.len() > 3 {
format!(" (+{} more)", files.len() - 3)
} else {
String::new()
};
let mut entry = format!(
"- **{shown}{more}** - {} lines, {} groups",
num(family, "total_duplicated_lines"),
arr(family, "groups").count(),
);
if let Some(best_group) = best_clone_group(family)
&& arr(best_group, "instances").next().is_some()
{
let locations = arr(best_group, "instances")
.map(instance_location)
.collect::<Vec<_>>()
.join(", ");
let _ = write!(entry, "\n - {locations}");
}
if arr(family, "suggestions").next().is_some() {
let suggestions = arr(family, "suggestions")
.map(|suggestion| {
format!(
" - {} (~{} lines)",
markdown_table_text(s(suggestion, "description")),
num(suggestion, "estimated_savings"),
)
})
.collect::<Vec<_>>()
.join("\n");
let _ = write!(entry, "\n{suggestions}");
}
entry
}
fn instance_location(instance: &Value) -> String {
markdown_code_span(&format!(
"{}:{}-{}",
s(instance, "file"),
num(instance, "start_line"),
num(instance, "end_line"),
))
}
type CloneGroupJsonRankKey = (
std::cmp::Reverse<u128>,
std::cmp::Reverse<u64>,
std::cmp::Reverse<u64>,
std::cmp::Reverse<usize>,
std::cmp::Reverse<u64>,
String,
u64,
);
fn clone_group_rank_key(group: &Value) -> CloneGroupJsonRankKey {
const WEIGHTS: [u64; 9] = [
1_000_000_000,
1_047_319_732,
1_075_000_000,
1_094_639_463,
1_109_873_014,
1_122_319_732,
1_132_843_281,
1_141_959_195,
1_150_000_000,
];
let spread = u(group, "spread");
let token_count = u(group, "token_count");
let instances: Vec<&Value> = arr(group, "instances").collect();
let first = instances
.iter()
.min_by_key(|instance| (s(instance, "file").to_string(), u(instance, "start_line")));
let weight = WEIGHTS[usize::try_from(spread.min(8)).unwrap_or(8)];
let score = u128::from(token_count)
.saturating_mul(u128::try_from(instances.len()).unwrap_or(u128::MAX))
.saturating_mul(u128::from(weight));
(
std::cmp::Reverse(score),
std::cmp::Reverse(spread),
std::cmp::Reverse(token_count),
std::cmp::Reverse(instances.len()),
std::cmp::Reverse(u(group, "line_count")),
first.map_or_else(String::new, |instance| s(instance, "file").to_string()),
first.map_or(0, |instance| u(instance, "start_line")),
)
}
/// Reapply the engine's canonical clone ranking from public JSON fields.
fn sorted_clone_groups(env: &Value) -> Vec<&Value> {
let mut groups: Vec<&Value> = arr(env, "clone_groups").collect();
groups.sort_by_cached_key(|group| clone_group_rank_key(group));
groups
}
fn best_clone_group(family: &Value) -> Option<&Value> {
arr(family, "groups").min_by_key(|group| clone_group_rank_key(group))
}
type CloneFamilyJsonRankKey = (bool, Option<CloneGroupJsonRankKey>, Vec<String>);
fn clone_family_rank_key(family: &Value) -> CloneFamilyJsonRankKey {
let best_group = best_clone_group(family).map(clone_group_rank_key);
let files = arr(family, "files")
.filter_map(Value::as_str)
.map(str::to_owned)
.collect();
(best_group.is_none(), best_group, files)
}
fn sorted_clone_families(env: &Value) -> Vec<&Value> {
let mut families: Vec<&Value> = arr(env, "clone_families").collect();
families.sort_by_cached_key(|family| clone_family_rank_key(family));
families
}
/// What the measured corpus holds, which is what the headline beside this block
/// counts: the entries that reached the envelope plus the ones a presentation
/// cap such as `--top` withheld before it got here.
///
/// `clone_groups_omitted` / `clone_families_omitted` are `0` on an untruncated
/// run, so this is the plain array length in the common case.
fn dupes_corpus_total(env: &Value, listed: usize, omitted_key: &str) -> usize {
listed.saturating_add(u(env, omitted_key) as usize)
}
/// Name what this listing does not show, so the corpus total in the headline is
/// never read as the size of the list below it.
///
/// `withheld` is measured against the corpus, not against the envelope array:
/// two caps stack here, `--top` before the envelope was built and this block's
/// own display limit after, and a reader who acts on the listing needs the
/// total of both. Empty when the listing is complete, which keeps an
/// untruncated run's summary byte-identical.
fn dupes_omission_tail(withheld: usize, capped_by_top: usize, noun: &str) -> String {
if withheld == 0 {
return String::new();
}
if capped_by_top == 0 {
return format!("\n- *... and {withheld} more {noun}*");
}
format!(
"\n- *... and {withheld} more {noun}, {capped_by_top} of them withheld by a display limit before this report*"
)
}
fn dupes_details(env: &Value) -> String {
let families = sorted_clone_families(env);
if families.is_empty() {
let groups = sorted_clone_groups(env);
let rows = groups
.iter()
.take(20)
.map(|group| {
let locations = arr(group, "instances")
.map(instance_location)
.collect::<Vec<_>>()
.join(", ");
format!(
"- **{} lines, {} tokens**, {locations}",
num(group, "line_count"),
num(group, "token_count"),
)
})
.collect::<Vec<_>>()
.join("\n");
// Subtract what this listing actually rendered, not the display limit:
// a `--top` below the limit leaves fewer rows than 20, and measuring
// the withholding against the limit would under-report it.
let total = dupes_corpus_total(env, groups.len(), "clone_groups_omitted");
let tail = dupes_omission_tail(
total.saturating_sub(groups.len().min(20)),
u(env, "clone_groups_omitted") as usize,
"groups",
);
format!("{rows}{tail}")
} else {
let entries = families
.iter()
.take(15)
.map(|family| dupes_family_entry(family))
.collect::<Vec<_>>()
.join("\n");
let total = dupes_corpus_total(env, families.len(), "clone_families_omitted");
let tail = dupes_omission_tail(
total.saturating_sub(families.len().min(15)),
u(env, "clone_families_omitted") as usize,
"families",
);
format!("**Clone Families ({total})**\n\n{entries}{tail}")
}
}
/// Port of `summary-dupes.jq`.
#[must_use]
fn render_dupes_summary(env: &Value) -> String {
let stats = env.get("stats").cloned().unwrap_or(Value::Null);
let elapsed = num(env, "elapsed_ms");
if u(&stats, "clone_groups") == 0 {
return format!(
"## Fallow - Code Duplication\n\nNo code duplication found.\n\n*Analyzed {} files in {elapsed}ms*",
num(&stats, "total_files"),
);
}
format!(
"## Fallow - Code Duplication\n\nFound **{} clone groups** ({} instances) across {} files in {elapsed}ms\n\n| Metric | Value |\n|--------|-------|\n| Files analyzed | {} |\n| Files with clones | {} |\n| Clone groups | {} |\n| Clone instances | {} |\n| Duplicated lines | {} / {} ({}%) |\n\n<details>\n<summary>View details</summary>\n\n{}\n\n</details>",
num(&stats, "clone_groups"),
num(&stats, "clone_instances"),
num(&stats, "files_with_clones"),
num(&stats, "total_files"),
num(&stats, "files_with_clones"),
num(&stats, "clone_groups"),
num(&stats, "clone_instances"),
num(&stats, "duplicated_lines"),
num(&stats, "total_lines"),
pct(f_or_zero(&stats, "duplication_percentage")),
dupes_details(env),
)
}
// ---------------------------------------------------------------------------
// summary-health.jq
// ---------------------------------------------------------------------------
fn metric_delta<'v>(score_env: &'v Value, name: &str) -> Option<&'v Value> {
score_env
.get("health_trend")
.and_then(|trend| trend.get("metrics"))
.and_then(Value::as_array)
.and_then(|metrics| metrics.iter().find(|metric| s(metric, "name") == name))
}
/// The `> **Health: ...**` trend header shared by `summary-health.jq` and
/// `summary-combined.jq`. Empty when no `health_score` block is present.
fn health_score_header(score_env: &Value) -> String {
let Some(score) = score_env
.get("health_score")
.filter(|value| !value.is_null())
else {
return String::new();
};
let mut header = format!(
"> **Health: {} ({})**",
s(score, "grade"),
pct(f_or_zero(score, "score")),
);
if let Some(score_delta) = metric_delta(score_env, "score") {
let compared = score_env
.get("health_trend")
.and_then(|trend| trend.get("compared_to"))
.cloned()
.unwrap_or(Value::Null);
let _ = write!(
header,
" \u{b7} {} pts vs previous ({} {})",
signed(f_or_zero(score_delta, "delta")),
s(&compared, "grade"),
pct(f_or_zero(&compared, "score")),
);
if let Some(dead_delta) = metric_delta(score_env, "dead_export_pct")
&& f_or_zero(dead_delta, "delta") != 0.0
{
let _ = write!(
header,
" \u{b7} {} {}% ({}%)",
s(dead_delta, "label").to_ascii_lowercase(),
pct(f_or_zero(dead_delta, "current")),
signed(f_or_zero(dead_delta, "delta")),
);
if f_or_zero(dead_delta, "delta") > 0.0 {
let _ = write!(header, " [suppress?]({SUPPRESSION_DOCS})");
}
}
if let Some(cx_delta) = metric_delta(score_env, "avg_cyclomatic")
&& f_or_zero(cx_delta, "delta") != 0.0
{
let _ = write!(
header,
" \u{b7} {} {} ({})",
s(cx_delta, "label").to_ascii_lowercase(),
pct(f_or_zero(cx_delta, "current")),
signed(f_or_zero(cx_delta, "delta")),
);
}
} else {
header.push_str("\n> _Enable `save-snapshot: true` to track score trends over time._");
}
header.push_str("\n\n");
header
}
fn exceeded_marker(it: &Value, needles: &[&str]) -> &'static str {
let exceeded = s(it, "exceeded");
if needles.iter().any(|needle| exceeded.contains(needle)) {
" **!**"
} else {
""
}
}
fn crap_cell(it: &Value) -> String {
match it.get("crap").filter(|crap| !crap.is_null()) {
None => "-".to_owned(),
Some(crap) => format!("{}{}", fmt_num(crap), exceeded_marker(it, &["crap", "all"])),
}
}
fn complexity_table_row(it: &Value) -> String {
format!(
"| {} | {} | {} | {}{} | {}{} | {} | {} |",
path_line_cell(it),
code_cell(it, "name"),
markdown_table_text(str_or(it, "severity", "moderate")),
num(it, "cyclomatic"),
exceeded_marker(it, &["cyclomatic", "both", "all"]),
num(it, "cognitive"),
exceeded_marker(it, &["cognitive", "both", "all"]),
crap_cell(it),
num(it, "line_count"),
)
}
const COMPLEXITY_TABLE_HEADER: &str = "| File | Function | Severity | Cyclomatic | Cognitive | CRAP | Lines |\n|:-----|:---------|:---------|:-----------|:----------|:-----|:------|\n";
fn health_thresholds_footer(env: &Value) -> String {
let summary = env.get("summary").cloned().unwrap_or(Value::Null);
format!(
"\n\n**!** marks the dimension that breached.\n\n**{}** files, **{}** functions analyzed (thresholds: cyclomatic > {}, cognitive > {}, CRAP >= {})",
num(&summary, "files_analyzed"),
num(&summary, "functions_analyzed"),
num(&summary, "max_cyclomatic_threshold"),
num(&summary, "max_cognitive_threshold"),
threshold_or(&summary, "max_crap_threshold", "30"),
)
}
fn threshold_or(summary: &Value, key: &str, default: &str) -> String {
summary
.get(key)
.filter(|value| !value.is_null())
.map_or_else(|| default.to_owned(), fmt_num)
}
fn complexity_rows(findings: &[&Value], cap: usize) -> String {
findings
.iter()
.take(cap)
.map(|finding| complexity_table_row(finding))
.collect::<Vec<_>>()
.join("\n")
}
fn runtime_finding_row(it: &Value) -> String {
let invocations = it
.get("invocations")
.filter(|value| !value.is_null())
.map_or_else(|| "-".to_owned(), fmt_num);
format!(
"| {} | {} | {} | {invocations} | {} |",
path_line_cell(it),
code_cell(it, "function"),
code_cell(it, "verdict"),
markdown_table_text(s(it, "confidence")),
)
}
fn render_health_complexity_only(env: &Value, complex: usize, elapsed: &str) -> String {
let summary = env.get("summary").cloned().unwrap_or(Value::Null);
if complex == 0 {
return format!(
"## Fallow - Code Complexity\n\n> [!NOTE]\n> **No functions exceed complexity thresholds** \u{b7} {elapsed}ms\n\n{} functions analyzed (max cyclomatic: {}, max cognitive: {}, max CRAP: {})",
num(&summary, "functions_analyzed"),
num(&summary, "max_cyclomatic_threshold"),
num(&summary, "max_cognitive_threshold"),
threshold_or(&summary, "max_crap_threshold", "30"),
);
}
let above = u(&summary, "functions_above_threshold");
let findings: Vec<&Value> = arr(env, "findings").collect();
let tail = if complex > 25 {
format!(
"\n\n> {} more - run `fallow health` locally for the full list",
complex - 25
)
} else {
String::new()
};
format!(
"## Fallow - Code Complexity\n\n> [!WARNING]\n> **{above} function{} exceed{} thresholds** \u{b7} {elapsed}ms\n\n{COMPLEXITY_TABLE_HEADER}{}{tail}{}",
if above == 1 { "" } else { "s" },
if above == 1 { "s" } else { "" },
complexity_rows(&findings, 25),
health_thresholds_footer(env),
)
}
fn prod_phrase(complex: usize, prod: usize) -> String {
let complexity = format!(
"{complex} complexity finding{}",
if complex == 1 { "" } else { "s" }
);
let runtime = format!(
"{prod} runtime coverage finding{}",
if prod == 1 { "" } else { "s" }
);
if complex > 0 && prod > 0 {
format!("{complexity} and {runtime}")
} else if complex > 0 {
complexity
} else {
runtime
}
}
fn render_health_with_runtime(env: &Value, complex: usize, elapsed: &str) -> String {
let runtime = env.get("runtime_coverage").cloned().unwrap_or(Value::Null);
let prod_findings: Vec<&Value> = arr(&runtime, "findings").collect();
let hot_paths: Vec<&Value> = arr(&runtime, "hot_paths").collect();
let prod = prod_findings.len();
let hot = hot_paths.len();
let mut out = String::from("## Fallow - Health\n\n");
if complex == 0 && prod == 0 {
let _ = write!(
out,
"> [!NOTE]\n> **No failing health findings** \u{b7} {elapsed}ms\n\n"
);
} else {
let _ = write!(
out,
"> [!WARNING]\n> **{}** \u{b7} {elapsed}ms\n\n",
prod_phrase(complex, prod),
);
}
if complex > 0 {
let findings: Vec<&Value> = arr(env, "findings").collect();
let _ = write!(
out,
"### Complexity\n\n{COMPLEXITY_TABLE_HEADER}{}",
complexity_rows(&findings, 25),
);
if complex > 25 {
let _ = write!(
out,
"\n\n> {} more complexity findings - run `fallow health` locally for the full list",
complex - 25,
);
}
}
if prod > 0 {
if complex > 0 {
out.push_str("\n\n");
}
out.push_str("### Runtime Coverage\n\n| File | Function | Verdict | Invocations | Confidence |\n|:-----|:---------|:--------|------------:|:-----------|\n");
out.push_str(
&prod_findings
.iter()
.take(25)
.map(|finding| runtime_finding_row(finding))
.collect::<Vec<_>>()
.join("\n"),
);
if prod > 25 {
let _ = write!(
out,
"\n\n> {} more runtime coverage findings - run `fallow health` locally for the full list",
prod - 25,
);
}
}
if hot > 0 {
if complex > 0 || prod > 0 {
out.push_str("\n\n");
}
out.push_str("### Hot Paths\n\n| File | Function | Invocations | Percentile |\n|:-----|:---------|------------:|-----------:|\n");
out.push_str(
&hot_paths
.iter()
.take(10)
.map(|path| {
format!(
"| {} | {} | {} | {} |",
path_line_cell(path),
code_cell(path, "function"),
num(path, "invocations"),
num(path, "percentile"),
)
})
.collect::<Vec<_>>()
.join("\n"),
);
if hot > 10 {
let _ = write!(out, "\n\n> {} more hot paths in the full report", hot - 10);
}
}
out.push_str(&health_runtime_footer(env, complex, prod, hot, &runtime));
out
}
fn health_runtime_footer(
env: &Value,
complex: usize,
prod: usize,
hot: usize,
runtime: &Value,
) -> String {
if complex > 0 {
return health_thresholds_footer(env);
}
if prod > 0 {
let summary = runtime.get("summary").cloned().unwrap_or(Value::Null);
return format!(
"\n\n**{}** tracked functions, **{}** hit, **{}** unhit, **{}** untracked",
num(&summary, "functions_tracked"),
num(&summary, "functions_hit"),
num(&summary, "functions_unhit"),
num(&summary, "functions_untracked"),
);
}
format!(
"\n\nObserved **{hot}** hot path{} in runtime coverage.",
if hot == 1 { "" } else { "s" },
)
}
/// Port of `summary-health.jq`.
#[must_use]
fn render_health_summary(env: &Value) -> String {
let elapsed = num(env, "elapsed_ms");
let complex = arr(env, "findings").count();
let runtime = env.get("runtime_coverage").cloned().unwrap_or(Value::Null);
let prod = arr(&runtime, "findings").count();
let hot = arr(&runtime, "hot_paths").count();
let body = if prod == 0 && hot == 0 {
render_health_complexity_only(env, complex, &elapsed)
} else {
render_health_with_runtime(env, complex, &elapsed)
};
format!("{}{body}", health_score_header(env))
}
// ---------------------------------------------------------------------------
// summary-audit.jq
// ---------------------------------------------------------------------------
const fn audit_verdict_label(verdict: &str) -> &'static str {
match verdict.as_bytes() {
b"fail" => "[!WARNING]\n> **Audit failed**",
b"warn" => "[!WARNING]\n> **Audit passed with warnings**",
_ => "[!NOTE]\n> **Audit passed**",
}
}
fn introduced_label(item: &Value) -> &'static str {
match item.get("introduced").and_then(Value::as_bool) {
Some(true) => "new",
Some(false) => "inherited",
None => "-",
}
}
struct AuditRow {
kind: &'static str,
location: String,
item: String,
status: &'static str,
}
fn audit_row(kind: &'static str, location: String, item: String, finding: &Value) -> AuditRow {
AuditRow {
kind,
location,
item,
status: introduced_label(finding),
}
}
type AuditRowSpec = (&'static str, &'static str, fn(&Value) -> String);
/// jq order positions 2-11: exports, types, leaks, dependencies, members,
/// unresolved imports.
const AUDIT_EXPORT_DEP_ROWS: &[AuditRowSpec] = &[
("Unused export", "unused_exports", |it| {
code_cell(it, "export_name")
}),
("Unused type", "unused_types", |it| {
code_cell(it, "export_name")
}),
("Private type leak", "private_type_leaks", |it| {
format!(
"{} -> {}",
code_cell(it, "export_name"),
code_cell(it, "type_name")
)
}),
(
"Deprecated export in use",
"deprecated_exports_in_use",
|it| code_cell(it, "export_name"),
),
("Unused dependency", "unused_dependencies", |it| {
code_cell(it, "package_name")
}),
("Unused devDependency", "unused_dev_dependencies", |it| {
code_cell(it, "package_name")
}),
(
"Unused optionalDependency",
"unused_optional_dependencies",
|it| code_cell(it, "package_name"),
),
("Unused enum member", "unused_enum_members", member_item),
("Unused class member", "unused_class_members", member_item),
("Unused store member", "unused_store_members", member_item),
("Unresolved import", "unresolved_imports", |it| {
code_cell(it, "specifier")
}),
];
/// jq order positions 26-33: component-model kinds.
const AUDIT_COMPONENT_ROWS: &[AuditRowSpec] = &[
("Unrendered component", "unrendered_components", |it| {
format!(
"{} ({})",
code_cell(it, "component_name"),
markdown_table_text(s(it, "framework"))
)
}),
("Unused component prop", "unused_component_props", |it| {
component_member_item(it, "prop_name")
}),
("Unused component emit", "unused_component_emits", |it| {
format!(
"{} emit {}",
code_cell(it, "component_name"),
code_cell(it, "emit_name")
)
}),
("Unused component input", "unused_component_inputs", |it| {
component_member_item(it, "input_name")
}),
(
"Unused component output",
"unused_component_outputs",
|it| {
format!(
"{} output {}",
code_cell(it, "component_name"),
code_cell(it, "output_name")
)
},
),
("Unused Svelte event", "unused_svelte_events", |it| {
format!(
"{} event {}",
code_cell(it, "component_name"),
code_cell(it, "event_name")
)
}),
("Unprovided inject", "unprovided_injects", |it| {
format!(
"{} ({})",
code_cell(it, "key_name"),
markdown_table_text(s(it, "framework"))
)
}),
("Unused load data key", "unused_load_data_keys", |it| {
code_cell(it, "key_name")
}),
];
/// jq order positions 34-42: dependency hygiene, suppressions, catalog.
const AUDIT_HYGIENE_ROWS: &[AuditRowSpec] = &[
("Type-only dependency", "type_only_dependencies", |it| {
code_cell(it, "package_name")
}),
("Test-only dependency", "test_only_dependencies", |it| {
code_cell(it, "package_name")
}),
(
"Dev dependency in production",
"dev_dependencies_in_production",
|it| code_cell(it, "package_name"),
),
("Stale suppression", "stale_suppressions", |it| {
// The description is a literal suppression directive carrying
// user-authored comment text, so it renders as a code span.
markdown_table_code_span(str_or(it, "description", "suppression"))
}),
("Unused catalog entry", "unused_catalog_entries", |it| {
format!(
"{} ({})",
code_cell(it, "entry_name"),
code_cell(it, "catalog_name")
)
}),
("Empty catalog group", "empty_catalog_groups", |it| {
code_cell(it, "catalog_name")
}),
(
"Unresolved catalog reference",
"unresolved_catalog_references",
|it| {
format!(
"{} -> {}",
code_cell(it, "entry_name"),
code_cell(it, "catalog_name")
)
},
),
(
"Unused dependency override",
"unused_dependency_overrides",
|it| format!("{} ({})", code_cell(it, "raw_key"), code_cell(it, "source")),
),
(
"Misconfigured dependency override",
"misconfigured_dependency_overrides",
|it| format!("{} ({})", code_cell(it, "raw_key"), code_cell(it, "source")),
),
];
fn audit_rows_from_table(dead_code: &Value, table: &[AuditRowSpec], rows: &mut Vec<AuditRow>) {
for (kind, key, item_fn) in table {
for finding in arr(dead_code, key) {
rows.push(audit_row(
kind,
path_line(finding),
item_fn(finding),
finding,
));
}
}
}
fn member_item(it: &Value) -> String {
markdown_table_code_span(&format!(
"{}.{}",
s(it, "parent_name"),
s(it, "member_name")
))
}
fn component_member_item(it: &Value, member_key: &str) -> String {
markdown_table_code_span(&format!(
"{}.{}",
s(it, "component_name"),
s(it, member_key)
))
}
fn first_import_site(it: &Value) -> String {
arr(it, "imported_from").next().map_or_else(
|| path_line(it),
|site| {
markdown_table_code_span(&format!(
"{}:{}",
rel_path_absolute_only(s(site, "path")),
num(site, "line"),
))
},
)
}
/// jq order positions 12-15: unlisted dependencies, duplicate exports,
/// circular dependencies, re-export cycles.
fn audit_rows_graph(dead_code: &Value, rows: &mut Vec<AuditRow>) {
for it in arr(dead_code, "unlisted_dependencies") {
rows.push(audit_row(
"Unlisted dependency",
first_import_site(it),
code_cell(it, "package_name"),
it,
));
}
for it in arr(dead_code, "duplicate_exports") {
let location = arr(it, "locations")
.take(3)
.map(|loc| {
markdown_table_code_span(&format!(
"{}:{}",
rel_path_absolute_only(s(loc, "path")),
num(loc, "line")
))
})
.collect::<Vec<_>>()
.join(", ");
rows.push(audit_row(
"Duplicate export",
location,
code_cell(it, "export_name"),
it,
));
}
for it in arr(dead_code, "circular_dependencies") {
let location = arr(it, "files")
.filter_map(Value::as_str)
.map(|file| markdown_table_code_span(&rel_path_absolute_only(file)))
.collect::<Vec<_>>()
.join(" -> ");
rows.push(audit_row(
"Circular dependency",
location,
"cycle".to_owned(),
it,
));
}
for it in arr(dead_code, "re_export_cycles") {
let location = arr(it, "files")
.filter_map(Value::as_str)
.map(|file| markdown_table_code_span(&rel_path_absolute_only(file)))
.collect::<Vec<_>>()
.join(" <-> ");
rows.push(audit_row(
"Re-export cycle",
location,
markdown_table_text(str_or(it, "kind", "cycle")),
it,
));
}
}
/// jq order positions 16-19: boundary and policy kinds.
fn audit_rows_boundaries(dead_code: &Value, rows: &mut Vec<AuditRow>) {
for it in arr(dead_code, "boundary_violations") {
rows.push(audit_row(
"Boundary violation",
rel_path_line_cell(it, "from_path"),
format!(
"{} -> {}",
markdown_table_code_span(s(it, "from_zone")),
markdown_table_code_span(s(it, "to_zone"))
),
it,
));
}
for it in arr(dead_code, "boundary_coverage_violations") {
rows.push(audit_row(
"Boundary coverage",
rel_path_line_cell(it, "path"),
"no matching zone".to_owned(),
it,
));
}
for it in arr(dead_code, "boundary_call_violations") {
rows.push(audit_row(
"Boundary call",
rel_path_line_cell(it, "path"),
format!(
"{} in {}",
code_cell(it, "callee"),
markdown_table_code_span(s(it, "zone"))
),
it,
));
}
for it in arr(dead_code, "policy_violations") {
rows.push(audit_row(
"Policy violation",
rel_path_line_cell(it, "path"),
format!(
"{} banned by {}",
code_cell(it, "matched"),
markdown_table_code_span(&format!("{}/{}", s(it, "pack"), s(it, "rule_id")))
),
it,
));
}
}
/// jq order positions 20-25: RSC/Next.js kinds, including unused server
/// actions between misplaced directives and route collisions.
fn audit_rows_frameworks(dead_code: &Value, rows: &mut Vec<AuditRow>) {
for it in arr(dead_code, "invalid_client_exports") {
rows.push(audit_row(
"Invalid client export",
rel_path_line_cell(it, "path"),
format!(
"{} in {}",
code_cell(it, "export_name"),
markdown_table_code_span(&format!("\"{}\"", s(it, "directive")))
),
it,
));
}
for it in arr(dead_code, "mixed_client_server_barrels") {
rows.push(audit_row(
"Mixed client/server barrel",
rel_path_line_cell(it, "path"),
format!(
"{} + {}",
code_cell(it, "client_origin"),
code_cell(it, "server_origin")
),
it,
));
}
for it in arr(dead_code, "misplaced_directives") {
rows.push(audit_row(
"Misplaced directive",
rel_path_line_cell(it, "path"),
markdown_table_code_span(&format!("\"{}\"", s(it, "directive"))),
it,
));
}
for it in arr(dead_code, "unused_server_actions") {
rows.push(audit_row(
"Unused server action",
path_line(it),
code_cell(it, "action_name"),
it,
));
}
for it in arr(dead_code, "route_collisions") {
rows.push(audit_row(
"Route collision",
markdown_table_code_span(&rel_path_absolute_only(s(it, "path"))),
code_cell(it, "url"),
it,
));
}
for it in arr(dead_code, "dynamic_segment_name_conflicts") {
rows.push(audit_row(
"Dynamic segment conflict",
markdown_table_code_span(&rel_path_absolute_only(s(it, "path"))),
markdown_table_code_span(&plain_join(it, "conflicting_segments", ", ")),
it,
));
}
}
/// Rows in `summary-audit.jq`'s `dead_code_rows` declaration order.
fn audit_dead_code_rows(dead_code: &Value) -> Vec<AuditRow> {
let mut rows: Vec<AuditRow> = Vec::new();
for it in arr(dead_code, "unused_files") {
rows.push(audit_row(
"Unused file",
markdown_table_code_span(&rel_path_absolute_only(s(it, "path"))),
"-".to_owned(),
it,
));
}
audit_rows_from_table(dead_code, AUDIT_EXPORT_DEP_ROWS, &mut rows);
audit_rows_graph(dead_code, &mut rows);
audit_rows_boundaries(dead_code, &mut rows);
audit_rows_frameworks(dead_code, &mut rows);
audit_rows_from_table(dead_code, AUDIT_COMPONENT_ROWS, &mut rows);
audit_rows_from_table(dead_code, AUDIT_HYGIENE_ROWS, &mut rows);
rows
}
fn audit_complexity_section(env: &Value) -> String {
let complexity = env.get("complexity").cloned().unwrap_or(Value::Null);
let findings: Vec<&Value> = arr(&complexity, "findings").collect();
if findings.is_empty() {
return String::new();
}
let rows = findings
.iter()
.take(15)
.map(|it| {
format!(
"| {} | {} | {} | {} | {} | {} | {} | {} |",
path_line_cell(it),
code_cell(it, "name"),
introduced_label(it),
markdown_table_text(str_or(it, "severity", "moderate")),
num(it, "cyclomatic"),
num(it, "cognitive"),
markdown_table_text(str_or(it, "coverage_tier", "-")),
it.get("crap")
.filter(|crap| !crap.is_null())
.map_or_else(|| "-".to_owned(), fmt_num),
)
})
.collect::<Vec<_>>()
.join("\n");
let tail = if findings.len() > 15 {
format!(
"\n\n> {} more complexity findings in the full audit report",
findings.len() - 15,
)
} else {
String::new()
};
format!(
"### Complexity\n\n| File | Function | Status | Severity | Cyclomatic | Cognitive | Coverage | CRAP |\n|:-----|:---------|:-------|:---------|:-----------|:----------|:---------|:-----|\n{rows}{tail}{}\n\n",
audit_coverage_model_note(&complexity),
)
}
/// Point at the coverage root only when the coverage file failed to join.
///
/// A low function match rate is ordinary: a coverage file covers the files its
/// test run touched, so a project whose suite exercises a fraction of its
/// modules matches a fraction of its functions with nothing wrong. The signal
/// that a path is misconfigured is files in the coverage file that no analyzed
/// file matched.
fn audit_coverage_join_suffix(summary: &Value) -> String {
let matched = summary
.get("istanbul_files_matched")
.and_then(Value::as_u64);
let total = summary.get("istanbul_files_total").and_then(Value::as_u64);
match (matched, total) {
(Some(matched), Some(total)) if total > 0 && matched == 0 => format!(
", from a coverage file describing {total} files that none of the analyzed files matched; check `--coverage-root` is correct for this checkout."
),
(Some(matched), Some(total)) if total > 0 && matched < total => format!(
", from {matched} of {total} files in the coverage file; the rest matched no analyzed file."
),
_ => ".".to_owned(),
}
}
fn audit_coverage_model_note(complexity: &Value) -> String {
let summary = complexity.get("summary").cloned().unwrap_or(Value::Null);
let model = summary.get("coverage_model").and_then(Value::as_str);
match model {
Some("istanbul") => {
let matched = summary.get("istanbul_matched").and_then(Value::as_u64);
let total = summary.get("istanbul_total").and_then(Value::as_u64);
match (matched, total) {
(Some(matched), Some(total)) if total > 0 => {
format!(
"\n\n*Coverage model: istanbul. Matched {matched}/{total} functions{}*",
audit_coverage_join_suffix(&summary)
)
}
_ => "\n\n*Coverage model: istanbul (exact, from `--coverage`).*".to_owned(),
}
}
Some("static_estimated" | "static_binary") => {
"\n\n*Coverage model: static (estimated). Pair with `--coverage <coverage-final.json>` for measured coverage instead of estimates.*".to_owned()
}
_ => String::new(),
}
}
fn audit_duplication_section(env: &Value) -> String {
let duplication = env.get("duplication").cloned().unwrap_or(Value::Null);
let groups: Vec<&Value> = arr(&duplication, "clone_groups").collect();
if groups.is_empty() {
return String::new();
}
let rows = groups
.iter()
.take(10)
.map(|group| {
let instances: Vec<&Value> = arr(group, "instances").collect();
let location = instances.first().map_or_else(
|| "-".to_owned(),
|first| {
let file = s(first, "file");
if file.is_empty() {
"-".to_owned()
} else {
let start = first
.get("start_line")
.filter(|line| !line.is_null())
.map_or_else(|| "1".to_owned(), fmt_num);
markdown_table_code_span(&format!(
"{}:{start}",
rel_path_absolute_only(file)
))
}
},
);
let mut files: Vec<String> = instances
.iter()
.map(|instance| {
markdown_table_code_span(&rel_path_absolute_only(s(instance, "file")))
})
.collect();
files.sort();
files.dedup();
let files = files.into_iter().take(3).collect::<Vec<_>>().join(", ");
format!(
"| {location} | {files} | {} lines / {} tokens | {} | {} |",
num(group, "line_count"),
num(group, "token_count"),
instances.len(),
introduced_label(group),
)
})
.collect::<Vec<_>>()
.join("\n");
let tail = if groups.len() > 10 {
format!(
"\n\n> {} more clone groups in the full audit report",
groups.len() - 10
)
} else {
String::new()
};
format!(
"### Duplication\n\n| Location | Files | Size | Instances | Status |\n|:---------|:------|:-----|----------:|:-------|\n{rows}{tail}\n\n"
)
}
/// Port of `summary-audit.jq`.
#[must_use]
fn render_audit_summary(env: &Value) -> String {
let verdict = str_or(env, "verdict", "pass");
let summary = env.get("summary").cloned().unwrap_or(Value::Null);
let attribution = env.get("attribution").cloned().unwrap_or(Value::Null);
let dead_code = env.get("dead_code").cloned().unwrap_or(Value::Null);
let dead_rows = audit_dead_code_rows(&dead_code);
let mut out = format!(
"## Fallow Audit\n\n> {} \u{b7} {} \u{b7} {}ms\n\n| Category | Findings | Introduced | Inherited |\n|:---------|---------:|-----------:|----------:|\n| Dead code | {} | {} | {} |\n| Complexity | {} | {} | {} |\n| Duplication | {} | {} | {} |\n\n",
audit_verdict_label(verdict),
plural_n(u(env, "changed_files_count") as usize, "changed file"),
num(env, "elapsed_ms"),
num(&summary, "dead_code_issues"),
num(&attribution, "dead_code_introduced"),
num(&attribution, "dead_code_inherited"),
num(&summary, "complexity_findings"),
num(&attribution, "complexity_introduced"),
num(&attribution, "complexity_inherited"),
num(&summary, "duplication_clone_groups"),
num(&attribution, "duplication_introduced"),
num(&attribution, "duplication_inherited"),
);
if !dead_rows.is_empty() {
let rows = dead_rows
.iter()
.take(10)
.map(|row| {
format!(
"| {} | {} | {} | {} |",
row.kind, row.location, row.item, row.status
)
})
.collect::<Vec<_>>()
.join("\n");
let tail = if dead_rows.len() > 10 {
format!(
"\n\n> {} more dead-code findings in the full audit report",
dead_rows.len() - 10
)
} else {
String::new()
};
let _ = write!(
out,
"### Dead Code\n\n| Type | Location | Item | Status |\n|:-----|:---------|:-----|:-------|\n{rows}{tail}\n\n"
);
}
out.push_str(&audit_complexity_section(env));
out.push_str(&audit_duplication_section(env));
out.push_str(if s(&attribution, "gate") == "all" {
"*Audit gate: all. Every finding in changed files affects the verdict.*"
} else {
"*Audit gate: new-only. Inherited findings are reported but do not fail the verdict.*"
});
out
}
// ---------------------------------------------------------------------------
// summary-security.jq
// ---------------------------------------------------------------------------
/// Port of `summary-security.jq`.
#[must_use]
fn render_security_summary(env: &Value) -> String {
let findings: Vec<&Value> = arr(env, "security_findings").collect();
let gate = env.get("gate").filter(|gate| !gate.is_null());
let count = gate.map_or_else(
|| {
env.get("summary")
.and_then(|summary| summary.get("security_findings"))
.and_then(Value::as_u64)
.unwrap_or(findings.len() as u64) as usize
},
|gate| u(gate, "new_count") as usize,
);
let mut out = String::from("## Fallow Security\n\n");
if count == 0 {
let _ = write!(
out,
"> [!NOTE]\n> **No security candidates matched** \u{b7} {}ms",
num(env, "elapsed_ms"),
);
} else {
let _ = write!(
out,
"> [!WARNING]\n> **{} matched** \u{b7} {}ms",
plural_n(count, "security candidate"),
num(env, "elapsed_ms"),
);
}
if let Some(gate) = gate {
let _ = write!(
out,
"\n\nSecurity gate: `{}`, verdict: `{}`, matching candidates: **{}**.",
s(gate, "mode"),
s(gate, "verdict"),
num(gate, "new_count"),
);
}
if !findings.is_empty() {
let rows = findings
.iter()
.take(15)
.map(|finding| {
format!(
"| {} | {} | {} | {} |",
path_line(finding),
markdown_table_text(s(finding, "kind")),
markdown_table_text(str_or(finding, "severity", "unknown")),
markdown_table_code_span(
finding
.get("candidate")
.and_then(|candidate| candidate.get("sink"))
.and_then(|sink| sink.get("callee"))
.and_then(Value::as_str)
.unwrap_or("-")
),
)
})
.collect::<Vec<_>>()
.join("\n");
let _ = write!(
out,
"\n\n| Location | Kind | Severity | Sink |\n|:---------|:-----|:---------|:-----|\n{rows}"
);
if findings.len() > 15 {
let _ = write!(
out,
"\n\n> {} more candidates in the full report",
findings.len() - 15,
);
}
}
out.push_str("\n\nTreat these as candidates for verification, not confirmed vulnerabilities.");
out
}
// ---------------------------------------------------------------------------
// summary-fix.jq
// ---------------------------------------------------------------------------
/// The entries of one fix type that actually landed (or, in a dry run, would).
/// A withheld entry keeps its `type` so a caller can see which finding was
/// declined, so the skipped flag is what separates the two; listing a withheld
/// removal under "Dependencies removed" would report a write that never
/// happened.
fn fix_entries<'v>(env: &'v Value, entry_type: &str) -> Vec<&'v Value> {
arr(env, "fixes")
.filter(|fix| s(fix, "type") == entry_type && !b(fix, "skipped"))
.collect()
}
fn fix_detail_block(label: &str, entries: &[&Value], row: impl Fn(&Value) -> String) -> String {
let rows = entries
.iter()
.take(25)
.map(|entry| row(entry))
.collect::<Vec<_>>()
.join("\n");
let tail = if entries.len() > 25 {
format!("\n- *... and {} more*", entries.len() - 25)
} else {
String::new()
};
format!("**{label} ({})**\n{rows}{tail}", entries.len())
}
/// Port of `summary-fix.jq`.
#[must_use]
pub fn render_fix_summary(env: &Value) -> String {
let exports = fix_entries(env, "remove_export");
let dependencies = fix_entries(env, "remove_dependency");
let fix_attempts = arr(env, "fixes").filter(|fix| !b(fix, "skipped")).count();
let content_changed = u(env, "skipped_content_changed") as usize;
let mixed_eol = u(env, "skipped_mixed_line_endings") as usize;
let low_confidence = u(env, "skipped_low_confidence_exports") as usize;
let low_confidence_deps = u(env, "skipped_low_confidence_dependencies") as usize;
let low_confidence_members = u(env, "skipped_low_confidence_members") as usize;
let dry_run = b(env, "dry_run");
if fix_attempts == 0
&& content_changed == 0
&& mixed_eol == 0
&& low_confidence == 0
&& low_confidence_deps == 0
&& low_confidence_members == 0
{
return "## Fallow - Auto-fix\n\nNo fixable issues found.".to_owned();
}
let mut out = String::from("## Fallow - Auto-fix\n\n");
out.push_str(if dry_run {
"**Dry run**: would apply"
} else {
"Applied"
});
let fix_noun = if fix_attempts == 1 { "fix" } else { "fixes" };
let _ = write!(out, " **{fix_attempts} {fix_noun}**");
if !dry_run {
let _ = write!(out, " ({} succeeded)", num(env, "total_fixed"));
}
if content_changed > 0 {
let _ = write!(
out,
", skipped {content_changed} file(s) that changed since analysis"
);
}
if mixed_eol > 0 {
let _ = write!(out, ", skipped {mixed_eol} file(s) with mixed line endings");
}
if low_confidence > 0 {
let _ = write!(
out,
", kept exports in {low_confidence} file(s) where consumers may be hidden from static analysis"
);
}
if low_confidence_deps > 0 {
let _ = write!(
out,
", kept {low_confidence_deps} declared package(s) whose only import may sit in a file this run did not fully read"
);
}
if low_confidence_members > 0 {
let _ = write!(
out,
", kept {low_confidence_members} unused enum member(s) whose only reference may sit in a file this run did not fully analyze"
);
}
out.push_str("\n\n| Type | Count |\n|------|-------|\n");
if !exports.is_empty() {
let _ = writeln!(out, "| Export removals | {} |", exports.len());
}
if !dependencies.is_empty() {
let _ = writeln!(out, "| Dependency removals | {} |", dependencies.len());
}
out.push_str("\n<details>\n<summary>View details</summary>\n\n");
if !exports.is_empty() {
out.push_str(&fix_detail_block("Export removals", &exports, |it| {
format!(
"- {} - {}",
markdown_code_span(&format!("{}:{}", s(it, "path"), num(it, "line"))),
markdown_code_span(s(it, "name"))
)
}));
out.push_str("\n\n");
}
if !dependencies.is_empty() {
out.push_str(&fix_detail_block(
"Dependency removals",
&dependencies,
|it| {
format!(
"- {} from {} in {}",
markdown_code_span(s(it, "package")),
markdown_code_span(s(it, "location")),
markdown_code_span(s(it, "file")),
)
},
));
out.push('\n');
}
out.push_str("\n\n</details>");
out
}
// ---------------------------------------------------------------------------
// summary-combined.jq
// ---------------------------------------------------------------------------
fn file_link(links: &LinkContext, path: &str, start: &str, end: &str) -> String {
let display = markdown_table_code_span(&format!("{}:{start}-{end}", last_three_segments(path)));
if links.repo.is_empty() || links.sha.is_empty() {
display
} else {
format!(
"[{display}](https://github.com/{}/blob/{}/{}{}#L{start}-L{end})",
links.repo,
links.sha,
links.prefix,
encode_link_path(path),
)
}
}
/// Percent-encode the characters that would break a Markdown link destination
/// or the surrounding table cell.
fn encode_link_path(path: &str) -> String {
path.replace('%', "%25")
.replace(' ', "%20")
.replace('(', "%28")
.replace(')', "%29")
.replace('<', "%3C")
.replace('>', "%3E")
.replace('|', "%7C")
}
fn exceeded_priority(it: &Value) -> u8 {
match s(it, "exceeded") {
"all" => 5,
"cyclomatic_crap" | "cognitive_crap" => 4,
"crap" => 3,
"both" => 2,
"cyclomatic" | "cognitive" => 1,
_ => 0,
}
}
fn severity_priority(it: &Value) -> u8 {
match s(it, "severity") {
"critical" => 3,
"high" => 2,
"moderate" => 1,
_ => 0,
}
}
/// jq `ranked_health_findings`: stable ascending sort by the priority tuple,
/// then a full reverse (ties land in reverse input order).
fn ranked_health_findings(health: &Value) -> Vec<&Value> {
let mut findings: Vec<&Value> = arr(health, "findings").collect();
findings.sort_by_key(|it| {
(
exceeded_priority(it),
severity_priority(it),
it.get("crap").is_some_and(|crap| !crap.is_null()),
u(it, "cyclomatic"),
u(it, "cognitive"),
u(it, "line_count"),
)
});
findings.reverse();
findings
}
const PROD_FAILING_VERDICTS: &[&str] = &["safe_to_delete", "review_required", "low_traffic"];
struct CombinedCounts {
check: usize,
dupes: usize,
complex: usize,
prod_failing: usize,
prod_advisory: usize,
hot_paths: usize,
}
impl CombinedCounts {
fn health(&self) -> usize {
self.complex + self.prod_failing
}
fn total(&self) -> usize {
self.check + self.dupes + self.health()
}
}
fn combined_counts(env: &Value) -> CombinedCounts {
let check = env
.get("check")
.map_or(0, |check| u(check, "total_issues") as usize);
// Visible groups plus what a presentation cap withheld, NOT
// `stats.clone_groups`. The two differ for two unrelated reasons and only
// one of them belongs here: a filtered combined run leaves `stats`
// describing the unfiltered corpus while `clone_groups[]` holds the
// actionable set, and counting `stats` there reports issues a reader
// cannot inspect (issue #1250); a presentation cap such as `--top`
// truncates the array while `stats` stays right. `clone_groups_omitted`
// counts only the second, and no combined envelope carries it today
// because the bare command takes no `--top`.
let dupes = env.get("dupes").map_or(0, |dupes| {
arr(dupes, "clone_groups")
.count()
.saturating_add(u(dupes, "clone_groups_omitted") as usize)
});
let health = env.get("health").cloned().unwrap_or(Value::Null);
let complex = health.get("summary").map_or(0, |summary| {
u(summary, "functions_above_threshold") as usize
});
let runtime = health
.get("runtime_coverage")
.cloned()
.unwrap_or(Value::Null);
let prod_failing = arr(&runtime, "findings")
.filter(|finding| PROD_FAILING_VERDICTS.contains(&s(finding, "verdict")))
.count();
let prod_advisory = arr(&runtime, "findings")
.filter(|finding| !PROD_FAILING_VERDICTS.contains(&s(finding, "verdict")))
.count();
let hot_paths = arr(&runtime, "hot_paths").count();
CombinedCounts {
check,
dupes,
complex,
prod_failing,
prod_advisory,
hot_paths,
}
}
fn hot_path_label(env: &Value, n: usize) -> String {
let touched = env
.get("health")
.and_then(|health| health.get("runtime_coverage"))
.is_some_and(|runtime| s(runtime, "verdict") == "hot-path-touched");
let plural = if n == 1 { "" } else { "s" };
if touched {
format!("hot path{plural} touched")
} else {
format!("hot path{plural}")
}
}
fn combined_zero_case(env: &Value, counts: &CombinedCounts) -> String {
let vitals = env
.get("health")
.and_then(|health| health.get("vital_signs"))
.cloned()
.unwrap_or(Value::Null);
let mut out = String::from("# \u{1F33F} Fallow\n\n");
if counts.prod_advisory > 0 || counts.hot_paths > 0 {
out.push_str(
"> [!NOTE]\n> **Quality gate passed**\n\n:white_check_mark: No code issues \u{b7} :white_check_mark: No duplication \u{b7} :white_check_mark: No blocking health findings",
);
if counts.prod_advisory > 0 {
let _ = write!(
out,
" \u{b7} :information_source: **{}** runtime coverage advisory finding{}",
counts.prod_advisory,
if counts.prod_advisory == 1 { "" } else { "s" },
);
}
if counts.hot_paths > 0 {
let _ = write!(
out,
" \u{b7} :eyes: **{}** {}",
counts.hot_paths,
hot_path_label(env, counts.hot_paths),
);
}
} else {
out.push_str(
"> [!NOTE]\n> **Quality gate passed**\n\n:white_check_mark: No code issues \u{b7} :white_check_mark: No duplication \u{b7} :white_check_mark: No complex functions",
);
}
if let Some(maintainability) = opt_f(&vitals, "maintainability_avg") {
let _ = write!(
out,
"\n\n| Metric | Value |\n|:-------|------:|\n| [Maintainability]({HEALTH_DOCS}#maintainability-index-mi) | **{}** / 100 |\n",
pct(maintainability),
);
}
out
}
fn combined_status_line(env: &Value, counts: &CombinedCounts) -> String {
let mut out = String::new();
if counts.check > 0 {
let _ = write!(
out,
":warning: **{}** code {}",
counts.check,
if counts.check == 1 { "issue" } else { "issues" },
);
} else {
out.push_str(":white_check_mark: No code issues");
}
out.push_str(" \u{b7} ");
if counts.dupes > 0 {
let _ = write!(
out,
":warning: **{}** clone {}",
counts.dupes,
if counts.dupes == 1 { "group" } else { "groups" },
);
} else {
out.push_str(":white_check_mark: No duplication");
}
out.push_str(" \u{b7} ");
let health = counts.health();
if health > 0 {
let _ = write!(
out,
":warning: **{health}** health {}",
if health == 1 { "finding" } else { "findings" },
);
} else {
out.push_str(":white_check_mark: No blocking health findings");
}
if counts.prod_advisory > 0 {
let _ = write!(
out,
" \u{b7} :information_source: **{}** coverage advisory finding{}",
counts.prod_advisory,
if counts.prod_advisory == 1 { "" } else { "s" },
);
}
if counts.hot_paths > 0 {
let _ = write!(
out,
" \u{b7} :eyes: **{}** {}",
counts.hot_paths,
hot_path_label(env, counts.hot_paths),
);
}
out.push_str("\n\n");
out
}
fn combined_check_breakdown(env: &Value, counts: &CombinedCounts) -> String {
if counts.check == 0 {
return String::new();
}
let check = env.get("check").cloned().unwrap_or(Value::Null);
format!(
"<details>\n<summary><strong><a href=\"{DEAD_CODE_DOCS}\">Code issues</a> ({})</strong></summary>\n\n| Category | Count |\n|:---------|------:|\n{}\n\n</details>\n\n",
counts.check,
dead_code_category_table(&check),
)
}
fn combined_dupes_breakdown(env: &Value, counts: &CombinedCounts, links: &LinkContext) -> String {
if counts.dupes == 0 {
return String::new();
}
let dupes = env.get("dupes").cloned().unwrap_or(Value::Null);
let stats = dupes.get("stats").cloned().unwrap_or(Value::Null);
let groups = sorted_clone_groups(&dupes);
let files_with_clones = u(&stats, "files_with_clones") as usize;
let rows = groups
.iter()
.take(5)
.map(|group| {
let locations = arr(group, "instances")
.map(|instance| {
file_link(
links,
s(instance, "file"),
&num(instance, "start_line"),
&num(instance, "end_line"),
)
})
.collect::<Vec<_>>()
.join("<br>");
format!(
"| {locations} | {} | {} |",
num(group, "line_count"),
num(group, "token_count"),
)
})
.collect::<Vec<_>>()
.join("\n");
let tail = if counts.dupes > 5 {
format!("\n\n*\u{2026} and {} more groups.*", counts.dupes - 5)
} else {
String::new()
};
format!(
"<details>\n<summary><strong><a href=\"{DUPES_DOCS}\">Duplication</a> ({} {} \u{b7} {} lines \u{b7} {}%)</strong></summary>\n\n| Locations | Lines | Tokens |\n|:----------|------:|-------:|\n{rows}{tail}\n\nAcross {files_with_clones} {}.\n\n</details>\n\n",
counts.dupes,
if counts.dupes == 1 { "group" } else { "groups" },
num(&stats, "duplicated_lines"),
pct(f_or_zero(&stats, "duplication_percentage")),
if files_with_clones == 1 {
"file"
} else {
"files"
},
)
}
fn combined_complexity_breakdown(env: &Value, counts: &CombinedCounts) -> String {
if counts.complex == 0 {
return String::new();
}
let health = env.get("health").cloned().unwrap_or(Value::Null);
let summary = health.get("summary").cloned().unwrap_or(Value::Null);
let findings = ranked_health_findings(&health);
let show_crap = summary
.get("max_crap_threshold")
.is_some_and(|threshold| !threshold.is_null())
|| findings
.iter()
.any(|finding| finding.get("crap").is_some_and(|crap| !crap.is_null()));
let cyc_t = threshold_or(&summary, "max_cyclomatic_threshold", "default");
let cog_t = threshold_or(&summary, "max_cognitive_threshold", "default");
let crap_t = threshold_or(&summary, "max_crap_threshold", "default");
let crap_header = if show_crap {
format!(" | [CRAP]({HEALTH_DOCS}#crap-score)")
} else {
String::new()
};
let crap_separator = if show_crap { "|-----:" } else { "" };
let rows = findings
.iter()
.take(5)
.map(|it| {
let crap_column = if show_crap {
format!(" | {}", crap_cell(it))
} else {
String::new()
};
format!(
"| {} | {} | {} | {}{} | {}{}{crap_column} | {} |",
markdown_table_code_span(&format!(
"{}:{}",
last_three_segments(s(it, "path")),
num(it, "line")
)),
code_cell(it, "name"),
markdown_table_text(str_or(it, "severity", "moderate")),
num(it, "cyclomatic"),
exceeded_marker(it, &["cyclomatic", "both", "all"]),
num(it, "cognitive"),
exceeded_marker(it, &["cognitive", "both", "all"]),
num(it, "line_count"),
)
})
.collect::<Vec<_>>()
.join("\n");
let crap_footer = if show_crap {
format!(", CRAP >= {crap_t}")
} else {
String::new()
};
format!(
"<details>\n<summary><strong><a href=\"{HEALTH_DOCS}#complexity-metrics\">Complexity</a> ({} {} above threshold)</strong></summary>\n\n| File | Function | Severity | [Cyclomatic]({HEALTH_DOCS}#cyclomatic-complexity) | [Cognitive]({HEALTH_DOCS}#cognitive-complexity){crap_header} | Lines |\n|:-----|:---------|:---------|----------:|---------:{crap_separator}|------:|\n{rows}\n\n**{}** files, **{}** functions analyzed (thresholds: cyclomatic > {cyc_t}, cognitive > {cog_t}{crap_footer})\n\n</details>\n\n",
counts.complex,
if counts.complex == 1 {
"function"
} else {
"functions"
},
threshold_or(&summary, "files_analyzed", "unknown"),
threshold_or(&summary, "functions_analyzed", "unknown"),
)
}
fn combined_runtime_breakdown(env: &Value, counts: &CombinedCounts) -> String {
let prod_total = counts.prod_failing + counts.prod_advisory;
if prod_total == 0 && counts.hot_paths == 0 {
return String::new();
}
let runtime = env
.get("health")
.and_then(|health| health.get("runtime_coverage"))
.cloned()
.unwrap_or(Value::Null);
let hot_suffix = if counts.hot_paths > 0 {
format!(
", {} {}",
counts.hot_paths,
hot_path_label(env, counts.hot_paths)
)
} else {
String::new()
};
let mut out = format!(
"<details>\n<summary><strong><a href=\"{HEALTH_DOCS}#runtime-coverage\">Runtime coverage</a> ({prod_total} finding{}{hot_suffix})</strong></summary>\n\n",
if prod_total == 1 { "" } else { "s" },
);
if prod_total > 0 {
out.push_str("| File | Function | Verdict | Invocations | Confidence |\n|:-----|:---------|:--------|------------:|:-----------|\n");
out.push_str(
&arr(&runtime, "findings")
.take(5)
.map(|it| {
let invocations = it
.get("invocations")
.filter(|value| !value.is_null())
.map_or_else(|| "-".to_owned(), fmt_num);
format!(
"| {} | {} | {} | {invocations} | {} |",
markdown_table_code_span(&format!(
"{}:{}",
last_three_segments(s(it, "path")),
num(it, "line")
)),
code_cell(it, "function"),
code_cell(it, "verdict"),
markdown_table_text(s(it, "confidence")),
)
})
.collect::<Vec<_>>()
.join("\n"),
);
if counts.hot_paths > 0 {
out.push_str("\n\n");
}
}
if counts.hot_paths > 0 {
out.push_str("| File | Function | Invocations | Percentile |\n|:-----|:---------|------------:|-----------:|\n");
out.push_str(
&arr(&runtime, "hot_paths")
.take(5)
.map(|it| {
format!(
"| {} | {} | {} | {} |",
markdown_table_code_span(&format!(
"{}:{}",
last_three_segments(s(it, "path")),
num(it, "line")
)),
code_cell(it, "function"),
num(it, "invocations"),
num(it, "percentile"),
)
})
.collect::<Vec<_>>()
.join("\n"),
);
out.push_str("\n\n");
}
out.push_str("</details>\n\n");
out
}
fn combined_vitals(env: &Value) -> String {
let health = env.get("health").cloned().unwrap_or(Value::Null);
let vitals = health.get("vital_signs").cloned().unwrap_or(Value::Null);
let has_vitals = vitals.as_object().is_some_and(|vitals| !vitals.is_empty());
if !has_vitals {
return String::new();
}
let scores: Vec<f64> = arr(&health, "file_scores")
.filter_map(|score| opt_f(score, "maintainability_index"))
.collect();
let scoped_maintainability = if scores.is_empty() {
None
} else {
let avg = scores.iter().sum::<f64>() / scores.len() as f64;
Some((avg * 10.0).round() / 10.0)
};
let mut out = format!(
"#### [Codebase health]({HEALTH_DOCS})\n\n| Metric | Value |\n|:-------|------:|\n"
);
let maintainability = opt_f(&vitals, "maintainability_avg");
if let Some(avg) = maintainability {
let _ = writeln!(
out,
"| [Maintainability]({HEALTH_DOCS}#maintainability-index-mi) | **{}** / 100 |",
pct(avg),
);
}
if let Some(scoped) = scoped_maintainability {
let rounded_avg = (maintainability.unwrap_or_default() * 10.0).round() / 10.0;
if (scoped - rounded_avg).abs() > f64::EPSILON {
let _ = writeln!(
out,
"| [Maintainability]({HEALTH_DOCS}#maintainability-index-mi) (changed files) | **{}** / 100 |",
fmt_num(&serde_json::json!(scoped)),
);
}
}
if let Some(avg_cyclomatic) = opt_f(&vitals, "avg_cyclomatic") {
let _ = writeln!(
out,
"| [Avg complexity]({HEALTH_DOCS}#cyclomatic-complexity) | {} |",
pct(avg_cyclomatic),
);
}
if let Some(population) = vitals
.get("cyclomatic_population")
.filter(|value| value.is_object())
{
let _ = writeln!(
out,
"| Cyclomatic units | Functions: {}, module scopes: {}, templates: {} |",
num(&population["functions"], "count"),
num(&population["modules"], "count"),
num(&population["templates"], "count")
);
if let Some(max) = population["modules"]["max"].as_u64() {
let _ = writeln!(
out,
"| Module-scope max cyclomatic (aggregate only) | {max} |"
);
}
}
out.push('\n');
out
}
fn combined_tips(env: &Value) -> String {
let check = env.get("check").cloned().unwrap_or(Value::Null);
let fixable = arr(&check, "unused_exports").count()
+ arr(&check, "unused_dependencies").count()
+ arr(&check, "unused_enum_members").count();
if fixable == 0 {
return String::new();
}
let mut out = String::from("> [!TIP]\n> Run `fallow fix --dry-run` to preview auto-fixes.\n");
if arr(&check, "unused_exports").count() > 0 {
let _ = writeln!(
out,
"> Add [`/** @public */`]({SUPPRESSION_DOCS}) above exports to preserve them."
);
}
out
}
/// Port of `summary-combined.jq`.
#[must_use]
fn render_combined_summary(env: &Value, links: &LinkContext) -> String {
let counts = combined_counts(env);
let header = health_score_header(&env.get("health").cloned().unwrap_or(Value::Null));
if counts.total() == 0 {
return format!("{header}{}", combined_zero_case(env, &counts));
}
let pointer = if counts.check > 0 || counts.dupes > 0 || counts.health() > 0 {
"See inline review comments for per-finding details.\n\n"
} else {
""
};
format!(
"{header}# \u{1F33F} Fallow\n\n> [!WARNING]\n> **Review needed**\n\n{}{pointer}{}{}{}{}{}{}",
combined_status_line(env, &counts),
combined_check_breakdown(env, &counts),
combined_dupes_breakdown(env, &counts, links),
combined_complexity_breakdown(env, &counts),
combined_runtime_breakdown(env, &counts),
combined_vitals(env),
combined_tips(env),
)
}
#[cfg(test)]
mod tests {
use std::collections::BTreeSet;
use fallow_types::issue_meta::counted_result_issue_metas;
use super::DEAD_CODE_CATEGORIES;
/// Every counted dead-code kind needs a summary row with the registry
/// label and docs anchor, and every summary row needs a registry kind.
#[test]
fn dead_code_categories_match_counted_result_metadata() {
let native: BTreeSet<(&str, &str, &str)> = DEAD_CODE_CATEGORIES.iter().copied().collect();
assert_eq!(
native.len(),
DEAD_CODE_CATEGORIES.len(),
"DEAD_CODE_CATEGORIES has a duplicate row"
);
let registry: BTreeSet<(&str, &str, &str)> = counted_result_issue_metas()
.map(|meta| (meta.summary_label, meta.result_key, meta.docs_anchor))
.collect();
let missing: Vec<_> = registry.difference(&native).collect();
let extra: Vec<_> = native.difference(®istry).collect();
assert!(
missing.is_empty() && extra.is_empty(),
"DEAD_CODE_CATEGORIES must match the counted result metadata \
(label, result key, docs anchor).\nMissing rows: {missing:?}\nRows not in the registry: {extra:?}"
);
}
/// Rows without a path or line tie on the first two keys. The serialized
/// row breaks the tie, so the rendered order does not depend on the input
/// order. The rows past the rendered limit are counted, not listed.
#[test]
fn pathless_rows_render_in_one_order_for_any_input_order() {
let envelope = |names: &[String]| {
serde_json::json!({
"total_issues": names.len(),
"elapsed_ms": 1,
"unlisted_dependencies": names
.iter()
.map(|name| serde_json::json!({ "package_name": name, "imported_from": [] }))
.collect::<Vec<_>>(),
})
};
let names: Vec<String> = (0..40).map(|index| format!("pkg-{index:02}")).collect();
let mut reversed = names.clone();
reversed.reverse();
let ordered = super::render_check_summary(&envelope(&names));
assert_eq!(super::render_check_summary(&envelope(&reversed)), ordered);
let first = ordered.find("pkg-00").expect("first row");
let second = ordered.find("pkg-01").expect("second row");
assert!(
first < second,
"rows follow the serialized order: {ordered}"
);
assert!(
!ordered.contains("pkg-39"),
"rows past the limit are counted only"
);
assert!(
ordered.contains("15 more"),
"the tail counts the rest: {ordered}"
);
}
/// A saved `--group-by` envelope lists rows group by group. The summary
/// renders them in one order whatever the input order is.
#[test]
fn dead_code_rows_render_in_one_order_for_any_input_order() {
let envelope = |paths: [&str; 3]| {
serde_json::json!({
"total_issues": 3,
"elapsed_ms": 1,
"unused_files": paths.map(|path| serde_json::json!({ "path": path })),
})
};
let flat = super::render_check_summary(&envelope(["src/a.ts", "src/b.ts", "src/c.ts"]));
let grouped = super::render_check_summary(&envelope(["src/a.ts", "src/c.ts", "src/b.ts"]));
assert_eq!(grouped, flat);
let a = flat.find("`src/a.ts`").expect("a");
let b = flat.find("`src/b.ts`").expect("b");
let c = flat.find("`src/c.ts`").expect("c");
assert!(a < b && b < c, "rows follow path order: {flat}");
}
}