1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
use clap::{Args, ValueEnum};
/// Fix mode determines exit code behavior: Check/CheckFix exit 1 on violations, Format exits 0
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum FixMode {
#[default]
Check,
CheckFix,
Format,
}
/// Fail-on mode determines which severity triggers exit code 1
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, ValueEnum)]
pub enum FailOn {
/// Exit 1 on any violation (info, warning, or error)
#[default]
Any,
/// Exit 1 on warning or error severity violations
Warning,
/// Exit 1 only on error-severity violations
Error,
/// Always exit 0
Never,
}
#[derive(Args, Debug)]
pub struct CheckArgs {
/// Files or directories to lint (use '-' for stdin)
#[arg(required = false)]
pub paths: Vec<String>,
/// Fix issues automatically where possible
#[arg(short, long, default_value = "false")]
pub fix: bool,
/// Show diff of what would be fixed instead of fixing files
#[arg(long, help = "Show diff of what would be fixed instead of fixing files")]
pub diff: bool,
/// Exit with code 1 if any formatting changes would be made (like rustfmt --check)
#[arg(long, help = "Exit with code 1 if any formatting changes would be made (for CI)")]
pub check: bool,
/// List all available rules
#[arg(short, long, default_value = "false")]
pub list_rules: bool,
/// Disable specific rules (comma-separated)
#[arg(short, long)]
pub disable: Option<String>,
/// Enable only specific rules (comma-separated)
#[arg(short, long, visible_alias = "rules")]
pub enable: Option<String>,
/// Extend the list of enabled rules (additive with config)
#[arg(long)]
pub extend_enable: Option<String>,
/// Extend the list of disabled rules (additive with config)
#[arg(long)]
pub extend_disable: Option<String>,
/// Only allow these rules to be fixed (comma-separated). When specified,
/// only listed rules will be auto-fixed; all others are treated as unfixable.
#[arg(long)]
pub fixable: Option<String>,
/// Prevent these rules from being fixed (comma-separated). Takes precedence
/// over --fixable.
#[arg(long)]
pub unfixable: Option<String>,
/// Exclude specific files or directories (comma-separated glob patterns)
#[arg(long)]
pub exclude: Option<String>,
/// Disable all exclude patterns (lint all files regardless of exclude configuration)
#[arg(long, help = "Disable all exclude patterns")]
pub no_exclude: bool,
/// Include only specific files or directories (comma-separated glob patterns).
#[arg(long)]
pub include: Option<String>,
/// Respect .gitignore files when scanning directories
/// When not specified, uses config file value (default: true)
#[arg(
long,
num_args(0..=1),
require_equals(true),
default_missing_value = "true",
help = "Respect .gitignore files when scanning directories (does not apply to explicitly provided paths)"
)]
pub respect_gitignore: Option<bool>,
/// Show detailed output
#[arg(short, long)]
pub verbose: bool,
/// Show profiling information
#[arg(long)]
pub profile: bool,
/// Show statistics summary of rule violations
#[arg(long)]
pub statistics: bool,
/// Print diagnostics, but nothing else
#[arg(short, long, help = "Print diagnostics, but nothing else")]
pub quiet: bool,
/// Output format: text (default) or json
#[arg(long, short = 'o', default_value_t, value_enum)]
pub output: Output,
/// Output format for linting results (default: text).
///
/// Precedence: --output-format > $RUMDL_OUTPUT_FORMAT > config file > text
#[arg(long, value_enum)]
pub output_format: Option<OutputFormat>,
/// Show absolute file paths instead of project-relative paths
#[arg(long, help = "Show absolute file paths in output instead of relative paths")]
pub show_full_path: bool,
/// Markdown flavor to use for linting
#[arg(
long,
value_enum,
help = "Markdown flavor: standard/gfm/commonmark (default), mkdocs, mdx, quarto, obsidian, or kramdown"
)]
pub flavor: Option<Flavor>,
/// Read from stdin instead of files
#[arg(long, help = "Read from stdin instead of files")]
pub stdin: bool,
/// Filename to use for stdin input (for context and error messages)
#[arg(long, help = "Filename to use when reading from stdin (e.g., README.md)")]
pub stdin_filename: Option<String>,
/// Output linting results to stderr instead of stdout
#[arg(long, help = "Output diagnostics to stderr instead of stdout")]
pub stderr: bool,
/// Disable all logging (but still exit with status code upon detecting diagnostics)
#[arg(
short,
long,
help = "Disable all logging (but still exit with status code upon detecting diagnostics)"
)]
pub silent: bool,
/// Run in watch mode by re-running whenever files change
#[arg(short, long, help = "Run in watch mode by re-running whenever files change")]
pub watch: bool,
/// Enforce exclude patterns even for paths that are passed explicitly.
/// By default, rumdl will lint any paths passed in directly, even if they would typically be excluded.
/// Setting this flag will cause rumdl to respect exclusions unequivocally.
/// This is useful for pre-commit, which explicitly passes all changed files.
#[arg(long, help = "Enforce exclude patterns even for explicitly specified files")]
pub force_exclude: bool,
/// Disable caching of lint results
#[arg(long, help = "Disable caching (re-check all files)")]
pub no_cache: bool,
/// Directory to store cache files
#[arg(
long,
help = "Directory to store cache files (default: .rumdl_cache, or $RUMDL_CACHE_DIR, or cache-dir in config)"
)]
pub cache_dir: Option<String>,
/// Control when to exit with code 1: any (default), warning, error, or never
#[arg(
long,
value_enum,
default_value_t,
help = "Exit code behavior: 'any' (default) exits 1 on any violation, 'warning' on warning+error, 'error' only on errors, 'never' always exits 0"
)]
pub fail_on: FailOn,
#[arg(skip)]
pub fix_mode: FixMode,
#[arg(skip)]
pub fail_on_mode: FailOn,
}
#[derive(Clone, Debug, Default, ValueEnum)]
pub enum Output {
#[default]
Text,
Json,
}
#[derive(Clone, Copy, Debug, ValueEnum)]
pub enum OutputFormat {
/// One-line-per-warning with file, line, column, rule, and message (default)
Text,
/// Show source lines with caret underlines highlighting the violation
Full,
/// Minimal: file:line:col rule message
Concise,
/// Warnings grouped by file with a header per file
Grouped,
/// JSON array of all warnings (collected across files)
Json,
/// One JSON object per warning (streaming)
JsonLines,
/// GitHub Actions annotation format (::warning/::error)
#[value(name = "github")]
GitHub,
/// GitLab Code Quality report (JSON)
#[value(name = "gitlab")]
GitLab,
/// Pylint-compatible format
Pylint,
/// Azure Pipelines logging commands
Azure,
/// SARIF 2.1.0 for static analysis tools
Sarif,
/// JUnit XML for CI test reporters
Junit,
}
impl From<OutputFormat> for rumdl_lib::output::OutputFormat {
fn from(format: OutputFormat) -> Self {
match format {
OutputFormat::Text => Self::Text,
OutputFormat::Full => Self::Full,
OutputFormat::Concise => Self::Concise,
OutputFormat::Grouped => Self::Grouped,
OutputFormat::Json => Self::Json,
OutputFormat::JsonLines => Self::JsonLines,
OutputFormat::GitHub => Self::GitHub,
OutputFormat::GitLab => Self::GitLab,
OutputFormat::Pylint => Self::Pylint,
OutputFormat::Azure => Self::Azure,
OutputFormat::Sarif => Self::Sarif,
OutputFormat::Junit => Self::Junit,
}
}
}
#[derive(Clone, Copy, Debug, ValueEnum)]
#[value(rename_all = "lower")]
pub enum Flavor {
#[value(aliases(["gfm", "github", "commonmark"]))]
Standard,
MkDocs,
#[allow(clippy::upper_case_acronyms)]
MDX,
#[value(aliases(["qmd", "rmd", "rmarkdown"]))]
Quarto,
Obsidian,
#[value(alias("jekyll"))]
Kramdown,
}
impl From<Flavor> for rumdl_lib::config::MarkdownFlavor {
fn from(flavor: Flavor) -> Self {
match flavor {
Flavor::Standard => Self::Standard,
Flavor::MkDocs => Self::MkDocs,
Flavor::MDX => Self::MDX,
Flavor::Quarto => Self::Quarto,
Flavor::Obsidian => Self::Obsidian,
Flavor::Kramdown => Self::Kramdown,
}
}
}