@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
@@ -0,0 +1,420 @@
1
+ use std::collections::HashSet;
2
+
3
+ use saphyr_parser::{Event, Parser, ScalarStyle, Span, SpannedEventReceiver};
4
+
5
+ use crate::config::YamlLintConfig;
6
+
7
+ pub const ID: &str = "truthy";
8
+
9
+ const TRUTHY_VALUES_YAML_1_1: [&str; 18] = [
10
+ "YES", "Yes", "yes", "NO", "No", "no", "TRUE", "True", "true", "FALSE", "False",
11
+ "false", "ON", "On", "on", "OFF", "Off", "off",
12
+ ];
13
+
14
+ const TRUTHY_VALUES_YAML_1_2: [&str; 6] =
15
+ ["TRUE", "True", "true", "FALSE", "False", "false"];
16
+
17
+ #[derive(Debug, Clone)]
18
+ pub struct Config {
19
+ allowed: HashSet<String>,
20
+ allowed_display: String,
21
+ pub check_keys: bool,
22
+ }
23
+
24
+ impl Config {
25
+ /// Resolve the rule configuration from the parsed yamllint config.
26
+ ///
27
+ /// # Panics
28
+ ///
29
+ /// Panics when `allowed-values` contains a non-string entry. The parser rejects
30
+ /// that configuration, so this only occurs with manual construction in tests.
31
+ #[must_use]
32
+ pub fn resolve(cfg: &YamlLintConfig) -> Self {
33
+ let mut allowed: HashSet<String> = HashSet::new();
34
+ allowed.insert("true".to_string());
35
+ allowed.insert("false".to_string());
36
+ let mut check_keys = true;
37
+
38
+ if let Some(node) = cfg.rule_option(ID, "allowed-values")
39
+ && let Some(seq) = node.as_sequence()
40
+ {
41
+ allowed.clear();
42
+ for value in seq {
43
+ let text = value
44
+ .as_str()
45
+ .expect("truthy allowed-values should be strings");
46
+ allowed.insert(text.to_owned());
47
+ }
48
+ }
49
+
50
+ if let Some(node) = cfg.rule_option(ID, "check-keys")
51
+ && let Some(flag) = node.as_bool()
52
+ {
53
+ check_keys = flag;
54
+ }
55
+
56
+ let mut display_values: Vec<&str> =
57
+ allowed.iter().map(String::as_str).collect();
58
+ display_values.sort_unstable();
59
+ let allowed_display = format!("[{}]", display_values.join(", "));
60
+
61
+ Self {
62
+ allowed,
63
+ allowed_display,
64
+ check_keys,
65
+ }
66
+ }
67
+
68
+ fn allows(&self, value: &str) -> bool {
69
+ self.allowed.contains(value)
70
+ }
71
+
72
+ fn allowed_display(&self) -> &str {
73
+ &self.allowed_display
74
+ }
75
+ }
76
+
77
+ #[derive(Debug, Clone, PartialEq, Eq)]
78
+ pub struct Violation {
79
+ pub line: usize,
80
+ pub column: usize,
81
+ pub message: String,
82
+ }
83
+
84
+ #[derive(Debug)]
85
+ struct ContainerState {
86
+ kind: ContainerKind,
87
+ key_context: bool,
88
+ }
89
+
90
+ #[derive(Debug)]
91
+ enum ContainerKind {
92
+ Sequence,
93
+ Mapping { expect_key: bool },
94
+ }
95
+
96
+ #[derive(Debug)]
97
+ struct TruthyState<'cfg> {
98
+ config: &'cfg Config,
99
+ containers: Vec<ContainerState>,
100
+ key_depth: usize,
101
+ current_version: (u32, u32),
102
+ bad_truthy: Option<HashSet<String>>,
103
+ directives: Vec<(usize, (u32, u32))>,
104
+ directive_index: usize,
105
+ }
106
+
107
+ impl<'cfg> TruthyState<'cfg> {
108
+ const fn new(config: &'cfg Config, directives: Vec<(usize, (u32, u32))>) -> Self {
109
+ Self {
110
+ config,
111
+ containers: Vec::new(),
112
+ key_depth: 0,
113
+ current_version: (1, 1),
114
+ bad_truthy: None,
115
+ directives,
116
+ directive_index: 0,
117
+ }
118
+ }
119
+
120
+ fn document_start(&mut self, span: Span) {
121
+ self.current_version = self.version_for_document(span.start.index());
122
+ self.bad_truthy = None;
123
+ self.key_depth = 0;
124
+ self.containers.clear();
125
+ }
126
+
127
+ fn document_end(&mut self) {
128
+ self.key_depth = 0;
129
+ self.containers.clear();
130
+ }
131
+
132
+ fn version_for_document(&mut self, doc_start: usize) -> (u32, u32) {
133
+ let mut version = None;
134
+ while self.directive_index < self.directives.len()
135
+ && self.directives[self.directive_index].0 < doc_start
136
+ {
137
+ version = Some(self.directives[self.directive_index].1);
138
+ self.directive_index += 1;
139
+ }
140
+ version.unwrap_or((1, 1))
141
+ }
142
+
143
+ fn begin_node(&mut self) -> bool {
144
+ let mut is_key_node = false;
145
+ if let Some(ContainerState {
146
+ kind: ContainerKind::Mapping { expect_key },
147
+ ..
148
+ }) = self.containers.last_mut()
149
+ {
150
+ if *expect_key {
151
+ is_key_node = true;
152
+ *expect_key = false;
153
+ } else {
154
+ *expect_key = true;
155
+ }
156
+ }
157
+
158
+ let active_key = is_key_node || self.key_depth > 0;
159
+ if active_key {
160
+ self.key_depth += 1;
161
+ }
162
+ active_key
163
+ }
164
+
165
+ fn enter_mapping(&mut self) {
166
+ let active_key = self.begin_node();
167
+ self.containers.push(ContainerState {
168
+ kind: ContainerKind::Mapping { expect_key: true },
169
+ key_context: active_key,
170
+ });
171
+ }
172
+
173
+ fn enter_sequence(&mut self) {
174
+ let active_key = self.begin_node();
175
+ self.containers.push(ContainerState {
176
+ kind: ContainerKind::Sequence,
177
+ key_context: active_key,
178
+ });
179
+ }
180
+
181
+ fn exit_container(&mut self) {
182
+ if let Some(container) = self.containers.pop()
183
+ && container.key_context
184
+ && self.key_depth > 0
185
+ {
186
+ self.key_depth -= 1;
187
+ }
188
+ }
189
+
190
+ const fn finish_scalar(&mut self, active_key: bool) {
191
+ if active_key && self.key_depth > 0 {
192
+ self.key_depth -= 1;
193
+ }
194
+ }
195
+
196
+ fn is_bad_truthy(&mut self, value: &str) -> bool {
197
+ if self.bad_truthy.is_none() {
198
+ let base = if self.current_version == (1, 2) {
199
+ &TRUTHY_VALUES_YAML_1_2[..]
200
+ } else {
201
+ &TRUTHY_VALUES_YAML_1_1[..]
202
+ };
203
+ let mut set: HashSet<String> = HashSet::new();
204
+ for candidate in base {
205
+ if !self.config.allows(candidate) {
206
+ set.insert((*candidate).to_string());
207
+ }
208
+ }
209
+ self.bad_truthy = Some(set);
210
+ }
211
+ self.bad_truthy
212
+ .as_ref()
213
+ .expect("bad truthy set initialised")
214
+ .contains(value)
215
+ }
216
+
217
+ fn handle_scalar(
218
+ &mut self,
219
+ style: ScalarStyle,
220
+ value: &str,
221
+ tagged: bool,
222
+ span: Span,
223
+ diagnostics: &mut Vec<Violation>,
224
+ ) {
225
+ let active_key = self.begin_node();
226
+
227
+ if tagged {
228
+ self.finish_scalar(active_key);
229
+ return;
230
+ }
231
+
232
+ if !matches!(style, ScalarStyle::Plain) {
233
+ self.finish_scalar(active_key);
234
+ return;
235
+ }
236
+
237
+ if active_key && !self.config.check_keys {
238
+ self.finish_scalar(active_key);
239
+ return;
240
+ }
241
+
242
+ if self.is_bad_truthy(value) {
243
+ diagnostics.push(Violation {
244
+ line: span.start.line(),
245
+ column: span.start.col() + 1,
246
+ message: format!(
247
+ "truthy value should be one of {}",
248
+ self.config.allowed_display()
249
+ ),
250
+ });
251
+ }
252
+
253
+ self.finish_scalar(active_key);
254
+ }
255
+ }
256
+
257
+ struct TruthyReceiver<'cfg> {
258
+ state: TruthyState<'cfg>,
259
+ diagnostics: Vec<Violation>,
260
+ }
261
+
262
+ #[allow(clippy::missing_const_for_fn)]
263
+ impl<'cfg> TruthyReceiver<'cfg> {
264
+ fn new(cfg: &'cfg Config, directives: Vec<(usize, (u32, u32))>) -> Self {
265
+ Self {
266
+ state: TruthyState::new(cfg, directives),
267
+ diagnostics: Vec::new(),
268
+ }
269
+ }
270
+ }
271
+
272
+ impl SpannedEventReceiver<'_> for TruthyReceiver<'_> {
273
+ fn on_event(&mut self, event: Event<'_>, span: Span) {
274
+ match event {
275
+ Event::StreamStart => {
276
+ self.state.current_version = (1, 1);
277
+ self.state.bad_truthy = None;
278
+ self.state.directive_index = 0;
279
+ }
280
+ Event::DocumentStart(_) => self.state.document_start(span),
281
+ Event::DocumentEnd => self.state.document_end(),
282
+ Event::SequenceStart(_, _) => self.state.enter_sequence(),
283
+ Event::SequenceEnd | Event::MappingEnd => self.state.exit_container(),
284
+ Event::MappingStart(_, _) => self.state.enter_mapping(),
285
+ Event::Scalar(value, style, _, tag) => {
286
+ let tagged = tag.is_some();
287
+ self.state.handle_scalar(
288
+ style,
289
+ value.as_ref(),
290
+ tagged,
291
+ span,
292
+ &mut self.diagnostics,
293
+ );
294
+ }
295
+ Event::Alias(_) | Event::StreamEnd | Event::Nothing => {}
296
+ }
297
+ }
298
+ }
299
+
300
+ #[must_use]
301
+ pub fn check(buffer: &str, cfg: &Config) -> Vec<Violation> {
302
+ let directives = collect_yaml_directives(buffer);
303
+ let mut parser = Parser::new_from_str(buffer);
304
+ let mut receiver = TruthyReceiver::new(cfg, directives);
305
+ let _ = parser.load(&mut receiver, true);
306
+ let mut diagnostics = receiver.diagnostics;
307
+ let disabled_lines = collect_truthy_disable_lines(buffer);
308
+ if !disabled_lines.is_empty() {
309
+ diagnostics.retain(|violation| !disabled_lines.contains(&violation.line));
310
+ }
311
+ diagnostics
312
+ }
313
+
314
+ fn collect_yaml_directives(buffer: &str) -> Vec<(usize, (u32, u32))> {
315
+ let mut directives = Vec::new();
316
+ let mut offset = 0;
317
+ for segment in buffer.split_inclusive(['\n']) {
318
+ let line = segment.trim_end_matches(['\n', '\r']);
319
+ if let Some(version) = parse_yaml_directive(line) {
320
+ let leading = line.len() - line.trim_start().len();
321
+ directives.push((offset + leading, version));
322
+ }
323
+ offset += segment.len();
324
+ }
325
+ directives
326
+ }
327
+
328
+ fn parse_yaml_directive(line: &str) -> Option<(u32, u32)> {
329
+ let trimmed = line.trim_start();
330
+ if !trimmed.starts_with("%YAML") {
331
+ return None;
332
+ }
333
+ let mut parts = trimmed.split_whitespace();
334
+ let _ = parts.next();
335
+ let version = parts.next()?;
336
+ let (major_raw, minor_raw) = version.split_once('.')?;
337
+ let major = major_raw.parse().ok()?;
338
+ let minor = minor_raw.parse().ok()?;
339
+ Some((major, minor))
340
+ }
341
+
342
+ fn collect_truthy_disable_lines(buffer: &str) -> HashSet<usize> {
343
+ let mut disabled = HashSet::new();
344
+ let mut disable_next = false;
345
+
346
+ for (index, segment) in buffer.split_inclusive(['\n']).enumerate() {
347
+ let line_number = index + 1;
348
+ let line = segment.trim_end_matches(['\n', '\r']);
349
+
350
+ if disable_next {
351
+ disabled.insert(line_number);
352
+ disable_next = false;
353
+ }
354
+
355
+ let Some(comment_start) = find_comment_start(line) else {
356
+ continue;
357
+ };
358
+
359
+ let inline = !line[..comment_start].trim().is_empty();
360
+ let comment = line[comment_start..].trim_end();
361
+
362
+ if disables_truthy(comment) {
363
+ if inline {
364
+ disabled.insert(line_number);
365
+ } else {
366
+ disable_next = true;
367
+ }
368
+ }
369
+ }
370
+
371
+ disabled
372
+ }
373
+
374
+ fn find_comment_start(line: &str) -> Option<usize> {
375
+ let mut in_single = false;
376
+ let mut in_double = false;
377
+ let mut escaped = false;
378
+ for (idx, ch) in line.char_indices() {
379
+ match ch {
380
+ '\\' if !in_single => {
381
+ escaped = !escaped;
382
+ }
383
+ '\'' if !escaped && !in_double => {
384
+ in_single = !in_single;
385
+ }
386
+ '"' if !escaped && !in_single => {
387
+ in_double = !in_double;
388
+ }
389
+ '#' if !escaped && !in_single && !in_double => return Some(idx),
390
+ _ => {
391
+ escaped = false;
392
+ }
393
+ }
394
+ }
395
+ None
396
+ }
397
+
398
+ fn disables_truthy(comment: &str) -> bool {
399
+ let trimmed = comment.trim_start_matches('#').trim_start();
400
+ if !trimmed.starts_with("yamllint disable-line") {
401
+ return false;
402
+ }
403
+ let rest = trimmed.trim_start_matches("yamllint disable-line").trim();
404
+ if rest.is_empty() {
405
+ return true;
406
+ }
407
+
408
+ let mut any_rules = false;
409
+ let mut matches = false;
410
+ for token in rest.split_whitespace() {
411
+ if let Some(name) = token.strip_prefix("rule:") {
412
+ any_rules = true;
413
+ if name == ID {
414
+ matches = true;
415
+ }
416
+ }
417
+ }
418
+
419
+ if any_rules { matches } else { true }
420
+ }
@@ -0,0 +1,114 @@
1
+ use ryl::rules::brackets::{Config, Forbid, check};
2
+
3
+ fn config(forbid: Forbid) -> Config {
4
+ Config::new_for_tests(forbid, 0, 0, -1, -1)
5
+ }
6
+
7
+ #[test]
8
+ fn brackets_carriage_return_without_line_feed_is_retained() {
9
+ let violations = check("[\r]", &config(Forbid::None));
10
+ assert!(violations.is_empty());
11
+ }
12
+
13
+ #[test]
14
+ fn brackets_comment_crlf_is_ignored() {
15
+ let diagnostics = check("[# comment\r\n]", &config(Forbid::None));
16
+ assert!(diagnostics.is_empty());
17
+ }
18
+
19
+ #[test]
20
+ fn brackets_comment_carriage_return_is_ignored() {
21
+ let diagnostics = check("[# comment\r]", &config(Forbid::None));
22
+ assert!(diagnostics.is_empty());
23
+ }
24
+
25
+ #[test]
26
+ fn brackets_carriage_return_line_feed_is_ignored() {
27
+ let diagnostics = check("[\r\n]", &config(Forbid::None));
28
+ assert!(diagnostics.is_empty());
29
+ }
30
+
31
+ #[test]
32
+ fn brackets_comment_newline_is_ignored() {
33
+ let diagnostics = check("[# comment\n ]", &config(Forbid::None));
34
+ assert!(diagnostics.is_empty());
35
+ }
36
+
37
+ #[test]
38
+ fn brackets_non_scalar_marks_sequence_non_empty() {
39
+ let cfg = Config::new_for_tests(Forbid::NonEmpty, 0, 0, -1, -1);
40
+ let diagnostics = check("[ value ]", &cfg);
41
+ assert!(
42
+ diagnostics
43
+ .iter()
44
+ .any(|d| d.message == "forbidden flow sequence")
45
+ );
46
+ }
47
+
48
+ #[test]
49
+ fn brackets_records_spacing_after_open() {
50
+ let cfg = Config::new_for_tests(Forbid::None, 1, 1, -1, -1);
51
+ let diagnostics = check("[value]", &cfg);
52
+ assert!(
53
+ diagnostics
54
+ .iter()
55
+ .any(|d| d.message == "too few spaces inside brackets")
56
+ );
57
+ }
58
+
59
+ #[test]
60
+ fn brackets_records_spacing_too_many() {
61
+ let cfg = Config::new_for_tests(Forbid::None, -1, 0, -1, -1);
62
+ let diagnostics = check("[ value]", &cfg);
63
+ assert!(
64
+ diagnostics
65
+ .iter()
66
+ .any(|d| d.message == "too many spaces inside brackets")
67
+ );
68
+ }
69
+
70
+ #[test]
71
+ fn brackets_spacing_ignored_across_newline() {
72
+ let diagnostics = check("[\n]", &config(Forbid::None));
73
+ assert!(diagnostics.is_empty());
74
+ }
75
+
76
+ #[test]
77
+ fn brackets_unmatched_closing_is_ignored() {
78
+ let diagnostics = check("]", &config(Forbid::None));
79
+ assert!(diagnostics.is_empty());
80
+ }
81
+
82
+ #[test]
83
+ fn brackets_plain_character_marks_sequence_non_empty() {
84
+ let cfg = Config::new_for_tests(Forbid::NonEmpty, 0, 0, -1, -1);
85
+ let diagnostics = check("[a]", &cfg);
86
+ assert!(
87
+ diagnostics
88
+ .iter()
89
+ .any(|d| d.message == "forbidden flow sequence")
90
+ );
91
+ }
92
+
93
+ #[test]
94
+ fn brackets_comment_without_newline_is_ignored() {
95
+ let diagnostics = check("[# trailing comment", &config(Forbid::None));
96
+ assert!(diagnostics.is_empty());
97
+ }
98
+
99
+ #[test]
100
+ fn brackets_truncated_sequence_skips_spacing_checks() {
101
+ let diagnostics = check("[ ", &config(Forbid::None));
102
+ assert!(diagnostics.is_empty());
103
+ }
104
+
105
+ #[test]
106
+ fn brackets_brace_content_marks_sequence_non_empty() {
107
+ let cfg = Config::new_for_tests(Forbid::NonEmpty, 0, 0, -1, -1);
108
+ let diagnostics = check("[{key: 1}]", &cfg);
109
+ assert!(
110
+ diagnostics
111
+ .iter()
112
+ .any(|d| d.message == "forbidden flow sequence")
113
+ );
114
+ }
@@ -0,0 +1,23 @@
1
+ use std::fs;
2
+ use std::process::Command;
3
+
4
+ use tempfile::tempdir;
5
+
6
+ #[test]
7
+ fn config_file_argument_pointing_to_directory_errors() {
8
+ let td = tempdir().unwrap();
9
+ let root = td.path();
10
+ fs::create_dir(root.join("cfgdir")).unwrap();
11
+ fs::write(root.join("a.yaml"), "a: 1\n").unwrap();
12
+ let exe = env!("CARGO_BIN_EXE_ryl");
13
+ let out = Command::new(exe)
14
+ .arg("--list-files")
15
+ .arg("-c")
16
+ .arg(root.join("cfgdir"))
17
+ .arg(root)
18
+ .output()
19
+ .expect("run");
20
+ assert_eq!(out.status.code(), Some(2));
21
+ let err = String::from_utf8_lossy(&out.stderr);
22
+ assert!(err.contains("failed to read"));
23
+ }
@@ -0,0 +1,143 @@
1
+ use std::fs;
2
+ use std::process::Command;
3
+
4
+ use tempfile::tempdir;
5
+
6
+ fn run(cmd: &mut Command) -> (i32, String, String) {
7
+ let out = cmd.output().expect("process");
8
+ let code = out.status.code().unwrap_or(-1);
9
+ let stdout = String::from_utf8_lossy(&out.stdout).into_owned();
10
+ let stderr = String::from_utf8_lossy(&out.stderr).into_owned();
11
+ (code, stdout, stderr)
12
+ }
13
+
14
+ #[test]
15
+ fn anchors_reports_error() {
16
+ let dir = tempdir().unwrap();
17
+ let file = dir.path().join("invalid.yaml");
18
+ fs::write(&file, "---\n- *missing\n- &missing value\n").unwrap();
19
+
20
+ let exe = env!("CARGO_BIN_EXE_ryl");
21
+ let (code, stdout, stderr) = run(Command::new(exe).arg(&file));
22
+ assert_eq!(code, 1, "expected failure: stdout={stdout} stderr={stderr}");
23
+ let output = if stderr.is_empty() { stdout } else { stderr };
24
+ assert!(
25
+ output.contains("found undeclared alias \"missing\""),
26
+ "missing message: {output}"
27
+ );
28
+ assert!(
29
+ output.contains("anchors"),
30
+ "rule id missing from output: {output}"
31
+ );
32
+ }
33
+
34
+ #[test]
35
+ fn warning_level_does_not_fail() {
36
+ let dir = tempdir().unwrap();
37
+ let file = dir.path().join("warn.yaml");
38
+ fs::write(&file, "---\n- *missing\n- &missing value\n").unwrap();
39
+ let config = dir.path().join("config.yml");
40
+ fs::write(
41
+ &config,
42
+ "rules:\n document-start: disable\n anchors:\n level: warning\n",
43
+ )
44
+ .unwrap();
45
+
46
+ let exe = env!("CARGO_BIN_EXE_ryl");
47
+ let (code, stdout, stderr) =
48
+ run(Command::new(exe).arg("-c").arg(&config).arg(&file));
49
+ assert_eq!(
50
+ code, 0,
51
+ "warnings should not fail: stdout={stdout} stderr={stderr}"
52
+ );
53
+ let output = if stderr.is_empty() { stdout } else { stderr };
54
+ assert!(
55
+ output.contains("warning"),
56
+ "expected warning output: {output}"
57
+ );
58
+ }
59
+
60
+ #[test]
61
+ fn duplicate_anchor_reports_error_when_enabled() {
62
+ let dir = tempdir().unwrap();
63
+ let file = dir.path().join("dupe.yaml");
64
+ fs::write(&file, "---\n- &anchor one\n- &anchor two\n").unwrap();
65
+ let config = dir.path().join("config.yml");
66
+ fs::write(
67
+ &config,
68
+ "rules:\n document-start: disable\n anchors:\n forbid-duplicated-anchors: true\n",
69
+ )
70
+ .unwrap();
71
+
72
+ let exe = env!("CARGO_BIN_EXE_ryl");
73
+ let (code, stdout, stderr) =
74
+ run(Command::new(exe).arg("-c").arg(&config).arg(&file));
75
+ assert_eq!(code, 1, "expected failure: stdout={stdout} stderr={stderr}");
76
+ let output = if stderr.is_empty() { stdout } else { stderr };
77
+ assert!(
78
+ output.contains("found duplicated anchor \"anchor\""),
79
+ "missing duplicate message: {output}"
80
+ );
81
+ }
82
+
83
+ #[test]
84
+ fn unused_anchor_reports_error_when_enabled() {
85
+ let dir = tempdir().unwrap();
86
+ let file = dir.path().join("unused.yaml");
87
+ fs::write(&file, "---\n- &anchor value\n- 1\n").unwrap();
88
+ let config = dir.path().join("config.yml");
89
+ fs::write(
90
+ &config,
91
+ "rules:\n document-start: disable\n anchors:\n forbid-unused-anchors: true\n",
92
+ )
93
+ .unwrap();
94
+
95
+ let exe = env!("CARGO_BIN_EXE_ryl");
96
+ let (code, stdout, stderr) =
97
+ run(Command::new(exe).arg("-c").arg(&config).arg(&file));
98
+ assert_eq!(code, 1, "expected failure: stdout={stdout} stderr={stderr}");
99
+ let output = if stderr.is_empty() { stdout } else { stderr };
100
+ assert!(
101
+ output.contains("found unused anchor \"anchor\""),
102
+ "missing unused message: {output}"
103
+ );
104
+ }
105
+
106
+ #[test]
107
+ fn rule_ignore_skips_file() {
108
+ let dir = tempdir().unwrap();
109
+ let file = dir.path().join("ignored.yaml");
110
+ fs::write(&file, "---\n- *missing\n").unwrap();
111
+ let config = dir.path().join("config.yml");
112
+ fs::write(
113
+ &config,
114
+ "rules:\n document-start: disable\n anchors:\n ignore:\n - ignored.yaml\n",
115
+ )
116
+ .unwrap();
117
+
118
+ let exe = env!("CARGO_BIN_EXE_ryl");
119
+ let (code, stdout, stderr) =
120
+ run(Command::new(exe).arg("-c").arg(&config).arg(&file));
121
+ assert_eq!(
122
+ code, 0,
123
+ "ignored file should pass: stdout={stdout} stderr={stderr}"
124
+ );
125
+ assert!(stdout.trim().is_empty(), "expected no stdout: {stdout}");
126
+ assert!(stderr.trim().is_empty(), "expected no stderr: {stderr}");
127
+ }
128
+
129
+ #[test]
130
+ fn alias_value_with_only_indent_prefix_is_supported() {
131
+ let dir = tempdir().unwrap();
132
+ let file = dir.path().join("alias.yaml");
133
+ fs::write(&file, "---\nvalue: &anchor literal\nalias:\n *anchor\n").unwrap();
134
+
135
+ let exe = env!("CARGO_BIN_EXE_ryl");
136
+ let (code, stdout, stderr) = run(Command::new(exe).arg(&file));
137
+ assert_eq!(
138
+ code, 0,
139
+ "alias resolved successfully: stdout={stdout} stderr={stderr}"
140
+ );
141
+ assert!(stdout.trim().is_empty(), "expected no stdout: {stdout}");
142
+ assert!(stderr.trim().is_empty(), "expected no stderr: {stderr}");
143
+ }