1pub mod analysis;
2mod cargo_clippy;
3pub mod config;
4mod config_file;
5pub mod emit;
6pub mod fix;
7mod fix_apply;
8mod path_pattern;
9pub mod policy;
10pub mod report;
11mod report_html;
12mod report_sarif;
13pub mod rules;
14pub mod runner;
15pub mod semantic;
16pub mod span;
17#[cfg(test)]
18pub(crate) mod test_support;
19mod text_report;
20mod toolchain;
21
22use crate::analysis::Workspace;
23use crate::config::{OutputFormat, Policy, ToolchainMode};
24use crate::report::{AdapterRun, Report};
25use crate::runner::Runner;
26use cargo_clippy::run_clippy;
27use clap::Parser;
28use config_file::{default_config_path, load_from, workspace_root, write_default_config};
29use fix_apply::{ApplyError, PlannedEdits, apply_planned_edits, plan_edits, print_dry_run};
30use std::path::{Path, PathBuf};
31use std::process::ExitCode as ProcessExitCode;
32use std::{fs, io};
33use text_report::render_text_report;
34use toolchain::{ResolvedToolchain, resolve_toolchain};
35
36#[derive(Clone, Copy)]
37#[repr(transparent)]
38pub struct ExitCode(pub i32);
39
40impl From<i32> for ExitCode {
41 fn from(value: i32) -> Self {
42 Self(value)
43 }
44}
45
46impl std::process::Termination for ExitCode {
47 fn report(self) -> ProcessExitCode {
48 ProcessExitCode::from(self.0 as u8)
49 }
50}
51
52#[derive(Debug, Parser)]
53#[command(name = "rscheck", version)]
54pub struct Cli {
55 #[command(subcommand)]
56 pub cmd: Command,
57}
58
59#[derive(Debug, clap::Subcommand)]
60pub enum Command {
61 Check(CheckArgs),
62 ListRules,
63 Explain { rule_id: String },
64 Init(InitArgs),
65}
66
67#[derive(Debug, clap::Args)]
68pub struct CommonOutputArgs {
69 #[arg(long)]
70 pub config: Option<PathBuf>,
71
72 #[arg(long, value_enum)]
73 pub format: Option<FormatArg>,
74
75 #[arg(long)]
76 pub output: Option<PathBuf>,
77}
78
79#[derive(Debug, Clone, Copy, clap::ValueEnum)]
80pub enum FormatArg {
81 Text,
82 Json,
83 Sarif,
84 Html,
85}
86
87impl From<FormatArg> for OutputFormat {
88 fn from(value: FormatArg) -> Self {
89 match value {
90 FormatArg::Text => OutputFormat::Text,
91 FormatArg::Json => OutputFormat::Json,
92 FormatArg::Sarif => OutputFormat::Sarif,
93 FormatArg::Html => OutputFormat::Html,
94 }
95 }
96}
97
98#[derive(Debug, clap::Args)]
99pub struct CheckArgs {
100 #[command(flatten)]
101 pub out: CommonOutputArgs,
102
103 #[arg(long, default_value_t = true)]
104 pub rscheck: bool,
105
106 #[arg(long)]
107 pub write: bool,
108
109 #[arg(long = "unsafe")]
110 pub unsafe_fixes: bool,
111
112 #[arg(long)]
113 pub dry_run: bool,
114
115 #[arg(long, default_value_t = 10)]
116 pub max_fix_iterations: u32,
117
118 #[arg(long, value_enum)]
119 pub toolchain: Option<ToolchainArg>,
120
121 #[arg(trailing_var_arg = true)]
122 pub cargo_args: Vec<String>,
123}
124
125#[derive(Debug, Clone, Copy, clap::ValueEnum)]
126pub enum ToolchainArg {
127 Current,
128 Auto,
129 Nightly,
130}
131
132impl From<ToolchainArg> for ToolchainMode {
133 fn from(value: ToolchainArg) -> Self {
134 match value {
135 ToolchainArg::Current => ToolchainMode::Current,
136 ToolchainArg::Auto => ToolchainMode::Auto,
137 ToolchainArg::Nightly => ToolchainMode::Nightly,
138 }
139 }
140}
141
142#[derive(Debug, clap::Args)]
143pub struct InitArgs {
144 #[arg(long)]
145 pub path: Option<PathBuf>,
146}
147
148pub fn main_entry() -> ExitCode {
149 init_tracing();
150 let cli = Cli::parse();
151 match cli.cmd {
152 Command::Check(args) => run_check(args),
153 Command::ListRules => run_list_rules(),
154 Command::Explain { rule_id } => run_explain(&rule_id),
155 Command::Init(args) => run_init(args),
156 }
157}
158
159fn init_tracing() {
160 let _ = tracing_subscriber::fmt()
161 .with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
162 .try_init();
163}
164
165fn run_list_rules() -> ExitCode {
166 for info in rules::rule_catalog() {
167 println!(
168 "{}\t{:?}\t{:?}\t{:?}\t{}",
169 info.id, info.family, info.backend, info.default_level, info.summary
170 );
171 }
172 ExitCode::from(0)
173}
174
175fn run_explain(rule_id: &str) -> ExitCode {
176 let info = rules::rule_catalog().into_iter().find(|i| i.id == rule_id);
177 match info {
178 Some(info) => {
179 println!(
180 "{}\n\nfamily: {:?}\nbackend: {:?}\ndefault: {:?}\nschema: {}\n\n{}\n",
181 info.id,
182 info.family,
183 info.backend,
184 info.default_level,
185 info.schema,
186 info.config_example
187 );
188 ExitCode::from(0)
189 }
190 None => {
191 eprintln!("unknown rule: {rule_id}");
192 ExitCode::from(2)
193 }
194 }
195}
196
197fn run_init(args: InitArgs) -> ExitCode {
198 let root = match workspace_root() {
199 Ok(p) => p,
200 Err(err) => {
201 eprintln!("{err}");
202 return ExitCode::from(2);
203 }
204 };
205
206 let path = args.path.unwrap_or_else(|| default_config_path(&root));
207 if path.exists() {
208 eprintln!("config already exists: {}", path.to_string_lossy());
209 return ExitCode::from(1);
210 }
211
212 if let Err(err) = write_default_config(&path) {
213 eprintln!("failed to write config: {err}");
214 return ExitCode::from(2);
215 }
216
217 println!("{}", path.to_string_lossy());
218 ExitCode::from(0)
219}
220
221fn run_check(args: CheckArgs) -> ExitCode {
222 if let Err(code) = validate_check_args(&args) {
223 return code;
224 }
225
226 let root = match resolve_workspace_root() {
227 Ok(root) => root,
228 Err(code) => return code,
229 };
230 let policy = match load_check_policy(&args, &root) {
231 Ok(policy) => policy,
232 Err(code) => return code,
233 };
234 let resolved_toolchain = match resolve_toolchain(&policy, args.toolchain.map(Into::into)) {
235 Ok(toolchain) => toolchain,
236 Err(err) => return toolchain_error_to_exit_code(err),
237 };
238
239 execute_check_iterations(args, root, policy, resolved_toolchain)
240}
241
242fn validate_check_args(args: &CheckArgs) -> Result<(), ExitCode> {
243 if args.write && args.dry_run {
244 eprintln!("`--write` and `--dry-run` are mutually exclusive");
245 return Err(ExitCode::from(2));
246 }
247 Ok(())
248}
249
250fn resolve_workspace_root() -> Result<PathBuf, ExitCode> {
251 workspace_root().map_err(|err| {
252 eprintln!("{err}");
253 ExitCode::from(2)
254 })
255}
256
257fn load_check_policy(args: &CheckArgs, root: &Path) -> Result<Policy, ExitCode> {
258 let config_path = args
259 .out
260 .config
261 .clone()
262 .unwrap_or_else(|| default_config_path(root));
263 let mut policy = if config_path.exists() {
264 load_from(&config_path).map_err(|err| {
265 eprintln!("{err}");
266 ExitCode::from(2)
267 })?
268 } else {
269 Policy::default_with_rules(rules::default_rule_settings())
270 };
271 if let Some(format) = args.out.format {
272 policy.output.format = format.into();
273 }
274 if let Some(output) = args.out.output.clone() {
275 policy.output.output = Some(output);
276 }
277 Ok(policy)
278}
279
280fn execute_check_iterations(
281 args: CheckArgs,
282 root: PathBuf,
283 policy: Policy,
284 resolved_toolchain: ResolvedToolchain,
285) -> ExitCode {
286 let iterations = iteration_count(&args);
287 let mut last_report = Report::default();
288
289 for is_last in (0..iterations).map(|iter| iter + 1 == iterations) {
290 let ws = match load_workspace(root.clone(), &policy) {
291 Ok(ws) => ws,
292 Err(code) => return code,
293 };
294 let report = match build_iteration_report(&args, &policy, &resolved_toolchain, &ws) {
295 Ok(report) => report,
296 Err(code) => return code,
297 };
298 let action = match handle_iteration(&args, &policy, &report) {
299 Ok(action) => action,
300 Err(code) => return code,
301 };
302 match action {
303 IterationAction::Return(code) => return code,
304 IterationAction::ContinueWithReport(report) => {
305 last_report = *report;
306 if is_last {
307 break;
308 }
309 }
310 }
311 }
312
313 finish_write_mode(&last_report, &policy)
314}
315
316fn iteration_count(args: &CheckArgs) -> u32 {
317 if args.write || args.dry_run {
318 args.max_fix_iterations.max(1)
319 } else {
320 1
321 }
322}
323
324enum IterationAction {
325 Return(ExitCode),
326 ContinueWithReport(Box<Report>),
327}
328
329fn handle_iteration(
330 args: &CheckArgs,
331 policy: &Policy,
332 report: &Report,
333) -> Result<IterationAction, ExitCode> {
334 let planned = plan_edits(report, args.unsafe_fixes);
335 if args.dry_run {
336 return dry_run_result(policy, report, &planned).map(IterationAction::Return);
337 }
338 if !args.write {
339 return write_and_return(policy, report).map(IterationAction::Return);
340 }
341 apply_write_iteration(report, &planned)
342 .map(Box::new)
343 .map(IterationAction::ContinueWithReport)
344}
345
346fn dry_run_result(
347 policy: &Policy,
348 report: &Report,
349 planned: &PlannedEdits,
350) -> Result<ExitCode, ExitCode> {
351 let would_change = print_dry_run(planned).map_err(io_error_to_exit_code)?;
352 write_report(report, policy).map_err(output_error_to_exit_code)?;
353 Ok(ExitCode::from(if would_change { 1 } else { 0 }))
354}
355
356fn write_and_return(policy: &Policy, report: &Report) -> Result<ExitCode, ExitCode> {
357 write_report(report, policy).map_err(output_error_to_exit_code)?;
358 Ok(ExitCode::from(report.worst_severity().exit_code()))
359}
360
361fn apply_write_iteration(report: &Report, planned: &PlannedEdits) -> Result<Report, ExitCode> {
362 if planned.is_empty() {
363 return Ok(report.clone());
364 }
365 let _applied = apply_planned_edits(planned).map_err(io_error_to_exit_code)?;
366 Ok(report.clone())
367}
368
369fn finish_write_mode(report: &Report, policy: &Policy) -> ExitCode {
370 if let Err(err) = write_report(report, policy) {
371 return output_error_to_exit_code(err);
372 }
373 ExitCode::from(report.worst_severity().exit_code())
374}
375
376fn load_workspace(root: PathBuf, policy: &Policy) -> Result<Workspace, ExitCode> {
377 Workspace::new(root).load_files(policy).map_err(|err| {
378 eprintln!("{err}");
379 ExitCode::from(2)
380 })
381}
382
383fn build_iteration_report(
384 args: &CheckArgs,
385 policy: &Policy,
386 resolved_toolchain: &ResolvedToolchain,
387 ws: &Workspace,
388) -> Result<Report, ExitCode> {
389 let mut report = run_rscheck_engine(args.rscheck, policy, resolved_toolchain, ws)?;
390 run_clippy_adapter(
391 &mut report,
392 policy,
393 resolved_toolchain,
394 ws,
395 &args.cargo_args,
396 )?;
397 report.summary.toolchain = Some(resolved_toolchain.summary());
398 Ok(report)
399}
400
401fn run_rscheck_engine(
402 enabled: bool,
403 policy: &Policy,
404 resolved_toolchain: &ResolvedToolchain,
405 ws: &Workspace,
406) -> Result<Report, ExitCode> {
407 if !enabled {
408 return Ok(Report::default());
409 }
410
411 Runner::run_with_semantic_status(ws, policy, resolved_toolchain.semantic_status()).map_err(
412 |err| {
413 eprintln!("{err}");
414 ExitCode::from(2)
415 },
416 )
417}
418
419fn run_clippy_adapter(
420 report: &mut Report,
421 policy: &Policy,
422 resolved_toolchain: &ResolvedToolchain,
423 ws: &Workspace,
424 cargo_args: &[String],
425) -> Result<(), ExitCode> {
426 if !policy.adapters.clippy.enabled {
427 return Ok(());
428 }
429
430 ensure_clippy_adapter_run(report);
431 let toolchain = resolved_toolchain
432 .clippy_selector(policy.adapters.clippy.toolchain)
433 .map_err(toolchain_error_to_exit_code)?;
434 let runtime = resolved_toolchain
435 .clippy_runtime_label(policy.adapters.clippy.toolchain)
436 .map_err(toolchain_error_to_exit_code)?;
437 let mut clippy_args = policy.adapters.clippy.args.clone();
438 clippy_args.extend(cargo_args.iter().cloned());
439 let mut findings = run_clippy(&ws.root, toolchain, &clippy_args).map_err(|err| {
440 eprintln!("{err}");
441 ExitCode::from(2)
442 })?;
443 report.findings.append(&mut findings);
444 set_clippy_adapter_status(report, runtime);
445 Ok(())
446}
447
448fn ensure_clippy_adapter_run(report: &mut Report) {
449 if report
450 .summary
451 .adapter_runs
452 .iter()
453 .any(|run| run.name == "clippy")
454 {
455 return;
456 }
457 report.summary.adapter_runs.push(AdapterRun {
458 name: "clippy".to_string(),
459 enabled: true,
460 toolchain: None,
461 status: None,
462 });
463}
464
465fn set_clippy_adapter_status(report: &mut Report, runtime: String) {
466 if let Some(adapter_run) = report
467 .summary
468 .adapter_runs
469 .iter_mut()
470 .find(|run| run.name == "clippy")
471 {
472 adapter_run.toolchain = Some(runtime);
473 adapter_run.status = Some("ok".to_string());
474 }
475}
476
477fn toolchain_error_to_exit_code(err: toolchain::ToolchainError) -> ExitCode {
478 eprintln!("{err}");
479 ExitCode::from(2)
480}
481
482fn io_error_to_exit_code(err: ApplyError) -> ExitCode {
483 eprintln!("{err}");
484 ExitCode::from(2)
485}
486
487fn output_error_to_exit_code(err: OutputError) -> ExitCode {
488 eprintln!("{err}");
489 ExitCode::from(2)
490}
491
492#[derive(Debug, thiserror::Error)]
493pub enum OutputError {
494 #[error("failed to serialize report")]
495 Serialize(#[source] serde_json::Error),
496 #[error("failed to write output")]
497 Write(#[source] io::Error),
498}
499
500fn write_report(report: &Report, policy: &Policy) -> Result<(), OutputError> {
501 let text = match policy.output.format {
502 OutputFormat::Text => render_text_report(report),
503 OutputFormat::Json => {
504 serde_json::to_string_pretty(report).map_err(OutputError::Serialize)?
505 }
506 OutputFormat::Sarif => serde_json::to_string_pretty(&report_sarif::to_sarif(report))
507 .map_err(OutputError::Serialize)?,
508 OutputFormat::Html => report_html::to_html(report),
509 };
510
511 match &policy.output.output {
512 Some(path) => fs::write(path, text).map_err(OutputError::Write),
513 None => {
514 print!("{text}");
515 Ok(())
516 }
517 }
518}