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