| //! Hard-coded and user-controlled diagnostics |
| //! |
| //! Diagnostics are user messages, like warnings and errors. |
| //! When they are named for setting a user-overridable level, |
| //! they are called lints. |
| //! |
| //! # When should a diagnostic be a lint |
| //! |
| //! Lints are generally preferred because of the level of control for users. |
| //! |
| //! Use a hard-coded diagnostic when: |
| //! - Critical errors |
| //! - There is no associated package or workspace. The diagnostic must still be suppressible |
| //! somehow (e.g. a user explicitly opting in to a config field's default value) |
| //! - The warning message is too important to allow a user to hide (rare) |
| //! |
| //! # Adding a diagnostic |
| //! |
| //! The mechanics of adding a diagnostic is dependent on the requirements: |
| //! - TOML syntax or manifest schema: [`passes::emit_parse_diagnostics`], [`rules::PARSE_PASS_RULES`] |
| //! - Lockfile |
| //! - May be overly broad for what dependencies are checked |
| //! - Pre-build unit graph |
| //! - Tailored to a specific configuration (features, targets) but requires users to enumerate every configuration |
| //! - Post-build unit graph: [`rules::unused_dependencies::lint_build_results`] |
| //! - Slow feedback cycle since a build needs to happen |
| //! - Does not fit into any idea of a pass: directly call [`cargo_util_terminal::Shell::warn`] or [`crate::CargoResult::Err`] |
| //! |
| //! When evaluating a diagnostic: |
| //! - Only evaluate and emit for local packages unless it is for a [future-incompat lint] |
| //! |
| //! When generating a diagnostic [report][cargo_util_terminal::report::Report]: |
| //! - Try to keep the report succinct while ensuring a beginner can understand what is wrong and how to fix. |
| //! It is a difficult balance to hit; err on the side of providing extra information. |
| //! - Messages should generally be a phrase, starting with a lowercase letter. |
| //! If multiple sentences are needed, consider if a [message][cargo_util_terminal::report::Message] or sub-diagnostic would be more |
| //! appropriate. |
| //! - Only the first lint for a package should emit the [`lint::Lint::emitted_source`] |
| //! |
| //! See also [rustc's Errors and Lints](https://rustc-dev-guide.rust-lang.org/diagnostics.html) |
| //! |
| //! # Adding a pass |
| //! |
| //! When a diagnostic requires adding a new pass, keep in mind: |
| //! - Cap lints (skip everything for non-local) |
| //! - Support for `build.warnings` |
| //! - When errors should block further evaluation within the pass |
| //! - Providing a summary at the end, like what is provided by [`ScopedDiagnosticStats::report_summary`] |
| //! - Prefer data driven passes to simplify adding rules |
| //! - Ensure the pass' lints are in [`rules::LINTS`], e.g. `ensure_parse_passed_in_lints` |
| //! - Prefer evaluating the lint level within the pass |
| //! |
| //! See [`passes::emit_parse_diagnostics`] as an example. |
| //! |
| //! [future-incompat lint]: https://rustc-dev-guide.rust-lang.org/diagnostics.html#future-incompatible-lints |
| |
| use cargo_util_schemas::manifest::RustVersion; |
| use cargo_util_schemas::manifest::TomlToolLints; |
| |
| use crate::CargoResult; |
| use crate::util::GlobalContext; |
| use crate::workspace::Workspace; |
| use crate::workspace::{Edition, Features, MaybePackage, Package}; |
| |
| mod lint; |
| mod report; |
| |
| pub mod passes; |
| pub mod rules; |
| |
| pub use lint::{Lint, LintGroup, LintLevel, LintLevelProduct, LintLevelSource}; |
| pub use report::{AsIndex, cwd_rel_path, get_key_value, get_key_value_span, workspace_rel_path}; |
| pub use rules::{LINT_GROUPS, LINTS}; |
| |
| pub struct PassOutput { |
| pub lint_warning_count: usize, |
| } |
| |
| pub struct GlobalDiagnosticStats { |
| error_count: usize, |
| lint_warning_count: usize, |
| } |
| |
| impl GlobalDiagnosticStats { |
| pub fn new() -> Self { |
| Self { |
| error_count: 0, |
| lint_warning_count: 0, |
| } |
| } |
| |
| pub fn scope(&mut self) -> ScopedDiagnosticStats<'_> { |
| ScopedDiagnosticStats { |
| warning_count: 0, |
| error_count: 0, |
| global: self, |
| } |
| } |
| |
| pub fn error_count(&self) -> usize { |
| self.error_count |
| } |
| |
| pub fn lint_warning_count(&self) -> usize { |
| self.lint_warning_count |
| } |
| |
| pub fn ok(&self) -> CargoResult<PassOutput> { |
| if 0 < self.error_count { |
| Err(crate::Error::new(crate::AlreadyPrintedError::new( |
| anyhow::format_err!("see above"), |
| ))) |
| } else { |
| Ok(PassOutput { |
| lint_warning_count: self.lint_warning_count, |
| }) |
| } |
| } |
| } |
| |
| pub struct ScopedDiagnosticStats<'g> { |
| warning_count: usize, |
| error_count: usize, |
| global: &'g mut GlobalDiagnosticStats, |
| } |
| |
| impl ScopedDiagnosticStats<'_> { |
| pub fn warning_count(&self) -> usize { |
| self.warning_count |
| } |
| |
| pub fn error_count(&self) -> usize { |
| self.error_count |
| } |
| |
| pub fn record_warning(&mut self) { |
| self.warning_count += 1; |
| } |
| |
| pub fn record_error(&mut self) { |
| self.error_count += 1; |
| self.global.error_count += 1; |
| } |
| |
| pub fn record_lint(&mut self, lint: LintLevel) { |
| match lint { |
| LintLevel::Forbid | LintLevel::Deny => { |
| self.record_error(); |
| } |
| LintLevel::Warn => { |
| self.global.lint_warning_count += 1; |
| self.record_warning(); |
| } |
| LintLevel::Allow => {} |
| } |
| } |
| |
| /// Print a summary to the user |
| /// |
| /// **Note:** be sure to call `GlobalDiagnosticStats::ok` or equivalent to fail the operation |
| pub fn report_summary( |
| &self, |
| action: &str, |
| name: Option<&str>, |
| gctx: &GlobalContext, |
| ) -> CargoResult<()> { |
| if 0 < self.warning_count { |
| let plural = if self.warning_count == 1 { "" } else { "s" }; |
| let name = name |
| .map(|n| format!("`{n}`")) |
| .unwrap_or_else(|| "workspace".to_owned()); |
| gctx.shell().warn(format!( |
| "{name} (manifest) generated {} warning{plural}", |
| self.warning_count |
| ))?; |
| } |
| |
| if 0 < self.error_count { |
| let plural = if self.error_count == 1 { "" } else { "s" }; |
| let name = name |
| .map(|n| format!("`{n}`")) |
| .unwrap_or_else(|| "workspace".to_owned()); |
| gctx.shell().error(format!( |
| "could not {action} {name} (manifest) due to {} previous error{plural}", |
| self.error_count |
| ))?; |
| } |
| |
| Ok(()) |
| } |
| } |
| |
| /// Scope at which a lint runs: package-level or workspace-level. |
| pub enum ManifestFor<'a> { |
| /// Lint runs for a specific package. |
| Package(&'a Package), |
| /// Lint runs for workspace-level config. |
| Workspace { |
| ws: &'a Workspace<'a>, |
| maybe_pkg: &'a MaybePackage, |
| }, |
| } |
| |
| impl ManifestFor<'_> { |
| fn lint_level( |
| &self, |
| pkg_lints: &TomlToolLints, |
| lint: &Lint, |
| gctx: &GlobalContext, |
| ) -> LintLevelProduct { |
| lint.level( |
| pkg_lints, |
| self.rust_version(), |
| self.unstable_features(), |
| gctx, |
| ) |
| } |
| |
| pub fn rust_version(&self) -> Option<&RustVersion> { |
| match self { |
| ManifestFor::Package(p) => p.rust_version(), |
| ManifestFor::Workspace { ws, maybe_pkg: _ } => ws.lowest_rust_version(), |
| } |
| } |
| |
| pub fn contents(&self) -> Option<&str> { |
| match self { |
| ManifestFor::Package(p) => p.manifest().contents(), |
| ManifestFor::Workspace { ws: _, maybe_pkg } => maybe_pkg.contents(), |
| } |
| } |
| |
| pub fn document(&self) -> Option<&toml::Spanned<toml::de::DeTable<'static>>> { |
| match self { |
| ManifestFor::Package(p) => p.manifest().document(), |
| ManifestFor::Workspace { ws: _, maybe_pkg } => maybe_pkg.document(), |
| } |
| } |
| |
| pub fn edition(&self) -> Edition { |
| match self { |
| ManifestFor::Package(p) => p.manifest().edition(), |
| ManifestFor::Workspace { ws: _, maybe_pkg } => maybe_pkg.edition(), |
| } |
| } |
| |
| pub fn unstable_features(&self) -> &Features { |
| match self { |
| ManifestFor::Package(p) => p.manifest().unstable_features(), |
| ManifestFor::Workspace { ws: _, maybe_pkg } => maybe_pkg.unstable_features(), |
| } |
| } |
| } |
| |
| impl<'a> From<&'a Package> for ManifestFor<'a> { |
| fn from(value: &'a Package) -> ManifestFor<'a> { |
| ManifestFor::Package(value) |
| } |
| } |
| |
| impl<'a> From<(&'a Workspace<'a>, &'a MaybePackage)> for ManifestFor<'a> { |
| fn from((ws, maybe_pkg): (&'a Workspace<'a>, &'a MaybePackage)) -> ManifestFor<'a> { |
| ManifestFor::Workspace { ws, maybe_pkg } |
| } |
| } |