@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,517 @@
1
+ use std::collections::HashMap;
2
+
3
+ use crate::config::YamlLintConfig;
4
+
5
+ pub const ID: &str = "anchors";
6
+ pub const MESSAGE_UNDECLARED_ALIAS: &str = "found undeclared alias";
7
+ pub const MESSAGE_DUPLICATED_ANCHOR: &str = "found duplicated anchor";
8
+ pub const MESSAGE_UNUSED_ANCHOR: &str = "found unused anchor";
9
+
10
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
11
+ pub struct Config {
12
+ forbid_undeclared_aliases: bool,
13
+ forbid_duplicated_anchors: bool,
14
+ forbid_unused_anchors: bool,
15
+ }
16
+
17
+ impl Config {
18
+ #[must_use]
19
+ pub fn resolve(cfg: &YamlLintConfig) -> Self {
20
+ let forbid_undeclared_aliases = cfg
21
+ .rule_option(ID, "forbid-undeclared-aliases")
22
+ .and_then(saphyr::YamlOwned::as_bool)
23
+ .unwrap_or(true);
24
+ let forbid_duplicated_anchors = cfg
25
+ .rule_option(ID, "forbid-duplicated-anchors")
26
+ .and_then(saphyr::YamlOwned::as_bool)
27
+ .unwrap_or(false);
28
+ let forbid_unused_anchors = cfg
29
+ .rule_option(ID, "forbid-unused-anchors")
30
+ .and_then(saphyr::YamlOwned::as_bool)
31
+ .unwrap_or(false);
32
+
33
+ Self {
34
+ forbid_undeclared_aliases,
35
+ forbid_duplicated_anchors,
36
+ forbid_unused_anchors,
37
+ }
38
+ }
39
+
40
+ #[must_use]
41
+ pub const fn new_for_tests(
42
+ forbid_undeclared_aliases: bool,
43
+ forbid_duplicated_anchors: bool,
44
+ forbid_unused_anchors: bool,
45
+ ) -> Self {
46
+ Self {
47
+ forbid_undeclared_aliases,
48
+ forbid_duplicated_anchors,
49
+ forbid_unused_anchors,
50
+ }
51
+ }
52
+
53
+ #[must_use]
54
+ pub const fn forbid_undeclared_aliases(&self) -> bool {
55
+ self.forbid_undeclared_aliases
56
+ }
57
+
58
+ #[must_use]
59
+ pub const fn forbid_duplicated_anchors(&self) -> bool {
60
+ self.forbid_duplicated_anchors
61
+ }
62
+
63
+ #[must_use]
64
+ pub const fn forbid_unused_anchors(&self) -> bool {
65
+ self.forbid_unused_anchors
66
+ }
67
+ }
68
+
69
+ #[derive(Debug, Clone, PartialEq, Eq)]
70
+ pub struct Violation {
71
+ pub line: usize,
72
+ pub column: usize,
73
+ pub message: String,
74
+ }
75
+
76
+ #[must_use]
77
+ pub fn check(buffer: &str, cfg: &Config) -> Vec<Violation> {
78
+ let mut analyzer = Analyzer::new(buffer, cfg);
79
+ analyzer.run();
80
+ analyzer.into_violations()
81
+ }
82
+
83
+ struct Analyzer<'cfg, 'src> {
84
+ source: &'src str,
85
+ cfg: &'cfg Config,
86
+ doc: DocState,
87
+ block_state: Option<BlockState>,
88
+ in_single_quote: bool,
89
+ in_double_quote: bool,
90
+ violations: Vec<Violation>,
91
+ }
92
+
93
+ impl<'cfg, 'src> Analyzer<'cfg, 'src> {
94
+ fn new(source: &'src str, cfg: &'cfg Config) -> Self {
95
+ Self {
96
+ source,
97
+ cfg,
98
+ doc: DocState::new(),
99
+ block_state: None,
100
+ in_single_quote: false,
101
+ in_double_quote: false,
102
+ violations: Vec::new(),
103
+ }
104
+ }
105
+
106
+ fn run(&mut self) {
107
+ if self.source.is_empty() {
108
+ return;
109
+ }
110
+
111
+ let mut line_start = 0usize;
112
+ let mut line_number = 1usize;
113
+ while line_start <= self.source.len() {
114
+ let line_end = match self.source[line_start..].find('\n') {
115
+ Some(rel) => line_start + rel + 1,
116
+ None => self.source.len(),
117
+ };
118
+ let mut line = &self.source[line_start..line_end];
119
+ if line.ends_with('\n') {
120
+ line = &line[..line.len() - 1];
121
+ }
122
+ if line.ends_with('\r') {
123
+ line = &line[..line.len() - 1];
124
+ }
125
+ self.process_line(line, line_number);
126
+ if line_end == self.source.len() {
127
+ break;
128
+ }
129
+ line_start = line_end;
130
+ line_number += 1;
131
+ }
132
+
133
+ self.finish_doc();
134
+ }
135
+
136
+ #[allow(clippy::too_many_lines)]
137
+ fn process_line(&mut self, line: &str, line_number: usize) {
138
+ let chars: Vec<char> = line.chars().collect();
139
+ let indent_count = chars
140
+ .iter()
141
+ .take_while(|ch| matches!(ch, ' ' | '\t'))
142
+ .count();
143
+
144
+ if self.handle_block_state(indent_count, line) {
145
+ return;
146
+ }
147
+
148
+ if chars.is_empty() {
149
+ return;
150
+ }
151
+
152
+ let skip_until = if self.detect_doc_boundary(&chars, indent_count) {
153
+ (indent_count + 3).min(chars.len())
154
+ } else {
155
+ 0usize
156
+ };
157
+
158
+ let mut idx = 0usize;
159
+ let mut column = 1usize;
160
+ let mut comment_active = false;
161
+
162
+ while idx < chars.len() {
163
+ if idx < skip_until {
164
+ idx += 1;
165
+ column += 1;
166
+ continue;
167
+ }
168
+
169
+ let ch = chars[idx];
170
+ if comment_active {
171
+ break;
172
+ }
173
+
174
+ if self.in_single_quote {
175
+ if ch == '\'' {
176
+ if idx + 1 < chars.len() && chars[idx + 1] == '\'' {
177
+ idx += 2;
178
+ column += 2;
179
+ } else {
180
+ self.in_single_quote = false;
181
+ idx += 1;
182
+ column += 1;
183
+ }
184
+ } else {
185
+ idx += 1;
186
+ column += 1;
187
+ }
188
+ continue;
189
+ }
190
+
191
+ if self.in_double_quote {
192
+ if ch == '"' {
193
+ let escaped = idx > 0 && chars[idx - 1] == '\\';
194
+ if !escaped {
195
+ self.in_double_quote = false;
196
+ }
197
+ }
198
+ idx += 1;
199
+ column += 1;
200
+ continue;
201
+ }
202
+
203
+ match ch {
204
+ '\'' => {
205
+ self.in_single_quote = true;
206
+ idx += 1;
207
+ column += 1;
208
+ }
209
+ '"' => {
210
+ self.in_double_quote = true;
211
+ idx += 1;
212
+ column += 1;
213
+ }
214
+ '#' => {
215
+ comment_active = true;
216
+ }
217
+ '|' | '>' => {
218
+ if self.is_block_indicator(&chars, idx, indent_count) {
219
+ let explicit = parse_explicit_indent(&chars, idx + 1);
220
+ self.block_state = Some(BlockState {
221
+ indent_base: indent_count,
222
+ explicit_indent: explicit,
223
+ required_indent: None,
224
+ activate_next_line: true,
225
+ active: false,
226
+ });
227
+ }
228
+ idx += 1;
229
+ column += 1;
230
+ }
231
+ '&' => {
232
+ if let Some((anchor_name, len)) = parse_name(&chars, idx + 1) {
233
+ self.register_anchor(&anchor_name, line_number, column);
234
+ idx += len + 1;
235
+ column += len + 1;
236
+ } else {
237
+ idx += 1;
238
+ column += 1;
239
+ }
240
+ }
241
+ '*' => {
242
+ if self.is_alias_indicator(&chars, idx) {
243
+ if let Some((alias_name, len)) = parse_name(&chars, idx + 1) {
244
+ self.register_alias(&alias_name, line_number, column);
245
+ idx += len + 1;
246
+ column += len + 1;
247
+ } else {
248
+ idx += 1;
249
+ column += 1;
250
+ }
251
+ } else {
252
+ idx += 1;
253
+ column += 1;
254
+ }
255
+ }
256
+ _ => {
257
+ idx += 1;
258
+ column += 1;
259
+ }
260
+ }
261
+ }
262
+ }
263
+
264
+ fn handle_block_state(&mut self, indent_count: usize, line: &str) -> bool {
265
+ if let Some(block) = self.block_state.as_mut() {
266
+ if block.activate_next_line {
267
+ block.activate_next_line = false;
268
+ block.active = true;
269
+ if line.trim().is_empty() {
270
+ return true;
271
+ }
272
+ }
273
+
274
+ if line.trim().is_empty() {
275
+ return true;
276
+ }
277
+ let explicit_indent =
278
+ block.explicit_indent.map(|value| block.indent_base + value);
279
+ let required_indent = match (explicit_indent, block.required_indent) {
280
+ (Some(explicit), _) => explicit,
281
+ (None, Some(required)) => required,
282
+ (None, None) => {
283
+ if indent_count > block.indent_base {
284
+ block.required_indent = Some(indent_count);
285
+ indent_count
286
+ } else {
287
+ self.block_state = None;
288
+ return false;
289
+ }
290
+ }
291
+ };
292
+ let stays_in_block = indent_count >= required_indent;
293
+ if !stays_in_block {
294
+ self.block_state = None;
295
+ }
296
+ return stays_in_block;
297
+ }
298
+ false
299
+ }
300
+
301
+ fn detect_doc_boundary(&mut self, chars: &[char], indent_count: usize) -> bool {
302
+ if self.in_single_quote || self.in_double_quote {
303
+ return false;
304
+ }
305
+ if chars.len() < indent_count + 3 {
306
+ return false;
307
+ }
308
+
309
+ let candidate = &chars[indent_count..];
310
+ let marker = &candidate[..3];
311
+ let is_boundary_marker = matches!(marker, ['-', '-', '-'] | ['.', '.', '.']);
312
+ if is_boundary_marker
313
+ && (candidate.len() == 3
314
+ || candidate.get(3).is_some_and(|ch| ch.is_whitespace()))
315
+ {
316
+ self.finish_doc();
317
+ self.reset_block_and_quotes();
318
+ return true;
319
+ }
320
+ false
321
+ }
322
+
323
+ const fn reset_block_and_quotes(&mut self) {
324
+ self.block_state = None;
325
+ self.in_single_quote = false;
326
+ self.in_double_quote = false;
327
+ }
328
+
329
+ fn register_anchor(&mut self, name: &str, line: usize, column: usize) {
330
+ let is_duplicate = self.doc.add_anchor(name.to_string(), line, column);
331
+ if self.cfg.forbid_duplicated_anchors() && is_duplicate {
332
+ self.violations.push(Violation {
333
+ line,
334
+ column,
335
+ message: format!("{MESSAGE_DUPLICATED_ANCHOR} \"{name}\""),
336
+ });
337
+ }
338
+ }
339
+
340
+ fn register_alias(&mut self, name: &str, line: usize, column: usize) {
341
+ if self.doc.mark_alias(name) {
342
+ return;
343
+ }
344
+ if self.cfg.forbid_undeclared_aliases() {
345
+ self.violations.push(Violation {
346
+ line,
347
+ column,
348
+ message: format!("{MESSAGE_UNDECLARED_ALIAS} \"{name}\""),
349
+ });
350
+ }
351
+ }
352
+
353
+ fn finish_doc(&mut self) {
354
+ if self.cfg.forbid_unused_anchors() {
355
+ for anchor in &self.doc.anchors {
356
+ if !anchor.used {
357
+ self.violations.push(Violation {
358
+ line: anchor.line,
359
+ column: anchor.column,
360
+ message: format!("{MESSAGE_UNUSED_ANCHOR} \"{}\"", anchor.name),
361
+ });
362
+ }
363
+ }
364
+ }
365
+ self.doc = DocState::new();
366
+ }
367
+
368
+ fn into_violations(self) -> Vec<Violation> {
369
+ self.violations
370
+ }
371
+
372
+ fn is_block_indicator(
373
+ &self,
374
+ chars: &[char],
375
+ idx: usize,
376
+ indent_count: usize,
377
+ ) -> bool {
378
+ debug_assert!(!self.in_single_quote && !self.in_double_quote);
379
+ debug_assert!(idx >= indent_count);
380
+ let prefix = &chars[..idx];
381
+ let last_non_ws = prefix.iter().rev().find(|ch| !ch.is_whitespace());
382
+ matches!(last_non_ws, None | Some(':' | '-' | '?'))
383
+ }
384
+
385
+ fn is_alias_indicator(&self, chars: &[char], idx: usize) -> bool {
386
+ debug_assert!(!self.in_single_quote && !self.in_double_quote);
387
+ let Some(prev) = idx.checked_sub(1).and_then(|prev_idx| chars.get(prev_idx))
388
+ else {
389
+ return true;
390
+ };
391
+ matches!(prev, ' ' | '\t' | ':' | '[' | '{' | ',' | '?')
392
+ }
393
+ }
394
+
395
+ struct DocState {
396
+ anchors: Vec<AnchorRecord>,
397
+ name_to_indices: HashMap<String, Vec<usize>>,
398
+ }
399
+
400
+ impl DocState {
401
+ fn new() -> Self {
402
+ Self {
403
+ anchors: Vec::new(),
404
+ name_to_indices: HashMap::new(),
405
+ }
406
+ }
407
+
408
+ fn add_anchor(&mut self, name: String, line: usize, column: usize) -> bool {
409
+ let entry_indices = self.name_to_indices.entry(name.clone()).or_default();
410
+ let duplicate = !entry_indices.is_empty();
411
+ let index = self.anchors.len();
412
+ entry_indices.push(index);
413
+ self.anchors.push(AnchorRecord {
414
+ name,
415
+ line,
416
+ column,
417
+ used: false,
418
+ });
419
+ duplicate
420
+ }
421
+
422
+ fn mark_alias(&mut self, name: &str) -> bool {
423
+ let Some(indices) = self.name_to_indices.get(name) else {
424
+ return false;
425
+ };
426
+ let last_index = *indices
427
+ .last()
428
+ .expect("anchor indices should contain at least one entry");
429
+ let anchor = self
430
+ .anchors
431
+ .get_mut(last_index)
432
+ .expect("anchor record must exist for referenced name");
433
+ anchor.used = true;
434
+ true
435
+ }
436
+ }
437
+
438
+ struct AnchorRecord {
439
+ name: String,
440
+ line: usize,
441
+ column: usize,
442
+ used: bool,
443
+ }
444
+
445
+ struct BlockState {
446
+ indent_base: usize,
447
+ explicit_indent: Option<usize>,
448
+ required_indent: Option<usize>,
449
+ activate_next_line: bool,
450
+ active: bool,
451
+ }
452
+
453
+ fn parse_name(chars: &[char], start: usize) -> Option<(String, usize)> {
454
+ if start >= chars.len() {
455
+ return None;
456
+ }
457
+ let mut idx = start;
458
+ while idx < chars.len() && is_name_char(chars[idx]) {
459
+ idx += 1;
460
+ }
461
+ if idx == start {
462
+ return None;
463
+ }
464
+ let name: String = chars[start..idx].iter().collect();
465
+ Some((name, idx - start))
466
+ }
467
+
468
+ const fn is_name_char(ch: char) -> bool {
469
+ !matches!(
470
+ ch,
471
+ ' ' | '\t'
472
+ | '\r'
473
+ | '\n'
474
+ | ','
475
+ | '['
476
+ | ']'
477
+ | '{'
478
+ | '}'
479
+ | '*'
480
+ | '&'
481
+ | '#'
482
+ | '!'
483
+ | '|'
484
+ | '>'
485
+ | '\''
486
+ | '"'
487
+ | '%'
488
+ | '@'
489
+ | ':'
490
+ | '?'
491
+ )
492
+ }
493
+
494
+ fn parse_explicit_indent(chars: &[char], mut idx: usize) -> Option<usize> {
495
+ let mut indent = None;
496
+ while idx < chars.len() {
497
+ match chars[idx] {
498
+ '+' | '-' => idx += 1,
499
+ ch if ch.is_ascii_digit() => {
500
+ let mut val = 0usize;
501
+ while idx < chars.len() && chars[idx].is_ascii_digit() {
502
+ val = val
503
+ .saturating_mul(10)
504
+ .saturating_add((chars[idx] as u8 - b'0') as usize);
505
+ idx += 1;
506
+ }
507
+ if val > 0 {
508
+ indent = Some(val);
509
+ }
510
+ break;
511
+ }
512
+ ' ' | '\t' => idx += 1,
513
+ _ => break,
514
+ }
515
+ }
516
+ indent
517
+ }
@@ -0,0 +1,77 @@
1
+ use crate::config::YamlLintConfig;
2
+ use crate::rules::flow_collection::{self, FlowCollectionDescriptor};
3
+
4
+ pub use crate::rules::flow_collection::{Forbid, Violation};
5
+
6
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
7
+ pub struct Config(flow_collection::Config);
8
+
9
+ pub const ID: &str = "braces";
10
+
11
+ const DESCRIPTOR: FlowCollectionDescriptor = FlowCollectionDescriptor {
12
+ open: '{',
13
+ close: '}',
14
+ forbid_message: "forbidden flow mapping",
15
+ min_message: "too few spaces inside braces",
16
+ max_message: "too many spaces inside braces",
17
+ min_empty_message: "too few spaces inside empty braces",
18
+ max_empty_message: "too many spaces inside empty braces",
19
+ };
20
+
21
+ #[must_use]
22
+ pub fn check(buffer: &str, cfg: &Config) -> Vec<Violation> {
23
+ flow_collection::check(buffer, cfg.inner(), &DESCRIPTOR)
24
+ }
25
+
26
+ impl Config {
27
+ #[must_use]
28
+ pub fn resolve(cfg: &YamlLintConfig) -> Self {
29
+ Self(flow_collection::Config::resolve_for(cfg, ID))
30
+ }
31
+
32
+ #[must_use]
33
+ pub const fn new_for_tests(
34
+ forbid: Forbid,
35
+ min_spaces_inside: i64,
36
+ max_spaces_inside: i64,
37
+ min_spaces_inside_empty: i64,
38
+ max_spaces_inside_empty: i64,
39
+ ) -> Self {
40
+ Self(flow_collection::Config::new_for_tests(
41
+ forbid,
42
+ min_spaces_inside,
43
+ max_spaces_inside,
44
+ min_spaces_inside_empty,
45
+ max_spaces_inside_empty,
46
+ ))
47
+ }
48
+
49
+ #[must_use]
50
+ pub const fn effective_min_empty(&self) -> i64 {
51
+ self.0.effective_min_empty()
52
+ }
53
+
54
+ #[must_use]
55
+ pub const fn effective_max_empty(&self) -> i64 {
56
+ self.0.effective_max_empty()
57
+ }
58
+
59
+ #[must_use]
60
+ pub const fn forbid(&self) -> Forbid {
61
+ self.0.forbid()
62
+ }
63
+
64
+ #[must_use]
65
+ pub const fn min_spaces_inside(&self) -> i64 {
66
+ self.0.min_spaces_inside()
67
+ }
68
+
69
+ #[must_use]
70
+ pub const fn max_spaces_inside(&self) -> i64 {
71
+ self.0.max_spaces_inside()
72
+ }
73
+
74
+ const fn inner(&self) -> &flow_collection::Config {
75
+ &self.0
76
+ }
77
+ }
@@ -0,0 +1,77 @@
1
+ use crate::config::YamlLintConfig;
2
+ use crate::rules::flow_collection::{self, FlowCollectionDescriptor};
3
+
4
+ pub use crate::rules::flow_collection::{Forbid, Violation};
5
+
6
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
7
+ pub struct Config(flow_collection::Config);
8
+
9
+ pub const ID: &str = "brackets";
10
+
11
+ const DESCRIPTOR: FlowCollectionDescriptor = FlowCollectionDescriptor {
12
+ open: '[',
13
+ close: ']',
14
+ forbid_message: "forbidden flow sequence",
15
+ min_message: "too few spaces inside brackets",
16
+ max_message: "too many spaces inside brackets",
17
+ min_empty_message: "too few spaces inside empty brackets",
18
+ max_empty_message: "too many spaces inside empty brackets",
19
+ };
20
+
21
+ #[must_use]
22
+ pub fn check(buffer: &str, cfg: &Config) -> Vec<Violation> {
23
+ flow_collection::check(buffer, cfg.inner(), &DESCRIPTOR)
24
+ }
25
+
26
+ impl Config {
27
+ #[must_use]
28
+ pub fn resolve(cfg: &YamlLintConfig) -> Self {
29
+ Self(flow_collection::Config::resolve_for(cfg, ID))
30
+ }
31
+
32
+ #[must_use]
33
+ pub const fn new_for_tests(
34
+ forbid: Forbid,
35
+ min_spaces_inside: i64,
36
+ max_spaces_inside: i64,
37
+ min_spaces_inside_empty: i64,
38
+ max_spaces_inside_empty: i64,
39
+ ) -> Self {
40
+ Self(flow_collection::Config::new_for_tests(
41
+ forbid,
42
+ min_spaces_inside,
43
+ max_spaces_inside,
44
+ min_spaces_inside_empty,
45
+ max_spaces_inside_empty,
46
+ ))
47
+ }
48
+
49
+ #[must_use]
50
+ pub const fn effective_min_empty(&self) -> i64 {
51
+ self.0.effective_min_empty()
52
+ }
53
+
54
+ #[must_use]
55
+ pub const fn effective_max_empty(&self) -> i64 {
56
+ self.0.effective_max_empty()
57
+ }
58
+
59
+ #[must_use]
60
+ pub const fn forbid(&self) -> Forbid {
61
+ self.0.forbid()
62
+ }
63
+
64
+ #[must_use]
65
+ pub const fn min_spaces_inside(&self) -> i64 {
66
+ self.0.min_spaces_inside()
67
+ }
68
+
69
+ #[must_use]
70
+ pub const fn max_spaces_inside(&self) -> i64 {
71
+ self.0.max_spaces_inside()
72
+ }
73
+
74
+ const fn inner(&self) -> &flow_collection::Config {
75
+ &self.0
76
+ }
77
+ }