use std::path::Path;
use fallow_engine::duplicates::CloneFingerprintSet;
use fallow_output::normalize_uri;
use fallow_types::duplicates::DuplicationReport;
use fallow_types::output_dead_code::ReachabilityCaveat;
use fallow_types::results::{AnalysisResults, UnusedExport, UnusedMember};
use crate::ResultGroup;
fn relative_path<'a>(path: &'a Path, root: &Path) -> &'a Path {
path.strip_prefix(root).unwrap_or(path)
}
fn compact_path(path: &Path, root: &Path) -> String {
normalize_uri(&relative_path(path, root).display().to_string())
}
fn compact_caveat_field(caveats: &[ReachabilityCaveat]) -> String {
if caveats.is_empty() {
return String::new();
}
let tokens: Vec<&str> = caveats
.iter()
.map(|caveat| ReachabilityCaveat::token(*caveat))
.collect();
format!(",caveat={}", tokens.join("+"))
}
fn compact_circular_dependency_line(
cycle: &fallow_types::output_dead_code::CircularDependencyFinding,
root: &Path,
) -> String {
let mut display_chain: Vec<String> = cycle
.cycle
.files
.iter()
.map(|path| compact_path(path, root))
.collect();
if let Some(first) = display_chain.first() {
display_chain.push(first.clone());
}
let first_file = display_chain.first().map_or("", String::as_str);
let cross_pkg_tag = if cycle.cycle.is_cross_package {
" (cross-package)"
} else {
""
};
format!(
"circular-dependency:{}:{}:{}{}",
first_file,
cycle.cycle.line,
display_chain.join(" \u{2192} "),
cross_pkg_tag
)
}
fn compact_re_export_cycle_line(
cycle: &fallow_types::output_dead_code::ReExportCycleFinding,
root: &Path,
) -> String {
let chain: Vec<String> = cycle
.cycle
.files
.iter()
.map(|path| compact_path(path, root))
.collect();
let first_file = chain.first().map_or("", String::as_str);
let kind_tag = match cycle.cycle.kind {
fallow_types::results::ReExportCycleKind::SelfLoop => " (self-loop)",
fallow_types::results::ReExportCycleKind::MultiNode => "",
};
format!(
"re-export-cycle:{}:{}{}",
first_file,
chain.join(" <-> "),
kind_tag
)
}
fn compact_boundary_violation_line(
item: &fallow_types::output_dead_code::BoundaryViolationFinding,
root: &Path,
) -> String {
format!(
"boundary-violation:{}:{}:{} -> {} ({} -> {})",
compact_path(&item.violation.from_path, root),
item.violation.line,
compact_path(&item.violation.from_path, root),
compact_path(&item.violation.to_path, root),
item.violation.from_zone,
item.violation.to_zone,
)
}
fn compact_boundary_coverage_line(
item: &fallow_types::output_dead_code::BoundaryCoverageViolationFinding,
root: &Path,
) -> String {
format!(
"boundary-coverage:{}:{}:no matching boundary zone",
compact_path(&item.violation.path, root),
item.violation.line,
)
}
fn compact_boundary_call_line(
item: &fallow_types::output_dead_code::BoundaryCallViolationFinding,
root: &Path,
) -> String {
format!(
"boundary-call:{}:{}:{} forbidden in zone {} (pattern {})",
compact_path(&item.violation.path, root),
item.violation.line,
item.violation.callee,
item.violation.zone,
item.violation.pattern,
)
}
fn compact_stale_suppression_line(
item: &fallow_types::results::StaleSuppression,
root: &Path,
) -> String {
format!(
"stale-suppression:{}:{}:{}",
compact_path(&item.path, root),
item.line,
item.display_message(),
)
}
fn compact_catalog_reference_line(
item: &fallow_types::output_dead_code::UnresolvedCatalogReferenceFinding,
root: &Path,
) -> String {
format!(
"unresolved-catalog-reference:{}:{}:{}:{}",
compact_path(&item.reference.path, root),
item.reference.line,
item.reference.catalog_name,
item.reference.entry_name,
)
}
fn compact_unused_override_line(
item: &fallow_types::output_dead_code::UnusedDependencyOverrideFinding,
root: &Path,
) -> String {
format!(
"unused-dependency-override:{}:{}:{}:{}",
compact_path(&item.entry.path, root),
item.entry.line,
item.entry.source.as_label(),
item.entry.raw_key,
)
}
fn compact_misconfigured_override_line(
item: &fallow_types::output_dead_code::MisconfiguredDependencyOverrideFinding,
root: &Path,
) -> String {
format!(
"misconfigured-dependency-override:{}:{}:{}:{}",
compact_path(&item.entry.path, root),
item.entry.line,
item.entry.source.as_label(),
item.entry.raw_key,
)
}
pub fn build_compact_lines(results: &AnalysisResults, root: &Path) -> Vec<String> {
CompactLineBuilder::new(results, root).build()
}
struct CompactLineBuilder<'a> {
lines: Vec<String>,
results: &'a AnalysisResults,
root: &'a Path,
}
impl<'a> CompactLineBuilder<'a> {
fn new(results: &'a AnalysisResults, root: &'a Path) -> Self {
Self {
lines: Vec::new(),
results,
root,
}
}
fn build(mut self) -> Vec<String> {
self.push_core_lines();
self.push_unused_dependency_lines();
self.push_member_lines();
self.push_secondary_dependency_lines();
self.push_graph_lines();
self.push_workspace_lines();
self.lines
}
fn rel(&self, path: &Path) -> String {
compact_path(path, self.root)
}
fn unused_export_line(&self, export: &UnusedExport, caveats: &[ReachabilityCaveat]) -> String {
let tag = if export.is_re_export {
"unused-re-export"
} else {
"unused-export"
};
format!(
"{}:{}:{}:{}{}",
tag,
self.rel(&export.path),
export.line,
export.export_name,
compact_caveat_field(caveats)
)
}
fn unused_type_line(&self, export: &UnusedExport, caveats: &[ReachabilityCaveat]) -> String {
let tag = if export.is_re_export {
"unused-re-export-type"
} else {
"unused-type"
};
format!(
"{}:{}:{}:{}{}",
tag,
self.rel(&export.path),
export.line,
export.export_name,
compact_caveat_field(caveats)
)
}
fn compact_member(
&self,
member: &UnusedMember,
kind: &str,
caveats: &[ReachabilityCaveat],
) -> String {
format!(
"{}:{}:{}:{}.{}{}",
kind,
self.rel(&member.path),
member.line,
member.parent_name,
member.member_name,
compact_caveat_field(caveats)
)
}
fn push_core_lines(&mut self) {
for file in &self.results.unused_files {
self.lines.push(format!(
"unused-file:{}{}",
self.rel(&file.file.path),
compact_caveat_field(&file.reachability_caveats)
));
}
for export in &self.results.unused_exports {
self.lines
.push(self.unused_export_line(&export.export, &export.reachability_caveats));
}
for export in &self.results.unused_types {
self.lines
.push(self.unused_type_line(&export.export, &export.reachability_caveats));
}
for leak in &self.results.private_type_leaks {
self.lines.push(format!(
"private-type-leak:{}:{}:{}->{}",
self.rel(&leak.leak.path),
leak.leak.line,
leak.leak.export_name,
leak.leak.type_name
));
}
for finding in &self.results.deprecated_exports_in_use {
self.lines.push(format!(
"deprecated-export-in-use:{}:{}:{}",
self.rel(&finding.export.path),
finding.export.line,
finding.export.export_name
));
}
}
fn push_unused_dependency_lines(&mut self) {
for dep in &self.results.unused_dependencies {
self.lines.push(format!(
"unused-dep:{}{}",
dep.dep.package_name,
compact_caveat_field(&dep.reachability_caveats)
));
}
for dep in &self.results.unused_dev_dependencies {
self.lines.push(format!(
"unused-devdep:{}{}",
dep.dep.package_name,
compact_caveat_field(&dep.reachability_caveats)
));
}
for dep in &self.results.unused_optional_dependencies {
self.lines.push(format!(
"unused-optionaldep:{}{}",
dep.dep.package_name,
compact_caveat_field(&dep.reachability_caveats)
));
}
}
fn push_member_lines(&mut self) {
for member in &self.results.unused_enum_members {
self.lines.push(self.compact_member(
&member.member,
"unused-enum-member",
&member.reachability_caveats,
));
}
for member in &self.results.unused_class_members {
self.lines.push(self.compact_member(
&member.member,
"unused-class-member",
&member.reachability_caveats,
));
}
for member in &self.results.unused_store_members {
self.lines.push(self.compact_member(
&member.member,
"unused-store-member",
&member.reachability_caveats,
));
}
for import in &self.results.unresolved_imports {
self.lines.push(format!(
"unresolved-import:{}:{}:{}",
self.rel(&import.import.path),
import.import.line,
import.import.specifier
));
}
}
fn push_secondary_dependency_lines(&mut self) {
for dep in &self.results.unlisted_dependencies {
self.lines
.push(format!("unlisted-dep:{}", dep.dep.package_name));
}
for dup in &self.results.duplicate_exports {
self.lines
.push(format!("duplicate-export:{}", dup.export.export_name));
}
for dep in &self.results.type_only_dependencies {
self.lines
.push(format!("type-only-dep:{}", dep.dep.package_name));
}
for dep in &self.results.test_only_dependencies {
self.lines
.push(format!("test-only-dep:{}", dep.dep.package_name));
}
for dep in &self.results.dev_dependencies_in_production {
self.lines
.push(format!("dev-dep-in-prod:{}", dep.dep.package_name));
}
}
fn push_graph_lines(&mut self) {
self.push_structure_lines();
self.push_framework_lines();
self.push_component_lines();
self.push_route_lines();
self.push_suppression_lines();
}
fn push_structure_lines(&mut self) {
for cycle in &self.results.circular_dependencies {
self.lines
.push(compact_circular_dependency_line(cycle, self.root));
}
for cycle in &self.results.re_export_cycles {
self.lines
.push(compact_re_export_cycle_line(cycle, self.root));
}
for violation in &self.results.boundary_violations {
self.lines
.push(compact_boundary_violation_line(violation, self.root));
}
for violation in &self.results.boundary_coverage_violations {
self.lines
.push(compact_boundary_coverage_line(violation, self.root));
}
for violation in &self.results.boundary_call_violations {
self.lines
.push(compact_boundary_call_line(violation, self.root));
}
for violation in &self.results.policy_violations {
self.lines.push(format!(
"policy-violation:{}:{}:{} banned by {}/{}",
self.rel(&violation.violation.path),
violation.violation.line,
violation.violation.matched,
violation.violation.pack,
violation.violation.rule_id,
));
}
}
fn push_framework_lines(&mut self) {
for finding in &self.results.invalid_client_exports {
self.lines.push(format!(
"invalid-client-export:{}:{}:{} (from \"{}\")",
self.rel(&finding.export.path),
finding.export.line,
finding.export.export_name,
finding.export.directive,
));
}
for finding in &self.results.mixed_client_server_barrels {
self.lines.push(format!(
"mixed-client-server-barrel:{}:{}:{} (server-only \"{}\")",
self.rel(&finding.barrel.path),
finding.barrel.line,
finding.barrel.client_origin,
finding.barrel.server_origin,
));
}
for finding in &self.results.misplaced_directives {
self.lines.push(format!(
"misplaced-directive:{}:{}:{}",
self.rel(&finding.directive_site.path),
finding.directive_site.line,
finding.directive_site.directive,
));
}
for finding in &self.results.unprovided_injects {
self.lines.push(format!(
"unprovided-inject:{}:{}:{}",
self.rel(&finding.inject.path),
finding.inject.line,
finding.inject.key_name,
));
}
}
fn push_component_lines(&mut self) {
self.push_component_member_lines();
self.push_component_framework_lines();
}
fn push_component_member_lines(&mut self) {
for finding in &self.results.unrendered_components {
self.lines.push(format!(
"unrendered-component:{}:{}:{}",
self.rel(&finding.component.path),
finding.component.line,
finding.component.component_name,
));
}
for finding in &self.results.unused_component_props {
self.lines.push(format!(
"unused-component-prop:{}:{}:{}",
self.rel(&finding.prop.path),
finding.prop.line,
finding.prop.prop_name,
));
}
for finding in &self.results.unused_component_emits {
self.lines.push(format!(
"unused-component-emit:{}:{}:{}",
self.rel(&finding.emit.path),
finding.emit.line,
finding.emit.emit_name,
));
}
for finding in &self.results.unused_component_inputs {
self.lines.push(format!(
"unused-component-input:{}:{}:{}",
self.rel(&finding.input.path),
finding.input.line,
finding.input.input_name,
));
}
for finding in &self.results.unused_component_outputs {
self.lines.push(format!(
"unused-component-output:{}:{}:{}",
self.rel(&finding.output.path),
finding.output.line,
finding.output.output_name,
));
}
}
fn push_component_framework_lines(&mut self) {
for finding in &self.results.unused_svelte_events {
self.lines.push(format!(
"unused-svelte-event:{}:{}:{}",
self.rel(&finding.event.path),
finding.event.line,
finding.event.event_name,
));
}
for finding in &self.results.unused_server_actions {
self.lines.push(format!(
"unused-server-action:{}:{}:{}",
self.rel(&finding.action.path),
finding.action.line,
finding.action.action_name,
));
}
for finding in &self.results.unused_load_data_keys {
self.lines.push(format!(
"unused-load-data-key:{}:{}:{}",
self.rel(&finding.key.path),
finding.key.line,
finding.key.key_name,
));
}
}
fn push_route_lines(&mut self) {
for finding in &self.results.route_collisions {
self.lines.push(format!(
"route-collision:{}:{} (url {})",
self.rel(&finding.collision.path),
finding.collision.line,
finding.collision.url,
));
}
for finding in &self.results.dynamic_segment_name_conflicts {
self.lines.push(format!(
"dynamic-segment-name-conflict:{}:{} ({} at {})",
self.rel(&finding.conflict.path),
finding.conflict.line,
finding.conflict.conflicting_segments.join(" vs "),
finding.conflict.position,
));
}
}
fn push_suppression_lines(&mut self) {
for suppression in &self.results.stale_suppressions {
self.lines
.push(compact_stale_suppression_line(suppression, self.root));
}
}
fn push_workspace_lines(&mut self) {
for entry in &self.results.unused_catalog_entries {
self.lines.push(format!(
"unused-catalog-entry:{}:{}:{}:{}",
self.rel(&entry.entry.path),
entry.entry.line,
entry.entry.catalog_name,
entry.entry.entry_name,
));
}
for group in &self.results.empty_catalog_groups {
self.lines.push(format!(
"empty-catalog-group:{}:{}:{}",
self.rel(&group.group.path),
group.group.line,
group.group.catalog_name,
));
}
for finding in &self.results.unresolved_catalog_references {
self.lines
.push(compact_catalog_reference_line(finding, self.root));
}
for finding in &self.results.unused_dependency_overrides {
self.lines
.push(compact_unused_override_line(finding, self.root));
}
for finding in &self.results.misconfigured_dependency_overrides {
self.lines
.push(compact_misconfigured_override_line(finding, self.root));
}
}
}
#[must_use]
pub fn build_grouped_compact_lines(groups: &[ResultGroup], root: &Path) -> Vec<String> {
groups
.iter()
.flat_map(|group| {
build_compact_lines(&group.results, root)
.into_iter()
.map(|line| format!("{}\t{line}", group.key))
})
.collect()
}
#[must_use]
pub fn build_health_compact_lines(
report: &fallow_output::HealthReport,
root: &Path,
) -> Vec<String> {
let mut lines = Vec::new();
push_health_score_compact(&mut lines, report);
push_vital_signs_compact(&mut lines, report);
push_health_findings_compact(&mut lines, &report.findings, root);
push_styling_findings_compact(&mut lines, &report.styling_findings, root);
push_threshold_overrides_compact(&mut lines, &report.threshold_overrides, root);
push_file_scores_compact(&mut lines, &report.file_scores, root);
push_coverage_gaps_compact(&mut lines, report, root);
push_runtime_sections_compact(&mut lines, report, root);
push_hotspots_compact(&mut lines, &report.hotspots, root);
push_health_trend_compact(&mut lines, report);
push_refactoring_targets_compact(&mut lines, &report.targets, root);
lines
}
fn push_styling_findings_compact(
lines: &mut Vec<String>,
findings: &[fallow_output::StylingFinding],
root: &Path,
) {
for finding in findings {
let relative = compact_path(Path::new(&finding.path), root);
let severity = match finding.effective_severity {
fallow_output::StylingFindingSeverity::Error => "error",
fallow_output::StylingFindingSeverity::Warn => "warn",
};
let value = compact_field_value(&finding.value);
lines.push(format!(
"{}:{}:{}:{}:severity={},value={}",
finding.code, relative, finding.line, finding.sub_kind, severity, value
));
}
}
fn compact_field_value(value: &str) -> String {
value
.replace([':', ',', '\n', '\r'], " ")
.split_whitespace()
.collect::<Vec<_>>()
.join(" ")
}
fn push_threshold_overrides_compact(
lines: &mut Vec<String>,
entries: &[fallow_output::ThresholdOverrideState],
root: &Path,
) {
for entry in entries {
let status = match entry.status {
fallow_output::ThresholdOverrideStatus::Active => "active",
fallow_output::ThresholdOverrideStatus::Stale => "stale",
fallow_output::ThresholdOverrideStatus::Insufficient => "insufficient",
fallow_output::ThresholdOverrideStatus::NoMatch => "no_match",
};
let target = entry.path.as_ref().map_or_else(
|| "no-match".to_string(),
|path| entry.target_label(&compact_path(path, root)),
);
let dimension = threshold_override_dimension_label(entry.dimension);
let metrics = entry.metrics.map_or(String::new(), |metrics| {
let crap = metrics
.crap
.map_or(String::new(), |value| format!(",crap={value:.1}"));
let line_count = metrics
.line_count
.map_or(String::new(), |value| format!(",lines={value}"));
format!(
",cyclomatic={},cognitive={}{}{}",
metrics.cyclomatic, metrics.cognitive, line_count, crap
)
});
let outstanding = if entry.outstanding.is_empty() {
String::new()
} else {
format!(
",outstanding={}",
entry
.outstanding
.iter()
.map(|value| threshold_override_dimension_label(*value))
.collect::<Vec<_>>()
.join("|")
)
};
lines.push(format!(
"threshold-override:{}:{}:{}:{}{}{}",
entry.override_index, dimension, status, target, metrics, outstanding
));
}
}
fn threshold_override_dimension_label(
dimension: fallow_output::ThresholdOverrideDimension,
) -> &'static str {
match dimension {
fallow_output::ThresholdOverrideDimension::Complexity => "complexity",
fallow_output::ThresholdOverrideDimension::Crap => "crap",
}
}
fn push_health_score_compact(lines: &mut Vec<String>, report: &fallow_output::HealthReport) {
if let Some(ref hs) = report.health_score {
lines.push(format!("health-score:{:.1}:{}", hs.score, hs.grade));
}
}
fn push_vital_signs_compact(lines: &mut Vec<String>, report: &fallow_output::HealthReport) {
if let Some(ref vs) = report.vital_signs {
let mut parts = Vec::new();
if vs.total_loc > 0 {
parts.push(format!("total_loc={}", vs.total_loc));
}
parts.push(format!("avg_cyclomatic={:.1}", vs.avg_cyclomatic));
parts.push(format!("p90_cyclomatic={}", vs.p90_cyclomatic));
if let Some(v) = vs.dead_file_pct {
parts.push(format!("dead_file_pct={v:.1}"));
}
if let Some(v) = vs.dead_export_pct {
parts.push(format!("dead_export_pct={v:.1}"));
}
if let Some(v) = vs.maintainability_avg {
parts.push(format!("maintainability_avg={v:.1}"));
}
if let Some(v) = vs.hotspot_count {
parts.push(format!("hotspot_count={v}"));
}
if let Some(v) = vs.circular_dep_count {
parts.push(format!("circular_dep_count={v}"));
}
if let Some(v) = vs.unused_dep_count {
parts.push(format!("unused_dep_count={v}"));
}
lines.push(format!("vital-signs:{}", parts.join(",")));
push_cyclomatic_population_compact(lines, vs);
}
}
fn push_cyclomatic_population_compact(lines: &mut Vec<String>, vs: &fallow_output::VitalSigns) {
let Some(population) = &vs.cyclomatic_population else {
return;
};
for (kind, group) in [
("functions", &population.functions),
("modules", &population.modules),
("templates", &population.templates),
] {
let max = group
.max
.map_or_else(|| "null".to_string(), |value| value.to_string());
lines.push(format!(
"cyclomatic-population:{kind}:count={},sum={},max={max}",
group.count, group.sum
));
}
}
fn exceeded_compact_label(exceeded: fallow_output::ExceededThreshold) -> &'static str {
match exceeded {
fallow_output::ExceededThreshold::Cyclomatic => "cyclomatic",
fallow_output::ExceededThreshold::Cognitive => "cognitive",
fallow_output::ExceededThreshold::Both => "both",
fallow_output::ExceededThreshold::Crap => "crap",
fallow_output::ExceededThreshold::CyclomaticCrap => "cyclomatic_crap",
fallow_output::ExceededThreshold::CognitiveCrap => "cognitive_crap",
fallow_output::ExceededThreshold::All => "all",
}
}
fn push_health_findings_compact(
lines: &mut Vec<String>,
findings: &[fallow_output::HealthFinding],
root: &Path,
) {
for finding in findings {
let relative = compact_path(&finding.path, root);
let severity = match finding.severity {
fallow_output::FindingSeverity::Critical => "critical",
fallow_output::FindingSeverity::High => "high",
fallow_output::FindingSeverity::Moderate => "moderate",
};
let crap_suffix = match finding.crap {
Some(crap) => {
let coverage = finding
.coverage_pct
.map(|pct| format!(",coverage_pct={pct:.1}"))
.unwrap_or_default();
format!(",crap={crap:.1}{coverage}")
}
None => String::new(),
};
lines.push(format!(
"high-complexity:{}:{}:{}:cyclomatic={},cognitive={},severity={},exceeded={}{}",
relative,
finding.line,
finding.name,
finding.cyclomatic,
finding.cognitive,
severity,
exceeded_compact_label(finding.exceeded),
crap_suffix,
));
}
}
fn push_file_scores_compact(
lines: &mut Vec<String>,
scores: &[fallow_output::FileHealthScore],
root: &Path,
) {
for score in scores {
let relative = compact_path(&score.path, root);
lines.push(format!(
"file-score:{}:mi={:.1},fan_in={},fan_out={},dead={:.2},density={:.2},crap_max={:.1},crap_above={}",
relative,
score.maintainability_index,
score.fan_in,
score.fan_out,
score.dead_code_ratio,
score.complexity_density,
score.crap_max,
score.crap_above_threshold,
));
}
}
fn push_coverage_gaps_compact(
lines: &mut Vec<String>,
report: &fallow_output::HealthReport,
root: &Path,
) {
if let Some(ref gaps) = report.coverage_gaps {
lines.push(format!(
"coverage-gap-summary:runtime_files={},covered_files={},file_coverage_pct={:.1},untested_files={},untested_exports={}",
gaps.summary.runtime_files,
gaps.summary.covered_files,
gaps.summary.file_coverage_pct,
gaps.summary.untested_files,
gaps.summary.untested_exports,
));
for item in &gaps.files {
let relative = compact_path(&item.file.path, root);
lines.push(format!(
"untested-file:{}:value_exports={}",
relative, item.file.value_export_count,
));
}
for item in &gaps.exports {
let relative = compact_path(&item.export.path, root);
lines.push(format!(
"untested-export:{}:{}:{}",
relative, item.export.line, item.export.export_name,
));
}
}
}
fn push_runtime_sections_compact(
lines: &mut Vec<String>,
report: &fallow_output::HealthReport,
root: &Path,
) {
if let Some(ref production) = report.runtime_coverage {
lines.extend(build_runtime_coverage_compact_lines(production, root));
}
if let Some(ref intelligence) = report.coverage_intelligence {
lines.extend(build_coverage_intelligence_compact_lines(
intelligence,
root,
));
}
}
fn compact_ownership_suffix(ownership: Option<&fallow_output::OwnershipMetrics>) -> String {
ownership.map_or_else(String::new, |o| {
let mut parts = vec![
format!("bus={}", o.bus_factor),
format!("contributors={}", o.contributor_count),
format!("top={}", o.top_contributor.identifier),
format!("top_share={:.3}", o.top_contributor.share),
];
if let Some(owner) = &o.declared_owner {
parts.push(format!("owner={owner}"));
}
if let Some(unowned) = o.unowned {
parts.push(format!("unowned={unowned}"));
}
let state = match o.ownership_state {
fallow_output::OwnershipState::Active => "active",
fallow_output::OwnershipState::Unowned => "unowned",
fallow_output::OwnershipState::DeclaredInactive => "declared_inactive",
fallow_output::OwnershipState::Drifting => "drifting",
};
parts.push(format!("ownership_state={state}"));
if o.drift {
parts.push("drift=true".to_string());
}
format!(",{}", parts.join(","))
})
}
fn push_hotspots_compact(
lines: &mut Vec<String>,
hotspots: &[fallow_output::HotspotFinding],
root: &Path,
) {
for entry in hotspots {
let relative = compact_path(&entry.path, root);
let ownership_suffix = compact_ownership_suffix(entry.ownership.as_ref());
lines.push(format!(
"hotspot:{}:score={:.1},commits={},churn={},density={:.2},fan_in={},trend={}{}",
relative,
entry.score,
entry.commits,
entry.lines_added + entry.lines_deleted,
entry.complexity_density,
entry.fan_in,
entry.trend,
ownership_suffix,
));
}
}
fn push_health_trend_compact(lines: &mut Vec<String>, report: &fallow_output::HealthReport) {
if let Some(ref trend) = report.health_trend {
lines.push(format!(
"trend:overall:direction={}",
trend.overall_direction.label()
));
for m in &trend.metrics {
lines.push(format!(
"trend:{}:previous={:.1},current={:.1},delta={:+.1},direction={}",
m.name,
m.previous,
m.current,
m.delta,
m.direction.label(),
));
}
}
}
fn push_refactoring_targets_compact(
lines: &mut Vec<String>,
targets: &[fallow_output::RefactoringTargetFinding],
root: &Path,
) {
for target in targets {
let relative = compact_path(&target.path, root);
let category = target.category.compact_label();
let effort = target.effort.label();
let confidence = target.confidence.label();
lines.push(format!(
"refactoring-target:{}:priority={:.1},efficiency={:.1},category={},effort={},confidence={}:{}",
relative,
target.priority,
target.efficiency,
category,
effort,
confidence,
target.recommendation,
));
}
}
fn build_runtime_coverage_compact_lines(
production: &fallow_output::RuntimeCoverageReport,
root: &Path,
) -> Vec<String> {
let mut lines = vec![format!(
"runtime-coverage-summary:functions_tracked={},functions_hit={},functions_unhit={},functions_untracked={},coverage_percent={:.1},trace_count={},period_days={},deployments_seen={}",
production.summary.functions_tracked,
production.summary.functions_hit,
production.summary.functions_unhit,
production.summary.functions_untracked,
production.summary.coverage_percent,
production.summary.trace_count,
production.summary.period_days,
production.summary.deployments_seen,
)];
for finding in &production.findings {
let relative = compact_path(&finding.path, root);
let invocations = finding
.invocations
.map_or_else(|| "null".to_owned(), |hits| hits.to_string());
lines.push(format!(
"runtime-coverage:{}:{}:{}:id={},verdict={},invocations={},confidence={}",
relative,
finding.line,
finding.function,
finding.id,
finding.verdict,
invocations,
finding.confidence,
));
}
for entry in &production.hot_paths {
let relative = compact_path(&entry.path, root);
lines.push(format!(
"production-hot-path:{}:{}:{}:id={},invocations={},percentile={}",
relative, entry.line, entry.function, entry.id, entry.invocations, entry.percentile,
));
}
lines
}
fn build_coverage_intelligence_compact_lines(
intelligence: &fallow_output::CoverageIntelligenceReport,
root: &Path,
) -> Vec<String> {
let mut lines = vec![format!(
"coverage-intelligence-summary:verdict={},findings={},risky_changes={},high_confidence_deletes={},review_required={},refactor_carefully={},skipped_ambiguous_matches={}",
intelligence.verdict,
intelligence.summary.findings,
intelligence.summary.risky_changes,
intelligence.summary.high_confidence_deletes,
intelligence.summary.review_required,
intelligence.summary.refactor_carefully,
intelligence.summary.skipped_ambiguous_matches,
)];
for finding in &intelligence.findings {
let relative = compact_path(&finding.path, root);
let identity = finding.identity.as_deref().unwrap_or("-");
let signals = finding
.signals
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join("+");
lines.push(format!(
"coverage-intelligence:{}:{}:{}:id={},verdict={},recommendation={},confidence={},signals={}",
relative,
finding.line,
identity,
finding.id,
finding.verdict,
finding.recommendation,
finding.confidence,
signals,
));
}
lines
}
#[must_use]
pub fn build_duplication_compact_lines(report: &DuplicationReport, root: &Path) -> Vec<String> {
let fingerprints = CloneFingerprintSet::from_groups(&report.clone_groups);
let mut lines = Vec::new();
for (index, group) in report.clone_groups.iter().enumerate() {
let fingerprint = fingerprints.fingerprint_for_group(group);
for instance in &group.instances {
lines.push(format!(
"code-duplication:{}:{}-{}:fingerprint={},group={},tokens={},lines={},instances={}",
compact_path(&instance.file, root),
instance.start_line,
instance.end_line,
fingerprint,
index + 1,
group.token_count,
group.line_count,
group.instances.len(),
));
}
}
lines
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use fallow_types::duplicates::{CloneGroup, CloneInstance, DuplicationStats};
use fallow_types::output_dead_code::UnusedFileFinding;
use fallow_types::results::{AnalysisResults, UnusedFile};
use super::*;
#[test]
fn compact_cycles_preserve_empty_self_loop_and_cross_package_output() {
use fallow_types::output_dead_code::{CircularDependencyFinding, ReExportCycleFinding};
use fallow_types::results::{CircularDependency, ReExportCycle, ReExportCycleKind};
let root = Path::new("/project");
for (files, cross_package, expected) in [
(vec![], false, "circular-dependency::7:"),
(
vec![root.join("src/a.ts")],
false,
"circular-dependency:src/a.ts:7:src/a.ts → src/a.ts",
),
(
vec![root.join("src/a.ts"), root.join("src/b.ts")],
true,
"circular-dependency:src/a.ts:7:src/a.ts → src/b.ts → src/a.ts (cross-package)",
),
] {
let mut results = AnalysisResults::default();
results
.circular_dependencies
.push(CircularDependencyFinding {
cycle: CircularDependency {
length: files.len(),
files,
line: 7,
col: 0,
edges: vec![],
is_cross_package: cross_package,
},
actions: vec![],
introduced: None,
effective_severity: None,
});
assert_eq!(build_compact_lines(&results, root), vec![expected]);
}
for (files, kind, expected) in [
(vec![], ReExportCycleKind::MultiNode, "re-export-cycle::"),
(
vec![root.join("src/a.ts")],
ReExportCycleKind::SelfLoop,
"re-export-cycle:src/a.ts:src/a.ts (self-loop)",
),
(
vec![root.join("src/a.ts"), root.join("src/b.ts")],
ReExportCycleKind::MultiNode,
"re-export-cycle:src/a.ts:src/a.ts <-> src/b.ts",
),
] {
let mut results = AnalysisResults::default();
results.re_export_cycles.push(ReExportCycleFinding {
cycle: ReExportCycle { files, kind },
actions: vec![],
introduced: None,
effective_severity: None,
});
assert_eq!(build_compact_lines(&results, root), vec![expected]);
}
}
#[test]
fn a_caveated_finding_carries_a_trailing_caveat_field() {
use fallow_types::extract::MemberKind;
use fallow_types::output_dead_code::{ReachabilityCaveat, UnusedDependencyFinding};
use fallow_types::results::{UnusedDependency, UnusedExport};
let root = PathBuf::from("/project");
let mut results = AnalysisResults::default();
let mut file = UnusedFileFinding::with_actions(UnusedFile {
path: root.join("src/dead.ts"),
});
file.reachability_caveats = vec![ReachabilityCaveat::IncompleteImportGraph];
results.unused_files.push(file);
let mut export =
fallow_types::output_dead_code::UnusedExportFinding::with_actions(UnusedExport {
path: root.join("src/lib.ts"),
export_name: "needed".to_owned(),
is_type_only: false,
line: 3,
col: 0,
span_start: 0,
is_re_export: false,
deprecated: false,
deprecated_reason: None,
});
export.reachability_caveats = vec![
ReachabilityCaveat::IncompleteFileAnalysis,
ReachabilityCaveat::IncompleteImportGraph,
];
results.unused_exports.push(export);
let mut dep = UnusedDependencyFinding::with_actions(UnusedDependency {
package_name: "left-pad".to_owned(),
location: fallow_types::results::DependencyLocation::Dependencies,
path: root.join("package.json"),
line: 5,
used_in_workspaces: Vec::new(),
});
dep.reachability_caveats = vec![ReachabilityCaveat::IncompleteImportGraph];
results.unused_dependencies.push(dep);
let member = |parent: &str, name: &str, kind| fallow_types::results::UnusedMember {
path: root.join("src/lib.ts"),
parent_name: parent.to_owned(),
member_name: name.to_owned(),
kind,
line: 7,
col: 2,
};
let mut enum_member = fallow_types::output_dead_code::UnusedEnumMemberFinding::with_actions(
member("Mode", "Legacy", MemberKind::EnumMember),
);
enum_member.reachability_caveats = vec![ReachabilityCaveat::IncompleteImportGraph];
results.unused_enum_members.push(enum_member);
let mut class_member =
fallow_types::output_dead_code::UnusedClassMemberFinding::with_actions(member(
"Widget",
"render",
MemberKind::ClassMethod,
));
class_member.reachability_caveats = vec![ReachabilityCaveat::IncompleteImportGraph];
results.unused_class_members.push(class_member);
let mut store_member =
fallow_types::output_dead_code::UnusedStoreMemberFinding::with_actions(member(
"useCart",
"subtotal",
MemberKind::StoreMember,
));
store_member.reachability_caveats = vec![ReachabilityCaveat::IncompleteImportGraph];
results.unused_store_members.push(store_member);
let lines = build_compact_lines(&results, &root);
assert_eq!(
lines,
vec![
"unused-file:src/dead.ts,caveat=incomplete-import-graph",
"unused-export:src/lib.ts:3:needed,caveat=incomplete-file-analysis+incomplete-import-graph",
"unused-dep:left-pad,caveat=incomplete-import-graph",
"unused-enum-member:src/lib.ts:7:Mode.Legacy,caveat=incomplete-import-graph",
"unused-class-member:src/lib.ts:7:Widget.render,caveat=incomplete-import-graph",
"unused-store-member:src/lib.ts:7:useCart.subtotal,caveat=incomplete-import-graph",
]
);
}
#[test]
fn compact_unused_file_format_uses_relative_paths() {
let root = PathBuf::from("/project");
let mut results = AnalysisResults::default();
results
.unused_files
.push(UnusedFileFinding::with_actions(UnusedFile {
path: root.join("src/dead.ts"),
}));
let lines = build_compact_lines(&results, &root);
assert_eq!(lines, vec!["unused-file:src/dead.ts"]);
}
#[test]
fn grouped_compact_prefixes_each_issue_with_group_key() {
let root = PathBuf::from("/project");
let mut results = AnalysisResults::default();
results
.unused_files
.push(UnusedFileFinding::with_actions(UnusedFile {
path: root.join("src/dead.ts"),
}));
let groups = vec![ResultGroup {
key: "team-a".to_owned(),
owners: Some(vec!["@team-a".to_owned()]),
results,
}];
let lines = build_grouped_compact_lines(&groups, &root);
assert_eq!(lines, vec!["team-a\tunused-file:src/dead.ts"]);
}
#[test]
fn duplication_compact_lines_include_stable_group_context() {
let root = PathBuf::from("/project");
let report = DuplicationReport {
clone_groups: vec![CloneGroup {
instances: vec![CloneInstance {
file: root.join("src/a.ts"),
start_line: 2,
end_line: 6,
start_col: 0,
end_col: 10,
fragment: "const duplicated = true;".to_owned(),
}],
token_count: 12,
line_count: 5,
similarity: None,
}],
clone_families: Vec::new(),
mirrored_directories: Vec::new(),
stats: DuplicationStats::default(),
};
let lines = build_duplication_compact_lines(&report, &root);
assert_eq!(lines.len(), 1);
assert!(lines[0].starts_with("code-duplication:src/a.ts:2-6:fingerprint="));
assert!(lines[0].contains(",group=1,tokens=12,lines=5,instances=1"));
}
#[test]
fn health_compact_lines_include_score_and_vital_signs() {
let root = PathBuf::from("/project");
let report = fallow_output::HealthReport {
health_score: Some(fallow_output::HealthScore {
formula_version: 1,
score: 91.2,
grade: "A",
penalties: fallow_output::HealthScorePenalties {
dead_files: None,
dead_exports: None,
complexity: 0.0,
p90_complexity: 0.0,
maintainability: None,
hotspots: None,
unused_deps: None,
circular_deps: None,
unit_size: None,
coupling: None,
duplication: None,
prop_drilling: None,
},
}),
vital_signs: Some(fallow_output::VitalSigns {
total_loc: 120,
avg_cyclomatic: 3.4,
p90_cyclomatic: 8,
..Default::default()
}),
..Default::default()
};
let lines = build_health_compact_lines(&report, &root);
assert_eq!(lines[0], "health-score:91.2:A");
assert_eq!(
lines[1],
"vital-signs:total_loc=120,avg_cyclomatic=3.4,p90_cyclomatic=8"
);
assert!(
lines
.iter()
.all(|line| !line.starts_with("cyclomatic-population:"))
);
}
#[test]
fn health_compact_population_rows_preserve_vital_signs_and_order() {
let root = PathBuf::from("/project");
let vitals = fallow_output::VitalSigns {
avg_cyclomatic: 16.0,
p90_cyclomatic: 31,
..Default::default()
};
let legacy = build_health_compact_lines(
&fallow_output::HealthReport {
vital_signs: Some(vitals.clone()),
..Default::default()
},
&root,
);
let report = fallow_output::HealthReport {
vital_signs: Some(fallow_output::VitalSigns {
cyclomatic_population: Some(fallow_output::CyclomaticPopulation {
functions: fallow_output::CyclomaticUnitPopulation {
count: 1,
sum: 1,
max: Some(1),
},
modules: fallow_output::CyclomaticUnitPopulation {
count: 1,
sum: 31,
max: Some(31),
},
templates: fallow_output::CyclomaticUnitPopulation::default(),
}),
..vitals
}),
..Default::default()
};
let lines = build_health_compact_lines(&report, &root);
assert_eq!(lines[0], legacy[0]);
assert_eq!(
&lines[1..],
[
"cyclomatic-population:functions:count=1,sum=1,max=1",
"cyclomatic-population:modules:count=1,sum=31,max=31",
"cyclomatic-population:templates:count=0,sum=0,max=null",
]
);
}
#[test]
fn health_compact_measured_empty_populations_keep_null_maxima() {
let report = fallow_output::HealthReport {
vital_signs: Some(fallow_output::VitalSigns {
cyclomatic_population: Some(fallow_output::CyclomaticPopulation::default()),
..Default::default()
}),
..Default::default()
};
let lines = build_health_compact_lines(&report, &PathBuf::from("/project"));
assert_eq!(
&lines[1..],
[
"cyclomatic-population:functions:count=0,sum=0,max=null",
"cyclomatic-population:modules:count=0,sum=0,max=null",
"cyclomatic-population:templates:count=0,sum=0,max=null",
]
);
}
#[test]
fn health_compact_lines_include_styling_findings() {
let root = PathBuf::from("/project");
let report = fallow_output::HealthReport {
styling_findings: vec![fallow_output::StylingFinding {
code: "css-token-drift".to_string(),
sub_kind: "tailwind-arbitrary-value".to_string(),
path: "/project/src/app.css".to_string(),
line: 6,
value: "--color-brand: rgb(240, 90, 41)".to_string(),
effective_severity: fallow_output::StylingFindingSeverity::Warn,
blast_radius: None,
confidence: None,
agent_disposition: None,
nearest_token: None,
fix_hint: None,
actions: Vec::new(),
}],
..Default::default()
};
let lines = build_health_compact_lines(&report, &root);
assert_eq!(
lines,
vec![
"css-token-drift:src/app.css:6:tailwind-arbitrary-value:severity=warn,value=--color-brand rgb(240 90 41)"
]
);
}
}