Skip to content

Commit e274101

Browse files
committed
feat: Add unused_optional_dependency lint
1 parent 8d676dd commit e274101

File tree

9 files changed

+289
-2
lines changed

9 files changed

+289
-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: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,3 +236,89 @@ pub fn check_implicit_features(
236236
}
237237
Ok(())
238238
}
239+
240+
const UNUSED_OPTIONAL_DEPENDENCY: Lint = Lint {
241+
name: "unused_optional_dependency",
242+
desc: "unused optional dependency",
243+
groups: &[],
244+
default_level: LintLevel::Warn,
245+
edition_lint_opts: None,
246+
};
247+
248+
pub fn unused_dependencies(
249+
pkg: &Package,
250+
path: &Path,
251+
lints: &TomlToolLints,
252+
error_count: &mut usize,
253+
gctx: &GlobalContext,
254+
) -> CargoResult<()> {
255+
let edition = pkg.manifest().edition();
256+
// Unused optional dependencies can only exist on edition 2024+
257+
if edition <= Edition::Edition2021 {
258+
return Ok(());
259+
}
260+
261+
let lint_level = UNUSED_OPTIONAL_DEPENDENCY.level(lints, edition);
262+
if lint_level == LintLevel::Allow {
263+
return Ok(());
264+
}
265+
266+
let manifest = pkg.manifest();
267+
let activated_opt_deps = manifest
268+
.resolved_toml()
269+
.features()
270+
.map(|map| {
271+
map.values()
272+
.flatten()
273+
.filter_map(|f| match FeatureValue::new(InternedString::new(f)) {
274+
Dep { dep_name } => Some(dep_name.as_str()),
275+
_ => None,
276+
})
277+
.collect::<HashSet<_>>()
278+
})
279+
.unwrap_or_default();
280+
281+
let mut emitted_source = None;
282+
for dep in manifest.dependencies() {
283+
let dep_name_in_toml = dep.name_in_toml();
284+
if !dep.is_optional() || activated_opt_deps.contains(dep_name_in_toml.as_str()) {
285+
continue;
286+
}
287+
if lint_level == LintLevel::Forbid || lint_level == LintLevel::Deny {
288+
*error_count += 1;
289+
}
290+
let mut toml_path = vec![dep.kind().kind_table(), dep_name_in_toml.as_str()];
291+
let platform = dep.platform().map(|p| p.to_string());
292+
if let Some(platform) = platform.as_ref() {
293+
toml_path.insert(0, platform);
294+
toml_path.insert(0, "target");
295+
}
296+
let level = lint_level.to_diagnostic_level();
297+
let manifest_path = rel_cwd_manifest_path(path, gctx);
298+
let mut message = level.title(UNUSED_OPTIONAL_DEPENDENCY.desc).snippet(
299+
Snippet::source(manifest.contents())
300+
.origin(&manifest_path)
301+
.annotation(level.span(get_span(manifest.document(), &toml_path, false).unwrap()))
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: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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+
Package::new("target-dep", "0.1.0").publish();
11+
let p = project()
12+
.file(
13+
"Cargo.toml",
14+
r#"
15+
cargo-features = ["edition2024"]
16+
[package]
17+
name = "foo"
18+
version = "0.1.0"
19+
edition = "2024"
20+
21+
[dependencies]
22+
bar = { version = "0.1.0", optional = true }
23+
24+
[build-dependencies]
25+
baz = { version = "0.1.0", optional = true }
26+
27+
[target.'cfg(target_os = "linux")'.dependencies]
28+
target-dep = { version = "0.1.0", optional = true }
29+
"#,
30+
)
31+
.file("src/lib.rs", "")
32+
.build();
33+
34+
snapbox::cmd::Command::cargo_ui()
35+
.masquerade_as_nightly_cargo(&["edition2024"])
36+
.current_dir(p.root())
37+
.arg("check")
38+
.assert()
39+
.success()
40+
.stdout_matches(str![""])
41+
.stderr_matches(file!["stderr.term.svg"]);
42+
}
Lines changed: 81 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)