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) -> 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 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 .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 .args(output_base.map(|s| format!("--output_base={s}")));
48
49 cmd
50}
51
52fn 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 workspace: Utf8PathBuf,
109
110 execution_root: Utf8PathBuf,
112
113 output_base: Utf8PathBuf,
115
116 bazel: Utf8PathBuf,
118
119 bazel_args: Vec<String>,
123
124 targets: Vec<String>,
126}
127
128impl Config {
129 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 #[clap(long, env = "BUILD_WORKSPACE_DIRECTORY")]
182 workspace: Option<Utf8PathBuf>,
183
184 #[clap(long)]
186 execution_root: Option<Utf8PathBuf>,
187
188 #[clap(long, env = "OUTPUT_BASE")]
190 output_base: Option<Utf8PathBuf>,
191
192 #[clap(long, default_value = "bazel")]
194 bazel: Utf8PathBuf,
195
196 #[clap(long)]
198 config: Option<String>,
199
200 #[clap(default_value = "@//...")]
202 targets: Vec<String>,
203}