1use std::collections::{BTreeMap, HashSet, btree_map};
2use std::io::Read;
3use std::process::{Command, Stdio};
4
5use anyhow::{Context, bail};
6use camino::{Utf8Path, Utf8PathBuf};
7use clap::Parser;
8use rustfix::{CodeFix, Filter};
9
10fn bazel_info(
12 bazel: &Utf8Path,
13 workspace: Option<&Utf8Path>,
14 output_base: Option<&Utf8Path>,
15 bazel_startup_options: &[String],
16 bazel_args: &[String],
17) -> anyhow::Result<BTreeMap<String, String>> {
18 let output = bazel_command(bazel, workspace, output_base)
19 .args(bazel_startup_options)
20 .arg("info")
21 .args(bazel_args)
22 .output()?;
23
24 if !output.status.success() {
25 let status = output.status;
26 let stderr = String::from_utf8_lossy(&output.stderr);
27 bail!("bazel info failed: ({status:?})\n{stderr}");
28 }
29
30 let info_map = String::from_utf8(output.stdout)?
32 .trim()
33 .split('\n')
34 .filter_map(|line| line.split_once(':'))
35 .map(|(k, v)| (k.to_owned(), v.trim().to_owned()))
36 .collect();
37
38 Ok(info_map)
39}
40
41fn bazel_command(bazel: &Utf8Path, workspace: Option<&Utf8Path>, output_base: Option<&Utf8Path>) -> Command {
42 let mut cmd = Command::new(bazel);
43
44 cmd
45 .current_dir(workspace.unwrap_or(Utf8Path::new(".")))
47 .env_remove("BAZELISK_SKIP_WRAPPER")
48 .env_remove("BUILD_WORKING_DIRECTORY")
49 .env_remove("BUILD_WORKSPACE_DIRECTORY")
50 .args(output_base.map(|s| format!("--output_base={s}")));
52
53 cmd
54}
55
56fn main() -> anyhow::Result<()> {
60 env_logger::init();
61
62 let config = Config::parse()?;
63
64 log::info!("running build query");
65
66 let mut command = bazel_command(&config.bazel, Some(&config.workspace), Some(&config.output_base))
67 .arg("query")
68 .arg(format!(r#"kind("rust_clippy rule", set({}))"#, config.targets.join(" ")))
69 .stderr(Stdio::inherit())
70 .stdout(Stdio::piped())
71 .spawn()
72 .context("bazel query")?;
73
74 let mut stdout = command.stdout.take().unwrap();
75 let mut targets = String::new();
76 stdout.read_to_string(&mut targets).context("stdout read")?;
77 if !command.wait().context("query wait")?.success() {
78 bail!("failed to run bazel query")
79 }
80
81 let items: Vec<_> = targets.lines().map(|l| l.trim()).filter(|l| !l.is_empty()).collect();
82
83 let mut command = bazel_command(&config.bazel, Some(&config.workspace), Some(&config.output_base))
84 .arg("cquery")
85 .args(&config.bazel_args)
86 .arg(format!("set({})", items.join(" ")))
87 .arg("--output=starlark")
88 .arg("--starlark:expr=[file.path for file in target.files.to_list()]")
89 .arg("--build")
90 .arg("--output_groups=rust_clippy")
91 .stderr(Stdio::inherit())
92 .stdout(Stdio::piped())
93 .spawn()
94 .context("bazel cquery")?;
95
96 let mut stdout = command.stdout.take().unwrap();
97
98 let mut targets = String::new();
99 stdout.read_to_string(&mut targets).context("stdout read")?;
100
101 if !command.wait().context("cquery wait")?.success() {
102 bail!("failed to run bazel cquery")
103 }
104
105 let mut clippy_files = Vec::new();
106
107 for line in targets.lines().map(|l| l.trim()).filter(|l| !l.is_empty()) {
108 clippy_files.extend(serde_json::from_str::<Vec<String>>(line).context("parse line")?);
109 }
110
111 let only = HashSet::new();
112 let mut suggestions = Vec::new();
113 for file in clippy_files {
114 let path = config.execution_root.join(&file);
115 if !path.exists() {
116 log::warn!("missing {file}");
117 continue;
118 }
119
120 let content = std::fs::read_to_string(path).context("read")?;
121 for line in content.lines().map(|l| l.trim()).filter(|l| !l.is_empty()) {
122 if line.contains(r#""$message_type":"artifact""#) {
123 continue;
124 }
125
126 suggestions
127 .extend(rustfix::get_suggestions_from_json(line, &only, Filter::MachineApplicableOnly).context("items")?)
128 }
129 }
130
131 struct File {
132 codefix: CodeFix,
133 }
134
135 let mut files = BTreeMap::new();
136 let solutions: HashSet<_> = suggestions.iter().flat_map(|s| &s.solutions).collect();
137 for solution in solutions {
138 let Some(replacement) = solution.replacements.first() else {
139 continue;
140 };
141
142 let path = config.workspace.join(&replacement.snippet.file_name);
143 let mut entry = files.entry(path);
144 let file = match entry {
145 btree_map::Entry::Vacant(v) => {
146 let file = std::fs::read_to_string(v.key()).context("read source")?;
147
148 v.insert(File {
149 codefix: CodeFix::new(&file),
150 })
151 }
152 btree_map::Entry::Occupied(ref mut o) => o.get_mut(),
153 };
154
155 file.codefix.apply_solution(solution).context("apply solution")?;
156 }
157
158 for (path, file) in files {
159 if !file.codefix.modified() {
160 continue;
161 }
162
163 let modified = file.codefix.finish().context("finish")?;
164 std::fs::write(path, modified).context("write")?;
165 }
166
167 Ok(())
168}
169
170#[derive(Debug)]
171struct Config {
172 workspace: Utf8PathBuf,
174
175 execution_root: Utf8PathBuf,
177
178 output_base: Utf8PathBuf,
180
181 bazel: Utf8PathBuf,
183
184 bazel_args: Vec<String>,
188
189 targets: Vec<String>,
191}
192
193impl Config {
194 fn parse() -> anyhow::Result<Self> {
196 let ConfigParser {
197 workspace,
198 execution_root,
199 output_base,
200 bazel,
201 config,
202 targets,
203 } = ConfigParser::parse();
204
205 let bazel_args = vec![format!("--config={config}")];
206
207 match (workspace, execution_root, output_base) {
208 (Some(workspace), Some(execution_root), Some(output_base)) => Ok(Config {
209 workspace,
210 execution_root,
211 output_base,
212 bazel,
213 bazel_args,
214 targets,
215 }),
216 (workspace, _, output_base) => {
217 let mut info_map = bazel_info(&bazel, workspace.as_deref(), output_base.as_deref(), &[], &bazel_args)?;
218
219 let config = Config {
220 workspace: info_map
221 .remove("workspace")
222 .expect("'workspace' must exist in bazel info")
223 .into(),
224 execution_root: info_map
225 .remove("execution_root")
226 .expect("'execution_root' must exist in bazel info")
227 .into(),
228 output_base: info_map
229 .remove("output_base")
230 .expect("'output_base' must exist in bazel info")
231 .into(),
232 bazel,
233 bazel_args,
234 targets,
235 };
236
237 Ok(config)
238 }
239 }
240 }
241}
242
243#[derive(Debug, Parser)]
244struct ConfigParser {
245 #[clap(long, env = "BUILD_WORKSPACE_DIRECTORY")]
247 workspace: Option<Utf8PathBuf>,
248
249 #[clap(long)]
251 execution_root: Option<Utf8PathBuf>,
252
253 #[clap(long, env = "OUTPUT_BASE")]
255 output_base: Option<Utf8PathBuf>,
256
257 #[clap(long, default_value = "bazel")]
259 bazel: Utf8PathBuf,
260
261 #[clap(long, default_value = "wrapper")]
263 config: String,
264
265 #[clap(default_value = "@//...")]
267 targets: Vec<String>,
268}