use std::path::{Path, PathBuf};
use fallow_output::{
ComplexityViolation, ComponentRollup, DEFAULT_COGNITIVE_CRITICAL, DEFAULT_COGNITIVE_HIGH,
DEFAULT_CYCLOMATIC_CRITICAL, DEFAULT_CYCLOMATIC_HIGH, ExceededThreshold,
compute_finding_severity,
};
use super::threshold_overrides::{
AppliedHealthThresholds, ComplexityFunctionContext, ThresholdOverrideResolver,
ThresholdOverrideStateTracker,
};
const COMPONENT_ROLLUP_NAME: &str = "<component>";
pub(super) fn append_component_rollup_findings(
findings: &mut Vec<ComplexityViolation>,
template_owner_lookup: Option<&rustc_hash::FxHashMap<PathBuf, PathBuf>>,
resolver: &ThresholdOverrideResolver,
config_root: &Path,
state_tracker: &mut ThresholdOverrideStateTracker,
) {
let mut by_owner: rustc_hash::FxHashMap<PathBuf, (Vec<usize>, Vec<usize>)> =
rustc_hash::FxHashMap::default();
for (idx, finding) in findings.iter().enumerate() {
if finding.name == "<template>" {
if let Some(owner) = component_template_owner(finding, template_owner_lookup) {
by_owner.entry(owner).or_default().1.push(idx);
}
} else if is_component_class_finding(finding) {
by_owner
.entry(finding.path.clone())
.or_default()
.0
.push(idx);
}
}
let mut to_push: Vec<ComplexityViolation> = Vec::new();
for (owner, (class_idxs, template_idxs)) in by_owner {
if class_idxs.is_empty() || template_idxs.is_empty() || template_idxs.len() > 1 {
continue;
}
let template = &findings[template_idxs[0]];
let Some(worst_idx) = class_idxs
.iter()
.copied()
.max_by_key(|&index| findings[index].cyclomatic)
else {
continue;
};
let worst = &findings[worst_idx];
if let Some(rollup) =
build_component_rollup(owner, worst, template, resolver, config_root, state_tracker)
{
to_push.push(rollup);
}
}
findings.extend(to_push);
}
fn component_template_owner(
finding: &ComplexityViolation,
template_owner_lookup: Option<&rustc_hash::FxHashMap<PathBuf, PathBuf>>,
) -> Option<PathBuf> {
let ext = finding
.path
.extension()
.and_then(|extension| extension.to_str())
.map(str::to_ascii_lowercase);
match ext.as_deref() {
Some("html") => template_owner_lookup
.and_then(|lookup| lookup.get(&finding.path))
.cloned(),
Some("ts" | "tsx" | "mts" | "cts") => Some(finding.path.clone()),
_ => None,
}
}
fn is_component_class_finding(finding: &ComplexityViolation) -> bool {
finding.name != COMPONENT_ROLLUP_NAME
&& finding
.path
.extension()
.and_then(|extension| extension.to_str())
.is_some_and(|extension| {
matches!(
extension.to_ascii_lowercase().as_str(),
"ts" | "tsx" | "mts" | "cts"
)
})
}
struct ComponentRollupTotals {
rollup_cyc: u16,
rollup_cog: u16,
rollup_lines: u32,
exceeds_cyclomatic: bool,
exceeds_cognitive: bool,
}
fn make_component_rollup_violation(
owner: PathBuf,
worst: &ComplexityViolation,
template: &ComplexityViolation,
totals: &ComponentRollupTotals,
applied: AppliedHealthThresholds,
) -> ComplexityViolation {
let component = owner.file_stem().map_or_else(
|| "<unknown-component>".to_string(),
|stem| stem.to_string_lossy().into_owned(),
);
ComplexityViolation {
path: owner,
name: COMPONENT_ROLLUP_NAME.to_string(),
line: worst.line,
col: worst.col,
cyclomatic: totals.rollup_cyc,
cognitive: totals.rollup_cog,
line_count: totals.rollup_lines,
param_count: 0,
exceeded: ExceededThreshold::from_bools(
totals.exceeds_cyclomatic,
totals.exceeds_cognitive,
false,
),
severity: compute_finding_severity(
totals.rollup_cog,
totals.rollup_cyc,
None,
DEFAULT_COGNITIVE_HIGH,
DEFAULT_COGNITIVE_CRITICAL,
DEFAULT_CYCLOMATIC_HIGH,
DEFAULT_CYCLOMATIC_CRITICAL,
),
crap: None,
coverage_pct: None,
coverage_tier: None,
coverage_source: None,
inherited_from: None,
react_hook_count: 0,
react_jsx_max_depth: 0,
react_prop_count: 0,
react_hook_profile: None,
component_rollup: Some(ComponentRollup {
component,
class_worst_function: worst.name.clone(),
class_cyclomatic: worst.cyclomatic,
class_cognitive: worst.cognitive,
template_path: template.path.clone(),
template_cyclomatic: template.cyclomatic,
template_cognitive: template.cognitive,
}),
contributions: Vec::new(),
effective_thresholds: applied.override_index.map(|_| applied.effective),
threshold_source: applied
.override_index
.map(|_| fallow_output::ThresholdSource::Override),
}
}
fn build_component_rollup(
owner: PathBuf,
worst: &ComplexityViolation,
template: &ComplexityViolation,
resolver: &ThresholdOverrideResolver,
config_root: &Path,
state_tracker: &mut ThresholdOverrideStateTracker,
) -> Option<ComplexityViolation> {
let rollup_cyc = worst.cyclomatic.saturating_add(template.cyclomatic);
let rollup_cog = worst.cognitive.saturating_add(template.cognitive);
let rollup_lines = worst.line_count.saturating_add(template.line_count);
let relative = owner.strip_prefix(config_root).unwrap_or(owner.as_path());
let (applied, matches) = resolver.resolve(relative, COMPONENT_ROLLUP_NAME);
state_tracker.record_complexity(
ComplexityFunctionContext {
path: owner.as_path(),
function: COMPONENT_ROLLUP_NAME,
line: worst.line,
col: worst.col,
cyclomatic: rollup_cyc,
cognitive: rollup_cog,
line_count: 0,
},
&matches,
resolver.global,
applied.effective,
);
let exceeds_cyclomatic = rollup_cyc > applied.effective.max_cyclomatic;
let exceeds_cognitive = rollup_cog > applied.effective.max_cognitive;
if !exceeds_cyclomatic && !exceeds_cognitive {
return None;
}
let totals = ComponentRollupTotals {
rollup_cyc,
rollup_cog,
rollup_lines,
exceeds_cyclomatic,
exceeds_cognitive,
};
Some(make_component_rollup_violation(
owner, worst, template, &totals, applied,
))
}