@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,303 @@
1
+ use ryl::rules::colons::{self, Config};
2
+
3
+ fn violation_points(content: &str, cfg: Config) -> Vec<(usize, usize, String)> {
4
+ let mut hits = colons::check(content, &cfg);
5
+ hits.sort_by(|a, b| a.line.cmp(&b.line).then(a.column.cmp(&b.column)));
6
+ hits.into_iter()
7
+ .map(|hit| (hit.line, hit.column, hit.message))
8
+ .collect()
9
+ }
10
+
11
+ #[test]
12
+ fn config_getters_return_values() {
13
+ let cfg = Config::new_for_tests(3, 4);
14
+ assert_eq!(cfg.max_spaces_before(), 3);
15
+ assert_eq!(cfg.max_spaces_after(), 4);
16
+ }
17
+
18
+ #[test]
19
+ fn no_violation_with_defaults() {
20
+ let cfg = Config::new_for_tests(0, 1);
21
+ let points = violation_points("key: value\n", cfg);
22
+ assert!(points.is_empty());
23
+ }
24
+
25
+ #[test]
26
+ fn empty_input_returns_no_violations() {
27
+ let cfg = Config::new_for_tests(0, 1);
28
+ let points = violation_points("", cfg);
29
+ assert!(points.is_empty());
30
+ }
31
+
32
+ #[test]
33
+ fn detects_excess_spaces_before_colon() {
34
+ let cfg = Config::new_for_tests(0, -1);
35
+ let points = violation_points("key : value\n", cfg);
36
+ assert_eq!(
37
+ points,
38
+ vec![(1, 4, "too many spaces before colon".to_string())]
39
+ );
40
+ }
41
+
42
+ #[test]
43
+ fn detects_excess_spaces_after_colon() {
44
+ let cfg = Config::new_for_tests(-1, 1);
45
+ let points = violation_points("key: value\n", cfg);
46
+ assert_eq!(
47
+ points,
48
+ vec![(1, 6, "too many spaces after colon".to_string())]
49
+ );
50
+ }
51
+
52
+ #[test]
53
+ fn detects_excess_spaces_after_question_mark() {
54
+ let cfg = Config::new_for_tests(-1, 1);
55
+ let points = violation_points("? key\n: value\n", cfg);
56
+ assert_eq!(
57
+ points,
58
+ vec![(1, 3, "too many spaces after question mark".to_string())],
59
+ );
60
+ }
61
+
62
+ #[test]
63
+ fn ignores_alias_immediately_before_colon() {
64
+ let cfg = Config::new_for_tests(0, 1);
65
+ let points = violation_points("- anchor: &a key\n- *a: 42\n", cfg);
66
+ assert!(points.is_empty());
67
+ }
68
+
69
+ #[test]
70
+ fn flags_alias_with_extra_spaces() {
71
+ let cfg = Config::new_for_tests(0, 1);
72
+ let points = violation_points("- anchor: &a key\n- *a : 42\n", cfg);
73
+ assert_eq!(
74
+ points,
75
+ vec![(2, 6, "too many spaces before colon".to_string())]
76
+ );
77
+ }
78
+
79
+ #[test]
80
+ fn skips_colons_inside_comments() {
81
+ let cfg = Config::new_for_tests(0, 1);
82
+ let points = violation_points("# comment: text\nkey: value\n", cfg);
83
+ assert!(points.is_empty());
84
+ }
85
+
86
+ #[test]
87
+ fn handles_crlf_after_colon() {
88
+ let cfg = Config::new_for_tests(-1, 1);
89
+ let points = violation_points("key:\r\n value\rnext: pair\n", cfg);
90
+ assert!(points.is_empty());
91
+ }
92
+
93
+ #[test]
94
+ fn question_mark_not_explicit_is_ignored() {
95
+ let cfg = Config::new_for_tests(-1, 1);
96
+ let points = violation_points("value? trailing\n", cfg);
97
+ assert!(points.is_empty());
98
+ }
99
+
100
+ #[test]
101
+ fn sequence_question_mark_spacing_detected() {
102
+ let cfg = Config::new_for_tests(-1, 1);
103
+ let points = violation_points("- ? key\n : value\n", cfg);
104
+ assert_eq!(
105
+ points,
106
+ vec![
107
+ (1, 5, "too many spaces after question mark".to_string()),
108
+ (2, 5, "too many spaces after colon".to_string()),
109
+ ]
110
+ );
111
+ }
112
+
113
+ #[test]
114
+ fn question_mark_spacing_disabled_skips_check() {
115
+ let cfg = Config::new_for_tests(-1, -1);
116
+ let points = violation_points("? key\n: value\n", cfg);
117
+ assert!(points.is_empty());
118
+ }
119
+
120
+ #[test]
121
+ fn comment_with_crlf_is_ignored() {
122
+ let cfg = Config::new_for_tests(0, 1);
123
+ let points = violation_points("# note: here\r\nkey: value\r\n", cfg);
124
+ assert!(points.is_empty());
125
+ }
126
+
127
+ #[test]
128
+ fn colon_at_line_start_reports_after_spacing() {
129
+ let cfg = Config::new_for_tests(-1, 1);
130
+ let points = violation_points(": value\n", cfg);
131
+ assert_eq!(
132
+ points,
133
+ vec![(1, 3, "too many spaces after colon".to_string())]
134
+ );
135
+ }
136
+
137
+ #[test]
138
+ fn colon_at_end_of_file_is_ignored() {
139
+ let cfg = Config::new_for_tests(-1, 1);
140
+ let points = violation_points("key:", cfg);
141
+ assert!(points.is_empty());
142
+ }
143
+
144
+ #[test]
145
+ fn colon_followed_by_carriage_return_is_ignored() {
146
+ let cfg = Config::new_for_tests(-1, 1);
147
+ let points = violation_points("key:\rvalue\n", cfg);
148
+ assert!(points.is_empty());
149
+ }
150
+
151
+ #[test]
152
+ fn flow_question_mark_spacing_detected() {
153
+ let cfg = Config::new_for_tests(-1, 1);
154
+ let points = violation_points("[? key: value]\n", cfg);
155
+ assert_eq!(
156
+ points,
157
+ vec![(1, 4, "too many spaces after question mark".to_string())]
158
+ );
159
+ }
160
+
161
+ #[test]
162
+ fn indented_sequence_question_mark_spacing_detected() {
163
+ let cfg = Config::new_for_tests(-1, 1);
164
+ let points = violation_points("parent:\n - ? child\n : value\n", cfg);
165
+ assert_eq!(
166
+ points,
167
+ vec![
168
+ (2, 7, "too many spaces after question mark".to_string()),
169
+ (3, 7, "too many spaces after colon".to_string()),
170
+ ]
171
+ );
172
+ }
173
+
174
+ #[test]
175
+ fn hyphen_not_sequence_does_not_trigger_question_mark_rule() {
176
+ let cfg = Config::new_for_tests(-1, 1);
177
+ let points = violation_points("a- ? key\n", cfg);
178
+ assert!(points.is_empty());
179
+ }
180
+
181
+ #[test]
182
+ fn question_mark_without_space_not_explicit() {
183
+ let cfg = Config::new_for_tests(-1, 1);
184
+ let points = violation_points("?key: value\n", cfg);
185
+ assert!(points.is_empty());
186
+ }
187
+
188
+ #[test]
189
+ fn dash_without_space_before_question_mark_is_ignored() {
190
+ let cfg = Config::new_for_tests(-1, 1);
191
+ let points = violation_points("-? key\n : value\n", cfg);
192
+ assert!(points.is_empty());
193
+ }
194
+
195
+ #[test]
196
+ fn colons_inside_scalars_with_multibyte_chars_are_ignored() {
197
+ let cfg = Config::new_for_tests(0, 1);
198
+ let points = violation_points("key: \"café: menu\"\n", cfg);
199
+ assert!(points.is_empty());
200
+ }
201
+
202
+ #[test]
203
+ fn inline_comment_after_colon_is_ignored_for_spacing() {
204
+ let cfg = Config::new_for_tests(-1, 0);
205
+ let points = violation_points("key: # note\n", cfg);
206
+ assert!(
207
+ points.is_empty(),
208
+ "inline comment should bypass spacing check: {points:?}"
209
+ );
210
+ }
211
+
212
+ #[test]
213
+ fn coverage_explicit_question_mark_handles_non_whitespace_next() {
214
+ let chars: Vec<(usize, char)> = "?key".char_indices().collect();
215
+ assert!(!colons::coverage_is_explicit_question_mark(&chars, 0));
216
+ }
217
+
218
+ #[test]
219
+ fn coverage_explicit_question_mark_handles_sequence_indicator_branch() {
220
+ let chars: Vec<(usize, char)> = " - ? key".char_indices().collect();
221
+ assert!(colons::coverage_is_explicit_question_mark(&chars, 3));
222
+ }
223
+
224
+ #[test]
225
+ fn coverage_sequence_indicator_false_branch() {
226
+ let chars: Vec<(usize, char)> = "a-".char_indices().collect();
227
+ assert!(!colons::coverage_is_sequence_indicator(&chars, 1));
228
+ }
229
+
230
+ #[test]
231
+ fn coverage_explicit_question_mark_handles_regular_char_before() {
232
+ let chars: Vec<(usize, char)> = "a ? ".char_indices().collect();
233
+ assert!(!colons::coverage_is_explicit_question_mark(&chars, 2));
234
+ }
235
+
236
+ #[test]
237
+ fn coverage_evaluate_question_mark_reports_violation() {
238
+ let cfg = Config::new_for_tests(-1, 1);
239
+ let violations = colons::coverage_evaluate_question_mark("? key\n: value\n", &cfg);
240
+ assert!(
241
+ violations
242
+ .iter()
243
+ .any(|v| v.message.contains("too many spaces after question mark"))
244
+ );
245
+ }
246
+
247
+ #[test]
248
+ fn coverage_explicit_question_mark_handles_flow_prefix_variants() {
249
+ let bracket_chars: Vec<(usize, char)> = "[ ? key".char_indices().collect();
250
+ assert!(colons::coverage_is_explicit_question_mark(
251
+ &bracket_chars,
252
+ 2
253
+ ));
254
+ let brace_chars: Vec<(usize, char)> = "{ ? key".char_indices().collect();
255
+ assert!(colons::coverage_is_explicit_question_mark(&brace_chars, 2));
256
+ let comma_chars: Vec<(usize, char)> = ", ? key".char_indices().collect();
257
+ assert!(colons::coverage_is_explicit_question_mark(&comma_chars, 2));
258
+ }
259
+
260
+ #[test]
261
+ fn coverage_question_mark_immediate_newline_is_ignored() {
262
+ let cfg = Config::new_for_tests(-1, 1);
263
+ let violations = colons::coverage_evaluate_question_mark("? \n: value\n", &cfg);
264
+ assert!(violations.is_empty());
265
+ }
266
+
267
+ #[test]
268
+ fn coverage_question_mark_crlf_is_ignored() {
269
+ let cfg = Config::new_for_tests(-1, 1);
270
+ let violations = colons::coverage_evaluate_question_mark("? \r\n: value\n", &cfg);
271
+ assert!(violations.is_empty());
272
+ }
273
+
274
+ #[test]
275
+ fn coverage_skip_comment_handles_crlf() {
276
+ assert!(colons::coverage_skip_comment("# comment\r\nrest"));
277
+ }
278
+
279
+ #[test]
280
+ fn coverage_skip_comment_handles_standalone_cr() {
281
+ assert!(!colons::coverage_skip_comment("# comment\rrest"));
282
+ }
283
+
284
+ #[test]
285
+ fn coverage_evaluate_question_mark_without_marker_is_noop() {
286
+ let cfg = Config::new_for_tests(-1, 1);
287
+ let violations = colons::coverage_evaluate_question_mark("key: value\n", &cfg);
288
+ assert!(violations.is_empty());
289
+ }
290
+
291
+ #[test]
292
+ fn coverage_question_mark_within_limit_on_same_line() {
293
+ let cfg = Config::new_for_tests(-1, 1);
294
+ let violations = colons::coverage_evaluate_question_mark("? key: value\n", &cfg);
295
+ assert!(violations.is_empty());
296
+ }
297
+
298
+ #[test]
299
+ fn coverage_check_handles_comment_crlf() {
300
+ let cfg = Config::new_for_tests(0, 1);
301
+ let result = colons::check("# heading\r\nkey: value\n", &cfg);
302
+ assert!(result.is_empty());
303
+ }
@@ -0,0 +1,114 @@
1
+ use std::process::Command;
2
+
3
+ pub fn run(cmd: &mut Command) -> (i32, String, String) {
4
+ let out = cmd.output().expect("process");
5
+ let code = out.status.code().unwrap_or(-1);
6
+ let stdout = String::from_utf8_lossy(&out.stdout).into_owned();
7
+ let stderr = String::from_utf8_lossy(&out.stderr).into_owned();
8
+ (code, stdout, stderr)
9
+ }
10
+
11
+ pub fn ensure_yamllint_installed() {
12
+ let ok = Command::new("yamllint")
13
+ .arg("--version")
14
+ .output()
15
+ .map(|out| out.status.success())
16
+ .unwrap_or(false);
17
+ assert!(ok, "yamllint must be installed for compatibility tests");
18
+ }
19
+
20
+ pub fn normalize_output(stdout: String, stderr: String) -> String {
21
+ let output = if stderr.is_empty() { stdout } else { stderr };
22
+ // Normalize line endings to LF for cross-platform compatibility
23
+ output.replace("\r\n", "\n")
24
+ }
25
+
26
+ pub fn capture_with_env(
27
+ mut cmd: Command,
28
+ envs: &[(&str, Option<&str>)],
29
+ ) -> (i32, String) {
30
+ cmd.env_remove("GITHUB_ACTIONS");
31
+ cmd.env_remove("GITHUB_WORKFLOW");
32
+ cmd.env_remove("CI");
33
+ cmd.env_remove("FORCE_COLOR");
34
+ cmd.env_remove("NO_COLOR");
35
+ for (key, value) in envs {
36
+ if let Some(v) = value {
37
+ cmd.env(key, v);
38
+ } else {
39
+ cmd.env_remove(key);
40
+ }
41
+ }
42
+ let (code, stdout, stderr) = run(&mut cmd);
43
+ (code, normalize_output(stdout, stderr))
44
+ }
45
+
46
+ #[derive(Clone, Copy)]
47
+ pub struct Scenario {
48
+ pub label: &'static str,
49
+ pub envs: &'static [(&'static str, Option<&'static str>)],
50
+ pub ryl_format: Option<&'static str>,
51
+ pub yam_format: Option<&'static str>,
52
+ }
53
+
54
+ pub const STANDARD_ENV: &[(&str, Option<&str>)] = &[];
55
+ pub const GITHUB_ENV: &[(&str, Option<&str>)] = &[
56
+ ("GITHUB_ACTIONS", Some("true")),
57
+ ("GITHUB_WORKFLOW", Some("test-workflow")),
58
+ ("CI", Some("true")),
59
+ ];
60
+
61
+ pub const SCENARIOS: &[Scenario] = &[
62
+ Scenario {
63
+ label: "auto-standard",
64
+ envs: STANDARD_ENV,
65
+ ryl_format: None,
66
+ yam_format: None,
67
+ },
68
+ Scenario {
69
+ label: "auto-github",
70
+ envs: GITHUB_ENV,
71
+ ryl_format: None,
72
+ yam_format: None,
73
+ },
74
+ Scenario {
75
+ label: "format-standard",
76
+ envs: STANDARD_ENV,
77
+ ryl_format: Some("standard"),
78
+ yam_format: Some("standard"),
79
+ },
80
+ Scenario {
81
+ label: "format-colored",
82
+ envs: STANDARD_ENV,
83
+ ryl_format: Some("colored"),
84
+ yam_format: Some("colored"),
85
+ },
86
+ Scenario {
87
+ label: "format-github",
88
+ envs: STANDARD_ENV,
89
+ ryl_format: Some("github"),
90
+ yam_format: Some("github"),
91
+ },
92
+ Scenario {
93
+ label: "format-parsable",
94
+ envs: STANDARD_ENV,
95
+ ryl_format: Some("parsable"),
96
+ yam_format: Some("parsable"),
97
+ },
98
+ ];
99
+
100
+ pub fn build_ryl_command(exe: &str, format: Option<&str>) -> Command {
101
+ let mut cmd = Command::new(exe);
102
+ if let Some(fmt) = format {
103
+ cmd.arg("--format").arg(fmt);
104
+ }
105
+ cmd
106
+ }
107
+
108
+ pub fn build_yamllint_command(format: Option<&str>) -> Command {
109
+ let mut cmd = Command::new("yamllint");
110
+ if let Some(fmt) = format {
111
+ cmd.arg("-f").arg(fmt);
112
+ }
113
+ cmd
114
+ }
@@ -0,0 +1,93 @@
1
+ use std::collections::{HashMap, HashSet};
2
+ use std::path::{Path, PathBuf};
3
+
4
+ use ryl::config::Env;
5
+
6
+ #[derive(Clone, Default)]
7
+ pub struct FakeEnv {
8
+ cwd: PathBuf,
9
+ files: HashMap<PathBuf, String>,
10
+ exists: HashSet<PathBuf>,
11
+ vars: HashMap<String, String>,
12
+ config_dir: Option<PathBuf>,
13
+ home: Option<PathBuf>,
14
+ }
15
+
16
+ #[allow(dead_code)]
17
+ impl FakeEnv {
18
+ pub fn new() -> Self {
19
+ Self {
20
+ cwd: PathBuf::from("."),
21
+ ..Self::default()
22
+ }
23
+ }
24
+
25
+ pub fn with_cwd(mut self, path: impl Into<PathBuf>) -> Self {
26
+ self.cwd = path.into();
27
+ self
28
+ }
29
+
30
+ pub fn with_config_dir(mut self, path: impl Into<PathBuf>) -> Self {
31
+ self.config_dir = Some(path.into());
32
+ self
33
+ }
34
+
35
+ pub fn with_file(
36
+ mut self,
37
+ path: impl Into<PathBuf>,
38
+ content: impl Into<String>,
39
+ ) -> Self {
40
+ self.files.insert(path.into(), content.into());
41
+ self
42
+ }
43
+
44
+ pub fn with_exists(mut self, path: impl Into<PathBuf>) -> Self {
45
+ self.exists.insert(path.into());
46
+ self
47
+ }
48
+
49
+ pub fn with_var(
50
+ mut self,
51
+ key: impl Into<String>,
52
+ value: impl Into<String>,
53
+ ) -> Self {
54
+ self.vars.insert(key.into(), value.into());
55
+ self
56
+ }
57
+
58
+ pub fn with_home(mut self, path: impl Into<PathBuf>) -> Self {
59
+ self.home = Some(path.into());
60
+ self
61
+ }
62
+ }
63
+
64
+ impl Env for FakeEnv {
65
+ fn current_dir(&self) -> PathBuf {
66
+ self.cwd.clone()
67
+ }
68
+
69
+ fn config_dir(&self) -> Option<PathBuf> {
70
+ self.config_dir.clone()
71
+ }
72
+
73
+ fn read_to_string(&self, p: &Path) -> Result<String, String> {
74
+ self.files.get(p).cloned().ok_or_else(|| {
75
+ format!("failed to read config file {}: not found", p.display())
76
+ })
77
+ }
78
+
79
+ fn path_exists(&self, p: &Path) -> bool {
80
+ self.files.contains_key(p) || self.exists.contains(p)
81
+ }
82
+
83
+ fn env_var(&self, key: &str) -> Option<String> {
84
+ self.vars.get(key).cloned()
85
+ }
86
+
87
+ fn home_dir(&self) -> Option<PathBuf> {
88
+ self.home
89
+ .clone()
90
+ .or_else(|| self.vars.get("HOME").map(PathBuf::from))
91
+ .or_else(|| self.vars.get("USERPROFILE").map(PathBuf::from))
92
+ }
93
+ }
@@ -0,0 +1 @@
1
+ pub mod fake_env;
@@ -0,0 +1,9 @@
1
+ use ryl::conf::builtin;
2
+
3
+ #[test]
4
+ fn builtin_presets_are_available() {
5
+ assert!(builtin("default").is_some());
6
+ assert!(builtin("relaxed").is_some());
7
+ assert!(builtin("empty").is_some());
8
+ assert!(builtin("nonexistent").is_none());
9
+ }
@@ -0,0 +1,84 @@
1
+ use ryl::config::{Overrides, discover_config};
2
+
3
+ #[test]
4
+ fn anchors_allows_boolean_options() {
5
+ let cfg = r#"
6
+ rules:
7
+ anchors:
8
+ forbid-undeclared-aliases: false
9
+ forbid-duplicated-anchors: true
10
+ forbid-unused-anchors: true
11
+ "#;
12
+
13
+ let ctx = discover_config(
14
+ &[],
15
+ &Overrides {
16
+ config_file: None,
17
+ config_data: Some(cfg.into()),
18
+ },
19
+ )
20
+ .expect("parse");
21
+
22
+ assert!(ctx.config.rule_names().iter().any(|r| r == "anchors"));
23
+ }
24
+
25
+ #[test]
26
+ fn anchors_rejects_non_bool_option() {
27
+ let cfg = r#"
28
+ rules:
29
+ anchors:
30
+ forbid-duplicated-anchors: "yes"
31
+ "#;
32
+
33
+ let err = discover_config(
34
+ &[],
35
+ &Overrides {
36
+ config_file: None,
37
+ config_data: Some(cfg.into()),
38
+ },
39
+ )
40
+ .expect_err("invalid value");
41
+
42
+ assert!(err.contains("forbid-duplicated-anchors"));
43
+ }
44
+
45
+ #[test]
46
+ fn anchors_rejects_unknown_option() {
47
+ let cfg = r#"
48
+ rules:
49
+ anchors:
50
+ unknown: true
51
+ "#;
52
+
53
+ let err = discover_config(
54
+ &[],
55
+ &Overrides {
56
+ config_file: None,
57
+ config_data: Some(cfg.into()),
58
+ },
59
+ )
60
+ .expect_err("unknown option");
61
+
62
+ assert!(err.contains("unknown option \"unknown\" for rule \"anchors\""));
63
+ }
64
+
65
+ #[test]
66
+ fn anchors_rejects_non_string_option_key() {
67
+ let cfg = r#"
68
+ rules:
69
+ anchors:
70
+ ? [1, 2]
71
+ : true
72
+ "#;
73
+
74
+ let err = discover_config(
75
+ &[],
76
+ &Overrides {
77
+ config_file: None,
78
+ config_data: Some(cfg.into()),
79
+ },
80
+ )
81
+ .expect_err("non-string key");
82
+
83
+ assert!(err.contains("unknown option") && err.contains("anchors"));
84
+ }