Skip to main content

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