Skip to main content

ravenrs_lint/
workspace.rs

1//! Which packages to check and where their sources are, from `cargo metadata`.
2
3use std::path::{Path, PathBuf};
4use std::process::Command;
5
6use serde_json::Value;
7
8use crate::{Config, Error};
9
10/// One package of the workspace.
11#[derive(Clone, Debug)]
12pub struct Package {
13    /// Its name.
14    pub name: String,
15    /// Its `Cargo.toml`.
16    pub manifest: PathBuf,
17    /// The directory of its `Cargo.toml`; diagnostics are relative to it.
18    pub root: PathBuf,
19    /// The directories of its library and binaries. Tests, examples and benches are left out.
20    pub source_dirs: Vec<PathBuf>,
21    /// Whether it depends on a RavenRS crate.
22    pub uses_ravenrs: bool,
23    /// Its configuration, the workspace's merged under its own.
24    pub config: Config,
25}
26
27/// The packages of the workspace that `manifest` belongs to.
28pub fn packages(manifest: &Path) -> Result<Vec<Package>, Error> {
29    let cargo = std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into());
30    let output = Command::new(cargo)
31        .args(["metadata", "--no-deps", "--format-version", "1", "--manifest-path"])
32        .arg(manifest)
33        .output()
34        .map_err(|error| Error::Cargo(error.to_string()))?;
35    if !output.status.success() {
36        return Err(Error::Cargo(String::from_utf8_lossy(&output.stderr).trim().to_owned()));
37    }
38    let metadata: Value =
39        serde_json::from_slice(&output.stdout).map_err(|error| Error::Cargo(error.to_string()))?;
40    let workspace = metadata.pointer("/metadata/ravenrs-lint");
41    let Some(packages) = metadata.get("packages").and_then(Value::as_array) else {
42        return Err(Error::Cargo("no packages in the answer".into()));
43    };
44    packages.iter().map(|package| read_package(package, workspace)).collect()
45}
46
47fn read_package(package: &Value, workspace: Option<&Value>) -> Result<Package, Error> {
48    let text = |key: &str| package.get(key).and_then(Value::as_str).unwrap_or_default().to_owned();
49    let name = text("name");
50    let manifest = PathBuf::from(text("manifest_path"));
51    let root = manifest.parent().map(Path::to_path_buf).unwrap_or_default();
52    let config = Config::from_metadata(workspace, package.pointer("/metadata/ravenrs-lint"))
53        .map_err(|error| Error::Config(format!("{name}: {error}")))?;
54    let uses_ravenrs = package.get("dependencies").and_then(Value::as_array).is_some_and(|deps| {
55        deps.iter()
56            .filter_map(|d| d.get("name").and_then(Value::as_str))
57            .any(|d| (d == "ravenrs" || d.starts_with("ravenrs-")) && d != "ravenrs-lint")
58    });
59    let mut dirs: Vec<PathBuf> = package
60        .get("targets")
61        .and_then(Value::as_array)
62        .into_iter()
63        .flatten()
64        .filter(|target| {
65            target.get("kind").and_then(Value::as_array).is_some_and(|kinds| {
66                kinds.iter().filter_map(Value::as_str).any(|kind| {
67                    matches!(kind, "lib" | "rlib" | "dylib" | "cdylib" | "staticlib" | "bin" | "proc-macro")
68                })
69            })
70        })
71        .filter_map(|target| target.get("src_path").and_then(Value::as_str))
72        .filter_map(|path| Path::new(path).parent().map(Path::to_path_buf))
73        .collect();
74    dirs.sort();
75    dirs.dedup();
76    // `src/bin` is already inside `src`.
77    let source_dirs = dirs
78        .iter()
79        .filter(|dir| !dirs.iter().any(|other| other != *dir && dir.starts_with(other)))
80        .cloned()
81        .collect();
82    Ok(Package { name, manifest, root, source_dirs, uses_ravenrs, config })
83}