use duckdb::params;
use crate::facts::FactsDb;
use crate::{Options, Result};
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct GodClassRow {
pub path: String,
pub cognitive: f64,
pub fan_in: u32,
pub fan_out: u32,
pub god_score: f64,
}
const DEFAULT_MIN_COGNITIVE: f64 = 30.0;
const DEFAULT_MIN_TOTAL_FAN: u32 = 10;
const SQL: &str = "
WITH file_complexity AS (
SELECT path, MAX(cognitive)::DOUBLE AS cognitive
FROM {cm_src}
WHERE cognitive IS NOT NULL
GROUP BY path
),
fan_in AS (
-- One row per `target_path`; counts distinct importers.
-- Only resolved imports contribute.
SELECT target_path AS path, COUNT(DISTINCT src_path)::UINTEGER AS fan_in
FROM {imports_src}
WHERE target_path IS NOT NULL
GROUP BY target_path
),
fan_out AS (
-- One row per `src_path`; counts distinct raw targets. Includes
-- both resolved + unresolved targets, so external (npm / pypi
-- / std) imports count toward fan-out — reflects the full
-- structural-dependency surface.
SELECT src_path AS path, COUNT(DISTINCT target)::UINTEGER AS fan_out
FROM {imports_src}
GROUP BY src_path
)
SELECT
fc.path,
fc.cognitive,
COALESCE(fi.fan_in, 0)::UINTEGER AS fan_in,
COALESCE(fo.fan_out, 0)::UINTEGER AS fan_out,
(fc.cognitive / 100.0) * (COALESCE(fi.fan_in, 0) + COALESCE(fo.fan_out, 0))::DOUBLE AS god_score
FROM file_complexity fc
LEFT JOIN fan_in fi ON fi.path = fc.path
LEFT JOIN fan_out fo ON fo.path = fc.path
WHERE fc.cognitive >= ?
AND (COALESCE(fi.fan_in, 0) + COALESCE(fo.fan_out, 0)) >= ?
ORDER BY god_score DESC, fc.path ASC
LIMIT ?
";
#[tracing::instrument(name = "god-classes", skip_all, fields(min_revs = opts.min_revs))]
pub fn run_god_classes(db: &FactsDb, opts: &Options) -> Result<Vec<GodClassRow>> {
let cm_src = crate::analyses::grouped_complexity::source_table(opts);
run_god_classes_scoped(db, opts, cm_src, "imports")
}
pub fn run_god_classes_scoped(
db: &FactsDb,
opts: &Options,
complexity_source: &str,
imports_source: &str,
) -> Result<Vec<GodClassRow>> {
let row_limit: i64 = opts.rows_limit.map_or(i64::MAX, i64::from);
let sql = SQL
.replace("{cm_src}", complexity_source)
.replace("{imports_src}", imports_source);
super::query::explain_if_requested(
db,
&sql,
params![DEFAULT_MIN_COGNITIVE, DEFAULT_MIN_TOTAL_FAN, row_limit],
"god-classes",
opts,
)?;
super::query::query_map_collect(
db,
&sql,
params![DEFAULT_MIN_COGNITIVE, DEFAULT_MIN_TOTAL_FAN, row_limit],
"god-classes",
|r| {
Ok(GodClassRow {
path: r.get::<_, String>(0)?,
cognitive: r.get::<_, f64>(1)?,
fan_in: r.get::<_, u32>(2)?,
fan_out: r.get::<_, u32>(3)?,
god_score: r.get::<_, f64>(4)?,
})
},
)
}