Skip to content

Commit cb4fc60

Browse files
committed
feat: Add unused_optional_dependency lint
1 parent 1ac4ed5 commit cb4fc60

File tree

9 files changed

+279
-2
lines changed

9 files changed

+279
-2
lines changed

src/cargo/core/workspace.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ use crate::sources::{PathSource, CRATES_IO_INDEX, CRATES_IO_REGISTRY};
2424
use crate::util::edit_distance;
2525
use crate::util::errors::{CargoResult, ManifestError};
2626
use crate::util::interning::InternedString;
27-
use crate::util::lints::check_implicit_features;
27+
use crate::util::lints::{check_implicit_features, unused_dependencies};
2828
use crate::util::toml::{read_manifest, InheritableFields};
2929
use crate::util::{context::ConfigRelativePath, Filesystem, GlobalContext, IntoUrl};
3030
use cargo_util::paths;
@@ -1158,6 +1158,7 @@ impl<'gctx> Workspace<'gctx> {
11581158
.collect();
11591159

11601160
check_implicit_features(pkg, &path, &normalized_lints, &mut error_count, self.gctx)?;
1161+
unused_dependencies(pkg, &path, &normalized_lints, &mut error_count, self.gctx)?;
11611162
if error_count > 0 {
11621163
Err(crate::util::errors::AlreadyPrintedError::new(anyhow!(
11631164
"encountered {error_count} errors(s) while running lints"

src/cargo/util/lints.rs

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use annotate_snippets::{Level, Renderer, Snippet};
66
use cargo_util_schemas::manifest::{TomlLintLevel, TomlToolLints};
77
use pathdiff::diff_paths;
88
use std::collections::HashSet;
9+
use std::fmt::Display;
910
use std::ops::Range;
1011
use std::path::Path;
1112
use toml_edit::ImDocument;
@@ -107,6 +108,17 @@ pub enum LintLevel {
107108
Forbid,
108109
}
109110

111+
impl Display for LintLevel {
112+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
113+
match self {
114+
LintLevel::Allow => write!(f, "allow"),
115+
LintLevel::Warn => write!(f, "warn"),
116+
LintLevel::Deny => write!(f, "deny"),
117+
LintLevel::Forbid => write!(f, "forbid"),
118+
}
119+
}
120+
}
121+
110122
impl LintLevel {
111123
pub fn to_diagnostic_level(self) -> Level {
112124
match self {
@@ -215,3 +227,98 @@ pub fn check_implicit_features(
215227
}
216228
Ok(())
217229
}
230+
231+
const UNUSED_OPTIONAL_DEPENDENCY: Lint = Lint {
232+
name: "unused_optional_dependency",
233+
desc: "unused optional dependency",
234+
groups: &[],
235+
default_level: LintLevel::Warn,
236+
edition_lint_opts: None,
237+
};
238+
239+
pub fn unused_dependencies(
240+
pkg: &Package,
241+
path: &Path,
242+
lints: &TomlToolLints,
243+
error_count: &mut usize,
244+
gctx: &GlobalContext,
245+
) -> CargoResult<()> {
246+
let edition = pkg.manifest().edition();
247+
// This lint is only relevant for editions 2024 and later.
248+
if edition <= Edition::Edition2021 {
249+
return Ok(());
250+
}
251+
252+
let lint_level = UNUSED_OPTIONAL_DEPENDENCY.level(lints, edition);
253+
if lint_level == LintLevel::Allow {
254+
return Ok(());
255+
}
256+
257+
let manifest = pkg.manifest();
258+
let user_defined_features = manifest.resolved_toml().features();
259+
let features = user_defined_features.map_or(HashSet::new(), |f| {
260+
f.keys().map(|k| InternedString::new(&k)).collect()
261+
});
262+
// Add implicit features for optional dependencies if they weren't
263+
// explicitly listed anywhere.
264+
let explicitly_listed = user_defined_features.map_or(HashSet::new(), |f| {
265+
f.values()
266+
.flatten()
267+
.filter_map(|v| match FeatureValue::new(v.into()) {
268+
Dep { dep_name } => Some(dep_name),
269+
_ => None,
270+
})
271+
.collect()
272+
});
273+
274+
let mut emitted_source = None;
275+
for dep in manifest.dependencies() {
276+
let dep_name_in_toml = dep.name_in_toml();
277+
if !dep.is_optional()
278+
|| features.contains(&dep_name_in_toml)
279+
|| explicitly_listed.contains(&dep_name_in_toml)
280+
{
281+
continue;
282+
}
283+
if lint_level == LintLevel::Forbid || lint_level == LintLevel::Deny {
284+
*error_count += 1;
285+
}
286+
let level = lint_level.to_diagnostic_level();
287+
let manifest_path = rel_cwd_manifest_path(path, gctx);
288+
289+
let mut message = level.title(UNUSED_OPTIONAL_DEPENDENCY.desc).snippet(
290+
Snippet::source(manifest.contents())
291+
.origin(&manifest_path)
292+
.annotation(
293+
level.span(
294+
get_span(
295+
manifest.document(),
296+
&["dependencies", &dep_name_in_toml],
297+
false,
298+
)
299+
.unwrap(),
300+
),
301+
)
302+
.fold(true),
303+
);
304+
if emitted_source.is_none() {
305+
emitted_source = Some(format!(
306+
"`cargo::{}` is set to `{lint_level}`",
307+
UNUSED_OPTIONAL_DEPENDENCY.name
308+
));
309+
message = message.footer(Level::Note.title(emitted_source.as_ref().unwrap()));
310+
}
311+
let help = format!(
312+
"remove the dependency or activate it in a feature with `dep:{dep_name_in_toml}`"
313+
);
314+
message = message.footer(Level::Help.title(&help));
315+
let renderer = Renderer::styled().term_width(
316+
gctx.shell()
317+
.err_width()
318+
.diagnostic_terminal_width()
319+
.unwrap_or(annotate_snippets::renderer::DEFAULT_TERM_WIDTH),
320+
);
321+
writeln!(gctx.shell().err(), "{}", renderer.render(message))?;
322+
}
323+
Ok(())
324+
}

tests/testsuite/lints/implicit_features/edition_2024/mod.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,19 @@ baz = { version = "0.1.0", optional = true }
2323
2424
[features]
2525
baz = ["dep:baz"]
26+
27+
[lints.cargo]
28+
unused-optional-dependency = "allow"
2629
"#,
2730
)
2831
.file("src/lib.rs", "")
2932
.build();
3033

3134
snapbox::cmd::Command::cargo_ui()
32-
.masquerade_as_nightly_cargo(&["edition2024"])
35+
.masquerade_as_nightly_cargo(&["cargo-lints", "edition2024"])
3336
.current_dir(p.root())
3437
.arg("check")
38+
.arg("-Zcargo-lints")
3539
.assert()
3640
.success()
3741
.stdout_matches(str![""])

tests/testsuite/lints/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
mod implicit_features;
2+
mod unused_optional_dependencies;
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
use cargo_test_support::prelude::*;
2+
use cargo_test_support::project;
3+
use cargo_test_support::registry::Package;
4+
use cargo_test_support::{file, str};
5+
6+
#[cargo_test]
7+
fn case() {
8+
Package::new("bar", "0.1.0").publish();
9+
let p = project()
10+
.file(
11+
"Cargo.toml",
12+
r#"
13+
[package]
14+
name = "foo"
15+
version = "0.1.0"
16+
edition = "2021"
17+
18+
[dependencies]
19+
bar = { version = "0.1.0", optional = true }
20+
21+
[lints.cargo]
22+
implicit-features = "allow"
23+
"#,
24+
)
25+
.file("src/lib.rs", "")
26+
.build();
27+
28+
snapbox::cmd::Command::cargo_ui()
29+
.masquerade_as_nightly_cargo(&["cargo-lints"])
30+
.current_dir(p.root())
31+
.arg("check")
32+
.arg("-Zcargo-lints")
33+
.assert()
34+
.success()
35+
.stdout_matches(str![""])
36+
.stderr_matches(file!["stderr.term.svg"]);
37+
}
Lines changed: 33 additions & 0 deletions
Loading
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
use cargo_test_support::prelude::*;
2+
use cargo_test_support::registry::Package;
3+
use cargo_test_support::str;
4+
use cargo_test_support::{file, project};
5+
6+
#[cargo_test(nightly, reason = "edition2024 is not stable")]
7+
fn case() {
8+
Package::new("bar", "0.1.0").publish();
9+
Package::new("baz", "0.1.0").publish();
10+
let p = project()
11+
.file(
12+
"Cargo.toml",
13+
r#"
14+
cargo-features = ["edition2024"]
15+
[package]
16+
name = "foo"
17+
version = "0.1.0"
18+
edition = "2024"
19+
20+
[dependencies]
21+
bar = { version = "0.1.0", optional = true }
22+
baz = { version = "0.1.0", optional = true }
23+
24+
[features]
25+
baz = ["dep:baz"]
26+
"#,
27+
)
28+
.file("src/lib.rs", "")
29+
.build();
30+
31+
snapbox::cmd::Command::cargo_ui()
32+
.masquerade_as_nightly_cargo(&["edition2024"])
33+
.current_dir(p.root())
34+
.arg("check")
35+
.assert()
36+
.success()
37+
.stdout_matches(str![""])
38+
.stderr_matches(file!["stderr.term.svg"]);
39+
}
Lines changed: 53 additions & 0 deletions
Loading
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
mod edition_2021;
2+
mod edition_2024;

0 commit comments

Comments
 (0)