1use std::cell::Cell;
10use std::ffi::OsString;
11use std::io::Write;
12use std::sync::Once;
13
14use clap::Parser;
15use serde_json::Map;
16
17mod args;
18mod baseline;
19mod commands;
20mod config;
21mod config_toml;
22mod env;
23mod error;
24mod explain;
25mod filter;
26mod fix;
27mod format;
28mod install;
29mod instances;
30mod lint;
31mod lsp;
32mod mcp;
33mod migrate;
34mod module_cache;
35mod npmrc;
36mod output;
37mod registry;
38mod roots;
39mod trace;
40mod update;
41mod watch;
42mod write;
43
44use args::{Cli, Command, Format};
45use env::Env;
46pub use error::{CliError, Kind};
47use lint::Teardown;
48pub use lint::lint_to_json;
49use output::{emit_error, emit_error_with};
50
51pub fn run(args: impl IntoIterator<Item = OsString>) -> u8 {
68 let raw: Vec<OsString> = args.into_iter().collect();
69 exit_two_on_panic(&raw, &mut std::io::stdout(), || run_with(raw.clone(), Teardown::Free))
70}
71
72thread_local! {
73 static LAST_PANIC: Cell<Option<String>> = const { Cell::new(None) };
77}
78
79fn record_panics() {
82 static ONCE: Once = Once::new();
83 ONCE.call_once(|| {
84 let previous = std::panic::take_hook();
85 std::panic::set_hook(Box::new(move |info| {
86 let _ = LAST_PANIC.try_with(|last| last.set(Some(info.to_string())));
88 previous(info);
89 }));
90 });
91}
92
93fn exit_two_on_panic(raw: &[OsString], stdout: &mut dyn Write, body: impl FnOnce() -> u8) -> u8 {
97 record_panics();
98 LAST_PANIC.with(|last| last.set(None));
99 std::panic::catch_unwind(std::panic::AssertUnwindSafe(body)).unwrap_or_else(|payload| {
100 let recorded = LAST_PANIC.with(Cell::take);
101 if wants_json(raw) {
102 let report = recorded.unwrap_or_else(|| {
103 payload
104 .downcast_ref::<&str>()
105 .copied()
106 .or_else(|| payload.downcast_ref::<String>().map(String::as_str))
107 .unwrap_or("panic")
108 .to_owned()
109 });
110 emit_error_with(&internal_error(&report), Format::Json, Map::new(), stdout);
111 }
112 2
113 })
114}
115
116fn internal_error(report: &str) -> CliError {
119 CliError::new(Kind::Internal, format!("internal error: {report}"))
120}
121
122pub fn run_to_exit(args: impl IntoIterator<Item = OsString>) -> u8 {
130 let raw: Vec<OsString> = args.into_iter().collect();
131 if let Err(error) = env::enter_bazel_run_directory() {
132 emit_error(&error, if wants_json(&raw) { Format::Json } else { Format::Human });
133 return 2;
134 }
135 run_with(raw, Teardown::Leak)
136}
137
138fn run_with(args: impl IntoIterator<Item = OsString>, teardown: Teardown) -> u8 {
139 let raw: Vec<OsString> = args.into_iter().collect();
140 env::enable_timings();
141 let cli = match Cli::try_parse_from(&raw) {
142 Ok(cli) => cli,
143 Err(error) => return usage(&raw, &error),
144 };
145 match cli.command {
146 None if cli.lint.watch => watch::run(&cli.lint, Env::detect()),
147 None => lint::run(&cli.lint, Env::detect(), teardown),
148 Some(Command::Rules(args)) => commands::rules(&args),
149 Some(Command::Schema) => commands::schema(),
150 Some(Command::Init(args)) => commands::init(&args),
151 Some(Command::Config(args)) => config::run(&args),
152 Some(Command::ExplainUsage(args)) => explain::run(&args),
153 Some(Command::Migrate(args)) => migrate::run(&args),
154 Some(Command::Baseline(args)) => baseline::run(&args),
155 Some(Command::Update(args)) => update::run(&args, Env::detect()),
156 Some(Command::List(args)) => instances::list(&args),
157 Some(Command::Json(args)) => instances::json(&args),
158 Some(Command::Format(args)) => format::run(&args, Env::detect(), teardown),
159 Some(Command::Mcp) => mcp::run(),
160 Some(Command::Lsp) => lsp::run(),
161 }
162}
163
164pub fn install_panic_hook(raw: &[OsString]) {
171 let json = wants_json(raw);
172 let default = std::panic::take_hook();
173 std::panic::set_hook(Box::new(move |info| {
174 default(info);
175 if json {
176 emit_error(&internal_error(&info.to_string()), Format::Json);
177 }
178 std::process::exit(2);
179 }));
180}
181
182fn usage(raw: &[OsString], error: &clap::Error) -> u8 {
185 if !error.use_stderr() {
186 let _ = error.print();
187 return 0;
188 }
189 if wants_json(raw) {
190 let message = error.to_string();
191 emit_error(&CliError::new(Kind::Usage, message.trim_end()), Format::Json);
192 } else {
193 let _ = error.print();
194 }
195 2
196}
197
198fn wants_json(raw: &[OsString]) -> bool {
201 let args: Vec<&str> = raw.iter().filter_map(|arg| arg.to_str()).collect();
202 let mut values = Vec::new();
203 for (index, arg) in args.iter().enumerate() {
204 if let Some(value) = arg.strip_prefix("--format=") {
205 values.push(value);
206 } else if *arg == "--format" {
207 values.extend(args.get(index + 1));
208 }
209 }
210 values
211 .iter()
212 .find(|value| !value.contains('='))
213 .or_else(|| values.first())
214 .is_some_and(|value| value.split('=').next() == Some("json"))
215}
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220
221 fn args(list: &[&str]) -> Vec<OsString> {
222 list.iter().map(OsString::from).collect()
223 }
224
225 #[test]
226 fn a_panic_exits_two_in_every_format() {
227 for (raw, json) in [(args(&["skif"]), false), (args(&["skif", "--format", "json"]), true)] {
228 let mut stdout = Vec::new();
229 assert_eq!(exit_two_on_panic(&raw, &mut stdout, || panic!("boom")), 2);
230 if !json {
231 assert!(stdout.is_empty(), "{}", String::from_utf8_lossy(&stdout));
232 continue;
233 }
234 let document: serde_json::Value = serde_json::from_slice(&stdout).unwrap();
235 assert_eq!(document["error"]["kind"], "internal", "{document:#}");
236 let message = document["error"]["message"].as_str().unwrap();
237 assert!(
238 message.starts_with("internal error: panicked at ")
239 && message.contains("lib.rs:")
240 && message.ends_with(":\nboom"),
241 "the message names the location and the panic: {message}"
242 );
243 }
244 }
245
246 fn panic_message(body: impl FnOnce() -> u8) -> String {
248 let mut stdout = Vec::new();
249 assert_eq!(
250 exit_two_on_panic(&args(&["skif", "--format", "json"]), &mut stdout, body),
251 2
252 );
253 let document: serde_json::Value = serde_json::from_slice(&stdout).unwrap();
254 document["error"]["message"].as_str().unwrap().to_owned()
255 }
256
257 #[test]
258 fn a_panic_on_another_thread_is_not_reported_as_this_one() {
259 let message = panic_message(|| {
262 let other = std::thread::spawn(|| std::panic::panic_any("another thread's panic"));
263 assert!(other.join().is_err(), "the other thread panicked");
264 std::panic::resume_unwind(Box::new("boom"))
265 });
266 assert_eq!(message, "internal error: boom");
267 }
268
269 #[test]
270 fn a_panic_caught_earlier_on_this_thread_is_not_reported_again() {
271 record_panics();
272 let earlier = std::panic::catch_unwind(|| std::panic::panic_any("an earlier panic"));
273 assert!(earlier.is_err(), "the earlier body panicked");
274 let message = panic_message(|| std::panic::resume_unwind(Box::new(String::from("boom"))));
275 assert_eq!(message, "internal error: boom");
276 }
277
278 #[test]
279 fn without_a_panic_the_exit_code_is_the_body_s() {
280 let mut stdout = Vec::new();
281 assert_eq!(
282 exit_two_on_panic(&args(&["skif", "--format", "json"]), &mut stdout, || 1),
283 1
284 );
285 assert_eq!(stdout, b"");
286 }
287}