use std::collections::VecDeque;
use skif_core::{HashMap, HashSet};
use std::fmt::Write as _;
use std::path::Path;
use serde::Serialize;
use skif_js_syntax::{ExportKind, Import, ImportKind, ImportedKind, ImportedName, ModuleInfo, is_declaration_file};
use skif_workspace::PackageId;
use crate::analysis::JsAnalysis;
use crate::discover::relative;
use crate::globs::Globs;
use crate::graph::{Module, ModuleId, ResolvedImport, Target};
use crate::input::IncompleteEntries;
use crate::manifest_paths::{crossing_stars, manifest_targets, source_candidates, source_patterns};
use crate::resolve::join_normalized;
use crate::setup::PackageSetup;
use crate::trace::dir_of;
use crate::usage;
const MAX_DEPTH: usize = 8;
const MAX_LISTED: usize = 12;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
pub enum EntryReason {
EntryGlob,
ConfigFile,
Script,
Published,
BinStub,
ExternalEntryGlob,
EnclosingEntryGlob,
IncompleteScope,
EnclosingIncompleteScope,
Unknown,
}
impl EntryReason {
const fn describe(self) -> &'static str {
match self {
Self::EntryGlob => {
"matches an entry glob of its package (defaults, configuration, test conventions or a plugin)"
}
Self::ConfigFile => "a configuration file of a tool",
Self::Script => "run by a script of package.json",
Self::Published => {
"published by package.json (main, module, types, bin, exports, n8n nodes and credentials)"
}
Self::BinStub => "loaded by a bin file of package.json that is no module (an extensionless node stub)",
Self::ExternalEntryGlob => "matches an entry glob of another package that points outside it",
Self::EnclosingEntryGlob => "matches an entry glob a plugin gives a package that contains its package",
Self::IncompleteScope => {
"may be an entry of a configuration that is not fully known, so it is treated as one (narrowed scope)"
}
Self::EnclosingIncompleteScope => {
"may be an entry of a configuration of a package that contains its package and is not fully known, so \
it is treated as one (narrowed scope)"
}
Self::Unknown => "unknown: no entry rule of the analysis accounts for it",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
pub enum Absence {
UnusedFile,
PackageExcluded,
DeclarationFile,
EntriesIncomplete,
OutsideProject,
Ignored,
NotDiscovered,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
pub struct Importer {
pub file: String,
pub line: u32,
pub kind: ImportKind,
pub names: Vec<String>,
pub reachable: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
pub struct ExportUser {
pub file: String,
pub line: u32,
pub how: String,
pub reachable: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
pub enum Verdict {
NotAnExport,
Public,
Used,
Unused,
NotJudged,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
pub struct ReExport {
pub file: String,
pub name: String,
pub export: ExportReport,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[allow(
clippy::struct_excessive_bools,
reason = "independent facts of one serialized report"
)]
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
pub struct ExportReport {
pub name: String,
pub file: String,
pub verdict: Verdict,
pub entry_public: bool,
pub tagged_public: bool,
pub users: Vec<ExportUser>,
pub re_exports: Vec<ReExport>,
pub in_used_signature: bool,
pub is_re_export: bool,
#[serde(skip_serializing_if = "is_false")]
pub registered: bool,
#[serde(skip_serializing_if = "is_false")]
pub entries_incomplete: bool,
#[serde(skip_serializing_if = "is_false")]
pub already_explained: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
pub struct UsageReport {
pub file: String,
pub in_graph: bool,
pub absence: Option<Absence>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub incomplete: Vec<IncompleteEntries>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub incomplete_scope: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub scope_package: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub widened_by: Option<String>,
pub entry: Option<EntryReason>,
#[serde(skip_serializing_if = "is_false")]
pub build_source: bool,
pub imported_by: Vec<Importer>,
pub chain_from_entry: Option<Vec<String>>,
#[serde(skip_serializing_if = "is_false")]
pub chain_from_build_source: bool,
pub export: Option<ExportReport>,
}
impl UsageReport {
fn render_absence(&self, absence: Absence, out: &mut String) {
match absence {
Absence::UnusedFile => {
let _ = writeln!(out, " no entry file reaches it: reported by `unused-files`");
if !self.incomplete_scope.is_empty() {
let _ = writeln!(
out,
" the entry files of its package are not fully known, but the unknown ones can only match {} \
(narrowed scope); this file does not, so it is judged",
self.incomplete_scope.join(", ")
);
self.render_incomplete(out);
}
}
Absence::PackageExcluded => {
let _ = writeln!(out, " not analysed: its package is excluded from the run");
}
Absence::DeclarationFile => {
let _ = writeln!(
out,
" not in the module graph: a declaration file is never reported as unused"
);
}
Absence::EntriesIncomplete => {
let _ = writeln!(
out,
" not in the module graph, and not reported as unused: the entry files of its package are \
not fully known and may be anywhere in it (package-wide)"
);
if let Some(at) = &self.widened_by {
let _ = writeln!(
out,
" {at} imports a computed path, which may load any file of the package, so the narrowed \
scope of its configs is not used"
);
}
self.render_incomplete(out);
}
Absence::OutsideProject => {
let _ = writeln!(out, " not analysed: outside the project globs of its package");
}
Absence::Ignored => {
let _ = writeln!(out, " not analysed: matched by an `ignore` glob");
}
Absence::NotDiscovered => {
let _ = writeln!(
out,
" not analysed: missing, ignored by Git, or not a source file the analysis looks at"
);
}
}
}
fn render_incomplete(&self, out: &mut String) {
for incomplete in &self.incomplete {
let source = match (&incomplete.plugin, &incomplete.file) {
(Some(plugin), Some(file)) => format!("{plugin} reads {}: ", slashed(file)),
(None, Some(file)) => format!("{}: ", slashed(file)),
(Some(plugin), None) => format!("{plugin}: "),
(None, None) => String::new(),
};
let _ = writeln!(out, " {source}{}", incomplete.cause.describe());
}
}
#[must_use]
pub fn render(&self) -> String {
let mut out = String::new();
let _ = writeln!(out, "{}", self.file);
if let Some(absence) = self.absence {
self.render_absence(absence, &mut out);
return out;
}
if let Some(entry) = self.entry {
let _ = writeln!(out, " entry file: {}", entry.describe());
if entry == EntryReason::IncompleteScope {
let _ = writeln!(
out,
" the narrowed scope of its package: {}",
self.incomplete_scope.join(", ")
);
self.render_incomplete(&mut out);
}
if entry == EntryReason::EnclosingIncompleteScope {
let _ = writeln!(
out,
" the narrowed scope of the enclosing package `{}`: {}",
self.scope_package.as_deref().unwrap_or("."),
self.incomplete_scope.join(", ")
);
self.render_incomplete(&mut out);
}
}
if self.build_source {
let _ = writeln!(
out,
" build source: a plugin names it as a file a tool builds from (such as a Bazel rule's srcs or data), \
so it is used, but it is no entry and its exports are judged"
);
}
if self.imported_by.is_empty() {
let _ = writeln!(out, " imported by no module");
} else {
let _ = writeln!(out, " imported by {} place(s):", self.imported_by.len());
for importer in self.imported_by.iter().take(MAX_LISTED) {
if importer.line == 0 {
let _ = writeln!(out, " {} ({COMPONENT_USE})", importer.file);
continue;
}
let _ = writeln!(
out,
" {}:{} ({}: {})",
importer.file,
importer.line,
kind_label(importer.kind),
names_label(&importer.names)
);
}
if self.imported_by.len() > MAX_LISTED {
let _ = writeln!(out, " ... and {} more", self.imported_by.len() - MAX_LISTED);
}
}
match &self.chain_from_entry {
Some(chain) if self.chain_from_build_source => {
let _ = writeln!(out, " reached from a build source through: {}", chain.join(" -> "));
}
Some(chain) => {
let _ = writeln!(out, " reached from an entry through: {}", chain.join(" -> "));
}
None => {
let _ = writeln!(out, " no chain of imports from an entry file reaches it");
}
}
if let Some(export) = &self.export {
render_export(export, 0, &mut out);
}
out
}
}
const COMPONENT_USE: &str = "a template uses it as a registered component";
fn import_line(resolved: &ResolvedImport, import: &Import) -> u32 {
if resolved.component { 0 } else { import.span.start_line }
}
const fn kind_label(kind: ImportKind) -> &'static str {
match kind {
ImportKind::Static => "static",
ImportKind::TypeOnly => "type-only",
ImportKind::SideEffect => "side-effect",
ImportKind::Dynamic => "dynamic",
ImportKind::Require => "require",
ImportKind::ImportMetaGlob => "import-meta-glob",
ImportKind::SourceGlob => "source-glob",
ImportKind::ExportFrom => "export-from",
ImportKind::Mock => "mock",
ImportKind::Url => "url",
}
}
fn names_label(names: &[String]) -> String {
if names.is_empty() {
"the whole module".to_owned()
} else {
names.join(", ")
}
}
fn render_export(export: &ExportReport, depth: usize, out: &mut String) {
let pad = " ".repeat(depth + 1);
if export.verdict == Verdict::NotAnExport {
let _ = writeln!(out, "{pad}`{}` is not an export of {}", export.name, export.file);
return;
}
if export.already_explained {
let _ = writeln!(
out,
"{pad}`{}` of {} is already explained above",
export.name, export.file
);
return;
}
let _ = writeln!(out, "{pad}export `{}` of {}:", export.name, export.file);
if export.entry_public {
let _ = writeln!(out, "{pad} the file is an entry, so its exports are public");
}
if export.tagged_public {
let _ = writeln!(out, "{pad} tagged `@public` (or another ignored tag)");
}
for user in &export.users {
let unreached = if user.reachable {
""
} else {
"; no entry file reaches the importer"
};
if user.line == 0 {
let _ = writeln!(out, "{pad} imported by {} ({COMPONENT_USE}{unreached})", user.file);
continue;
}
let _ = writeln!(
out,
"{pad} imported by {}:{} ({}{unreached})",
user.file, user.line, user.how
);
}
for re_export in &export.re_exports {
let _ = writeln!(out, "{pad} re-exported by {} as `{}`:", re_export.file, re_export.name);
render_export(&re_export.export, depth + 1, out);
}
if export.users.is_empty() && export.re_exports.is_empty() {
let _ = writeln!(out, "{pad} no module imports it by name or as a whole");
}
if export.in_used_signature {
let _ = writeln!(
out,
"{pad} mentioned in the signature of a used export of the same file"
);
}
if export.is_re_export {
let _ = writeln!(out, "{pad} itself a re-export of another module");
}
if export.registered {
let _ = writeln!(out, "{pad} registered as a custom element");
}
if export.entries_incomplete {
let _ = writeln!(
out,
"{pad} the entry files of its package are not fully known and this file may be one, so its exports are not judged"
);
}
let verdict = match export.verdict {
Verdict::Public => "public",
Verdict::Used => "used by another module",
Verdict::NotJudged => "not judged: no known module uses it, but its package's entries are incomplete",
_ => "not used by another module",
};
let _ = writeln!(out, "{pad}=> {verdict}");
}
fn describe_name(name: &ImportedName) -> String {
match &name.kind {
ImportedKind::Default => "default".to_owned(),
ImportedKind::Named(export) => export.clone(),
ImportedKind::Namespace if name.escapes => "the namespace, used as a whole".to_owned(),
ImportedKind::Namespace if name.accessed_members.is_empty() => "the namespace, no member read".to_owned(),
ImportedKind::Namespace => format!("the namespace, members {}", name.accessed_members.join(", ")),
}
}
fn describe_imported(kind: ImportKind, name: &ImportedName) -> String {
let star = kind == ImportKind::ExportFrom
&& name.kind == ImportedKind::Namespace
&& !name.escapes
&& name.accessed_members.is_empty();
if star {
"every export, re-exported".to_owned()
} else {
describe_name(name)
}
}
fn reached(target: &Target) -> &[ModuleId] {
match target {
Target::Module(id) => std::slice::from_ref(id),
Target::Modules(ids) => ids.as_slice(),
_ => &[],
}
}
fn uses_export(importer: &ModuleInfo, import: &Import, export: &str, is_type: bool) -> Option<String> {
if matches!(
import.kind,
ImportKind::ExportFrom | ImportKind::SideEffect | ImportKind::Mock
) {
return None;
}
if import.names.is_empty() {
let reads = !is_type || !usage::loads_module_value(import.kind);
return reads.then(|| "the whole module".to_owned());
}
import.names.iter().find_map(|name| {
let hit = match &name.kind {
ImportedKind::Default => export == "default",
ImportedKind::Named(named) => named == export,
ImportedKind::Namespace => {
let whole = name.escapes && (!is_type || usage::exports_binding(importer, name));
whole || name.accessed_members.iter().any(|member| member == export)
}
};
hit.then(|| describe_name(name))
})
}
struct Walk {
reachable: HashSet<ModuleId>,
seen: HashSet<(ModuleId, String)>,
}
#[allow(clippy::trivially_copy_pass_by_ref)]
fn is_false(value: &bool) -> bool {
!*value
}
fn slashed(path: &Path) -> String {
path.to_string_lossy().replace('\\', "/")
}
impl JsAnalysis<'_> {
#[must_use]
pub fn explain_usage(&self, file: &Path, export: Option<&str>) -> UsageReport {
let Some(module) = self
.graph
.modules()
.iter()
.find(|module| module.path == file || module.abs == file)
else {
let unused = self.unused_files.iter().find(|unused| unused.path == file);
let (absence, incomplete, incomplete_scope) = if let Some(unused) = unused {
let (incomplete, scope) = self.narrowed_scope(unused.owner);
(Absence::UnusedFile, incomplete, scope)
} else {
let (absence, incomplete) = self.absence_of(file);
(absence, incomplete, Vec::new())
};
let widened_by = (absence == Absence::EntriesIncomplete)
.then(|| self.widened_by(file))
.flatten();
return UsageReport {
file: slashed(file),
in_graph: false,
absence: Some(absence),
incomplete,
incomplete_scope,
scope_package: None,
widened_by,
entry: None,
build_source: false,
imported_by: Vec::new(),
chain_from_entry: None,
chain_from_build_source: false,
export: None,
};
};
let mut walk = Walk {
reachable: self.reachable_from_entries(),
seen: HashSet::default(),
};
let chain = self.chain_to_entry(module.id);
let chain_from_build_source = chain
.as_ref()
.and_then(|chain| chain.first())
.is_some_and(|first| self.graph.module(*first).is_build_source);
let chain = chain.map(|chain| chain.iter().map(|id| slashed(&self.graph.module(*id).path)).collect());
let entry = module.is_entry.then(|| self.entry_reason(module));
let scope_owner = match entry {
Some(EntryReason::IncompleteScope) => Some(module.owner),
Some(EntryReason::EnclosingIncompleteScope) => self.enclosing_incomplete_scope(module),
_ => None,
};
let (incomplete, incomplete_scope) =
scope_owner.map_or_else(|| (Vec::new(), Vec::new()), |owner| self.narrowed_scope(owner));
let scope_package = match entry {
Some(EntryReason::EnclosingIncompleteScope) => {
scope_owner.and_then(|owner| self.workspace.package(owner)).map(dir_of)
}
_ => None,
};
UsageReport {
file: slashed(&module.path),
in_graph: true,
absence: None,
incomplete,
incomplete_scope,
scope_package,
widened_by: None,
entry,
build_source: module.is_build_source,
imported_by: self.importers(module, &walk),
chain_from_entry: chain,
chain_from_build_source,
export: export.map(|name| self.explain_export(module, name, 0, &mut walk, false)),
}
}
pub(crate) fn reachable_from_entries(&self) -> HashSet<ModuleId> {
let modules = self.graph.modules();
let mut queue: VecDeque<ModuleId> = modules
.iter()
.filter(|module| module.is_entry || module.is_build_source)
.map(|module| module.id)
.collect();
let mut seen: HashSet<ModuleId> = queue.iter().copied().collect();
while let Some(current) = queue.pop_front() {
for resolved in &modules[current.index()].imports {
for next in reached(&resolved.target) {
if seen.insert(*next) {
queue.push_back(*next);
}
}
}
}
seen
}
fn entry_reason(&self, module: &Module) -> EntryReason {
if let Some(package) = self.setup.package(module.owner)
&& let Some(rel) = relative(&module.abs, &package.dir)
{
if package.entry.is_match(&rel) {
return EntryReason::EntryGlob;
}
if package.config_files.contains(&module.abs) {
return EntryReason::ConfigFile;
}
if self.is_script_file(&module.abs) {
return EntryReason::Script;
}
if self.is_published(module, &package.dir, &rel) {
return EntryReason::Published;
}
if let Some(&reason) = self.entry_origins.get(&module.abs) {
return reason;
}
if package.in_incomplete_scope(&rel) {
return EntryReason::IncompleteScope;
}
}
let enclosing = self.workspace.iter().any(|(id, _)| {
id != module.owner
&& self.setup.package(id).is_some_and(|other| {
relative(&module.abs, &other.dir).is_some_and(|rel| other.shared_entry.is_match(&rel))
})
});
if enclosing {
return EntryReason::EnclosingEntryGlob;
}
let path = slashed(&module.path);
let external = self.workspace.iter().any(|(id, _)| {
id != module.owner
&& self
.setup
.package(id)
.is_some_and(|other| other.external_entry.is_match(&path))
});
if external {
return EntryReason::ExternalEntryGlob;
}
if self.enclosing_incomplete_scope(module).is_some() {
return EntryReason::EnclosingIncompleteScope;
}
EntryReason::Unknown
}
fn enclosing_incomplete_scope(&self, module: &Module) -> Option<PackageId> {
let root = &self.workspace.root;
self.workspace
.iter()
.filter(|(id, _)| *id != module.owner)
.filter(|(id, _)| {
self.setup.package(*id).is_some_and(|other| {
relative(&module.abs, &other.dir).is_some_and(|rel| other.in_incomplete_scope(&rel))
})
})
.max_by_key(|(_, package)| root.join(&package.dir).components().count())
.map(|(id, _)| id)
}
fn is_script_file(&self, abs: &Path) -> bool {
self.workspace.iter().any(|(id, _)| {
self.setup.package(id).is_some_and(|package| {
package
.scripts
.files
.iter()
.any(|file| join_normalized(&package.dir, file) == abs)
})
})
}
fn absence_of(&self, file: &Path) -> (Absence, Vec<IncompleteEntries>) {
let root = &self.workspace.root;
let abs = if file.is_absolute() {
file.to_path_buf()
} else {
join_normalized(root, &slashed(file))
};
let owner = self
.workspace
.iter()
.map(|(id, package)| (id, root.join(&package.dir)))
.filter(|(_, dir)| abs.starts_with(dir))
.max_by_key(|(_, dir)| dir.components().count())
.map(|(id, _)| id);
let Some(owner) = owner else {
return (Absence::NotDiscovered, Vec::new());
};
let Some(package) = self.setup.package(owner) else {
return (Absence::PackageExcluded, Vec::new());
};
let Some(rel) = relative(&abs, root) else {
return (Absence::NotDiscovered, Vec::new());
};
if !self.outside_graph.contains(&rel) {
return (Absence::NotDiscovered, Vec::new());
}
if is_declaration_file(&rel) {
return (Absence::DeclarationFile, Vec::new());
}
let Some(in_package) = relative(&abs, &package.dir) else {
return (Absence::OutsideProject, Vec::new());
};
if !package.project.is_match(&in_package) {
return (Absence::OutsideProject, Vec::new());
}
if package.ignore.matches_any(&in_package) || self.setup.global_ignore.matches_any(&rel) {
return (Absence::Ignored, Vec::new());
}
if package.unjudged_package_wide() {
return (Absence::EntriesIncomplete, self.incomplete_reasons(package));
}
(Absence::NotDiscovered, Vec::new())
}
fn widened_by(&self, file: &Path) -> Option<String> {
let root = &self.workspace.root;
let abs = if file.is_absolute() {
file.to_path_buf()
} else {
join_normalized(root, &slashed(file))
};
self.workspace
.iter()
.filter(|(_, package)| abs.starts_with(root.join(&package.dir)))
.max_by_key(|(_, package)| root.join(&package.dir).components().count())
.and_then(|(id, _)| self.setup.package(id))
.and_then(|package| package.widened_by.as_ref())
.map(|(path, line)| format!("{}:{line}", slashed(path)))
}
fn incomplete_reasons(&self, package: &PackageSetup) -> Vec<IncompleteEntries> {
let root = &self.workspace.root;
package
.incomplete_reasons
.iter()
.cloned()
.map(|mut reason| {
reason.file = reason
.file
.map(|path| relative(&path, root).map_or(path, std::path::PathBuf::from));
reason
})
.collect()
}
fn narrowed_scope(&self, owner: PackageId) -> (Vec<IncompleteEntries>, Vec<String>) {
match self.setup.package(owner) {
Some(package) if package.entries_incomplete => package.incomplete_scope.as_ref().map_or_else(
|| (Vec::new(), Vec::new()),
|scope| (self.incomplete_reasons(package), scope.patterns.clone()),
),
_ => (Vec::new(), Vec::new()),
}
}
fn is_published(&self, module: &Module, package_dir: &Path, rel: &str) -> bool {
let Some(package) = self.workspace.package(module.owner) else {
return false;
};
manifest_targets(package.manifest.value()).into_iter().any(|target| {
if target.path.contains('*') {
let globs = Globs::new(
source_patterns(&target.path)
.into_iter()
.chain(std::iter::once(target.path.trim_start_matches("./").to_owned()))
.map(|pattern| {
if target.from_exports {
crossing_stars(&pattern)
} else {
pattern
}
}),
);
globs.is_match(rel)
} else {
source_candidates(&target.path)
.into_iter()
.any(|candidate| package_dir.join(candidate) == module.abs)
}
})
}
fn importers(&self, module: &Module, walk: &Walk) -> Vec<Importer> {
let mut found = Vec::new();
for importer_id in self.graph.imported_by(module.id) {
let importer = self.graph.module(*importer_id);
for resolved in &importer.imports {
if !reached(&resolved.target).contains(&module.id) {
continue;
}
if let Some(import) = importer.info.imports.get(resolved.import) {
found.push(Importer {
file: slashed(&importer.path),
line: import_line(resolved, import),
kind: import.kind,
names: import
.names
.iter()
.map(|name| describe_imported(import.kind, name))
.collect(),
reachable: walk.reachable.contains(importer_id),
});
}
}
}
found
}
fn chain_to_entry(&self, id: ModuleId) -> Option<Vec<ModuleId>> {
let modules = self.graph.modules();
let mut previous: HashMap<ModuleId, ModuleId> = HashMap::default();
let mut queue: VecDeque<ModuleId> = modules
.iter()
.filter(|module| module.is_entry || module.is_build_source)
.map(|module| module.id)
.collect();
let mut seen: HashSet<ModuleId> = queue.iter().copied().collect();
while let Some(current) = queue.pop_front() {
if current == id {
let mut chain = vec![id];
let mut at = id;
while let Some(before) = previous.get(&at) {
chain.push(*before);
at = *before;
}
chain.reverse();
return Some(chain);
}
for resolved in &modules[current.index()].imports {
for next in reached(&resolved.target) {
if seen.insert(*next) {
previous.insert(*next, current);
queue.push_back(*next);
}
}
}
}
None
}
fn explain_export(
&self,
module: &Module,
name: &str,
depth: usize,
walk: &mut Walk,
through_star: bool,
) -> ExportReport {
let mut report = ExportReport {
name: name.to_owned(),
file: slashed(&module.path),
verdict: Verdict::NotAnExport,
entry_public: false,
tagged_public: false,
users: Vec::new(),
re_exports: Vec::new(),
in_used_signature: false,
is_re_export: false,
registered: false,
entries_incomplete: false,
already_explained: false,
};
let export = module.info.exports.iter().find(|export| export.name == name);
if export.is_none() && !through_star {
return report;
}
if !walk.seen.insert((module.id, name.to_owned())) {
report.already_explained = true;
report.verdict = Verdict::Unused;
return report;
}
report.entry_public = module.is_entry && !self.setup.includes_entry_exports(module.owner);
report.tagged_public = export.is_some_and(|export| self.is_tagged_public(export));
report.is_re_export =
export.is_none_or(|export| matches!(export.kind, ExportKind::ReExport | ExportKind::StarReExport));
report.entries_incomplete = !self.judges_exports_of(module);
let is_type = export.is_some_and(|export| export.is_type);
for importer_id in self.graph.imported_by(module.id) {
let importer = self.graph.module(*importer_id);
for resolved in &importer.imports {
if !reached(&resolved.target).contains(&module.id) {
continue;
}
let Some(import) = importer.info.imports.get(resolved.import) else {
continue;
};
if let Some(how) = uses_export(&importer.info, import, name, is_type) {
report.users.push(ExportUser {
file: slashed(&importer.path),
line: import_line(resolved, import),
how,
reachable: walk.reachable.contains(importer_id),
});
}
}
if depth < MAX_DEPTH {
self.explain_reexports(importer, module, name, depth, walk, &mut report.re_exports);
}
}
report.registered = export.is_some_and(|export| self.setup.is_registered(module.owner, export));
let forwarded_use = export.is_none()
&& (!report.users.is_empty()
|| report
.re_exports
.iter()
.any(|re_export| matches!(re_export.export.verdict, Verdict::Used | Verdict::Public)));
let used = report.registered || forwarded_use || self.exports.is_used(module.id, name);
report.in_used_signature = !used && export.is_some_and(|export| self.in_used_signature(module, export));
report.verdict = if report.entry_public || report.tagged_public {
Verdict::Public
} else if used || report.in_used_signature {
Verdict::Used
} else if report.entries_incomplete {
Verdict::NotJudged
} else {
Verdict::Unused
};
report
}
fn explain_reexports(
&self,
importer: &Module,
module: &Module,
name: &str,
depth: usize,
walk: &mut Walk,
out: &mut Vec<ReExport>,
) {
for export in &importer.info.exports {
let Some(from) = export.from.as_deref() else {
continue;
};
if importer.reexport_source(from) != Some(module.id) {
continue;
}
let forwarded = match export.kind {
ExportKind::ReExport => export.imported_name.as_deref().unwrap_or(&export.name) == name,
ExportKind::StarReExport if export.name == "*" => {
name != "default"
&& !importer
.info
.exports
.iter()
.any(|own| own.name == name && own.from.is_none())
}
ExportKind::StarReExport => true,
_ => false,
};
if !forwarded {
continue;
}
let through_star = export.kind == ExportKind::StarReExport && export.name == "*";
let exposed = if through_star { name } else { export.name.as_str() };
out.push(ReExport {
file: slashed(&importer.path),
name: exposed.to_owned(),
export: self.explain_export(importer, exposed, depth + 1, walk, through_star),
});
}
}
}