Skip to content

Commit b28eef9

Browse files
committed
feat: Add missing_dep_diagnostic thanks to @Muscraft
1 parent 3eb6bdf commit b28eef9

File tree

4 files changed

+122
-38
lines changed

4 files changed

+122
-38
lines changed

src/cargo/util/lints.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,11 @@ fn verify_feature_enabled(
199199
Ok(())
200200
}
201201

202-
fn get_span(document: &ImDocument<String>, path: &[&str], get_value: bool) -> Option<Range<usize>> {
202+
pub fn get_span(
203+
document: &ImDocument<String>,
204+
path: &[&str],
205+
get_value: bool,
206+
) -> Option<Range<usize>> {
203207
let mut table = document.as_item().as_table_like()?;
204208
let mut iter = path.into_iter().peekable();
205209
while let Some(key) = iter.next() {
@@ -240,7 +244,7 @@ fn get_span(document: &ImDocument<String>, path: &[&str], get_value: bool) -> Op
240244

241245
/// Gets the relative path to a manifest from the current working directory, or
242246
/// the absolute path of the manifest if a relative path cannot be constructed
243-
fn rel_cwd_manifest_path(path: &Path, gctx: &GlobalContext) -> String {
247+
pub fn rel_cwd_manifest_path(path: &Path, gctx: &GlobalContext) -> String {
244248
diff_paths(path, gctx.cwd())
245249
.unwrap_or_else(|| path.to_path_buf())
246250
.display()

src/cargo/util/toml/mod.rs

Lines changed: 76 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ use cargo_util_schemas::manifest::{RustVersion, StringOrBool};
1515
use itertools::Itertools;
1616
use lazycell::LazyCell;
1717
use pathdiff::diff_paths;
18+
use toml_edit::ImDocument;
1819
use url::Url;
1920

2021
use crate::core::compiler::{CompileKind, CompileTarget};
@@ -29,6 +30,7 @@ use crate::core::{GitReference, PackageIdSpec, SourceId, WorkspaceConfig, Worksp
2930
use crate::sources::{CRATES_IO_INDEX, CRATES_IO_REGISTRY};
3031
use crate::util::errors::{CargoResult, ManifestError};
3132
use crate::util::interning::InternedString;
33+
use crate::util::lints::{get_span, rel_cwd_manifest_path};
3234
use crate::util::{self, context::ConfigRelativePath, GlobalContext, IntoUrl, OptVersionReq};
3335

3436
mod embedded;
@@ -1459,7 +1461,14 @@ fn to_real_manifest(
14591461
// need to check whether `dep_name` is stripped as unused dependency
14601462
if let Err(ref err) = summary {
14611463
if let Some(missing_dep) = err.downcast_ref::<MissingDependencyError>() {
1462-
check_weak_optional_dep_unused(&original_toml, missing_dep)?;
1464+
missing_dep_diagnostic(
1465+
missing_dep,
1466+
&original_toml,
1467+
&document,
1468+
&contents,
1469+
manifest_file,
1470+
gctx,
1471+
)?;
14631472
}
14641473
}
14651474
summary?
@@ -1570,37 +1579,83 @@ fn to_real_manifest(
15701579
Ok(manifest)
15711580
}
15721581

1573-
fn check_weak_optional_dep_unused(
1574-
original_toml: &TomlManifest,
1582+
fn missing_dep_diagnostic(
15751583
missing_dep: &MissingDependencyError,
1584+
orig_toml: &TomlManifest,
1585+
document: &ImDocument<String>,
1586+
contents: &str,
1587+
manifest_file: &Path,
1588+
gctx: &GlobalContext,
15761589
) -> CargoResult<()> {
1577-
if missing_dep.weak_optional {
1578-
// dev-dependencies are not allowed to be optional
1590+
let dep_name = missing_dep.dep_name;
1591+
let manifest_path = rel_cwd_manifest_path(manifest_file, gctx);
1592+
let feature_value_span =
1593+
get_span(&document, &["features", missing_dep.feature.as_str()], true).unwrap();
1594+
1595+
let title = format!(
1596+
"feature `{}` includes `{}`, but `{}` is not a dependency",
1597+
missing_dep.feature, missing_dep.feature_value, &dep_name
1598+
);
1599+
let help = format!("enable the dependency with `dep:{dep_name}`");
1600+
let info_label = format!(
1601+
"`{}` is an unused optional dependency since no feature enables it",
1602+
&dep_name
1603+
);
1604+
let message = Level::Error.title(&title);
1605+
let snippet = Snippet::source(&contents)
1606+
.origin(&manifest_path)
1607+
.fold(true)
1608+
.annotation(Level::Error.span(feature_value_span.start..feature_value_span.end));
1609+
let message = if missing_dep.weak_optional {
15791610
let mut orig_deps = vec![
1580-
original_toml.dependencies.as_ref(),
1581-
original_toml.build_dependencies.as_ref(),
1611+
(
1612+
orig_toml.dependencies.as_ref(),
1613+
vec![DepKind::Normal.kind_table()],
1614+
),
1615+
(
1616+
orig_toml.build_dependencies.as_ref(),
1617+
vec![DepKind::Build.kind_table()],
1618+
),
15821619
];
1583-
for (_, platform) in original_toml.target.iter().flatten() {
1584-
orig_deps.extend(vec![
1620+
for (name, platform) in orig_toml.target.iter().flatten() {
1621+
orig_deps.push((
15851622
platform.dependencies.as_ref(),
1623+
vec!["target", name, DepKind::Normal.kind_table()],
1624+
));
1625+
orig_deps.push((
15861626
platform.build_dependencies.as_ref(),
1587-
]);
1627+
vec!["target", name, DepKind::Normal.kind_table()],
1628+
));
15881629
}
1589-
for deps in orig_deps {
1630+
1631+
if let Some((_, toml_path)) = orig_deps.iter().find(|(deps, _)| {
15901632
if let Some(deps) = deps {
1591-
if deps.keys().any(|p| *p.as_str() == *missing_dep.dep_name) {
1592-
bail!(
1593-
"feature `{feature}` includes `{feature_value}`, but missing `dep:{dep_name}` to activate it",
1594-
feature = missing_dep.feature,
1595-
feature_value = missing_dep.feature_value,
1596-
dep_name = missing_dep.dep_name,
1597-
)
1598-
}
1633+
deps.keys().any(|p| *p.as_str() == *dep_name)
1634+
} else {
1635+
false
15991636
}
1637+
}) {
1638+
let toml_path = toml_path
1639+
.iter()
1640+
.map(|s| *s)
1641+
.chain(std::iter::once(dep_name.as_str()))
1642+
.collect::<Vec<_>>();
1643+
let dep_span = get_span(&document, &toml_path, false).unwrap();
1644+
1645+
message
1646+
.snippet(snippet.annotation(Level::Warning.span(dep_span).label(&info_label)))
1647+
.footer(Level::Help.title(&help))
1648+
} else {
1649+
message.snippet(snippet)
16001650
}
1601-
}
1651+
} else {
1652+
message.snippet(snippet)
1653+
};
16021654

1603-
Ok(())
1655+
if let Err(err) = gctx.shell().print_message(message) {
1656+
return Err(err.into());
1657+
}
1658+
Err(AlreadyPrintedError::new(anyhow!("").into()).into())
16041659
}
16051660

16061661
fn to_virtual_manifest(

tests/testsuite/features.rs

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -256,11 +256,14 @@ fn invalid6() {
256256
p.cargo("check --features foo")
257257
.with_status(101)
258258
.with_stderr_data(str![[r#"
259+
[ERROR] feature `foo` includes `bar/baz`, but `bar` is not a dependency
260+
--> Cargo.toml:9:23
261+
|
262+
9 | foo = ["bar/baz"]
263+
| ^^^^^^^^^^^
264+
|
259265
[ERROR] failed to parse manifest at `[ROOT]/foo/Cargo.toml`
260266
261-
Caused by:
262-
feature `foo` includes `bar/baz`, but `bar` is not a dependency
263-
264267
"#]])
265268
.run();
266269
}
@@ -288,11 +291,14 @@ fn invalid7() {
288291
p.cargo("check --features foo")
289292
.with_status(101)
290293
.with_stderr_data(str![[r#"
294+
[ERROR] feature `foo` includes `bar/baz`, but `bar` is not a dependency
295+
--> Cargo.toml:9:23
296+
|
297+
9 | foo = ["bar/baz"]
298+
| ^^^^^^^^^^^
299+
|
291300
[ERROR] failed to parse manifest at `[ROOT]/foo/Cargo.toml`
292301
293-
Caused by:
294-
feature `foo` includes `bar/baz`, but `bar` is not a dependency
295-
296302
"#]])
297303
.run();
298304
}

tests/testsuite/lints/unused_optional_dependencies.rs

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -254,11 +254,14 @@ fn inactive_weak_optional_dep() {
254254
.masquerade_as_nightly_cargo(&["cargo-lints", "edition2024"])
255255
.with_status(101)
256256
.with_stderr_data(str![[r#"
257+
[ERROR] feature `foo_feature` includes `dep_name?/dep_feature`, but `dep_name` is not a dependency
258+
--> Cargo.toml:11:27
259+
|
260+
11 | foo_feature = ["dep_name?/dep_feature"]
261+
| ^^^^^^^^^^^^^^^^^^^^^^^^^
262+
|
257263
[ERROR] failed to parse manifest at `[ROOT]/foo/Cargo.toml`
258264
259-
Caused by:
260-
feature `foo_feature` includes `dep_name?/dep_feature`, but `dep_name` is not a dependency
261-
262265
"#]])
263266
.run();
264267

@@ -287,11 +290,19 @@ Caused by:
287290
.masquerade_as_nightly_cargo(&["cargo-lints", "edition2024"])
288291
.with_status(101)
289292
.with_stderr_data(str![[r#"
293+
[ERROR] feature `foo_feature` includes `dep_name?/dep_feature`, but `dep_name` is not a dependency
294+
--> Cargo.toml:12:31
295+
|
296+
9 | dep_name = { version = "0.1.0", optional = true }
297+
| -------- `dep_name` is an unused optional dependency since no feature enables it
298+
10 |
299+
11 | [features]
300+
12 | foo_feature = ["dep_name?/dep_feature"]
301+
| ^^^^^^^^^^^^^^^^^^^^^^^^^
302+
|
303+
= [HELP] enable the dependency with `dep:dep_name`
290304
[ERROR] failed to parse manifest at `[ROOT]/foo/Cargo.toml`
291305
292-
Caused by:
293-
feature `foo_feature` includes `dep_name?/dep_feature`, but missing `dep:dep_name` to activate it
294-
295306
"#]])
296307
.run();
297308
// Check target.'cfg(unix)'.dependencies can work
@@ -319,11 +330,19 @@ Caused by:
319330
.masquerade_as_nightly_cargo(&["cargo-lints", "edition2024"])
320331
.with_status(101)
321332
.with_stderr_data(str![[r#"
333+
[ERROR] feature `foo_feature` includes `dep_name?/dep_feature`, but `dep_name` is not a dependency
334+
--> Cargo.toml:12:27
335+
|
336+
9 | dep_name = { version = "0.1.0", optional = true }
337+
| -------- `dep_name` is an unused optional dependency since no feature enables it
338+
10 |
339+
11 | [features]
340+
12 | foo_feature = ["dep_name?/dep_feature"]
341+
| ^^^^^^^^^^^^^^^^^^^^^^^^^
342+
|
343+
= [HELP] enable the dependency with `dep:dep_name`
322344
[ERROR] failed to parse manifest at `[ROOT]/foo/Cargo.toml`
323345
324-
Caused by:
325-
feature `foo_feature` includes `dep_name?/dep_feature`, but missing `dep:dep_name` to activate it
326-
327346
"#]])
328347
.run();
329348
}

0 commit comments

Comments
 (0)