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) -> anyhow::Result<BTreeMap<String, String>> {
17 let output = bazel_command(bazel, workspace, output_base)
18 .args(bazel_startup_options)
19 .arg("info")
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 env_logger::init();
59
60 let config = Config::parse()?;
61
62 log::info!("running build query");
63
64 let mut command = bazel_command(&config.bazel, Some(&config.workspace), Some(&config.output_base))
65 .arg("query")
66 .arg(format!(r#"kind("rust_clippy rule", set({}))"#, config.targets.join(" ")))
67 .stderr(Stdio::inherit())
68 .stdout(Stdio::piped())
69 .spawn()
70 .context("bazel query")?;
71
72 let mut stdout = command.stdout.take().unwrap();
73 let mut targets = String::new();
74 stdout.read_to_string(&mut targets).context("stdout read")?;
75 if !command.wait().context("query wait")?.success() {
76 bail!("failed to run bazel query")
77 }
78
79 let items: Vec<_> = targets.lines().map(|l| l.trim()).filter(|l| !l.is_empty()).collect();
80
81 let mut command = bazel_command(&config.bazel, Some(&config.workspace), Some(&config.output_base))
82 .arg("cquery")
83 .args(&config.bazel_args)
84 .arg(format!("set({})", items.join(" ")))
85 .arg("--output=starlark")
86 .arg("--starlark:expr=[file.path for file in target.files.to_list()]")
87 .arg("--build")
88 .arg("--output_groups=rust_clippy")
89 .stderr(Stdio::inherit())
90 .stdout(Stdio::piped())
91 .spawn()
92 .context("bazel cquery")?;
93
94 let mut stdout = command.stdout.take().unwrap();
95
96 let mut targets = String::new();
97 stdout.read_to_string(&mut targets).context("stdout read")?;
98
99 if !command.wait().context("cquery wait")?.success() {
100 bail!("failed to run bazel cquery")
101 }
102
103 let mut clippy_files = Vec::new();
104
105 for line in targets.lines().map(|l| l.trim()).filter(|l| !l.is_empty()) {
106 clippy_files.extend(serde_json::from_str::<Vec<String>>(line).context("parse line")?);
107 }
108
109 let only = HashSet::new();
110 let mut suggestions = Vec::new();
111 for file in clippy_files {
112 let path = config.execution_root.join(&file);
113 if !path.exists() {
114 log::warn!("missing {file}");
115 continue;
116 }
117
118 let content = std::fs::read_to_string(path).context("read")?;
119 for line in content.lines().map(|l| l.trim()).filter(|l| !l.is_empty()) {
120 if line.contains(r#""$message_type":"artifact""#) {
121 continue;
122 }
123
124 suggestions
125 .extend(rustfix::get_suggestions_from_json(line, &only, Filter::MachineApplicableOnly).context("items")?)
126 }
127 }
128
129 struct File {
130 codefix: CodeFix,
131 }
132
133 let mut files = BTreeMap::new();
134 let solutions: HashSet<_> = suggestions.iter().flat_map(|s| &s.solutions).collect();
135 for solution in solutions {
136 let Some(replacement) = solution.replacements.first() else {
137 continue;
138 };
139
140 let path = config.workspace.join(&replacement.snippet.file_name);
141 let mut entry = files.entry(path);
142 let file = match entry {
143 btree_map::Entry::Vacant(v) => {
144 let file = std::fs::read_to_string(v.key()).context("read source")?;
145
146 v.insert(File {
147 codefix: CodeFix::new(&file),
148 })
149 }
150 btree_map::Entry::Occupied(ref mut o) => o.get_mut(),
151 };
152
153 file.codefix.apply_solution(solution).context("apply solution")?;
154 }
155
156 for (path, file) in files {
157 if !file.codefix.modified() {
158 continue;
159 }
160
161 let modified = file.codefix.finish().context("finish")?;
162 std::fs::write(path, modified).context("write")?;
163 }
164
165 Ok(())
166}
167
168#[derive(Debug)]
169struct Config {
170 workspace: Utf8PathBuf,
172
173 execution_root: Utf8PathBuf,
175
176 output_base: Utf8PathBuf,
178
179 bazel: Utf8PathBuf,
181
182 bazel_args: Vec<String>,
186
187 targets: Vec<String>,
189}
190
191impl Config {
192 fn parse() -> anyhow::Result<Self> {
194 let ConfigParser {
195 workspace,
196 execution_root,
197 output_base,
198 bazel,
199 config,
200 targets,
201 } = ConfigParser::parse();
202
203 let bazel_args = vec![format!("--config={config}")];
204
205 match (workspace, execution_root, output_base) {
206 (Some(workspace), Some(execution_root), Some(output_base)) => Ok(Config {
207 workspace,
208 execution_root,
209 output_base,
210 bazel,
211 bazel_args,
212 targets,
213 }),
214 (workspace, _, output_base) => {
215 let mut info_map = bazel_info(&bazel, workspace.as_deref(), output_base.as_deref(), &[])?;
216
217 let config = Config {
218 workspace: info_map
219 .remove("workspace")
220 .expect("'workspace' must exist in bazel info")
221 .into(),
222 execution_root: info_map
223 .remove("execution_root")
224 .expect("'execution_root' must exist in bazel info")
225 .into(),
226 output_base: info_map
227 .remove("output_base")
228 .expect("'output_base' must exist in bazel info")
229 .into(),
230 bazel,
231 bazel_args,
232 targets,
233 };
234
235 Ok(config)
236 }
237 }
238 }
239}
240
241#[derive(Debug, Parser)]
242struct ConfigParser {
243 #[clap(long, env = "BUILD_WORKSPACE_DIRECTORY")]
245 workspace: Option<Utf8PathBuf>,
246
247 #[clap(long)]
249 execution_root: Option<Utf8PathBuf>,
250
251 #[clap(long, env = "OUTPUT_BASE")]
253 output_base: Option<Utf8PathBuf>,
254
255 #[clap(long, default_value = "bazel")]
257 bazel: Utf8PathBuf,
258
259 #[clap(long, default_value = "wrapper")]
261 config: String,
262
263 #[clap(default_value = "@//...")]
265 targets: Vec<String>,
266}