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
52fn main() -> anyhow::Result<()> {
53    let config = Config::parse()?;
54
55    let command = bazel_command(&config.bazel, Some(&config.workspace), Some(&config.output_base))
56        .arg("query")
57        .arg(format!(r#"kind("rust_clippy rule", set({}))"#, config.targets.join(" ")))
58        .output()
59        .context("bazel query")?;
60
61    if !command.status.success() {
62        anyhow::bail!("failed to query targets: {}", String::from_utf8_lossy(&command.stderr))
63    }
64
65    let targets = String::from_utf8_lossy(&command.stdout);
66    let items: Vec<_> = targets.lines().map(|l| l.trim()).filter(|l| !l.is_empty()).collect();
67
68    let command = bazel_command(&config.bazel, Some(&config.workspace), Some(&config.output_base))
69        .arg("cquery")
70        .args(&config.bazel_args)
71        .arg(format!("set({})", items.join(" ")))
72        .arg("--output=starlark")
73        .arg("--keep_going")
74        .arg("--starlark:expr=[file.path for file in target.files.to_list()]")
75        .arg("--build")
76        .arg("--output_groups=rust_clippy")
77        .output()
78        .context("bazel cquery")?;
79
80    let targets = String::from_utf8_lossy(&command.stdout);
81
82    let mut clippy_files = Vec::new();
83    for line in targets.lines().map(|l| l.trim()).filter(|l| !l.is_empty()) {
84        clippy_files.extend(serde_json::from_str::<Vec<String>>(line).context("parse line")?);
85    }
86
87    for file in clippy_files {
88        let path = config.execution_root.join(&file);
89        if !path.exists() {
90            continue;
91        }
92
93        let content = std::fs::read_to_string(path).context("read")?;
94        for line in content.lines().map(|l| l.trim()).filter(|l| !l.is_empty()) {
95            println!("{line}");
96        }
97    }
98
99    Ok(())
100}
101
102#[derive(Debug)]
103struct Config {
104    /// The path to the Bazel workspace directory. If not specified, uses the result of `bazel info workspace`.
105    workspace: Utf8PathBuf,
106
107    /// The path to the Bazel execution root. If not specified, uses the result of `bazel info execution_root`.
108    execution_root: Utf8PathBuf,
109
110    /// The path to the Bazel output user root. If not specified, uses the result of `bazel info output_base`.
111    output_base: Utf8PathBuf,
112
113    /// The path to a Bazel binary.
114    bazel: Utf8PathBuf,
115
116    /// Arguments to pass to `bazel` invocations.
117    /// See the [Command-Line Reference](<https://bazel.build/reference/command-line-reference>)
118    /// for more details.
119    bazel_args: Vec<String>,
120
121    /// Space separated list of target patterns that comes after all other args.
122    targets: Vec<String>,
123}
124
125impl Config {
126    // Parse the configuration flags and supplement with bazel info as needed.
127    fn parse() -> anyhow::Result<Self> {
128        let ConfigParser {
129            workspace,
130            execution_root,
131            output_base,
132            bazel,
133            config,
134            targets,
135        } = ConfigParser::parse();
136
137        let bazel_args = config.into_iter().map(|s| format!("--config={s}")).collect();
138
139        match (workspace, execution_root, output_base) {
140            (Some(workspace), Some(execution_root), Some(output_base)) => Ok(Config {
141                workspace,
142                execution_root,
143                output_base,
144                bazel,
145                bazel_args,
146                targets,
147            }),
148            (workspace, _, output_base) => {
149                let mut info_map = bazel_info(&bazel, workspace.as_deref(), output_base.as_deref(), &[])?;
150
151                let config = Config {
152                    workspace: info_map
153                        .remove("workspace")
154                        .expect("'workspace' must exist in bazel info")
155                        .into(),
156                    execution_root: info_map
157                        .remove("execution_root")
158                        .expect("'execution_root' must exist in bazel info")
159                        .into(),
160                    output_base: info_map
161                        .remove("output_base")
162                        .expect("'output_base' must exist in bazel info")
163                        .into(),
164                    bazel,
165                    bazel_args,
166                    targets,
167                };
168
169                Ok(config)
170            }
171        }
172    }
173}
174
175#[derive(Debug, Parser)]
176struct ConfigParser {
177    /// The path to the Bazel workspace directory. If not specified, uses the result of `bazel info workspace`.
178    #[clap(long, env = "BUILD_WORKSPACE_DIRECTORY")]
179    workspace: Option<Utf8PathBuf>,
180
181    /// The path to the Bazel execution root. If not specified, uses the result of `bazel info execution_root`.
182    #[clap(long)]
183    execution_root: Option<Utf8PathBuf>,
184
185    /// The path to the Bazel output user root. If not specified, uses the result of `bazel info output_base`.
186    #[clap(long, env = "OUTPUT_BASE")]
187    output_base: Option<Utf8PathBuf>,
188
189    /// The path to a Bazel binary.
190    #[clap(long, default_value = "bazel")]
191    bazel: Utf8PathBuf,
192
193    /// A config to pass to Bazel invocations with `--config=<config>`.
194    #[clap(long)]
195    config: Option<String>,
196
197    /// Space separated list of target patterns that comes after all other args.
198    #[clap(default_value = "@//...")]
199    targets: Vec<String>,
200}