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//! - Cap lints (skip everything for non-local)
46//! - Support for `build.warnings`
47//! - When errors should block further evaluation within the pass
48//! - Providing a summary at the end, like what is provided by [`ScopedDiagnosticStats::report_summary`]
49//! - Prefer data driven passes to simplify adding rules
50//!   - Ensure the pass' lints are in [`rules::LINTS`], e.g. `ensure_parse_passed_in_lints`
51//!   - Prefer evaluating the lint level within the pass
52//!
53//! See [`passes::emit_parse_diagnostics`] as an example.
54//!
55//! [future-incompat lint]: https://rustc-dev-guide.rust-lang.org/diagnostics.html#future-incompatible-lints
56
57use cargo_util_schemas::manifest::RustVersion;
58use cargo_util_schemas::manifest::TomlToolLints;
59
60use crate::CargoResult;
61use crate::core::Workspace;
62use crate::core::{Edition, Features, MaybePackage, Package};
63use crate::util::GlobalContext;
64
65mod lint;
66mod report;
67
68pub mod passes;
69pub mod rules;
70
71pub use lint::{Lint, LintGroup, LintLevel, LintLevelProduct, LintLevelSource};
72pub use report::{AsIndex, cwd_rel_path, get_key_value, get_key_value_span, workspace_rel_path};
73pub use rules::{LINT_GROUPS, LINTS};
74
75pub struct PassOutput {
76    pub lint_warning_count: usize,
77}
78
79pub struct GlobalDiagnosticStats {
80    error_count: usize,
81    lint_warning_count: usize,
82}
83
84impl GlobalDiagnosticStats {
85    pub fn new() -> Self {
86        Self {
87            error_count: 0,
88            lint_warning_count: 0,
89        }
90    }
91
92    pub fn scope(&mut self) -> ScopedDiagnosticStats<'_> {
93        ScopedDiagnosticStats {
94            warning_count: 0,
95            error_count: 0,
96            global: self,
97        }
98    }
99
100    pub fn error_count(&self) -> usize {
101        self.error_count
102    }
103
104    pub fn lint_warning_count(&self) -> usize {
105        self.lint_warning_count
106    }
107
108    pub fn ok(&self) -> CargoResult<PassOutput> {
109        if 0 < self.error_count {
110            Err(crate::Error::new(crate::AlreadyPrintedError::new(
111                anyhow::format_err!("see above"),
112            )))
113        } else {
114            Ok(PassOutput {
115                lint_warning_count: self.lint_warning_count,
116            })
117        }
118    }
119}
120
121pub struct ScopedDiagnosticStats<'g> {
122    warning_count: usize,
123    error_count: usize,
124    global: &'g mut GlobalDiagnosticStats,
125}
126
127impl ScopedDiagnosticStats<'_> {
128    pub fn warning_count(&self) -> usize {
129        self.warning_count
130    }
131
132    pub fn error_count(&self) -> usize {
133        self.error_count
134    }
135
136    pub fn record_warning(&mut self) {
137        self.warning_count += 1;
138    }
139
140    pub fn record_error(&mut self) {
141        self.error_count += 1;
142        self.global.error_count += 1;
143    }
144
145    pub fn record_lint(&mut self, lint: LintLevel) {
146        match lint {
147            LintLevel::Forbid | LintLevel::Deny => {
148                self.record_error();
149            }
150            LintLevel::Warn => {
151                self.global.lint_warning_count += 1;
152                self.record_warning();
153            }
154            LintLevel::Allow => {}
155        }
156    }
157
158    /// Print a summary to the user
159    ///
160    /// **Note:** be sure to call `GlobalDiagnosticStats::ok` or equivalent to fail the operation
161    pub fn report_summary(
162        &self,
163        action: &str,
164        name: Option<&str>,
165        gctx: &GlobalContext,
166    ) -> CargoResult<()> {
167        if 0 < self.warning_count {
168            let plural = if self.warning_count == 1 { "" } else { "s" };
169            let name = name
170                .map(|n| format!("`{n}`"))
171                .unwrap_or_else(|| "workspace".to_owned());
172            gctx.shell().warn(format!(
173                "{name} (manifest) generated {} warning{plural}",
174                self.warning_count
175            ))?;
176        }
177
178        if 0 < self.error_count {
179            let plural = if self.error_count == 1 { "" } else { "s" };
180            let name = name
181                .map(|n| format!("`{n}`"))
182                .unwrap_or_else(|| "workspace".to_owned());
183            gctx.shell().error(format!(
184                "could not {action} {name} (manifest) due to {} previous error{plural}",
185                self.error_count
186            ))?;
187        }
188
189        Ok(())
190    }
191}
192
193/// Scope at which a lint runs: package-level or workspace-level.
194pub enum ManifestFor<'a> {
195    /// Lint runs for a specific package.
196    Package(&'a Package),
197    /// Lint runs for workspace-level config.
198    Workspace {
199        ws: &'a Workspace<'a>,
200        maybe_pkg: &'a MaybePackage,
201    },
202}
203
204impl ManifestFor<'_> {
205    fn lint_level(
206        &self,
207        pkg_lints: &TomlToolLints,
208        lint: &Lint,
209        gctx: &GlobalContext,
210    ) -> LintLevelProduct {
211        lint.level(
212            pkg_lints,
213            self.rust_version(),
214            self.unstable_features(),
215            gctx,
216        )
217    }
218
219    pub fn rust_version(&self) -> Option<&RustVersion> {
220        match self {
221            ManifestFor::Package(p) => p.rust_version(),
222            ManifestFor::Workspace { ws, maybe_pkg: _ } => ws.lowest_rust_version(),
223        }
224    }
225
226    pub fn contents(&self) -> Option<&str> {
227        match self {
228            ManifestFor::Package(p) => p.manifest().contents(),
229            ManifestFor::Workspace { ws: _, maybe_pkg } => maybe_pkg.contents(),
230        }
231    }
232
233    pub fn document(&self) -> Option<&toml::Spanned<toml::de::DeTable<'static>>> {
234        match self {
235            ManifestFor::Package(p) => p.manifest().document(),
236            ManifestFor::Workspace { ws: _, maybe_pkg } => maybe_pkg.document(),
237        }
238    }
239
240    pub fn edition(&self) -> Edition {
241        match self {
242            ManifestFor::Package(p) => p.manifest().edition(),
243            ManifestFor::Workspace { ws: _, maybe_pkg } => maybe_pkg.edition(),
244        }
245    }
246
247    pub fn unstable_features(&self) -> &Features {
248        match self {
249            ManifestFor::Package(p) => p.manifest().unstable_features(),
250            ManifestFor::Workspace { ws: _, maybe_pkg } => maybe_pkg.unstable_features(),
251        }
252    }
253}
254
255impl<'a> From<&'a Package> for ManifestFor<'a> {
256    fn from(value: &'a Package) -> ManifestFor<'a> {
257        ManifestFor::Package(value)
258    }
259}
260
261impl<'a> From<(&'a Workspace<'a>, &'a MaybePackage)> for ManifestFor<'a> {
262    fn from((ws, maybe_pkg): (&'a Workspace<'a>, &'a MaybePackage)) -> ManifestFor<'a> {
263        ManifestFor::Workspace { ws, maybe_pkg }
264    }
265}