@owenlamont/ryl 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (217) hide show
  1. package/.github/CODEOWNERS +1 -0
  2. package/.github/dependabot.yml +13 -0
  3. package/.github/workflows/ci.yml +107 -0
  4. package/.github/workflows/release.yml +613 -0
  5. package/.github/workflows/update_dependencies.yml +61 -0
  6. package/.github/workflows/update_linters.yml +56 -0
  7. package/.pre-commit-config.yaml +87 -0
  8. package/.yamllint +4 -0
  9. package/AGENTS.md +200 -0
  10. package/Cargo.lock +908 -0
  11. package/Cargo.toml +32 -0
  12. package/LICENSE +21 -0
  13. package/README.md +230 -0
  14. package/bin/ryl.js +1 -0
  15. package/clippy.toml +1 -0
  16. package/docs/config-presets.md +100 -0
  17. package/img/benchmark-5x5-5runs.svg +2176 -0
  18. package/package.json +28 -0
  19. package/pyproject.toml +42 -0
  20. package/ruff.toml +107 -0
  21. package/rumdl.toml +20 -0
  22. package/rust-toolchain.toml +3 -0
  23. package/rustfmt.toml +3 -0
  24. package/scripts/benchmark_perf_vs_yamllint.py +400 -0
  25. package/scripts/coverage-missing.ps1 +80 -0
  26. package/scripts/coverage-missing.sh +60 -0
  27. package/src/bin/discover_config_bin.rs +24 -0
  28. package/src/cli_support.rs +33 -0
  29. package/src/conf/mod.rs +85 -0
  30. package/src/config.rs +2099 -0
  31. package/src/decoder.rs +326 -0
  32. package/src/discover.rs +31 -0
  33. package/src/lib.rs +19 -0
  34. package/src/lint.rs +558 -0
  35. package/src/main.rs +535 -0
  36. package/src/migrate.rs +233 -0
  37. package/src/rules/anchors.rs +517 -0
  38. package/src/rules/braces.rs +77 -0
  39. package/src/rules/brackets.rs +77 -0
  40. package/src/rules/colons.rs +475 -0
  41. package/src/rules/commas.rs +372 -0
  42. package/src/rules/comments.rs +299 -0
  43. package/src/rules/comments_indentation.rs +243 -0
  44. package/src/rules/document_end.rs +175 -0
  45. package/src/rules/document_start.rs +84 -0
  46. package/src/rules/empty_lines.rs +152 -0
  47. package/src/rules/empty_values.rs +255 -0
  48. package/src/rules/float_values.rs +259 -0
  49. package/src/rules/flow_collection.rs +562 -0
  50. package/src/rules/hyphens.rs +104 -0
  51. package/src/rules/indentation.rs +803 -0
  52. package/src/rules/key_duplicates.rs +218 -0
  53. package/src/rules/key_ordering.rs +303 -0
  54. package/src/rules/line_length.rs +326 -0
  55. package/src/rules/mod.rs +25 -0
  56. package/src/rules/new_line_at_end_of_file.rs +23 -0
  57. package/src/rules/new_lines.rs +95 -0
  58. package/src/rules/octal_values.rs +121 -0
  59. package/src/rules/quoted_strings.rs +577 -0
  60. package/src/rules/span_utils.rs +37 -0
  61. package/src/rules/trailing_spaces.rs +65 -0
  62. package/src/rules/truthy.rs +420 -0
  63. package/tests/brackets_carriage_return.rs +114 -0
  64. package/tests/build_global_cfg_error.rs +23 -0
  65. package/tests/cli_anchors_rule.rs +143 -0
  66. package/tests/cli_braces_rule.rs +104 -0
  67. package/tests/cli_brackets_rule.rs +104 -0
  68. package/tests/cli_colons_rule.rs +65 -0
  69. package/tests/cli_commas_rule.rs +104 -0
  70. package/tests/cli_comments_indentation_rule.rs +61 -0
  71. package/tests/cli_comments_rule.rs +67 -0
  72. package/tests/cli_config_data_error.rs +30 -0
  73. package/tests/cli_config_flags.rs +66 -0
  74. package/tests/cli_config_migrate.rs +229 -0
  75. package/tests/cli_document_end_rule.rs +92 -0
  76. package/tests/cli_document_start_rule.rs +92 -0
  77. package/tests/cli_empty_lines_rule.rs +87 -0
  78. package/tests/cli_empty_values_rule.rs +68 -0
  79. package/tests/cli_env_config.rs +34 -0
  80. package/tests/cli_exit_and_errors.rs +41 -0
  81. package/tests/cli_file_encoding.rs +203 -0
  82. package/tests/cli_float_values_rule.rs +64 -0
  83. package/tests/cli_format_options.rs +316 -0
  84. package/tests/cli_global_cfg_relaxed.rs +20 -0
  85. package/tests/cli_hyphens_rule.rs +104 -0
  86. package/tests/cli_indentation_rule.rs +65 -0
  87. package/tests/cli_invalid_project_config.rs +39 -0
  88. package/tests/cli_key_duplicates_rule.rs +104 -0
  89. package/tests/cli_key_ordering_rule.rs +59 -0
  90. package/tests/cli_line_length_rule.rs +85 -0
  91. package/tests/cli_list_files.rs +29 -0
  92. package/tests/cli_new_line_rule.rs +141 -0
  93. package/tests/cli_new_lines_rule.rs +119 -0
  94. package/tests/cli_octal_values_rule.rs +60 -0
  95. package/tests/cli_quoted_strings_rule.rs +47 -0
  96. package/tests/cli_toml_config.rs +119 -0
  97. package/tests/cli_trailing_spaces_rule.rs +77 -0
  98. package/tests/cli_truthy_rule.rs +83 -0
  99. package/tests/cli_yaml_files_negation.rs +45 -0
  100. package/tests/colons_rule.rs +303 -0
  101. package/tests/common/compat.rs +114 -0
  102. package/tests/common/fake_env.rs +93 -0
  103. package/tests/common/mod.rs +1 -0
  104. package/tests/conf_builtin.rs +9 -0
  105. package/tests/config_anchors.rs +84 -0
  106. package/tests/config_braces.rs +121 -0
  107. package/tests/config_brackets.rs +127 -0
  108. package/tests/config_commas.rs +79 -0
  109. package/tests/config_comments.rs +65 -0
  110. package/tests/config_comments_indentation.rs +20 -0
  111. package/tests/config_deep_merge_nonstring_key.rs +24 -0
  112. package/tests/config_document_end.rs +54 -0
  113. package/tests/config_document_start.rs +55 -0
  114. package/tests/config_empty_lines.rs +48 -0
  115. package/tests/config_empty_values.rs +35 -0
  116. package/tests/config_env_errors.rs +23 -0
  117. package/tests/config_env_invalid_inline.rs +15 -0
  118. package/tests/config_env_missing.rs +63 -0
  119. package/tests/config_env_shim.rs +301 -0
  120. package/tests/config_explicit_file_parse_error.rs +55 -0
  121. package/tests/config_extended_features.rs +225 -0
  122. package/tests/config_extends_inline.rs +185 -0
  123. package/tests/config_extends_sequence.rs +18 -0
  124. package/tests/config_find_project_home_boundary.rs +54 -0
  125. package/tests/config_find_project_two_files_in_cwd.rs +47 -0
  126. package/tests/config_float_values.rs +34 -0
  127. package/tests/config_from_yaml_paths.rs +32 -0
  128. package/tests/config_hyphens.rs +51 -0
  129. package/tests/config_ignore_errors.rs +243 -0
  130. package/tests/config_ignore_overrides.rs +83 -0
  131. package/tests/config_indentation.rs +65 -0
  132. package/tests/config_invalid_globs.rs +16 -0
  133. package/tests/config_invalid_types.rs +19 -0
  134. package/tests/config_key_duplicates.rs +34 -0
  135. package/tests/config_key_ordering.rs +70 -0
  136. package/tests/config_line_length.rs +65 -0
  137. package/tests/config_locale.rs +111 -0
  138. package/tests/config_merge.rs +26 -0
  139. package/tests/config_new_lines.rs +89 -0
  140. package/tests/config_octal_values.rs +33 -0
  141. package/tests/config_quoted_strings.rs +195 -0
  142. package/tests/config_rule_level.rs +147 -0
  143. package/tests/config_rules_non_string_keys.rs +23 -0
  144. package/tests/config_scalar_overrides.rs +27 -0
  145. package/tests/config_to_toml.rs +110 -0
  146. package/tests/config_toml_coverage.rs +80 -0
  147. package/tests/config_toml_discovery.rs +304 -0
  148. package/tests/config_trailing_spaces.rs +152 -0
  149. package/tests/config_truthy.rs +77 -0
  150. package/tests/config_yaml_files.rs +62 -0
  151. package/tests/config_yaml_files_all_non_string.rs +15 -0
  152. package/tests/config_yaml_files_empty.rs +30 -0
  153. package/tests/coverage_commas.rs +46 -0
  154. package/tests/decoder_decode.rs +338 -0
  155. package/tests/discover_config_bin_all.rs +66 -0
  156. package/tests/discover_config_bin_env_invalid_yaml.rs +26 -0
  157. package/tests/discover_config_bin_project_config_parse_error.rs +24 -0
  158. package/tests/discover_config_bin_user_global_error.rs +26 -0
  159. package/tests/discover_module.rs +30 -0
  160. package/tests/discover_per_file_dir.rs +10 -0
  161. package/tests/discover_per_file_project_config_error.rs +21 -0
  162. package/tests/float_values.rs +43 -0
  163. package/tests/lint_multi_errors.rs +32 -0
  164. package/tests/main_yaml_ok_filtering.rs +30 -0
  165. package/tests/migrate_module.rs +259 -0
  166. package/tests/resolve_ctx_empty_parent.rs +16 -0
  167. package/tests/rule_anchors.rs +442 -0
  168. package/tests/rule_braces.rs +258 -0
  169. package/tests/rule_brackets.rs +217 -0
  170. package/tests/rule_commas.rs +205 -0
  171. package/tests/rule_comments.rs +197 -0
  172. package/tests/rule_comments_indentation.rs +127 -0
  173. package/tests/rule_document_end.rs +118 -0
  174. package/tests/rule_document_start.rs +60 -0
  175. package/tests/rule_empty_lines.rs +96 -0
  176. package/tests/rule_empty_values.rs +102 -0
  177. package/tests/rule_float_values.rs +109 -0
  178. package/tests/rule_hyphens.rs +65 -0
  179. package/tests/rule_indentation.rs +455 -0
  180. package/tests/rule_key_duplicates.rs +76 -0
  181. package/tests/rule_key_ordering.rs +207 -0
  182. package/tests/rule_line_length.rs +200 -0
  183. package/tests/rule_new_lines.rs +51 -0
  184. package/tests/rule_octal_values.rs +53 -0
  185. package/tests/rule_quoted_strings.rs +290 -0
  186. package/tests/rule_trailing_spaces.rs +41 -0
  187. package/tests/rule_truthy.rs +236 -0
  188. package/tests/user_global_invalid_yaml.rs +32 -0
  189. package/tests/yamllint_compat_anchors.rs +280 -0
  190. package/tests/yamllint_compat_braces.rs +411 -0
  191. package/tests/yamllint_compat_brackets.rs +364 -0
  192. package/tests/yamllint_compat_colons.rs +298 -0
  193. package/tests/yamllint_compat_colors.rs +80 -0
  194. package/tests/yamllint_compat_commas.rs +375 -0
  195. package/tests/yamllint_compat_comments.rs +167 -0
  196. package/tests/yamllint_compat_comments_indentation.rs +281 -0
  197. package/tests/yamllint_compat_config.rs +170 -0
  198. package/tests/yamllint_compat_document_end.rs +243 -0
  199. package/tests/yamllint_compat_document_start.rs +136 -0
  200. package/tests/yamllint_compat_empty_lines.rs +117 -0
  201. package/tests/yamllint_compat_empty_values.rs +179 -0
  202. package/tests/yamllint_compat_float_values.rs +216 -0
  203. package/tests/yamllint_compat_hyphens.rs +223 -0
  204. package/tests/yamllint_compat_indentation.rs +398 -0
  205. package/tests/yamllint_compat_key_duplicates.rs +139 -0
  206. package/tests/yamllint_compat_key_ordering.rs +170 -0
  207. package/tests/yamllint_compat_line_length.rs +375 -0
  208. package/tests/yamllint_compat_list.rs +127 -0
  209. package/tests/yamllint_compat_new_line.rs +133 -0
  210. package/tests/yamllint_compat_newline_types.rs +185 -0
  211. package/tests/yamllint_compat_octal_values.rs +172 -0
  212. package/tests/yamllint_compat_quoted_strings.rs +154 -0
  213. package/tests/yamllint_compat_syntax.rs +200 -0
  214. package/tests/yamllint_compat_trailing_spaces.rs +162 -0
  215. package/tests/yamllint_compat_truthy.rs +130 -0
  216. package/tests/yamllint_compat_yaml_files.rs +81 -0
  217. package/typos.toml +2 -0
package/src/main.rs ADDED
@@ -0,0 +1,535 @@
1
+ #![forbid(unsafe_code)]
2
+ #![deny(
3
+ clippy::all,
4
+ clippy::pedantic,
5
+ clippy::cargo,
6
+ clippy::cognitive_complexity
7
+ )]
8
+
9
+ use std::collections::HashMap;
10
+ use std::collections::HashSet;
11
+ use std::io::IsTerminal;
12
+ use std::path::{Path, PathBuf};
13
+ use std::process::ExitCode;
14
+
15
+ use clap::{Parser, ValueEnum};
16
+ use ignore::WalkBuilder;
17
+ use rayon::prelude::*;
18
+ use ryl::cli_support::resolve_ctx;
19
+ use ryl::config::{ConfigContext, Overrides, YamlLintConfig, discover_config};
20
+ use ryl::migrate::{
21
+ MigrateOptions, OutputMode as MigrateOutputMode, SourceCleanup, WriteMode,
22
+ migrate_configs,
23
+ };
24
+ use ryl::{LintProblem, Severity, lint_file};
25
+
26
+ fn gather_inputs(inputs: &[PathBuf]) -> (Vec<PathBuf>, Vec<PathBuf>) {
27
+ let mut explicit_files = Vec::new();
28
+ let mut candidates = Vec::new();
29
+ for p in inputs {
30
+ if p.is_dir() {
31
+ let walker = WalkBuilder::new(p)
32
+ .hidden(false)
33
+ .ignore(true)
34
+ .git_ignore(true)
35
+ .git_global(true)
36
+ .git_exclude(true)
37
+ .follow_links(false)
38
+ .build();
39
+ for e in walker.flatten() {
40
+ let fp = e.path().to_path_buf();
41
+ if fp.is_file() {
42
+ candidates.push(fp);
43
+ }
44
+ }
45
+ } else {
46
+ explicit_files.push(p.clone());
47
+ }
48
+ }
49
+ (candidates, explicit_files)
50
+ }
51
+
52
+ fn build_global_cfg(
53
+ inputs: &[PathBuf],
54
+ cli: &Cli,
55
+ ) -> Result<Option<ConfigContext>, String> {
56
+ if cli.config_data.is_some()
57
+ || cli.config_file.is_some()
58
+ || std::env::var("YAMLLINT_CONFIG_FILE").is_ok()
59
+ {
60
+ let config_data = cli.config_data.as_ref().map(|raw| {
61
+ if !raw.is_empty() && !raw.contains(':') {
62
+ format!("extends: {raw}")
63
+ } else {
64
+ raw.clone()
65
+ }
66
+ });
67
+ discover_config(
68
+ inputs,
69
+ &Overrides {
70
+ config_file: cli.config_file.clone(),
71
+ config_data,
72
+ },
73
+ )
74
+ .map(Some)
75
+ } else {
76
+ Ok(None)
77
+ }
78
+ }
79
+
80
+ fn run_migration(cli: &Cli) -> Result<ExitCode, String> {
81
+ let cleanup = if let Some(suffix) = &cli.migrate.rename_old {
82
+ SourceCleanup::RenameSuffix(suffix.clone())
83
+ } else if cli.migrate.delete_old {
84
+ SourceCleanup::Delete
85
+ } else {
86
+ SourceCleanup::Keep
87
+ };
88
+ let options = MigrateOptions {
89
+ root: cli
90
+ .migrate
91
+ .root
92
+ .clone()
93
+ .unwrap_or_else(|| PathBuf::from(".")),
94
+ write_mode: if cli.migrate.write {
95
+ WriteMode::Write
96
+ } else {
97
+ WriteMode::Preview
98
+ },
99
+ output_mode: if cli.migrate.stdout {
100
+ MigrateOutputMode::IncludeToml
101
+ } else {
102
+ MigrateOutputMode::SummaryOnly
103
+ },
104
+ cleanup,
105
+ };
106
+ let result = migrate_configs(&options)?;
107
+ for warning in result.warnings {
108
+ eprintln!("{warning}");
109
+ }
110
+ if result.entries.is_empty() {
111
+ println!(
112
+ "No legacy YAML config files found under {}",
113
+ options.root.display()
114
+ );
115
+ return Ok(ExitCode::SUCCESS);
116
+ }
117
+
118
+ for entry in &result.entries {
119
+ println!("{} -> {}", entry.source.display(), entry.target.display());
120
+ }
121
+ if options.output_mode == MigrateOutputMode::IncludeToml {
122
+ for entry in &result.entries {
123
+ println!("# {}", entry.target.display());
124
+ println!("{}", entry.toml);
125
+ }
126
+ }
127
+ Ok(ExitCode::SUCCESS)
128
+ }
129
+
130
+ #[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum)]
131
+ enum CliFormat {
132
+ Auto,
133
+ Standard,
134
+ Colored,
135
+ Github,
136
+ Parsable,
137
+ }
138
+
139
+ #[derive(Parser, Debug)]
140
+ #[command(name = "ryl", version, about = "Fast YAML linter written in Rust")]
141
+ struct Cli {
142
+ /// One or more paths: files and/or directories
143
+ #[arg(value_name = "PATH_OR_FILE")]
144
+ inputs: Vec<PathBuf>,
145
+
146
+ /// Path to configuration file (YAML or TOML)
147
+ #[arg(short = 'c', long = "config-file", value_name = "FILE")]
148
+ config_file: Option<PathBuf>,
149
+
150
+ /// Inline configuration data (yaml)
151
+ #[arg(short = 'd', long = "config-data", value_name = "YAML")]
152
+ config_data: Option<String>,
153
+
154
+ /// Output format (auto, standard, colored, github, parsable)
155
+ #[arg(short = 'f', long = "format", default_value_t = CliFormat::Auto, value_enum)]
156
+ format: CliFormat,
157
+
158
+ /// Convert discovered legacy YAML config files into .ryl.toml files
159
+ #[arg(long = "migrate-configs", default_value_t = false)]
160
+ migrate_configs: bool,
161
+
162
+ #[command(flatten)]
163
+ lint: LintFlags,
164
+
165
+ #[command(flatten)]
166
+ migrate: MigrateFlags,
167
+ }
168
+
169
+ #[derive(clap::Args, Debug, Default)]
170
+ struct LintFlags {
171
+ /// List files that would be linted (reserved)
172
+ #[arg(long = "list-files", default_value_t = false)]
173
+ list_files: bool,
174
+
175
+ /// Strict mode (reserved)
176
+ #[arg(short = 's', long = "strict", default_value_t = false)]
177
+ strict: bool,
178
+
179
+ /// Suppress warnings (reserved)
180
+ #[arg(long = "no-warnings", default_value_t = false)]
181
+ no_warnings: bool,
182
+ }
183
+
184
+ #[derive(clap::Args, Debug, Default)]
185
+ struct MigrateFlags {
186
+ /// Root path to search for legacy YAML config files (default: .)
187
+ #[arg(
188
+ long = "migrate-root",
189
+ value_name = "DIR",
190
+ requires = "migrate_configs"
191
+ )]
192
+ root: Option<PathBuf>,
193
+
194
+ /// Write migrated .ryl.toml files (otherwise preview only)
195
+ #[arg(
196
+ long = "migrate-write",
197
+ default_value_t = false,
198
+ requires = "migrate_configs"
199
+ )]
200
+ write: bool,
201
+
202
+ /// Print generated TOML to stdout during migration
203
+ #[arg(
204
+ long = "migrate-stdout",
205
+ default_value_t = false,
206
+ requires = "migrate_configs"
207
+ )]
208
+ stdout: bool,
209
+
210
+ /// Rename source YAML configs by appending this suffix after migration
211
+ #[arg(
212
+ long = "migrate-rename-old",
213
+ value_name = "SUFFIX",
214
+ conflicts_with = "delete_old",
215
+ requires_all = ["write", "migrate_configs"]
216
+ )]
217
+ rename_old: Option<String>,
218
+
219
+ /// Delete source YAML configs after migration
220
+ #[arg(
221
+ long = "migrate-delete-old",
222
+ default_value_t = false,
223
+ conflicts_with = "rename_old",
224
+ requires_all = ["write", "migrate_configs"]
225
+ )]
226
+ delete_old: bool,
227
+ }
228
+
229
+ #[derive(Clone, Copy, Debug, PartialEq, Eq)]
230
+ enum OutputFormat {
231
+ Standard,
232
+ Colored,
233
+ Github,
234
+ Parsable,
235
+ }
236
+
237
+ fn detect_output_format(choice: CliFormat) -> OutputFormat {
238
+ match choice {
239
+ CliFormat::Standard => OutputFormat::Standard,
240
+ CliFormat::Colored => OutputFormat::Colored,
241
+ CliFormat::Github => OutputFormat::Github,
242
+ CliFormat::Parsable => OutputFormat::Parsable,
243
+ CliFormat::Auto => {
244
+ if github_env_active() {
245
+ OutputFormat::Github
246
+ } else if supports_color() {
247
+ OutputFormat::Colored
248
+ } else {
249
+ OutputFormat::Standard
250
+ }
251
+ }
252
+ }
253
+ }
254
+
255
+ fn github_env_active() -> bool {
256
+ std::env::var_os("GITHUB_ACTIONS").is_some()
257
+ && std::env::var_os("GITHUB_WORKFLOW").is_some()
258
+ }
259
+
260
+ fn supports_color() -> bool {
261
+ if std::env::var_os("NO_COLOR").is_some() {
262
+ return false;
263
+ }
264
+ if std::env::var_os("FORCE_COLOR").is_some() {
265
+ return true;
266
+ }
267
+ std::io::stderr().is_terminal()
268
+ }
269
+
270
+ fn main() -> ExitCode {
271
+ let cli = Cli::parse();
272
+
273
+ if cli.migrate_configs {
274
+ return match run_migration(&cli) {
275
+ Ok(code) => code,
276
+ Err(err) => {
277
+ eprintln!("{err}");
278
+ ExitCode::from(2)
279
+ }
280
+ };
281
+ }
282
+
283
+ if cli.inputs.is_empty() {
284
+ eprintln!("error: expected one or more paths (files and/or directories)");
285
+ return ExitCode::from(2);
286
+ }
287
+
288
+ // Build a global config if -d/-c provided or env var set; else None for per-file discovery.
289
+ let global_cfg = match build_global_cfg(&cli.inputs, &cli) {
290
+ Ok(cfg) => cfg,
291
+ Err(e) => {
292
+ eprintln!("{e}");
293
+ return ExitCode::from(2);
294
+ }
295
+ };
296
+ if let Some(cfg) = &global_cfg {
297
+ for notice in &cfg.notices {
298
+ eprintln!("{notice}");
299
+ }
300
+ }
301
+ let inputs = cli.inputs;
302
+
303
+ // Determine files to parse from mixed inputs.
304
+ // - Directories: recursively gather only .yml/.yaml
305
+ // - Files: include as-is (even if extension isn't yaml)
306
+ let (candidates, explicit_files) = gather_inputs(&inputs);
307
+
308
+ // Filter directory candidates via ignores, respecting global vs per-file behavior.
309
+ let mut cache: HashMap<PathBuf, (PathBuf, YamlLintConfig)> = HashMap::new();
310
+ let mut emitted_notices: HashSet<String> = HashSet::new();
311
+ let mut files: Vec<(PathBuf, PathBuf, YamlLintConfig)> = Vec::new();
312
+ for f in candidates {
313
+ let (base_dir, cfg, notices) =
314
+ match resolve_ctx(&f, global_cfg.as_ref(), &mut cache) {
315
+ Ok(pair) => pair,
316
+ Err(e) => {
317
+ eprintln!("{e}");
318
+ return ExitCode::from(2);
319
+ }
320
+ };
321
+ for notice in notices {
322
+ if emitted_notices.insert(notice.clone()) {
323
+ eprintln!("{notice}");
324
+ }
325
+ }
326
+ let ignored = cfg.is_file_ignored(&f, &base_dir);
327
+ let yaml_ok = cfg.is_yaml_candidate(&f, &base_dir);
328
+ if !ignored && yaml_ok {
329
+ files.push((f, base_dir, cfg));
330
+ }
331
+ }
332
+
333
+ for ef in explicit_files {
334
+ let (base_dir, cfg, notices) =
335
+ match resolve_ctx(&ef, global_cfg.as_ref(), &mut cache) {
336
+ Ok(pair) => pair,
337
+ Err(e) => {
338
+ eprintln!("{e}");
339
+ return ExitCode::from(2);
340
+ }
341
+ };
342
+ for notice in notices {
343
+ if emitted_notices.insert(notice.clone()) {
344
+ eprintln!("{notice}");
345
+ }
346
+ }
347
+ let ignored = cfg.is_file_ignored(&ef, &base_dir);
348
+ let yaml_ok = cfg.is_yaml_candidate(&ef, &base_dir);
349
+ if !ignored && yaml_ok {
350
+ files.push((ef, base_dir, cfg));
351
+ }
352
+ }
353
+
354
+ if cli.lint.list_files {
355
+ for (path, ..) in &files {
356
+ println!("{}", path.display());
357
+ }
358
+ return ExitCode::SUCCESS;
359
+ }
360
+
361
+ if files.is_empty() {
362
+ return ExitCode::SUCCESS;
363
+ }
364
+
365
+ let mut results: Vec<(usize, Result<Vec<LintProblem>, String>)> = files
366
+ .par_iter()
367
+ .enumerate()
368
+ .map(|(idx, (path, base_dir, cfg))| (idx, lint_file(path, cfg, base_dir)))
369
+ .collect();
370
+
371
+ results.sort_by_key(|(idx, _)| *idx);
372
+
373
+ let output_format = detect_output_format(cli.format);
374
+ let (has_error, has_warning) =
375
+ process_results(&files, results, output_format, cli.lint.no_warnings);
376
+
377
+ if has_error {
378
+ ExitCode::from(1)
379
+ } else if has_warning && cli.lint.strict {
380
+ ExitCode::from(2)
381
+ } else {
382
+ ExitCode::SUCCESS
383
+ }
384
+ }
385
+
386
+ fn process_results(
387
+ files: &[(PathBuf, PathBuf, YamlLintConfig)],
388
+ results: Vec<(usize, Result<Vec<LintProblem>, String>)>,
389
+ output_format: OutputFormat,
390
+ no_warnings: bool,
391
+ ) -> (bool, bool) {
392
+ let mut has_error = false;
393
+ let mut has_warning = false;
394
+
395
+ for (idx, outcome) in results {
396
+ let (path, ..) = &files[idx];
397
+ match outcome {
398
+ Err(message) => {
399
+ eprintln!("{message}");
400
+ has_error = true;
401
+ }
402
+ Ok(diagnostics) => {
403
+ let mut problems = diagnostics
404
+ .iter()
405
+ .filter(|problem| {
406
+ !(no_warnings && problem.level == Severity::Warning)
407
+ })
408
+ .peekable();
409
+
410
+ if problems.peek().is_none() {
411
+ continue;
412
+ }
413
+
414
+ match output_format {
415
+ OutputFormat::Standard => {
416
+ eprintln!("{}", path.display());
417
+ for problem in problems {
418
+ eprintln!("{}", format_standard(problem));
419
+ match problem.level {
420
+ Severity::Error => has_error = true,
421
+ Severity::Warning => has_warning = true,
422
+ }
423
+ }
424
+ eprintln!();
425
+ }
426
+ OutputFormat::Colored => {
427
+ eprintln!("\u{001b}[4m{}\u{001b}[0m", path.display());
428
+ for problem in problems {
429
+ eprintln!("{}", format_colored(problem));
430
+ match problem.level {
431
+ Severity::Error => has_error = true,
432
+ Severity::Warning => has_warning = true,
433
+ }
434
+ }
435
+ eprintln!();
436
+ }
437
+ OutputFormat::Github => {
438
+ eprintln!("::group::{}", path.display());
439
+ for problem in problems {
440
+ eprintln!("{}", format_github(problem, path));
441
+ match problem.level {
442
+ Severity::Error => has_error = true,
443
+ Severity::Warning => has_warning = true,
444
+ }
445
+ }
446
+ eprintln!("::endgroup::");
447
+ eprintln!();
448
+ }
449
+ OutputFormat::Parsable => {
450
+ for problem in problems {
451
+ eprintln!("{}", format_parsable(problem, path));
452
+ match problem.level {
453
+ Severity::Error => has_error = true,
454
+ Severity::Warning => has_warning = true,
455
+ }
456
+ }
457
+ }
458
+ }
459
+ }
460
+ }
461
+ }
462
+
463
+ (has_error, has_warning)
464
+ }
465
+
466
+ fn format_standard(problem: &LintProblem) -> String {
467
+ let mut line = format!(" {}:{}", problem.line, problem.column);
468
+ line.push_str(&" ".repeat(12usize.saturating_sub(line.len())));
469
+ line.push_str(problem.level.as_str());
470
+ line.push_str(&" ".repeat(21usize.saturating_sub(line.len())));
471
+ line.push_str(&problem.message);
472
+ if let Some(rule) = problem.rule {
473
+ line.push_str(" (");
474
+ line.push_str(rule);
475
+ line.push(')');
476
+ }
477
+ line
478
+ }
479
+
480
+ fn format_colored(problem: &LintProblem) -> String {
481
+ let mut line = format!(
482
+ " \u{001b}[2m{}:{}\u{001b}[0m",
483
+ problem.line, problem.column
484
+ );
485
+ line.push_str(&" ".repeat(20usize.saturating_sub(line.len())));
486
+ let level_str = match problem.level {
487
+ Severity::Warning => "\u{001b}[33mwarning\u{001b}[0m",
488
+ Severity::Error => "\u{001b}[31merror\u{001b}[0m",
489
+ };
490
+ line.push_str(level_str);
491
+ line.push_str(&" ".repeat(38usize.saturating_sub(line.len())));
492
+ line.push_str(&problem.message);
493
+ if let Some(rule) = problem.rule {
494
+ line.push_str(" \u{001b}[2m(");
495
+ line.push_str(rule);
496
+ line.push_str(")\u{001b}[0m");
497
+ }
498
+ line
499
+ }
500
+
501
+ fn format_github(problem: &LintProblem, path: &Path) -> String {
502
+ let mut line = format!(
503
+ "::{} file={},line={},col={}::{}:{} ",
504
+ problem.level.as_str(),
505
+ path.display(),
506
+ problem.line,
507
+ problem.column,
508
+ problem.line,
509
+ problem.column
510
+ );
511
+ if let Some(rule) = problem.rule {
512
+ line.push('[');
513
+ line.push_str(rule);
514
+ line.push_str("] ");
515
+ }
516
+ line.push_str(&problem.message);
517
+ line
518
+ }
519
+
520
+ fn format_parsable(problem: &LintProblem, path: &Path) -> String {
521
+ let mut line = format!(
522
+ "{}:{}:{}: [{}] {}",
523
+ path.display(),
524
+ problem.line,
525
+ problem.column,
526
+ problem.level.as_str(),
527
+ problem.message
528
+ );
529
+ if let Some(rule) = problem.rule {
530
+ line.push_str(" (");
531
+ line.push_str(rule);
532
+ line.push(')');
533
+ }
534
+ line
535
+ }