use std::path::PathBuf;
use skif_core::{
DependencyKind, Diagnostic, Fix, FixOp, FixSafety, JsonAction, Location, PathSeg, RuleMeta, Span, TextEdit,
diagnostic,
};
use skif_js_syntax::{Import, ImportKind};
use skif_lint::Emitter;
use skif_workspace::{DependencyEntry, Package};
use crate::analysis::JsAnalysis;
use crate::fixture::in_fixture_directory;
use crate::graph::{Module, Target};
pub(super) fn module_location(module: &Module, span: Option<Span>) -> Location {
Location::new(module.path.clone(), span)
}
pub(super) fn is_fixture(module: &Module) -> bool {
in_fixture_directory(&module.path)
}
pub(super) fn unresolved_imports<'g>(
analysis: &'g JsAnalysis<'_>,
mocks: bool,
) -> impl Iterator<Item = (&'g Module, &'g Import)> {
analysis
.graph
.modules()
.iter()
.filter(move |module| !module.ignored && !is_fixture(module) && !analysis.is_skipped(module.owner))
.flat_map(move |module| {
module
.imports
.iter()
.filter(|resolved| resolved.target == Target::Unresolved)
.filter_map(move |resolved| module.info.imports.get(resolved.import))
.filter(move |import| (import.kind == ImportKind::Mock) == mocks)
.filter(move |import| !analysis.ignores_unresolved(module.owner, &import.specifier))
.map(move |import| (module, import))
})
}
pub(super) fn entry_location(package: &Package, entry: &DependencyEntry) -> Location {
Location::new(
package.manifest.path.clone(),
Some(
package
.manifest
.line_index()
.span(entry.key_span.start, entry.value_span.end),
),
)
}
pub(super) fn manifest_key_location(package: &Package, pointer: &[PathSeg]) -> Location {
let manifest = &package.manifest;
let span = manifest
.field_key_span(pointer)
.or_else(|| pointer.get(..1).and_then(|field| manifest.field_key_span(field)));
Location::new(manifest.path.clone(), span)
}
pub(super) fn remove_dependency_fix(package: &Package, entry: &DependencyEntry) -> Fix {
Fix::new(
format!("Remove `{}` from `{}`", entry.name, entry.kind.field()),
FixSafety::Unsafe,
vec![FixOp::json(
package.manifest.path.clone(),
vec![
PathSeg::Key(entry.kind.field().to_owned()),
PathSeg::Key(entry.name.to_string()),
],
JsonAction::Remove,
)],
)
}
pub(super) fn delete_range(module: &Module, start: u32, end: u32) -> FixOp {
FixOp::text(module.path.clone(), TextEdit::new(start, end, String::new()))
}
pub(super) fn report_unused_dependencies(
meta: &'static RuleMeta,
kinds: &[DependencyKind],
analysis: &JsAnalysis<'_>,
out: &mut Emitter<'_>,
) {
for (id, package) in analysis.workspace.iter() {
if analysis.is_skipped(id) || analysis.entries_incomplete(id) {
continue;
}
for entry in package.dependencies.iter().filter(|entry| kinds.contains(&entry.kind)) {
if analysis.dependencies.is_referenced(id, &entry.name)
|| analysis.unused_dependency_exemption(id, package, entry).is_some()
{
continue;
}
out.emit(unused_dependency(meta, package, entry));
}
}
}
fn unused_dependency(meta: &'static RuleMeta, package: &Package, entry: &DependencyEntry) -> Diagnostic {
diagnostic(
meta,
format!("`{}` is listed in `{}` but never used", entry.name, entry.kind.field()),
entry_location(package, entry),
)
.package(package.display_name())
.dependency(&*entry.name)
.help("remove it, or add it to `ignore-dependencies` when a tool uses it without importing it")
.fix(remove_dependency_fix(package, entry))
.build()
}
pub(super) fn file_location(path: PathBuf) -> Location {
Location::new(path, None)
}