use duckdb::params;
use crate::facts::FactsDb;
use crate::{Options, Result};
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct StaleCodeRow {
pub path: String,
pub last_touched: String,
pub months_since_touched: u32,
pub max_cognitive: f64,
}
const DEFAULT_MIN_MONTHS: u32 = 12;
const DEFAULT_MAX_COGNITIVE: f64 = 5.0;
const SQL: &str = "
WITH live_paths AS (
SELECT path, last_touched FROM (
SELECT c.path,
MAX(commits.date) AS last_touched,
arg_max(
c.change_type,
ROW(commits.date, -commits.rowid)
) AS change_type
FROM changes c
INNER JOIN commits ON commits.rev = c.rev
GROUP BY c.path
) WHERE change_type != 'deleted'
),
file_complexity AS (
SELECT path, MAX(cognitive) AS max_cognitive
FROM {cm_src}
WHERE cognitive IS NOT NULL
GROUP BY path
)
, months_calc AS (
SELECT
lp.path,
lp.last_touched,
(
12 * (EXTRACT(year FROM CAST(? AS TIMESTAMP))
- EXTRACT(year FROM lp.last_touched))
+ (EXTRACT(month FROM CAST(? AS TIMESTAMP))
- EXTRACT(month FROM lp.last_touched))
- CASE WHEN EXTRACT(day FROM CAST(? AS TIMESTAMP))
< EXTRACT(day FROM lp.last_touched) THEN 1 ELSE 0 END
)::INTEGER AS months_since
FROM live_paths lp
)
SELECT
mc.path,
CAST(CAST(mc.last_touched AS DATE) AS TEXT) AS last_touched,
mc.months_since,
COALESCE(fc.max_cognitive, 0)::DOUBLE AS max_cognitive
FROM months_calc mc
LEFT JOIN file_complexity fc ON fc.path = mc.path
WHERE mc.months_since >= ?
AND COALESCE(fc.max_cognitive, 0) <= ?
ORDER BY mc.months_since DESC, mc.path ASC
LIMIT ?
";
#[tracing::instrument(name = "stale-code", skip_all, fields(min_revs = opts.min_revs))]
pub fn run_stale_code(db: &FactsDb, opts: &Options) -> Result<Vec<StaleCodeRow>> {
let row_limit: i64 = opts.rows_limit.map_or(i64::MAX, i64::from);
let anchor = anchor_str(db, opts)?;
let cm_src = crate::analyses::grouped_complexity::source_table(opts);
crate::analyses::lineage::materialize_if_needed(db, opts)?;
let sql = crate::analyses::lineage::rewrite(&SQL.replace("{cm_src}", cm_src), opts);
super::query::explain_if_requested(
db,
&sql,
params![
anchor,
anchor,
anchor,
DEFAULT_MIN_MONTHS,
DEFAULT_MAX_COGNITIVE,
row_limit
],
"stale-code",
opts,
)?;
super::query::query_map_collect(
db,
&sql,
params![
anchor,
anchor,
anchor,
DEFAULT_MIN_MONTHS,
DEFAULT_MAX_COGNITIVE,
row_limit
],
"stale-code",
|r| {
Ok(StaleCodeRow {
path: r.get::<_, String>(0)?,
last_touched: r.get::<_, String>(1)?,
months_since_touched: r.get::<_, i32>(2).map(|v| u32::try_from(v).unwrap_or(0))?,
max_cognitive: r.get::<_, f64>(3)?,
})
},
)
}
fn anchor_str(db: &FactsDb, opts: &Options) -> Result<String> {
if let Some(d) = opts.age_time_now {
return Ok(format!(
"{:04}-{:02}-{:02} 23:59:59",
d.year(),
u8::from(d.month()),
d.day()
));
}
db.query_row(
&format!(
"SELECT COALESCE(CAST({now_anchor} AS TEXT), '1970-01-01 00:00:00') FROM commits",
now_anchor = crate::analyses::query::clamped_now_anchor("date")
),
[],
|r| r.get::<_, String>(0),
)
}