use std::collections::BTreeSet;
use crate::dcf::deps::dependency_entries;
use crate::linter::diagnostic::{Diagnostic, ViolationData};
use crate::linter::rules::{DcfRule, DcfRuleContext, Example};
use crate::project::DependencyField;
pub struct UnusedDependency;
const EXAMPLES: &[Example] = &[Example {
caption: "In a package whose only R source is `f <- function() rlang::abort(\"no\")`:",
source: "Package: mypkg\nVersion: 0.1.0\nImports: rlang, tibble\n",
}];
const EXAMPLE_PACKAGE: &[(&str, &str)] = &[
("NAMESPACE", "export(f)\n"),
("R/a.R", "f <- function() rlang::abort(\"no\")\n"),
];
impl DcfRule for UnusedDependency {
fn id(&self) -> &'static str {
"unused-dependency"
}
fn default_enabled(&self) -> bool {
false
}
fn description(&self) -> &'static str {
"Flag an `Imports:` entry that nothing in the package reaches.\n\nAn \
`Imports` entry promises that installing this package installs that \
one, so an entry no code reaches costs every user a download, a build, \
and a constraint to satisfy for nothing. `R CMD check` reports it \
too.\n\nA package counts as reached by `pkg::`, `pkg:::`, a `library`, \
`require`, `requireNamespace`, or `loadNamespace` call at any depth, a \
NAMESPACE `import()`/`importFrom()`/`importClassesFrom()`/\
`importMethodsFrom()`, or a roxygen `@import`/`@importFrom` tag. \
Exempt on top of that: anything also in `LinkingTo` (the Rcpp \
skeleton), `methods` when the package defines an S4 or reference \
class, and any package whose name appears as a plain string (a dynamic \
`do.call(\"::\", …)` or `system.file(package = …)`).\n\nOnly `Imports` \
is checked. `Depends` is an API decision the package's own code may \
never name; `Suggests` is for tests, vignettes, and examples, reached \
from outside the package's own code; `LinkingTo` and `Enhances` are \
invisible to R-source analysis.\n\nUsage is folded over every R file \
the run analyzed under the package root, so a package reached only \
from `tests/`, `inst/`, or `data-raw/` counts as used when those files \
are in the run (`arity lint .`) and is reported when they are not \
(`arity lint R/`). A vignette's R code is never analyzed either way, \
so a dependency used only there is reported—it belongs in \
`Suggests`.\n\nIt reports on \
*absence*, and a wrong finding would have a maintainer delete a \
dependency their package needs, so it stays silent unless the run \
analyzed the package's whole `R/` source set and read its \
NAMESPACE—which is also why it is off by default.\n\nThere is no \
autofix: removing an \
entry from a comma-separated list is not a local edit, and this is not \
a claim a tool should act on destructively."
}
fn examples(&self) -> &'static [Example] {
EXAMPLES
}
fn doc_package(&self) -> &'static [(&'static str, &'static str)] {
EXAMPLE_PACKAGE
}
fn check_file(&self, ctx: &DcfRuleContext<'_>, sink: &mut Vec<Diagnostic>) {
let Some(usage) = ctx.usage.filter(|usage| usage.complete) else {
return;
};
let Some(own) = ctx.facts.package.as_deref() else {
return;
};
let Some(field) = ctx.document.field(DependencyField::Imports.name()) else {
return;
};
let linking_to: BTreeSet<&str> = ctx
.facts
.in_field(DependencyField::LinkingTo)
.map(|d| d.name.as_str())
.collect();
for entry in dependency_entries(&field) {
let name = entry.name.as_str();
if name == "R" || name == own {
continue;
}
if usage.used.contains(name)
|| usage.mentioned.contains(name)
|| linking_to.contains(name)
{
continue;
}
sink.push(Diagnostic {
rule: "unused-dependency",
severity: Default::default(),
path: Default::default(),
range: entry.name_range,
message: ViolationData::new(
"unused-dependency",
format!(
"`{name}` is declared in `Imports:` but nothing in the package uses it"
),
)
.with_suggestion(format!(
"Remove `{name}` from `Imports:`, or move it to `Suggests:` if only \
tests, vignettes, or examples need it."
)),
fix: None,
});
}
}
}