Skip to main content

anza_xtask/commands/
dev_deps.rs

1use {
2    anyhow::{Context, Result},
3    clap::Subcommand,
4    log::{error, warn},
5    std::{
6        collections::HashMap,
7        fs,
8        path::{Path, PathBuf},
9    },
10    toml_edit::DocumentMut,
11};
12
13#[derive(clap::Args, Debug)]
14pub struct CommandArgs {
15    #[command(subcommand)]
16    command: DevDepsCommands,
17}
18
19#[derive(Subcommand, Debug)]
20enum DevDepsCommands {
21    #[command(about = "Check dev-dependencies for workspace members using 'workspace = true'")]
22    Check(CheckArgs),
23}
24
25#[derive(clap::Args, Debug)]
26pub struct CheckArgs {
27    /// Path to the workspace Cargo.toml (defaults to ./Cargo.toml)
28    #[arg(long, default_value = "Cargo.toml")]
29    pub manifest_path: PathBuf,
30}
31
32pub fn run(args: CommandArgs) -> Result<()> {
33    match args.command {
34        DevDepsCommands::Check(check_args) => run_check(check_args),
35    }
36}
37
38fn run_check(args: CheckArgs) -> Result<()> {
39    let workspace_root = args
40        .manifest_path
41        .parent()
42        .unwrap_or_else(|| Path::new("."));
43
44    // 1. Parse workspace Cargo.toml to get members
45    let workspace_toml_content = fs::read_to_string(&args.manifest_path)
46        .with_context(|| format!("Failed to read {:?}", args.manifest_path))?;
47    let workspace_doc = workspace_toml_content
48        .parse::<DocumentMut>()
49        .with_context(|| format!("Failed to parse {:?}", args.manifest_path))?;
50
51    let members = workspace_doc
52        .get("workspace")
53        .and_then(|ws| ws.get("members"))
54        .and_then(|m| m.as_array())
55        .context("No workspace.members found")?;
56
57    // 2. Build map of package names -> member paths
58    let mut package_to_member = HashMap::new();
59    for member in members.iter() {
60        let member_path = member.as_str().context("Member path is not a string")?;
61        let member_cargo_toml = workspace_root.join(member_path).join("Cargo.toml");
62
63        if !member_cargo_toml.exists() {
64            warn!("(Skipping) {member_path}/Cargo.toml not found");
65            continue;
66        }
67
68        let content = fs::read_to_string(&member_cargo_toml)?;
69        let doc = content.parse::<DocumentMut>()?;
70
71        if let Some(package_name) = doc
72            .get("package")
73            .and_then(|p| p.get("name"))
74            .and_then(|n| n.as_str())
75        {
76            package_to_member.insert(package_name.to_string(), member_path);
77        }
78    }
79
80    // 3. Check each member's dev-dependencies
81    let mut total_issues = 0;
82    for member in members.iter() {
83        let member_path = member.as_str().context("Member path is not a string")?;
84        let member_cargo_toml = workspace_root.join(member_path).join("Cargo.toml");
85
86        if !member_cargo_toml.exists() {
87            continue;
88        }
89
90        let content = fs::read_to_string(&member_cargo_toml)?;
91        let doc = content.parse::<DocumentMut>()?;
92
93        if let Some(dev_deps) = doc.get("dev-dependencies").and_then(|dd| dd.as_table()) {
94            for (dep_name, dep_value) in dev_deps.iter() {
95                // Check if this dependency uses workspace = true
96                if let Some(table) = dep_value.as_table_like() {
97                    if table
98                        .get("workspace")
99                        .and_then(|w| w.as_bool())
100                        .unwrap_or(false)
101                    {
102                        // Check if this is a workspace member
103                        if package_to_member.contains_key(dep_name) {
104                            error!(
105                                "{}/Cargo.toml - `{}` in dev-dependencies should use path = \"...\"",
106                                member_path, dep_name
107                            );
108                            total_issues += 1;
109                        }
110                    }
111                }
112            }
113        }
114    }
115
116    if total_issues == 0 {
117        Ok(())
118    } else {
119        anyhow::bail!(
120            "Found {} dev-dependencies using 'workspace = true' that should use path",
121            total_issues
122        )
123    }
124}