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