use skif_core::{Category, Diagnostic, Fixability, PathSeg, RuleMeta, diagnostic};
use skif_lint::Emitter;
use skif_workspace::{Package, PackageId};
use super::JsRule;
use super::support::manifest_key_location;
use crate::analysis::JsAnalysis;
use crate::subpaths::{export_entries, publishes_module, used_export_keys};
pub static META: RuleMeta = RuleMeta::new(
"unused-subpath-exports",
Category::Js,
None,
Fixability::None,
"A `package.json` `exports` subpath is not imported by any workspace package.",
);
pub struct UnusedSubpathExports;
impl JsRule for UnusedSubpathExports {
fn meta(&self) -> &'static RuleMeta {
&META
}
fn check(&self, analysis: &JsAnalysis<'_>, out: &mut Emitter<'_>) {
if analysis.production() {
return;
}
let used = used_export_keys(analysis.workspace, &analysis.graph);
let unknown_importers: Vec<&Package> = analysis
.workspace
.iter()
.filter(|(id, _)| unknown_imports(analysis, *id))
.map(|(_, package)| package)
.collect();
for (id, package) in analysis.workspace.iter() {
let Some(exports) = package.manifest.value().get("exports") else {
continue;
};
let listed_by_unknown = package.name.as_deref().is_some_and(|name| {
unknown_importers
.iter()
.any(|importer| importer.dependencies.iter().any(|entry| &*entry.name == name))
});
if unknown_imports(analysis, id) || listed_by_unknown || analysis.imported_by_other_roots.contains(&id) {
continue;
}
let used_here = used.get(&id);
for (key, value) in export_entries(exports) {
let in_use = used_here.is_some_and(|keys| keys.contains(key));
if !value.is_null() && publishes_module(value) && !in_use {
out.emit(unused_export(package, key));
}
}
}
}
}
fn unknown_imports(analysis: &JsAnalysis<'_>, id: PackageId) -> bool {
analysis.is_skipped(id) || analysis.entries_incomplete(id)
}
fn unused_export(package: &Package, key: &str) -> Diagnostic {
let pointer = [PathSeg::Key("exports".to_owned()), PathSeg::Key(key.to_owned())];
diagnostic(
&META,
format!("`{key}` in `exports` is never imported by a workspace package"),
manifest_key_location(package, &pointer),
)
.package(package.display_name())
.package_id(package.id)
.help("remove the subpath if no code outside the workspace imports it, or add an `[[ignore]]` entry")
.build()
}