cargo_workspace_lints/
lib.rs

1//! Parse a cargo workspace and check that all packages have `lints.workspace=true` set.
2
3use cargo_metadata::{MetadataCommand, PackageId};
4use std::{collections::HashSet, fs};
5use std::{error, fmt, io};
6
7/// Validate that all packages in the workspace have `lints.workspace = true`.
8///
9/// # Arguments
10/// * `metadata_command`: The command to run to generate metadata.
11/// * `verbose`: If set to true, provides more detailed output to stderr.
12///
13/// # Errors
14/// If the validation fails, it returns an error indicating the kind of failure. The failure may
15/// indicate I/O-related failures to read and parse data, or it may indicate that individual
16/// packages do not have `lints.workspace = true`.
17pub fn validate_workspace(
18    metadata_command: &MetadataCommand,
19    verbose: bool,
20) -> Result<(), WorkspaceValidationError> {
21    let metadata = metadata_command.exec()?;
22    let workspace_members = metadata
23        .workspace_members
24        .into_iter()
25        .collect::<HashSet<_>>();
26    let mut failing_packages = Vec::new();
27    for package in metadata.packages {
28        // Skip anything not in the workspace
29        if !workspace_members.contains(&package.id) {
30            continue;
31        }
32        let manifest_path = package.manifest_path.as_path();
33        let manifest: toml::Table = toml::from_str(&fs::read_to_string(manifest_path)?)?;
34        if let Err(kind) = validate_package(&package, &manifest, verbose) {
35            failing_packages.push(PackageValidationError {
36                kind,
37                package: package.id,
38            });
39        }
40    }
41    if failing_packages.is_empty() {
42        Ok(())
43    } else {
44        Err(WorkspaceValidationError::FailingPackages(failing_packages))
45    }
46}
47
48/// Validate that the given package has `lints.workspace = true`.
49///
50/// # Arguments
51/// * `package`: The package details, as returned by [`cargo_metadata`].
52/// * `manifest`: The `Cargo.toml` manifest for this package, parsed as `toml`.
53/// * `verbose`: If set to true, provides more detailed output to stderr.
54///
55/// # Errors
56/// If the validation fails, it returns an error indicating the kind of failure.
57pub fn validate_package(
58    package: &cargo_metadata::Package,
59    manifest: &toml::Table,
60    verbose: bool,
61) -> Result<(), PackageValidationErrorKind> {
62    match manifest
63        .get("lints")
64        .and_then(|lints| lints.get("workspace"))
65    {
66        Some(toml::Value::Boolean(true)) => {
67            if verbose {
68                eprintln!(
69                    "PASS: Package {} ({})",
70                    package.name,
71                    package.manifest_path.as_str()
72                );
73            }
74            Ok(())
75        }
76        Some(other_value) => {
77            if verbose {
78                eprintln!(
79                    "FAIL: Package {} ({}) has `lints.workspace = {other_value}`",
80                    package.name,
81                    package.manifest_path.as_str()
82                );
83            }
84            Err(PackageValidationErrorKind::WorkspaceLintsWrongValue(
85                other_value.clone(),
86            ))
87        }
88        None => {
89            if verbose {
90                eprintln!(
91                    "FAIL: Package {} ({}) missing `lints.workspace` field",
92                    package.name,
93                    package.manifest_path.as_str()
94                );
95            }
96            Err(PackageValidationErrorKind::WorkspaceLintsMissing)
97        }
98    }
99}
100
101/// All the reasons why we might fail a workspace.
102#[derive(Debug)]
103pub enum WorkspaceValidationError {
104    /// IO error.
105    Io(io::Error),
106    /// Error running `cargo metadata`.
107    CargoMetadata(cargo_metadata::Error),
108    /// Error parsing `Cargo.toml` manifest as TOML
109    Toml(toml::de::Error),
110    /// Packages successfully read but failed the check.
111    FailingPackages(Vec<PackageValidationError>),
112}
113impl From<io::Error> for WorkspaceValidationError {
114    fn from(error: io::Error) -> Self {
115        Self::Io(error)
116    }
117}
118impl From<cargo_metadata::Error> for WorkspaceValidationError {
119    fn from(error: cargo_metadata::Error) -> Self {
120        Self::CargoMetadata(error)
121    }
122}
123impl From<toml::de::Error> for WorkspaceValidationError {
124    fn from(error: toml::de::Error) -> Self {
125        Self::Toml(error)
126    }
127}
128impl fmt::Display for WorkspaceValidationError {
129    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130        match self {
131            Self::Io(e) => f.write_fmt(format_args!(
132                "Disk I/O Error reading `Cargo.toml` files:\n    {e}\n"
133            )),
134            Self::CargoMetadata(e) => f.write_fmt(format_args!(
135                "Error reading Cargo manifest data:\n    {e}\n"
136            )),
137            Self::Toml(e) => f.write_fmt(format_args!(
138                "Error parsing `Cargo.toml` files as TOML:\n    {e}\n"
139            )),
140            Self::FailingPackages(package_failures) => {
141                f.write_str("Failing packages:")?;
142                for failure in package_failures {
143                    f.write_fmt(format_args!("\n* {failure}"))?;
144                }
145                Ok(())
146            }
147        }
148    }
149}
150
151/// A package failed the check.
152#[derive(Debug)]
153pub struct PackageValidationError {
154    /// Why the package failed.
155    kind: PackageValidationErrorKind,
156    /// Which package failed.
157    package: PackageId,
158}
159
160impl fmt::Display for PackageValidationError {
161    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
162        f.write_fmt(format_args!(
163            "Package {}:\n     {}\n",
164            self.package, self.kind
165        ))
166    }
167}
168impl error::Error for PackageValidationError {}
169
170/// Why a package might fail the check.
171#[derive(Debug)]
172pub enum PackageValidationErrorKind {
173    /// There was no `lints.workspace` field.
174    WorkspaceLintsMissing,
175    /// The `lints.workspace` field was provided, but had the wrong value.
176    WorkspaceLintsWrongValue(toml::Value),
177}
178impl fmt::Display for PackageValidationErrorKind {
179    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
180        match self {
181            Self::WorkspaceLintsMissing => f.write_str("No `workspace.lints` field found"),
182            Self::WorkspaceLintsWrongValue(found) => {
183                f.write_fmt(format_args!("workspace.lints = {found}, expected `true`"))
184            }
185        }
186    }
187}