stern4rust/settings/args.rs
1// Copyright 2025 Umberto Gotti <umberto.gotti@umbertogotti.dev>
2// Licensed under the MIT License
3// SPDX-License-Identifier: MIT
4
5use crate::reporting::output_format::OutputFormat;
6use clap::Parser;
7use std::env;
8use std::path::PathBuf;
9
10#[derive(Debug, Parser)]
11#[command(name = "cargo-stern4rust")]
12#[command(bin_name = "cargo stern4rust")]
13#[command(version)]
14#[command(about = "Check Rust packages and fail the build when the rule is broken")]
15pub struct Args {
16 #[arg(long)]
17 pub manifest_path: Option<PathBuf>,
18
19 #[arg(long = "package")]
20 pub packages: Vec<String>,
21
22 /// File holding the header every .rs file must open with. It is data rather
23 /// than a built-in constant because it is never the same twice: MIT here,
24 /// Apache 2.0 in a sibling repository, and a different year again next year.
25 #[arg(long)]
26 pub header_file: Option<PathBuf>,
27
28 /// How to report. The table is for a person; `json` is the same run as a
29 /// document, for a gate script or an agent that would otherwise have to
30 /// guess where one column of the table ends and the next begins.
31 #[arg(long, value_enum, default_value_t = OutputFormat::Text)]
32 pub format: OutputFormat,
33
34 /// How many offences the report prints. A first run against a large
35 /// codebase can find a thousand, and a thousand rows is a wall rather than
36 /// a report. The cap is on what is shown and never on what is counted:
37 /// the summary, the omitted count and the exit code all see every offence.
38 /// Use 0 for no limit.
39 /// Option rather than a defaulted value so that "not passed" is
40 /// distinguishable from "passed the default": without that, a
41 /// stern4rust.toml could never set the threshold, because every run would
42 /// look like the reader had asked for 100 on the command line.
43 #[arg(long)]
44 pub offence_threshold: Option<usize>,
45
46 /// Apply only these rules; repeatable. Omit to apply every rule. Naming one
47 /// makes the selection a whitelist, which is what lets a codebase facing
48 /// hundreds of offences gate on one rule today and the rest as it goes.
49 #[arg(long = "rule")]
50 pub rules: Vec<String>,
51
52 /// Repair what can be repaired mechanically, then report what is left.
53 /// Only test-file-structure offences are fixable today: item order, section
54 /// order and blank lines. Everything else is reported unchanged, and the
55 /// report says how many offences were fixed and how many were not.
56 #[arg(long)]
57 pub fix: bool,
58
59 /// Offences recorded here are not reported and do not fail the run. What
60 /// lets a codebase with hundreds of existing offences enforce every rule
61 /// against new code without first fixing the old. The count of suppressed
62 /// offences is always in the summary, never hidden.
63 #[arg(long)]
64 pub baseline: Option<PathBuf>,
65
66 /// Record the current offences as the baseline and exit clean, instead of
67 /// judging against one. Writes to --baseline, or to stern4rust-baseline.json
68 /// beside the manifest.
69 #[arg(long)]
70 pub write_baseline: bool,
71
72 /// Keep these paths out of the run; repeatable, matched as a glob against
73 /// the package-relative path. For a tree the repository cannot move --
74 /// vendored source, generated output. Every pattern is named in the report
75 /// with how many files it removed, including zero, so an exclusion is
76 /// something the reader can see rather than a silence.
77 #[arg(long = "exclude")]
78 pub excludes: Vec<String>,
79
80 /// Do not apply these rules; repeatable. Subtracted from whatever --rule
81 /// selected, so skipping wins over selecting.
82 #[arg(long = "skip")]
83 pub skipped_rules: Vec<String>,
84}
85
86impl Args {
87 pub fn parse_args() -> Self {
88 Self::parse_from(Self::without_cargo_subcommand(env::args()))
89 }
90
91 /// Cargo invokes `cargo stern4rust` as `cargo-stern4rust stern4rust ...`, so
92 /// the subcommand name arrives as an extra leading argument that clap would
93 /// otherwise reject. Running the binary directly does not repeat it, which
94 /// is why the strip is conditional rather than unconditional.
95 pub fn without_cargo_subcommand<I>(args: I) -> Vec<String>
96 where
97 I: IntoIterator<Item = String>,
98 {
99 let args: Vec<String> = args.into_iter().collect();
100 if args.get(1).map(String::as_str) != Some("stern4rust") {
101 return args;
102 }
103 let mut forwarded = Vec::with_capacity(args.len() - 1);
104 forwarded.extend(args.iter().take(1).cloned());
105 forwarded.extend(args.into_iter().skip(2));
106 forwarded
107 }
108}