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