normalize_native_rules/
high_fan_out.rs1use normalize_output::diagnostics::{DiagnosticsReport, Issue, Severity, ToolFailure};
16use std::path::Path;
17
18pub async fn build_high_fan_out_report(root: &Path, threshold: usize) -> DiagnosticsReport {
24 let mut report = DiagnosticsReport::new();
25
26 let db_path = crate::check_refs::normalize_dir_for_root(root).join("index.sqlite");
27 let idx = match normalize_facts::FileIndex::open(&db_path, root).await {
28 Ok(idx) => idx,
29 Err(e) => {
30 report.tool_errors.push(ToolFailure {
31 tool: "high-fan-out".into(),
32 message: format!(
33 "failed to open index at {}: {}. Run `normalize structure rebuild` first.",
34 db_path.display(),
35 e
36 ),
37 });
38 return report;
39 }
40 };
41
42 let fan_out = match idx.import_fan_out_by_file().await {
43 Ok(v) => v,
44 Err(e) => {
45 report.tool_errors.push(ToolFailure {
46 tool: "high-fan-out".into(),
47 message: format!("failed to query imports table: {e}"),
48 });
49 return report;
50 }
51 };
52
53 for (file, count) in &fan_out {
54 if *count > threshold {
55 report.issues.push(Issue {
56 file: file.clone(),
57 line: Some(1),
58 column: None,
59 end_line: None,
60 end_column: None,
61 rule_id: "high-fan-out".into(),
62 message: format!(
63 "file imports from {count} modules (threshold: {threshold})"
64 ),
65 severity: Severity::Warning,
66 source: "high-fan-out".into(),
67 related: vec![],
68 suggestion: Some(
69 "consider splitting responsibilities or introducing an abstraction layer to reduce coupling".into(),
70 ),
71 });
72 }
73 }
74
75 report.files_checked = fan_out.len();
76 report.sources_run.push("high-fan-out".into());
77 report
78}