Skip to main content

cargo/diagnostics/rules/
mostly_unused_hint.rs

1use std::path::Path;
2
3use cargo_util_schemas::manifest::TomlProfile;
4use cargo_util_terminal::report::AnnotationKind;
5use cargo_util_terminal::report::Group;
6use cargo_util_terminal::report::Level;
7use cargo_util_terminal::report::Origin;
8use cargo_util_terminal::report::Snippet;
9use tracing::instrument;
10
11use crate::CargoResult;
12use crate::GlobalContext;
13use crate::diagnostics::ScopedDiagnosticStats;
14use crate::diagnostics::TomlSpan;
15use crate::diagnostics::get_key_value_span;
16use crate::diagnostics::workspace_rel_path;
17use crate::workspace::MaybePackage;
18use crate::workspace::Package;
19use crate::workspace::Workspace;
20
21const HELP: &str = "pass `-Zprofile-hint-mostly-unused` to enable it";
22const PROFILE_TITLE: &str = "ignoring `hint-mostly-unused` profile option";
23const HINT_KEY: &str = "hint-mostly-unused";
24
25/// Reports a `hints.mostly-unused` that is not a boolean, or that is ignored without
26/// `-Zprofile-hint-mostly-unused`.
27#[instrument(skip_all)]
28pub(crate) fn diagnose_package(
29    ws: &Workspace<'_>,
30    pkg: &Package,
31    path: &Path,
32    pkg_stats: &mut ScopedDiagnosticStats<'_>,
33    gctx: &GlobalContext,
34) -> CargoResult<()> {
35    let Some(value) = pkg.hints().and_then(|hints| hints.mostly_unused.as_ref()) else {
36        return Ok(());
37    };
38    let manifest_path = workspace_rel_path(ws, path);
39    let group = match value {
40        // Profiles are not resolved yet, so this also fires when a profile disables the hint for
41        // the package.
42        toml::Value::Boolean(true) if !gctx.cli_unstable().profile_hint_mostly_unused => {
43            let group =
44                Group::with_title(Level::WARNING.primary_title("ignoring `hints.mostly-unused`"));
45            let group = match hint_span(pkg) {
46                Some((contents, span)) => group.element(
47                    Snippet::source(contents)
48                        .path(&manifest_path)
49                        .annotation(AnnotationKind::Primary.span(span.key.start..span.value.end)),
50                ),
51                None => group.element(Origin::path(&manifest_path)),
52            };
53            group.element(Level::HELP.message(HELP))
54        }
55        toml::Value::Boolean(_) => return Ok(()),
56        value => {
57            let title = format!(
58                "ignoring unsupported value type ({}) for `hints.mostly-unused`",
59                value.type_str()
60            );
61            let group = Group::with_title(Level::WARNING.primary_title(title));
62            match hint_span(pkg) {
63                Some((contents, span)) => group.element(
64                    Snippet::source(contents).path(&manifest_path).annotation(
65                        AnnotationKind::Primary
66                            .span(span.value)
67                            .label("expected a boolean"),
68                    ),
69                ),
70                None => group.element(Origin::path(&manifest_path)),
71            }
72        }
73    };
74    pkg_stats.record_warning();
75    gctx.shell().print_report(&[group], false)?;
76    Ok(())
77}
78
79/// Locates `hints.mostly-unused` in the package's original manifest, if its source is available.
80fn hint_span(pkg: &Package) -> Option<(&str, TomlSpan)> {
81    let manifest = pkg.manifest();
82    let contents = manifest.contents()?;
83    let document = manifest.document()?;
84    let span = get_key_value_span(document, &["hints", "mostly-unused"])?;
85    Some((contents, span))
86}
87
88/// Reports each `hint-mostly-unused = true` in the root manifest's profiles that is ignored
89/// without `-Zprofile-hint-mostly-unused`. Config profiles are checked where they are loaded,
90/// see [`crate::workspace::profiles`].
91#[instrument(skip_all)]
92pub(crate) fn diagnose_workspace(
93    ws: &Workspace<'_>,
94    maybe_pkg: &MaybePackage,
95    path: &Path,
96    pkg_stats: &mut ScopedDiagnosticStats<'_>,
97    gctx: &GlobalContext,
98) -> CargoResult<()> {
99    if gctx.cli_unstable().profile_hint_mostly_unused {
100        return Ok(());
101    }
102    let Some(profiles) = maybe_pkg.profiles() else {
103        return Ok(());
104    };
105    let manifest_path = workspace_rel_path(ws, path);
106
107    // Every profile is checked, as the parse pass does not know which one is built.
108    for (name, profile) in profiles.get_all() {
109        for key in enabled_keys(name.as_str(), profile) {
110            let group = Group::with_title(Level::WARNING.primary_title(PROFILE_TITLE));
111            let group = if let Some(contents) = maybe_pkg.contents()
112                && let Some(document) = maybe_pkg.document()
113                && let Some(table_span) = get_key_value_span(document, &key[..key.len() - 1])
114                && let Some(span) = get_key_value_span(document, &key)
115            {
116                group.element(
117                    Snippet::source(contents)
118                        .path(&manifest_path)
119                        .annotation(AnnotationKind::Primary.span(span.key.start..span.value.end))
120                        .annotation(AnnotationKind::Context.span(table_span.key)),
121                )
122            } else {
123                group.element(Origin::path(&manifest_path))
124            };
125            let group = group.element(Level::HELP.message(HELP));
126            pkg_stats.record_warning();
127            gctx.shell().print_report(&[group], false)?;
128        }
129    }
130    Ok(())
131}
132
133/// The key path of each `hint-mostly-unused = true` under `[profile.<name>]`, whether in the
134/// profile itself, its `build-override`, or one of its `package` entries.
135fn enabled_keys(name: &str, profile: &TomlProfile) -> Vec<Vec<String>> {
136    let mut candidates = vec![(profile, vec![])];
137    if let Some(build_override) = &profile.build_override {
138        candidates.push((build_override, vec!["build-override".to_owned()]));
139    }
140    for (spec, package) in profile.package.iter().flatten() {
141        // The manifest key is not kept, only the parsed spec, which `to_string` normalizes. A
142        // legacy key like `"foo:1.0.0"` thus finds no span, and the warning has no snippet.
143        candidates.push((package, vec!["package".to_owned(), spec.to_string()]));
144    }
145    let mut keys = Vec::new();
146    for (sub_profile, suffix) in candidates {
147        if sub_profile.hint_mostly_unused == Some(true) {
148            let mut key = vec!["profile".to_owned(), name.to_owned()];
149            key.extend(suffix);
150            key.push(HINT_KEY.to_owned());
151            keys.push(key);
152        }
153    }
154    keys
155}