Skip to main content

cargo/diagnostics/
mod.rs

1//! Hard-coded and user-controlled diagnostics
2//!
3//! Diagnostics are user messages, like warnings and errors.
4//! When they are named for setting a user-overridable level,
5//! they are called lints.
6//!
7//! # When should a diagnostic be a lint
8//!
9//! Lints are generally preferred because of the level of control for users.
10//!
11//! Use a hard-coded diagnostic when:
12//! - Critical errors
13//! - There is no associated package or workspace. The diagnostic must still be suppressible
14//!   somehow (e.g. a user explicitly opting in to a config field's default value)
15//! - The warning message is too important to allow a user to hide (rare)
16//!
17//! # Adding a diagnostic
18//!
19//! The mechanics of adding a diagnostic is dependent on the requirements:
20//! - TOML syntax or manifest schema: [`passes::emit_parse_diagnostics`], [`rules::PARSE_PASS_RULES`]
21//! - Lockfile
22//!   - May be overly broad for what dependencies are checked
23//! - Pre-build unit graph
24//!   - Tailored to a specific configuration (features, targets) but requires users to enumerate every configuration
25//! - Post-build unit graph: [`rules::unused_dependencies::lint_build_results`]
26//!   - Slow feedback cycle since a build needs to happen
27//! - Does not fit into any idea of a pass: directly call [`cargo_util_terminal::Shell::warn`] or [`crate::CargoResult::Err`]
28//!
29//! When evaluating a diagnostic:
30//! - Only evaluate and emit for local packages unless it is for a [future-incompat lint]
31//!
32//! When generating a diagnostic [report][cargo_util_terminal::report::Report]:
33//! - Try to keep the report succinct while ensuring a beginner can understand what is wrong and how to fix.
34//!   It is a difficult balance to hit; err on the side of providing extra information.
35//! - Messages should generally be a phrase, starting with a lowercase letter.
36//!   If multiple sentences are needed, consider if a [message][cargo_util_terminal::report::Message] or sub-diagnostic would be more
37//!   appropriate.
38//! - Only the first lint for a package should emit the [`lint::Lint::emitted_source`]
39//!
40//! See also [rustc's Errors and Lints](https://rustc-dev-guide.rust-lang.org/diagnostics.html)
41//!
42//! # Adding a pass
43//!
44//! When a diagnostic requires adding a new pass, keep in mind:
45//! - Support for `build.warnings`
46//! - When errors should block further evaluation within the pass
47//! - Providing a summary at the end, like what is provided by [`ScopedDiagnosticStats::report_summary`]
48//! - Prefer data driven passes to simplify adding rules
49//!   - Ensure the pass' lints are in [`rules::LINTS`], e.g. `ensure_parse_passed_in_lints`
50//!   - Prefer evaluating the lint level within the pass
51//!
52//! See [`passes::emit_parse_diagnostics`] as an example.
53//!
54//! [future-incompat lint]: https://rustc-dev-guide.rust-lang.org/diagnostics.html#future-incompatible-lints
55
56use cargo_util_schemas::manifest::RustVersion;
57use cargo_util_schemas::manifest::TomlToolLints;
58
59use crate::CargoResult;
60use crate::core::Workspace;
61use crate::core::{Edition, Features, MaybePackage, Package};
62use crate::util::GlobalContext;
63
64mod lint;
65mod report;
66
67pub mod passes;
68pub mod rules;
69
70pub use lint::{Lint, LintGroup, LintLevel, LintLevelProduct, LintLevelSource};
71pub use report::{AsIndex, cwd_rel_path, get_key_value, get_key_value_span, workspace_rel_path};
72pub use rules::{LINT_GROUPS, LINTS};
73
74pub struct GlobalDiagnosticStats {
75    error_count: usize,
76}
77
78impl GlobalDiagnosticStats {
79    pub fn new() -> Self {
80        Self { error_count: 0 }
81    }
82
83    pub fn scope(&mut self) -> ScopedDiagnosticStats<'_> {
84        ScopedDiagnosticStats {
85            warning_count: 0,
86            lint_warning_count: 0,
87            error_count: 0,
88            global: self,
89        }
90    }
91
92    pub fn error_count(&self) -> usize {
93        self.error_count
94    }
95
96    pub fn ok(&self) -> CargoResult<()> {
97        if 0 < self.error_count {
98            Err(crate::Error::new(crate::AlreadyPrintedError::new(
99                anyhow::format_err!("see above"),
100            )))
101        } else {
102            Ok(())
103        }
104    }
105}
106
107pub struct ScopedDiagnosticStats<'g> {
108    warning_count: usize,
109    lint_warning_count: usize,
110    error_count: usize,
111    global: &'g mut GlobalDiagnosticStats,
112}
113
114impl ScopedDiagnosticStats<'_> {
115    pub fn lint_warning_count(&self) -> usize {
116        self.lint_warning_count
117    }
118
119    pub fn warning_count(&self) -> usize {
120        self.warning_count
121    }
122
123    pub fn error_count(&self) -> usize {
124        self.error_count
125    }
126
127    pub fn record_warning(&mut self) {
128        self.warning_count += 1;
129    }
130
131    pub fn record_error(&mut self) {
132        self.error_count += 1;
133        self.global.error_count += 1;
134    }
135
136    pub fn record_lint(&mut self, lint: LintLevel) {
137        match lint {
138            LintLevel::Forbid | LintLevel::Deny => {
139                self.record_error();
140            }
141            LintLevel::Warn => {
142                self.lint_warning_count += 1;
143                self.record_warning();
144            }
145            LintLevel::Allow => {}
146        }
147    }
148
149    /// Print a summary to the user
150    ///
151    /// **Note:** be sure to call `GlobalDiagnosticStats::ok` or equivalent to fail the operation
152    pub fn report_summary(
153        &self,
154        action: &str,
155        name: Option<&str>,
156        gctx: &GlobalContext,
157    ) -> CargoResult<()> {
158        if 0 < self.warning_count {
159            let plural = if self.warning_count == 1 { "" } else { "s" };
160            let name = name
161                .map(|n| format!("`{n}`"))
162                .unwrap_or_else(|| "workspace".to_owned());
163            gctx.shell().warn(format!(
164                "{name} (manifest) generated {} warning{plural}",
165                self.warning_count
166            ))?;
167        }
168
169        if 0 < self.error_count {
170            let plural = if self.error_count == 1 { "" } else { "s" };
171            let name = name
172                .map(|n| format!("`{n}`"))
173                .unwrap_or_else(|| "workspace".to_owned());
174            gctx.shell().error(format!(
175                "could not {action} {name} (manifest) due to {} previous error{plural}",
176                self.error_count
177            ))?;
178        }
179
180        Ok(())
181    }
182}
183
184/// Scope at which a lint runs: package-level or workspace-level.
185pub enum ManifestFor<'a> {
186    /// Lint runs for a specific package.
187    Package(&'a Package),
188    /// Lint runs for workspace-level config.
189    Workspace {
190        ws: &'a Workspace<'a>,
191        maybe_pkg: &'a MaybePackage,
192    },
193}
194
195impl ManifestFor<'_> {
196    fn lint_level(&self, pkg_lints: &TomlToolLints, lint: &Lint) -> LintLevelProduct {
197        lint.level(pkg_lints, self.rust_version(), self.unstable_features())
198    }
199
200    pub fn rust_version(&self) -> Option<&RustVersion> {
201        match self {
202            ManifestFor::Package(p) => p.rust_version(),
203            ManifestFor::Workspace { ws, maybe_pkg: _ } => ws.lowest_rust_version(),
204        }
205    }
206
207    pub fn contents(&self) -> Option<&str> {
208        match self {
209            ManifestFor::Package(p) => p.manifest().contents(),
210            ManifestFor::Workspace { ws: _, maybe_pkg } => maybe_pkg.contents(),
211        }
212    }
213
214    pub fn document(&self) -> Option<&toml::Spanned<toml::de::DeTable<'static>>> {
215        match self {
216            ManifestFor::Package(p) => p.manifest().document(),
217            ManifestFor::Workspace { ws: _, maybe_pkg } => maybe_pkg.document(),
218        }
219    }
220
221    pub fn edition(&self) -> Edition {
222        match self {
223            ManifestFor::Package(p) => p.manifest().edition(),
224            ManifestFor::Workspace { ws: _, maybe_pkg } => maybe_pkg.edition(),
225        }
226    }
227
228    pub fn unstable_features(&self) -> &Features {
229        match self {
230            ManifestFor::Package(p) => p.manifest().unstable_features(),
231            ManifestFor::Workspace { ws: _, maybe_pkg } => maybe_pkg.unstable_features(),
232        }
233    }
234}
235
236impl<'a> From<&'a Package> for ManifestFor<'a> {
237    fn from(value: &'a Package) -> ManifestFor<'a> {
238        ManifestFor::Package(value)
239    }
240}
241
242impl<'a> From<(&'a Workspace<'a>, &'a MaybePackage)> for ManifestFor<'a> {
243    fn from((ws, maybe_pkg): (&'a Workspace<'a>, &'a MaybePackage)) -> ManifestFor<'a> {
244        ManifestFor::Workspace { ws, maybe_pkg }
245    }
246}