rust_analyzer_check/
main.rs

1use std::collections::BTreeMap;
2use std::process::Command;
3
4use anyhow::{Context, bail};
5use camino::{Utf8Path, Utf8PathBuf};
6use clap::Parser;
7
8/// Executes `bazel info` to get a map of context information.
9fn bazel_info(
10    bazel: &Utf8Path,
11    workspace: Option<&Utf8Path>,
12    output_base: Option<&Utf8Path>,
13    bazel_startup_options: &[String],
14    bazel_args: &[String],
15) -> anyhow::Result<BTreeMap<String, String>> {
16    let output = bazel_command(bazel, workspace, output_base)
17        .args(bazel_startup_options)
18        .arg("info")
19        .args(bazel_args)
20        .output()?;
21
22    if !output.status.success() {
23        let status = output.status;
24        let stderr = String::from_utf8_lossy(&output.stderr);
25        bail!("bazel info failed: ({status:?})\n{stderr}");
26    }
27
28    // Extract and parse the output.
29    let info_map = String::from_utf8(output.stdout)?
30        .trim()
31        .split('\n')
32        .filter_map(|line| line.split_once(':'))
33        .map(|(k, v)| (k.to_owned(), v.trim().to_owned()))
34        .collect();
35
36    Ok(info_map)
37}
38
39fn bazel_command(bazel: &Utf8Path, workspace: Option<&Utf8Path>, output_base: Option<&Utf8Path>) -> Command {
40    let mut cmd = Command::new(bazel);
41
42    cmd
43        // Switch to the workspace directory if one was provided.
44        .current_dir(workspace.unwrap_or(Utf8Path::new(".")))
45        .env_remove("BAZELISK_SKIP_WRAPPER")
46        .env_remove("BUILD_WORKING_DIRECTORY")
47        .env_remove("BUILD_WORKSPACE_DIRECTORY")
48        // Set the output_base if one was provided.
49        .args(output_base.map(|s| format!("--output_base={s}")));
50
51    cmd
52}
53
54// TODO(david): This shells out to an expected rule in the workspace root //:rust_analyzer that the user must define.
55// It would be more convenient if it could automatically discover all the rust code in the workspace if this target
56// does not exist.
57fn main() -> anyhow::Result<()> {
58    let config = Config::parse()?;
59
60    let command = bazel_command(&config.bazel, Some(&config.workspace), Some(&config.output_base))
61        .arg("query")
62        .arg(format!(r#"kind("rust_clippy rule", set({}))"#, config.targets.join(" ")))
63        .output()
64        .context("bazel query")?;
65
66    if !command.status.success() {
67        anyhow::bail!("failed to query targets: {}", String::from_utf8_lossy(&command.stderr))
68    }
69
70    let targets = String::from_utf8_lossy(&command.stdout);
71    let items: Vec<_> = targets.lines().map(|l| l.trim()).filter(|l| !l.is_empty()).collect();
72
73    let command = bazel_command(&config.bazel, Some(&config.workspace), Some(&config.output_base))
74        .arg("cquery")
75        .args(&config.bazel_args)
76        .arg(format!("set({})", items.join(" ")))
77        .arg("--output=starlark")
78        .arg("--keep_going")
79        .arg("--starlark:expr=[file.path for file in target.files.to_list()]")
80        .arg("--build")
81        .arg("--output_groups=rust_clippy")
82        .output()
83        .context("bazel cquery")?;
84
85    let targets = String::from_utf8_lossy(&command.stdout);
86
87    let mut clippy_files = Vec::new();
88    for line in targets.lines().map(|l| l.trim()).filter(|l| !l.is_empty()) {
89        clippy_files.extend(serde_json::from_str::<Vec<String>>(line).context("parse line")?);
90    }
91
92    for file in clippy_files {
93        let path = config.execution_root.join(&file);
94        if !path.exists() {
95            continue;
96        }
97
98        let content = std::fs::read_to_string(path).context("read")?;
99        for line in content.lines().map(|l| l.trim()).filter(|l| !l.is_empty()) {
100            println!("{line}");
101        }
102    }
103
104    Ok(())
105}
106
107#[derive(Debug)]
108struct Config {
109    /// The path to the Bazel workspace directory. If not specified, uses the result of `bazel info workspace`.
110    workspace: Utf8PathBuf,
111
112    /// The path to the Bazel execution root. If not specified, uses the result of `bazel info execution_root`.
113    execution_root: Utf8PathBuf,
114
115    /// The path to the Bazel output user root. If not specified, uses the result of `bazel info output_base`.
116    output_base: Utf8PathBuf,
117
118    /// The path to a Bazel binary.
119    bazel: Utf8PathBuf,
120
121    /// Arguments to pass to `bazel` invocations.
122    /// See the [Command-Line Reference](<https://bazel.build/reference/command-line-reference>)
123    /// for more details.
124    bazel_args: Vec<String>,
125
126    /// Space separated list of target patterns that comes after all other args.
127    targets: Vec<String>,
128}
129
130impl Config {
131    // Parse the configuration flags and supplement with bazel info as needed.
132    fn parse() -> anyhow::Result<Self> {
133        let ConfigParser {
134            workspace,
135            execution_root,
136            output_base,
137            bazel,
138            config,
139            targets,
140        } = ConfigParser::parse();
141
142        let bazel_args = config.into_iter().map(|s| format!("--config={s}")).collect();
143
144        match (workspace, execution_root, output_base) {
145            (Some(workspace), Some(execution_root), Some(output_base)) => Ok(Config {
146                workspace,
147                execution_root,
148                output_base,
149                bazel,
150                bazel_args,
151                targets,
152            }),
153            (workspace, _, output_base) => {
154                let mut info_map = bazel_info(&bazel, workspace.as_deref(), output_base.as_deref(), &[], &bazel_args)?;
155
156                let config = Config {
157                    workspace: info_map
158                        .remove("workspace")
159                        .expect("'workspace' must exist in bazel info")
160                        .into(),
161                    execution_root: info_map
162                        .remove("execution_root")
163                        .expect("'execution_root' must exist in bazel info")
164                        .into(),
165                    output_base: info_map
166                        .remove("output_base")
167                        .expect("'output_base' must exist in bazel info")
168                        .into(),
169                    bazel,
170                    bazel_args,
171                    targets,
172                };
173
174                Ok(config)
175            }
176        }
177    }
178}
179
180#[derive(Debug, Parser)]
181struct ConfigParser {
182    /// The path to the Bazel workspace directory. If not specified, uses the result of `bazel info workspace`.
183    #[clap(long, env = "BUILD_WORKSPACE_DIRECTORY")]
184    workspace: Option<Utf8PathBuf>,
185
186    /// The path to the Bazel execution root. If not specified, uses the result of `bazel info execution_root`.
187    #[clap(long)]
188    execution_root: Option<Utf8PathBuf>,
189
190    /// The path to the Bazel output user root. If not specified, uses the result of `bazel info output_base`.
191    #[clap(long, env = "OUTPUT_BASE")]
192    output_base: Option<Utf8PathBuf>,
193
194    /// The path to a Bazel binary.
195    #[clap(long, default_value = "bazel")]
196    bazel: Utf8PathBuf,
197
198    /// A config to pass to Bazel invocations with `--config=<config>`.
199    #[clap(long)]
200    config: Option<String>,
201
202    /// Space separated list of target patterns that comes after all other args.
203    #[clap(default_value = "@//...")]
204    targets: Vec<String>,
205}