Skip to content

Commit 838d81d

Browse files
committed
Auto merge of #14026 - linyihai:weak-optional-inactive, r=weihanglo
fix: improve message for inactive weak optional feature with edition2024 through unused dep collection ### What does this PR try to resolve? Collect the unused dependencies to check whether a weak optional dependency had set. Then we can improve the message when weak optional dependency inactive. Fixes #14015 ### How should we test and review this PR? One commit test added, one commit fixed and updated ### Additional information Part of #14039 - migrate `tests/testsuite/lints/unused_optional_dependencies.rs` to snapshot And rename `MissingField` to `MissingFieldError`
2 parents a0b2803 + b28eef9 commit 838d81d

File tree

6 files changed

+371
-82
lines changed

6 files changed

+371
-82
lines changed

src/cargo/core/summary.rs

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,35 @@ struct Inner {
3030
rust_version: Option<RustVersion>,
3131
}
3232

33+
/// Indicates the dependency inferred from the `dep` syntax that should exist,
34+
/// but missing on the resolved dependencies tables.
35+
#[derive(Debug)]
36+
pub struct MissingDependencyError {
37+
pub dep_name: InternedString,
38+
pub feature: InternedString,
39+
pub feature_value: FeatureValue,
40+
/// Indicates the dependency inferred from the `dep?` syntax that is weak optional
41+
pub weak_optional: bool,
42+
}
43+
44+
impl std::error::Error for MissingDependencyError {}
45+
46+
impl fmt::Display for MissingDependencyError {
47+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48+
let Self {
49+
dep_name,
50+
feature,
51+
feature_value: fv,
52+
..
53+
} = self;
54+
55+
write!(
56+
f,
57+
"feature `{feature}` includes `{fv}`, but `{dep_name}` is not a dependency",
58+
)
59+
}
60+
}
61+
3362
impl Summary {
3463
#[tracing::instrument(skip_all)]
3564
pub fn new(
@@ -274,7 +303,12 @@ fn build_feature_map(
274303

275304
// Validation of the feature name will be performed in the resolver.
276305
if !is_any_dep {
277-
bail!("feature `{feature}` includes `{fv}`, but `{dep_name}` is not a dependency");
306+
bail!(MissingDependencyError {
307+
feature: *feature,
308+
feature_value: (*fv).clone(),
309+
dep_name: *dep_name,
310+
weak_optional: *weak,
311+
})
278312
}
279313
if *weak && !is_optional_dep {
280314
bail!(

src/cargo/util/context/mod.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2031,7 +2031,7 @@ impl ConfigError {
20312031
}
20322032

20332033
fn is_missing_field(&self) -> bool {
2034-
self.error.downcast_ref::<MissingField>().is_some()
2034+
self.error.downcast_ref::<MissingFieldError>().is_some()
20352035
}
20362036

20372037
fn missing(key: &ConfigKey) -> ConfigError {
@@ -2067,15 +2067,15 @@ impl fmt::Display for ConfigError {
20672067
}
20682068

20692069
#[derive(Debug)]
2070-
struct MissingField(String);
2070+
struct MissingFieldError(String);
20712071

2072-
impl fmt::Display for MissingField {
2072+
impl fmt::Display for MissingFieldError {
20732073
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
20742074
write!(f, "missing field `{}`", self.0)
20752075
}
20762076
}
20772077

2078-
impl std::error::Error for MissingField {}
2078+
impl std::error::Error for MissingFieldError {}
20792079

20802080
impl serde::de::Error for ConfigError {
20812081
fn custom<T: fmt::Display>(msg: T) -> Self {
@@ -2087,7 +2087,7 @@ impl serde::de::Error for ConfigError {
20872087

20882088
fn missing_field(field: &'static str) -> Self {
20892089
ConfigError {
2090-
error: anyhow::Error::new(MissingField(field.to_string())),
2090+
error: anyhow::Error::new(MissingFieldError(field.to_string())),
20912091
definition: None,
20922092
}
20932093
}

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: 118 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use std::path::{Path, PathBuf};
55
use std::rc::Rc;
66
use std::str::{self, FromStr};
77

8+
use crate::core::summary::MissingDependencyError;
89
use crate::AlreadyPrintedError;
910
use anyhow::{anyhow, bail, Context as _};
1011
use cargo_platform::Platform;
@@ -14,6 +15,7 @@ use cargo_util_schemas::manifest::{RustVersion, StringOrBool};
1415
use itertools::Itertools;
1516
use lazycell::LazyCell;
1617
use pathdiff::diff_paths;
18+
use toml_edit::ImDocument;
1719
use url::Url;
1820

1921
use crate::core::compiler::{CompileKind, CompileTarget};
@@ -28,6 +30,7 @@ use crate::core::{GitReference, PackageIdSpec, SourceId, WorkspaceConfig, Worksp
2830
use crate::sources::{CRATES_IO_INDEX, CRATES_IO_REGISTRY};
2931
use crate::util::errors::{CargoResult, ManifestError};
3032
use crate::util::interning::InternedString;
33+
use crate::util::lints::{get_span, rel_cwd_manifest_path};
3134
use crate::util::{self, context::ConfigRelativePath, GlobalContext, IntoUrl, OptVersionReq};
3235

3336
mod embedded;
@@ -1435,24 +1438,42 @@ fn to_real_manifest(
14351438
.unwrap_or_else(|| semver::Version::new(0, 0, 0)),
14361439
source_id,
14371440
);
1438-
let summary = Summary::new(
1439-
pkgid,
1440-
deps,
1441-
&resolved_toml
1442-
.features
1443-
.as_ref()
1444-
.unwrap_or(&Default::default())
1445-
.iter()
1446-
.map(|(k, v)| {
1447-
(
1448-
InternedString::new(k),
1449-
v.iter().map(InternedString::from).collect(),
1450-
)
1451-
})
1452-
.collect(),
1453-
resolved_package.links.as_deref(),
1454-
rust_version.clone(),
1455-
)?;
1441+
let summary = {
1442+
let summary = Summary::new(
1443+
pkgid,
1444+
deps,
1445+
&resolved_toml
1446+
.features
1447+
.as_ref()
1448+
.unwrap_or(&Default::default())
1449+
.iter()
1450+
.map(|(k, v)| {
1451+
(
1452+
InternedString::new(k),
1453+
v.iter().map(InternedString::from).collect(),
1454+
)
1455+
})
1456+
.collect(),
1457+
resolved_package.links.as_deref(),
1458+
rust_version.clone(),
1459+
);
1460+
// editon2024 stops exposing implicit features, which will strip weak optional dependencies from `dependencies`,
1461+
// need to check whether `dep_name` is stripped as unused dependency
1462+
if let Err(ref err) = summary {
1463+
if let Some(missing_dep) = err.downcast_ref::<MissingDependencyError>() {
1464+
missing_dep_diagnostic(
1465+
missing_dep,
1466+
&original_toml,
1467+
&document,
1468+
&contents,
1469+
manifest_file,
1470+
gctx,
1471+
)?;
1472+
}
1473+
}
1474+
summary?
1475+
};
1476+
14561477
if summary.features().contains_key("default-features") {
14571478
warnings.push(
14581479
"`default-features = [\"..\"]` was found in [features]. \
@@ -1558,6 +1579,85 @@ fn to_real_manifest(
15581579
Ok(manifest)
15591580
}
15601581

1582+
fn missing_dep_diagnostic(
1583+
missing_dep: &MissingDependencyError,
1584+
orig_toml: &TomlManifest,
1585+
document: &ImDocument<String>,
1586+
contents: &str,
1587+
manifest_file: &Path,
1588+
gctx: &GlobalContext,
1589+
) -> CargoResult<()> {
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 {
1610+
let mut orig_deps = vec![
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+
),
1619+
];
1620+
for (name, platform) in orig_toml.target.iter().flatten() {
1621+
orig_deps.push((
1622+
platform.dependencies.as_ref(),
1623+
vec!["target", name, DepKind::Normal.kind_table()],
1624+
));
1625+
orig_deps.push((
1626+
platform.build_dependencies.as_ref(),
1627+
vec!["target", name, DepKind::Normal.kind_table()],
1628+
));
1629+
}
1630+
1631+
if let Some((_, toml_path)) = orig_deps.iter().find(|(deps, _)| {
1632+
if let Some(deps) = deps {
1633+
deps.keys().any(|p| *p.as_str() == *dep_name)
1634+
} else {
1635+
false
1636+
}
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)
1650+
}
1651+
} else {
1652+
message.snippet(snippet)
1653+
};
1654+
1655+
if let Err(err) = gctx.shell().print_message(message) {
1656+
return Err(err.into());
1657+
}
1658+
Err(AlreadyPrintedError::new(anyhow!("").into()).into())
1659+
}
1660+
15611661
fn to_virtual_manifest(
15621662
contents: String,
15631663
document: toml_edit::ImDocument<String>,

tests/testsuite/features.rs

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

0 commit comments

Comments
 (0)