Skip to content

Commit 02499ab

Browse files
committed
Auto merge of #13778 - Muscraft:unused-dependencies, r=epage
Unused dependencies cleanup The implementation of #12826 was used as a demonstration for #12235, in #13621. The demonstration implementation was far from ideal and was lacking a few features. This PR makes the implementation "feature complete", and fixes known bugs with the initial implementation.
2 parents 80d5b60 + 064184a commit 02499ab

File tree

23 files changed

+833
-107
lines changed

23 files changed

+833
-107
lines changed

Diff for: src/cargo/core/workspace.rs

+2-1
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::{
3030
context::CargoResolverConfig, context::CargoResolverPrecedence, context::ConfigRelativePath,
@@ -1194,6 +1194,7 @@ impl<'gctx> Workspace<'gctx> {
11941194
.collect();
11951195

11961196
check_implicit_features(pkg, &path, &normalized_lints, &mut error_count, self.gctx)?;
1197+
unused_dependencies(pkg, &path, &normalized_lints, &mut error_count, self.gctx)?;
11971198
if error_count > 0 {
11981199
Err(crate::util::errors::AlreadyPrintedError::new(anyhow!(
11991200
"encountered {error_count} errors(s) while running lints"

Diff for: src/cargo/ops/fix.rs

+42-1
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,13 @@ use tracing::{debug, trace, warn};
5353
use crate::core::compiler::RustcTargetData;
5454
use crate::core::resolver::features::{DiffMap, FeatureOpts, FeatureResolver, FeaturesFor};
5555
use crate::core::resolver::{HasDevUnits, Resolve, ResolveBehavior};
56-
use crate::core::PackageIdSpecQuery as _;
5756
use crate::core::{Edition, MaybePackage, Package, PackageId, Workspace};
57+
use crate::core::{FeatureValue, PackageIdSpecQuery as _};
5858
use crate::ops::resolve::WorkspaceResolve;
5959
use crate::ops::{self, CompileOptions};
6060
use crate::util::diagnostic_server::{Message, RustfixDiagnosticServer};
6161
use crate::util::errors::CargoResult;
62+
use crate::util::interning::InternedString;
6263
use crate::util::GlobalContext;
6364
use crate::util::{existing_vcs_repo, LockServer, LockServerClient};
6465
use crate::{drop_eprint, drop_eprintln};
@@ -253,6 +254,7 @@ fn migrate_manifests(ws: &Workspace<'_>, pkgs: &[&Package]) -> CargoResult<()> {
253254
let mut fixes = 0;
254255

255256
let root = document.as_table_mut();
257+
fixes += add_feature_for_unused_deps(pkg, root);
256258
if rename_table(root, "project", "package") {
257259
fixes += 1;
258260
}
@@ -287,6 +289,45 @@ fn rename_table(parent: &mut dyn toml_edit::TableLike, old: &str, new: &str) ->
287289
true
288290
}
289291

292+
fn add_feature_for_unused_deps(pkg: &Package, parent: &mut dyn toml_edit::TableLike) -> usize {
293+
let manifest = pkg.manifest();
294+
295+
let activated_opt_deps = manifest
296+
.resolved_toml()
297+
.features()
298+
.map(|map| {
299+
map.values()
300+
.flatten()
301+
.filter_map(|f| match FeatureValue::new(InternedString::new(f)) {
302+
FeatureValue::Dep { dep_name } => Some(dep_name.as_str()),
303+
_ => None,
304+
})
305+
.collect::<HashSet<_>>()
306+
})
307+
.unwrap_or_default();
308+
309+
let mut fixes = 0;
310+
for dep in manifest.dependencies() {
311+
let dep_name_in_toml = dep.name_in_toml();
312+
if dep.is_optional() && !activated_opt_deps.contains(dep_name_in_toml.as_str()) {
313+
fixes += 1;
314+
if let Some(features) = parent
315+
.entry("features")
316+
.or_insert(toml_edit::table())
317+
.as_table_like_mut()
318+
{
319+
features.insert(
320+
dep_name_in_toml.as_str(),
321+
toml_edit::Item::Value(toml_edit::Value::Array(toml_edit::Array::from_iter(
322+
&[format!("dep:{}", dep_name_in_toml)],
323+
))),
324+
);
325+
}
326+
}
327+
}
328+
fixes
329+
}
330+
290331
fn check_resolver_change(ws: &Workspace<'_>, opts: &FixOptions) -> CargoResult<()> {
291332
let root = ws.root_maybe();
292333
match root {

Diff for: src/cargo/util/lints.rs

+164-33
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use crate::core::dependency::DepKind;
12
use crate::core::FeatureValue::Dep;
23
use crate::core::{Edition, FeatureValue, Package};
34
use crate::util::interning::InternedString;
@@ -6,6 +7,7 @@ use annotate_snippets::{Level, Renderer, Snippet};
67
use cargo_util_schemas::manifest::{TomlLintLevel, TomlToolLints};
78
use pathdiff::diff_paths;
89
use std::collections::HashSet;
10+
use std::fmt::Display;
911
use std::ops::Range;
1012
use std::path::Path;
1113
use toml_edit::ImDocument;
@@ -107,6 +109,17 @@ pub enum LintLevel {
107109
Forbid,
108110
}
109111

112+
impl Display for LintLevel {
113+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
114+
match self {
115+
LintLevel::Allow => write!(f, "allow"),
116+
LintLevel::Warn => write!(f, "warn"),
117+
LintLevel::Deny => write!(f, "deny"),
118+
LintLevel::Forbid => write!(f, "forbid"),
119+
}
120+
}
121+
}
122+
110123
impl LintLevel {
111124
pub fn to_diagnostic_level(self) -> Level {
112125
match self {
@@ -144,10 +157,10 @@ impl From<TomlLintLevel> for LintLevel {
144157
/// [RFC #3491]: https://rust-lang.github.io/rfcs/3491-remove-implicit-features.html
145158
const IMPLICIT_FEATURES: Lint = Lint {
146159
name: "implicit_features",
147-
desc: "warn about the use of unstable features",
160+
desc: "implicit features for optional dependencies is deprecated and will be unavailable in the 2024 edition",
148161
groups: &[],
149162
default_level: LintLevel::Allow,
150-
edition_lint_opts: Some((Edition::Edition2024, LintLevel::Deny)),
163+
edition_lint_opts: None,
151164
};
152165

153166
pub fn check_implicit_features(
@@ -157,56 +170,63 @@ pub fn check_implicit_features(
157170
error_count: &mut usize,
158171
gctx: &GlobalContext,
159172
) -> CargoResult<()> {
160-
let lint_level = IMPLICIT_FEATURES.level(lints, pkg.manifest().edition());
173+
let edition = pkg.manifest().edition();
174+
// In Edition 2024+, instead of creating optional features, the dependencies are unused.
175+
// See `UNUSED_OPTIONAL_DEPENDENCY`
176+
if edition >= Edition::Edition2024 {
177+
return Ok(());
178+
}
179+
180+
let lint_level = IMPLICIT_FEATURES.level(lints, edition);
161181
if lint_level == LintLevel::Allow {
162182
return Ok(());
163183
}
164184

165185
let manifest = pkg.manifest();
166-
let user_defined_features = manifest.resolved_toml().features();
167-
let features = user_defined_features.map_or(HashSet::new(), |f| {
168-
f.keys().map(|k| InternedString::new(&k)).collect()
169-
});
170-
// Add implicit features for optional dependencies if they weren't
171-
// explicitly listed anywhere.
172-
let explicitly_listed = user_defined_features.map_or(HashSet::new(), |f| {
173-
f.values()
174-
.flatten()
175-
.filter_map(|v| match FeatureValue::new(v.into()) {
176-
Dep { dep_name } => Some(dep_name),
177-
_ => None,
178-
})
179-
.collect()
180-
});
186+
let activated_opt_deps = manifest
187+
.resolved_toml()
188+
.features()
189+
.map(|map| {
190+
map.values()
191+
.flatten()
192+
.filter_map(|f| match FeatureValue::new(InternedString::new(f)) {
193+
Dep { dep_name } => Some(dep_name.as_str()),
194+
_ => None,
195+
})
196+
.collect::<HashSet<_>>()
197+
})
198+
.unwrap_or_default();
181199

200+
let mut emitted_source = None;
182201
for dep in manifest.dependencies() {
183202
let dep_name_in_toml = dep.name_in_toml();
184-
if !dep.is_optional()
185-
|| features.contains(&dep_name_in_toml)
186-
|| explicitly_listed.contains(&dep_name_in_toml)
187-
{
203+
if !dep.is_optional() || activated_opt_deps.contains(dep_name_in_toml.as_str()) {
188204
continue;
189205
}
190206
if lint_level == LintLevel::Forbid || lint_level == LintLevel::Deny {
191207
*error_count += 1;
192208
}
209+
let mut toml_path = vec![dep.kind().kind_table(), dep_name_in_toml.as_str()];
210+
let platform = dep.platform().map(|p| p.to_string());
211+
if let Some(platform) = platform.as_ref() {
212+
toml_path.insert(0, platform);
213+
toml_path.insert(0, "target");
214+
}
193215
let level = lint_level.to_diagnostic_level();
194216
let manifest_path = rel_cwd_manifest_path(path, gctx);
195-
let message = level.title("unused optional dependency").snippet(
217+
let mut message = level.title(IMPLICIT_FEATURES.desc).snippet(
196218
Snippet::source(manifest.contents())
197219
.origin(&manifest_path)
198-
.annotation(
199-
level.span(
200-
get_span(
201-
manifest.document(),
202-
&["dependencies", &dep_name_in_toml],
203-
false,
204-
)
205-
.unwrap(),
206-
),
207-
)
220+
.annotation(level.span(get_span(manifest.document(), &toml_path, false).unwrap()))
208221
.fold(true),
209222
);
223+
if emitted_source.is_none() {
224+
emitted_source = Some(format!(
225+
"`cargo::{}` is set to `{lint_level}`",
226+
IMPLICIT_FEATURES.name
227+
));
228+
message = message.footer(Level::Note.title(emitted_source.as_ref().unwrap()));
229+
}
210230
let renderer = Renderer::styled().term_width(
211231
gctx.shell()
212232
.err_width()
@@ -217,3 +237,114 @@ pub fn check_implicit_features(
217237
}
218238
Ok(())
219239
}
240+
241+
const UNUSED_OPTIONAL_DEPENDENCY: Lint = Lint {
242+
name: "unused_optional_dependency",
243+
desc: "unused optional dependency",
244+
groups: &[],
245+
default_level: LintLevel::Warn,
246+
edition_lint_opts: None,
247+
};
248+
249+
pub fn unused_dependencies(
250+
pkg: &Package,
251+
path: &Path,
252+
lints: &TomlToolLints,
253+
error_count: &mut usize,
254+
gctx: &GlobalContext,
255+
) -> CargoResult<()> {
256+
let edition = pkg.manifest().edition();
257+
// Unused optional dependencies can only exist on edition 2024+
258+
if edition < Edition::Edition2024 {
259+
return Ok(());
260+
}
261+
262+
let lint_level = UNUSED_OPTIONAL_DEPENDENCY.level(lints, edition);
263+
if lint_level == LintLevel::Allow {
264+
return Ok(());
265+
}
266+
let mut emitted_source = None;
267+
let manifest = pkg.manifest();
268+
let original_toml = manifest.original_toml();
269+
// Unused dependencies were stripped from the manifest, leaving only the used ones
270+
let used_dependencies = manifest
271+
.dependencies()
272+
.into_iter()
273+
.map(|d| d.name_in_toml().to_string())
274+
.collect::<HashSet<String>>();
275+
let mut orig_deps = vec![
276+
(
277+
original_toml.dependencies.as_ref(),
278+
vec![DepKind::Normal.kind_table()],
279+
),
280+
(
281+
original_toml.dev_dependencies.as_ref(),
282+
vec![DepKind::Development.kind_table()],
283+
),
284+
(
285+
original_toml.build_dependencies.as_ref(),
286+
vec![DepKind::Build.kind_table()],
287+
),
288+
];
289+
for (name, platform) in original_toml.target.iter().flatten() {
290+
orig_deps.push((
291+
platform.dependencies.as_ref(),
292+
vec!["target", name, DepKind::Normal.kind_table()],
293+
));
294+
orig_deps.push((
295+
platform.dev_dependencies.as_ref(),
296+
vec!["target", name, DepKind::Development.kind_table()],
297+
));
298+
orig_deps.push((
299+
platform.build_dependencies.as_ref(),
300+
vec!["target", name, DepKind::Normal.kind_table()],
301+
));
302+
}
303+
for (deps, toml_path) in orig_deps {
304+
if let Some(deps) = deps {
305+
for name in deps.keys() {
306+
if !used_dependencies.contains(name.as_str()) {
307+
if lint_level == LintLevel::Forbid || lint_level == LintLevel::Deny {
308+
*error_count += 1;
309+
}
310+
let toml_path = toml_path
311+
.iter()
312+
.map(|s| *s)
313+
.chain(std::iter::once(name.as_str()))
314+
.collect::<Vec<_>>();
315+
let level = lint_level.to_diagnostic_level();
316+
let manifest_path = rel_cwd_manifest_path(path, gctx);
317+
318+
let mut message = level.title(UNUSED_OPTIONAL_DEPENDENCY.desc).snippet(
319+
Snippet::source(manifest.contents())
320+
.origin(&manifest_path)
321+
.annotation(level.span(
322+
get_span(manifest.document(), toml_path.as_slice(), false).unwrap(),
323+
))
324+
.fold(true),
325+
);
326+
if emitted_source.is_none() {
327+
emitted_source = Some(format!(
328+
"`cargo::{}` is set to `{lint_level}`",
329+
UNUSED_OPTIONAL_DEPENDENCY.name
330+
));
331+
message =
332+
message.footer(Level::Note.title(emitted_source.as_ref().unwrap()));
333+
}
334+
let help = format!(
335+
"remove the dependency or activate it in a feature with `dep:{name}`"
336+
);
337+
message = message.footer(Level::Help.title(&help));
338+
let renderer = Renderer::styled().term_width(
339+
gctx.shell()
340+
.err_width()
341+
.diagnostic_terminal_width()
342+
.unwrap_or(annotate_snippets::renderer::DEFAULT_TERM_WIDTH),
343+
);
344+
writeln!(gctx.shell().err(), "{}", renderer.render(message))?;
345+
}
346+
}
347+
}
348+
}
349+
Ok(())
350+
}

0 commit comments

Comments
 (0)