use skif_core::HashSet;
use std::fmt::Write as _;
use serde::Serialize;
use skif_js_syntax::ImportKind;
use crate::analysis::JsAnalysis;
use crate::graph::ModuleId;
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
pub struct DependencyListing {
pub package: String,
pub manifest: String,
pub section: &'static str,
pub specifier: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
pub struct DependencyImport {
pub file: String,
pub line: u32,
pub specifier: String,
pub kind: ImportKind,
pub type_only: bool,
pub package: String,
pub reachable: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
pub struct DependencyTrace {
pub dependency: String,
pub listed_in: Vec<DependencyListing>,
pub imported_by: Vec<DependencyImport>,
pub used_by: Vec<String>,
}
impl DependencyTrace {
#[must_use]
pub fn render(&self) -> String {
let mut out = String::new();
let _ = writeln!(out, "{}", self.dependency);
if self.listed_in.is_empty() {
let _ = writeln!(out, " listed by no manifest of the workspace");
} else {
let _ = writeln!(out, " listed in:");
for listing in &self.listed_in {
let _ = writeln!(
out,
" {} {} \"{}\" ({})",
listing.manifest, listing.section, listing.specifier, listing.package
);
}
}
if self.imported_by.is_empty() {
let _ = writeln!(out, " imported by no file of the module graph");
} else {
let _ = writeln!(out, " imported by:");
for import in &self.imported_by {
let mut notes = Vec::new();
if import.specifier != self.dependency {
notes.push(format!("as `{}`", import.specifier));
}
if import.type_only {
notes.push("types only".to_owned());
}
if !import.reachable {
notes.push("not reached from an entry file".to_owned());
}
let notes = if notes.is_empty() {
String::new()
} else {
format!(", {}", notes.join(", "))
};
let _ = writeln!(out, " {}:{} ({}{notes})", import.file, import.line, import.package);
}
}
if self.used_by.is_empty() {
let _ = writeln!(out, " used by no package");
} else {
let _ = writeln!(out, " used by: {}", self.used_by.join(", "));
}
out
}
}
fn slashed(path: &std::path::Path) -> String {
path.to_string_lossy().replace('\\', "/")
}
impl JsAnalysis<'_> {
#[must_use]
pub fn trace_dependency(&self, name: &str) -> DependencyTrace {
let label = |id| {
self.workspace.package(id).map_or_else(String::new, |package| {
package.name.clone().unwrap_or_else(|| {
let dir = slashed(&package.dir);
if dir.is_empty() { ".".to_owned() } else { dir }
})
})
};
let listed_in = self
.workspace
.iter()
.flat_map(|(id, package)| {
let manifest = slashed(&package.dir.join("package.json"));
package
.dependencies
.iter()
.filter(|entry| entry.name == name)
.map(move |entry| DependencyListing {
package: label(id),
manifest: manifest.clone(),
section: entry.kind.field(),
specifier: entry.specifier.clone(),
})
})
.collect();
let reachable: HashSet<ModuleId> = self.reachable_from_entries();
let mut imported_by = Vec::new();
for module in self.graph.modules() {
for resolved in &module.imports {
if resolved.package.as_deref() != Some(name) {
continue;
}
let Some(import) = module.info.imports.get(resolved.import) else {
continue;
};
imported_by.push(DependencyImport {
file: slashed(&module.path),
line: import.span.start_line,
specifier: import.specifier.clone(),
kind: import.kind,
type_only: resolved.type_only,
package: label(module.owner),
reachable: reachable.contains(&module.id),
});
}
}
imported_by.sort_by(|a, b| a.file.cmp(&b.file).then(a.line.cmp(&b.line)));
let used_by = self
.workspace
.package_ids()
.filter(|id| self.dependencies.is_referenced(*id, name))
.map(label)
.collect();
DependencyTrace {
dependency: name.to_owned(),
listed_in,
imported_by,
used_by,
}
}
}