use crate::error::{PassengerError, Result};
use crate::gate::{scratch_copy, EditSet};
use custodian_kit::reading::facts::Facts;
use custodian_kit::reading::scope::{self, Unavailable};
use custodian_kit::reading::spec::Depth;
use klepto::{FeatureModel, Klepto, Receipt};
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet};
use std::path::Path;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SiteUnit {
pub file: String,
pub line: Option<u32>,
pub dep: String,
pub scope: String,
pub unit: String,
pub unit_name: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Caller {
pub caller: String,
pub package: String,
pub loses: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FeatureOff {
pub feature: String,
pub package: String,
pub deps: Vec<String>,
pub scope: Unavailable,
pub sites: Vec<SiteUnit>,
pub callers_elsewhere: Vec<Caller>,
}
impl FeatureOff {
pub fn depth(&self) -> &'static str {
self.scope.depth.map(Depth::as_str).unwrap_or("none")
}
pub fn files_wholly(&self) -> Vec<&str> {
self.scope.files.iter().filter(|p| p.wholly).map(|p| p.name.as_str()).collect()
}
pub fn whole_file(&self, file: &str) -> bool {
self.scope.files.iter().any(|p| p.wholly && p.name == file)
}
}
fn line_maps(root: &Path, edits: &EditSet) -> BTreeMap<String, Vec<Option<u32>>> {
let mut out = BTreeMap::new();
for e in &edits.files {
let Ok(rel) = crate::gate::relative(root, &e.path) else { continue };
let diff = similar::TextDiff::from_lines(&e.before, &e.after);
let mut map: Vec<Option<u32>> = vec![None; e.after.lines().count() + 1];
for op in diff.ops() {
if let similar::DiffOp::Equal { old_index, new_index, len } = *op {
for i in 0..len {
if let Some(slot) = map.get_mut(new_index + i) {
*slot = Some((old_index + i + 1) as u32);
}
}
}
}
out.insert(rel.to_string_lossy().replace('\\', "/"), map);
}
out
}
fn unit_of(u: &Unavailable, package: &str, file: &str, scope_id: &str, module: Option<&str>) -> (String, String) {
if u.units.iter().any(|x| x.depth == Depth::Crate) {
return ("crate".into(), package.to_string());
}
if u.units.iter().any(|x| x.depth == Depth::File && x.name == file) {
return ("file".into(), file.to_string());
}
if let Some(m) = module
&& let Some(x) = u.units.iter().find(|x| x.depth == Depth::Module && (m == x.name || m.starts_with(&format!("{}::", x.name)))) {
return ("module".into(), x.name.clone());
}
match u.units.iter().find(|x| x.name == scope_id) {
Some(x) => (x.depth.as_str().to_string(), x.name.clone()),
None => ("item".into(), scope_id.to_string()),
}
}
pub fn feature_off(root: &Path, desired: &BTreeMap<String, FeatureModel>, k: &Klepto, edits: &EditSet) -> Result<Vec<FeatureOff>> {
let wanted: Vec<(String, String)> = desired
.iter()
.flat_map(|(p, m)| m.features.iter().filter(|(f, i)| !i.implicit && f.as_str() != "default").map(move |(f, _)| (p.clone(), f.clone())))
.collect();
if wanted.is_empty() {
return Ok(Vec::new());
}
let scratch;
let planned_root;
let owned;
let k2: &Klepto = if edits.is_empty() {
planned_root = root.canonicalize()?;
k
} else {
scratch = scratch_copy(root)?;
edits.write_to(root, scratch.path())?;
planned_root = scratch.path().canonicalize()?;
owned = crate::commands::klepto_for(&planned_root)?;
&owned
};
let receipt = Receipt::from_klepto(k2);
let lines = line_maps(root, edits);
struct Pending {
package: String,
feature: String,
deps: Vec<String>,
direct: BTreeSet<String>,
transitive: Vec<String>,
}
let mut pending = Vec::new();
for (p, f) in wanted {
let a = k2.affected_by_feature(&p, &f);
let deps: Vec<String> = k2.features.get(&p).map(|m| m.deps_enabled_by(&f).into_iter().collect()).unwrap_or_default();
let direct: BTreeSet<String> = a.gated.iter().chain(a.uses_lost_dep.iter()).cloned().collect();
pending.push(Pending { package: p, feature: f, deps, direct, transitive: a.transitive });
}
let graph = k2.graph.clone();
let modules = k2.modules.clone();
let k_owned: Klepto = k2.clone();
let facts = Facts::from_klepto(planned_root.clone(), k_owned, custodian_kit::api::Resolving::Free).map_err(PassengerError::from)?;
let mut out = Vec::new();
for w in pending {
if w.direct.is_empty() {
continue;
}
let u = scope::unavailable(&facts, &w.package, &w.feature, &w.direct);
let gone: BTreeSet<&String> = u.items.iter().collect();
let mut sites = Vec::new();
for d in receipt.dep_uses.iter().filter(|d| d.package == w.package && w.deps.contains(&d.site.dep)) {
let file = facts.rel(&d.site.location.path);
let module = modules.for_file(&d.site.location.path).map(|m| m.path.clone()).or_else(|| facts.item(&d.site.scope).map(|i| i.module.clone()));
let (unit, unit_name) = unit_of(&u, &w.package, &file, &d.site.scope, module.as_deref());
let line = match (lines.get(&file), d.site.location.line) {
(Some(map), Some(l)) => map.get(l as usize - 1).copied().flatten().or(Some(l)),
(_, l) => l,
};
sites.push(SiteUnit { file, line, dep: d.site.dep.clone(), scope: d.site.scope.clone(), unit, unit_name });
}
sites.sort_by(|a, b| (&a.file, a.line, &a.dep).cmp(&(&b.file, b.line, &b.dep)));
sites.dedup();
let mut callers = Vec::new();
for y in &w.transitive {
let Some(pkg) = graph.node(y).and_then(|n| n.package.clone()) else { continue };
if pkg == w.package {
continue;
}
let loses: Vec<String> = graph.transitive_dependencies(y).into_iter().filter(|d| gone.contains(d)).collect();
if !loses.is_empty() {
callers.push(Caller { caller: y.clone(), package: pkg, loses });
}
}
callers.sort_by(|a, b| a.caller.cmp(&b.caller));
out.push(FeatureOff { feature: w.feature, package: w.package, deps: w.deps, scope: u, sites, callers_elsewhere: callers });
}
out.sort_by(|a, b| (&a.feature, &a.package).cmp(&(&b.feature, &b.package)));
Ok(out)
}
pub fn render(off: &[FeatureOff]) -> String {
let mut s = String::new();
for o in off {
s.push_str(&format!("with {} off in {} (depth: {}):\n", o.feature, o.package, o.depth()));
s.push_str(&format!(" unavailable: {}\n", o.scope.items.join(", ")));
let whole = o.files_wholly();
if !whole.is_empty() {
s.push_str(&format!(" files wholly gated: {}\n", whole.join(", ")));
}
for x in &o.scope.units {
s.push_str(&format!(" unit {} {} ({} item(s))\n", x.depth.as_str(), x.name, x.items));
}
for site in &o.sites {
let line = site.line.map(|l| l.to_string()).unwrap_or_else(|| "?".into());
s.push_str(&format!(" site {}:{line} {} in {} — {} {}\n", site.file, site.dep, site.scope, site.unit, site.unit_name));
}
for c in &o.callers_elsewhere {
s.push_str(&format!(" {} ({}) loses {}\n", c.caller, c.package, c.loses.join(", ")));
}
}
s
}