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 [`DiagnosticStats::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 anyhow::bail;
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, get_key_value, get_key_value_span, rel_cwd_manifest_path};
73pub use rules::{LINT_GROUPS, LINTS};
74
75pub struct DiagnosticStats {
76    warning_count: usize,
77    lint_warning_count: usize,
78    error_count: usize,
79}
80
81impl DiagnosticStats {
82    pub fn new() -> Self {
83        Self {
84            warning_count: 0,
85            lint_warning_count: 0,
86            error_count: 0,
87        }
88    }
89
90    pub fn lint_warning_count(&self) -> usize {
91        self.lint_warning_count
92    }
93
94    pub fn warning_count(&self) -> usize {
95        self.warning_count
96    }
97
98    pub fn error_count(&self) -> usize {
99        self.error_count
100    }
101
102    pub fn record_warning(&mut self) {
103        self.warning_count += 1;
104    }
105
106    pub fn record_error(&mut self) {
107        self.error_count += 1;
108    }
109
110    pub fn record_lint(&mut self, lint: LintLevel) {
111        match lint {
112            LintLevel::Forbid | LintLevel::Deny => {
113                self.record_error();
114            }
115            LintLevel::Warn => {
116                self.lint_warning_count += 1;
117                self.record_warning();
118            }
119            LintLevel::Allow => {}
120        }
121    }
122
123    pub fn report_summary(
124        &self,
125        action: &str,
126        name: Option<&str>,
127        gctx: &GlobalContext,
128    ) -> CargoResult<()> {
129        if 0 < self.warning_count {
130            let plural = if self.warning_count == 1 { "" } else { "s" };
131            let name = name
132                .map(|n| format!("`{n}`"))
133                .unwrap_or_else(|| "workspace".to_owned());
134            gctx.shell().warn(format!(
135                "{name} (manifest) generated {} warning{plural}",
136                self.warning_count
137            ))?;
138        }
139
140        if 0 < self.error_count {
141            let plural = if self.error_count == 1 { "" } else { "s" };
142            let name = name
143                .map(|n| format!("`{n}`"))
144                .unwrap_or_else(|| "workspace".to_owned());
145            bail!(
146                "could not {action} {name} (manifest) due to {} previous error{plural}",
147                self.error_count
148            )
149        }
150
151        Ok(())
152    }
153}
154
155impl std::ops::Add for DiagnosticStats {
156    type Output = DiagnosticStats;
157
158    fn add(mut self, rhs: Self) -> Self::Output {
159        self += rhs;
160        self
161    }
162}
163
164impl std::ops::AddAssign for DiagnosticStats {
165    fn add_assign(&mut self, rhs: Self) {
166        let DiagnosticStats {
167            warning_count,
168            lint_warning_count,
169            error_count,
170        } = rhs;
171        self.warning_count += warning_count;
172        self.lint_warning_count += lint_warning_count;
173        self.error_count += error_count;
174    }
175}
176
177/// Scope at which a lint runs: package-level or workspace-level.
178pub enum ManifestFor<'a> {
179    /// Lint runs for a specific package.
180    Package(&'a Package),
181    /// Lint runs for workspace-level config.
182    Workspace {
183        ws: &'a Workspace<'a>,
184        maybe_pkg: &'a MaybePackage,
185    },
186}
187
188impl ManifestFor<'_> {
189    fn lint_level(&self, pkg_lints: &TomlToolLints, lint: &Lint) -> LintLevelProduct {
190        lint.level(pkg_lints, self.rust_version(), self.unstable_features())
191    }
192
193    pub fn rust_version(&self) -> Option<&RustVersion> {
194        match self {
195            ManifestFor::Package(p) => p.rust_version(),
196            ManifestFor::Workspace { ws, maybe_pkg: _ } => ws.lowest_rust_version(),
197        }
198    }
199
200    pub fn contents(&self) -> Option<&str> {
201        match self {
202            ManifestFor::Package(p) => p.manifest().contents(),
203            ManifestFor::Workspace { ws: _, maybe_pkg } => maybe_pkg.contents(),
204        }
205    }
206
207    pub fn document(&self) -> Option<&toml::Spanned<toml::de::DeTable<'static>>> {
208        match self {
209            ManifestFor::Package(p) => p.manifest().document(),
210            ManifestFor::Workspace { ws: _, maybe_pkg } => maybe_pkg.document(),
211        }
212    }
213
214    pub fn edition(&self) -> Edition {
215        match self {
216            ManifestFor::Package(p) => p.manifest().edition(),
217            ManifestFor::Workspace { ws: _, maybe_pkg } => maybe_pkg.edition(),
218        }
219    }
220
221    pub fn unstable_features(&self) -> &Features {
222        match self {
223            ManifestFor::Package(p) => p.manifest().unstable_features(),
224            ManifestFor::Workspace { ws: _, maybe_pkg } => maybe_pkg.unstable_features(),
225        }
226    }
227}
228
229impl<'a> From<&'a Package> for ManifestFor<'a> {
230    fn from(value: &'a Package) -> ManifestFor<'a> {
231        ManifestFor::Package(value)
232    }
233}
234
235impl<'a> From<(&'a Workspace<'a>, &'a MaybePackage)> for ManifestFor<'a> {
236    fn from((ws, maybe_pkg): (&'a Workspace<'a>, &'a MaybePackage)) -> ManifestFor<'a> {
237        ManifestFor::Workspace { ws, maybe_pkg }
238    }
239}