| //! 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, TomlSpan, 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 } | |
| } | |
| } |