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<()> {
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 workspace: Utf8PathBuf,
106
107 execution_root: Utf8PathBuf,
109
110 output_base: Utf8PathBuf,
112
113 bazel: Utf8PathBuf,
115
116 bazel_args: Vec<String>,
120
121 targets: Vec<String>,
123}
124
125impl Config {
126 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 #[clap(long, env = "BUILD_WORKSPACE_DIRECTORY")]
179 workspace: Option<Utf8PathBuf>,
180
181 #[clap(long)]
183 execution_root: Option<Utf8PathBuf>,
184
185 #[clap(long, env = "OUTPUT_BASE")]
187 output_base: Option<Utf8PathBuf>,
188
189 #[clap(long, default_value = "bazel")]
191 bazel: Utf8PathBuf,
192
193 #[clap(long)]
195 config: Option<String>,
196
197 #[clap(default_value = "@//...")]
199 targets: Vec<String>,
200}