rust_analyzer_check/
main.rs1use std::collections::BTreeMap;
2use std::process::Command;
3
4use anyhow::{Context, bail};
5use camino::{Utf8Path, Utf8PathBuf};
6use clap::Parser;
7
8fn 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 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 .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 .args(output_base.map(|s| format!("--output_base={s}")));
50
51 cmd
52}
53
54fn 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 workspace: Utf8PathBuf,
111
112 execution_root: Utf8PathBuf,
114
115 output_base: Utf8PathBuf,
117
118 bazel: Utf8PathBuf,
120
121 bazel_args: Vec<String>,
125
126 targets: Vec<String>,
128}
129
130impl Config {
131 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 #[clap(long, env = "BUILD_WORKSPACE_DIRECTORY")]
184 workspace: Option<Utf8PathBuf>,
185
186 #[clap(long)]
188 execution_root: Option<Utf8PathBuf>,
189
190 #[clap(long, env = "OUTPUT_BASE")]
192 output_base: Option<Utf8PathBuf>,
193
194 #[clap(long, default_value = "bazel")]
196 bazel: Utf8PathBuf,
197
198 #[clap(long)]
200 config: Option<String>,
201
202 #[clap(default_value = "@//...")]
204 targets: Vec<String>,
205}