@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,203 @@
1
+ use std::fs;
2
+ use std::path::Path;
3
+ use std::process::Command;
4
+
5
+ use tempfile::tempdir;
6
+
7
+ fn write_utf16le(path: &Path, content: &str) {
8
+ let mut data = Vec::with_capacity(2 + content.len() * 2);
9
+ data.extend_from_slice(&[0xFF, 0xFE]);
10
+ for unit in content.encode_utf16() {
11
+ let bytes = unit.to_le_bytes();
12
+ data.extend_from_slice(&bytes);
13
+ }
14
+ fs::write(path, data).unwrap();
15
+ }
16
+
17
+ fn write_latin1(path: &Path, content: &str) {
18
+ let mut data = Vec::with_capacity(content.len());
19
+ for ch in content.chars() {
20
+ let code = ch as u32;
21
+ assert!(
22
+ code <= 0xFF,
23
+ "latin-1 helper only supports code points <= 0xFF"
24
+ );
25
+ data.push(code as u8);
26
+ }
27
+ fs::write(path, data).unwrap();
28
+ }
29
+
30
+ fn write_utf32le(path: &Path, content: &str) {
31
+ let mut data = Vec::with_capacity(4 + content.len() * 4);
32
+ data.extend_from_slice(&[0xFF, 0xFE, 0x00, 0x00]);
33
+ for ch in content.chars() {
34
+ let code = ch as u32;
35
+ data.extend_from_slice(&code.to_le_bytes());
36
+ }
37
+ fs::write(path, data).unwrap();
38
+ }
39
+
40
+ fn run(cmd: &mut Command) -> (i32, String, String) {
41
+ let out = cmd.output().expect("failed to run command");
42
+ let code = out.status.code().unwrap_or(-1);
43
+ let stdout = String::from_utf8_lossy(&out.stdout).into_owned();
44
+ let stderr = String::from_utf8_lossy(&out.stderr).into_owned();
45
+ (code, stdout, stderr)
46
+ }
47
+
48
+ #[test]
49
+ fn cli_reads_utf16_config_file() {
50
+ let dir = tempdir().unwrap();
51
+ let cfg_path = dir.path().join("utf16-config.yml");
52
+ let yaml_path = dir.path().join("bad.yaml");
53
+
54
+ write_utf16le(&cfg_path, "rules:\n truthy: enable\n");
55
+ fs::write(&yaml_path, "value: Yes\n").unwrap();
56
+
57
+ let exe = env!("CARGO_BIN_EXE_ryl");
58
+ let (code, stdout, stderr) =
59
+ run(Command::new(exe).arg("-c").arg(&cfg_path).arg(&yaml_path));
60
+ assert_eq!(
61
+ code, 1,
62
+ "expected lint failures: stdout={stdout} stderr={stderr}"
63
+ );
64
+ let output = if stderr.is_empty() { stdout } else { stderr };
65
+ assert!(
66
+ output.contains("truthy"),
67
+ "expected truthy diagnostics in output, got:\n{output}"
68
+ );
69
+ }
70
+
71
+ #[test]
72
+ fn cli_reads_utf16_yaml_input() {
73
+ let dir = tempdir().unwrap();
74
+ let cfg_path = dir.path().join("config.yml");
75
+ let yaml_path = dir.path().join("utf16.yaml");
76
+
77
+ write_utf16le(&cfg_path, "rules:\n truthy: enable\n");
78
+ write_utf16le(&yaml_path, "value: Yes\n");
79
+
80
+ let exe = env!("CARGO_BIN_EXE_ryl");
81
+ let (code, stdout, stderr) =
82
+ run(Command::new(exe).arg("-c").arg(&cfg_path).arg(&yaml_path));
83
+ assert_eq!(
84
+ code, 1,
85
+ "expected lint failures: stdout={stdout} stderr={stderr}"
86
+ );
87
+ let output = if stderr.is_empty() { stdout } else { stderr };
88
+ assert!(
89
+ output.contains("truthy"),
90
+ "expected truthy diagnostics in output, got:\n{output}"
91
+ );
92
+ }
93
+
94
+ #[test]
95
+ fn cli_honors_yamllint_file_encoding_override() {
96
+ let dir = tempdir().unwrap();
97
+ let cfg_path = dir.path().join("latin-config.yml");
98
+ let yaml_path = dir.path().join("latin.yaml");
99
+
100
+ write_latin1(&cfg_path, "rules:\n truthy: enable\n");
101
+ write_latin1(&yaml_path, "acción: yes\n");
102
+
103
+ let exe = env!("CARGO_BIN_EXE_ryl");
104
+ let (code, stdout, stderr) = run(Command::new(exe)
105
+ .env("YAMLLINT_FILE_ENCODING", "latin-1")
106
+ .arg("-c")
107
+ .arg(&cfg_path)
108
+ .arg(&yaml_path));
109
+
110
+ assert_eq!(
111
+ code, 1,
112
+ "expected lint failures: stdout={stdout} stderr={stderr}"
113
+ );
114
+ let message = if stderr.is_empty() {
115
+ stdout.clone()
116
+ } else {
117
+ stderr.clone()
118
+ };
119
+ assert!(
120
+ message.contains("truthy"),
121
+ "expected truthy diagnostics in output, got:\n{message}"
122
+ );
123
+ assert!(
124
+ stderr.contains("YAMLLINT_FILE_ENCODING is meant for temporary workarounds"),
125
+ "expected override warning on stderr, got:\n{stderr}"
126
+ );
127
+ }
128
+
129
+ #[test]
130
+ fn cli_override_utf16_variants() {
131
+ let dir = tempdir().unwrap();
132
+ let yaml_path = dir.path().join("data.yaml");
133
+
134
+ write_utf16le(&yaml_path, "value: Yes\n");
135
+
136
+ let exe = env!("CARGO_BIN_EXE_ryl");
137
+ let (code, stdout, stderr) = run(Command::new(exe)
138
+ .env("YAMLLINT_FILE_ENCODING", "UTF_16")
139
+ .arg("--config-data")
140
+ .arg("rules:\n truthy: enable\n")
141
+ .arg(&yaml_path));
142
+
143
+ assert_eq!(
144
+ code, 1,
145
+ "expected lint failures: stdout={stdout} stderr={stderr}"
146
+ );
147
+ let message = if stderr.is_empty() { stdout } else { stderr };
148
+ assert!(
149
+ message.contains("truthy"),
150
+ "expected truthy diagnostics, got:\n{message}"
151
+ );
152
+ }
153
+
154
+ #[test]
155
+ fn cli_override_utf32_variants() {
156
+ let dir = tempdir().unwrap();
157
+ let yaml_path = dir.path().join("utf32.yaml");
158
+
159
+ write_utf32le(&yaml_path, "value: Yes\n");
160
+
161
+ let exe = env!("CARGO_BIN_EXE_ryl");
162
+ let (code, stdout, stderr) = run(Command::new(exe)
163
+ .env("YAMLLINT_FILE_ENCODING", "utf32le")
164
+ .arg("--config-data")
165
+ .arg("rules:\n truthy: enable\n")
166
+ .arg(&yaml_path));
167
+
168
+ assert_eq!(
169
+ code, 1,
170
+ "expected lint failures: stdout={stdout} stderr={stderr}"
171
+ );
172
+ let message = if stderr.is_empty() { stdout } else { stderr };
173
+ assert!(
174
+ message.contains("truthy"),
175
+ "expected truthy diagnostics, got:\n{message}"
176
+ );
177
+ }
178
+
179
+ #[test]
180
+ fn cli_override_unknown_label_errors() {
181
+ let dir = tempdir().unwrap();
182
+ let cfg_path = dir.path().join("config.yml");
183
+ let yaml_path = dir.path().join("file.yaml");
184
+
185
+ fs::write(&cfg_path, "rules:\n truthy: enable\n").unwrap();
186
+ fs::write(&yaml_path, "value: Yes\n").unwrap();
187
+
188
+ let exe = env!("CARGO_BIN_EXE_ryl");
189
+ let (code, stdout, stderr) = run(Command::new(exe)
190
+ .env("YAMLLINT_FILE_ENCODING", "unsupported-encoding")
191
+ .arg("-c")
192
+ .arg(&cfg_path)
193
+ .arg(&yaml_path));
194
+
195
+ assert_eq!(
196
+ code, 2,
197
+ "expected usage error: stdout={stdout} stderr={stderr}"
198
+ );
199
+ assert!(
200
+ stderr.contains("unsupported label"),
201
+ "expected unsupported label error, got:\n{stderr}"
202
+ );
203
+ }
@@ -0,0 +1,64 @@
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
+ fn command_output<'a>(stdout: &'a str, stderr: &'a str) -> &'a str {
15
+ if stderr.is_empty() { stdout } else { stderr }
16
+ }
17
+
18
+ #[test]
19
+ fn float_values_rule_reports_forbidden_variants() {
20
+ let dir = tempdir().unwrap();
21
+ let file = dir.path().join("values.yaml");
22
+ fs::write(&file, "a: .5\nb: 1e2\nc: .nan\nd: .inf\n").unwrap();
23
+
24
+ let config = dir.path().join("config.yaml");
25
+ fs::write(
26
+ &config,
27
+ "rules:\n document-start: disable\n float-values:\n require-numeral-before-decimal: true\n forbid-scientific-notation: true\n forbid-nan: true\n forbid-inf: true\n",
28
+ )
29
+ .unwrap();
30
+
31
+ let exe = env!("CARGO_BIN_EXE_ryl");
32
+ let (code, stdout, stderr) =
33
+ run(Command::new(exe).arg("-c").arg(&config).arg(&file));
34
+ assert_eq!(
35
+ code, 1,
36
+ "expected lint failure: stdout={stdout} stderr={stderr}"
37
+ );
38
+
39
+ let output = command_output(&stdout, &stderr);
40
+ assert!(
41
+ output.contains("forbidden decimal missing 0 prefix \".5\""),
42
+ "missing decimal prefix message: {output}"
43
+ );
44
+ assert!(
45
+ output.contains("forbidden scientific notation \"1e2\""),
46
+ "missing scientific notation message: {output}"
47
+ );
48
+ assert!(
49
+ output.contains("forbidden not a number value \".nan\""),
50
+ "missing nan message: {output}"
51
+ );
52
+ assert!(
53
+ output.contains("forbidden infinite value \".inf\""),
54
+ "missing inf message: {output}"
55
+ );
56
+ assert!(
57
+ output.contains("float-values"),
58
+ "rule label missing: {output}"
59
+ );
60
+ assert!(output.contains("1:4"), "expected .5 position: {output}");
61
+ assert!(output.contains("2:4"), "expected 1e2 position: {output}");
62
+ assert!(output.contains("3:4"), "expected .nan position: {output}");
63
+ assert!(output.contains("4:4"), "expected .inf position: {output}");
64
+ }
@@ -0,0 +1,316 @@
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("failed to run ryl");
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
+ fn disable_doc_start_config(dir: &std::path::Path) -> std::path::PathBuf {
15
+ let cfg = dir.join("config.yml");
16
+ fs::write(
17
+ &cfg,
18
+ "rules:\n document-start: disable\n new-line-at-end-of-file: enable\n",
19
+ )
20
+ .unwrap();
21
+ cfg
22
+ }
23
+
24
+ #[test]
25
+ fn parsable_format_outputs_expected_diagnostic() {
26
+ let dir = tempdir().unwrap();
27
+ let cfg = disable_doc_start_config(dir.path());
28
+ let file = dir.path().join("missing.yaml");
29
+ fs::write(&file, "key: value").unwrap();
30
+
31
+ let exe = env!("CARGO_BIN_EXE_ryl");
32
+ let (code, stdout, stderr) = run(Command::new(exe)
33
+ .arg("--format")
34
+ .arg("parsable")
35
+ .arg("-c")
36
+ .arg(&cfg)
37
+ .arg(&file));
38
+ assert_eq!(code, 1, "parsable format should keep error exit");
39
+ assert!(stdout.is_empty(), "parsable format should write to stderr");
40
+ let lines: Vec<&str> = stderr.lines().collect();
41
+ assert_eq!(lines.len(), 1, "expected single diagnostic line: {stderr}");
42
+ let line = lines[0];
43
+ assert!(
44
+ line.contains(&format!(":{}:{}: [error]", 1, 11)),
45
+ "missing location: {line}"
46
+ );
47
+ assert!(
48
+ line.contains(
49
+ "no new line character at the end of file (new-line-at-end-of-file)"
50
+ ),
51
+ "unexpected diagnostic payload: {line}"
52
+ );
53
+
54
+ let warn_cfg = dir.path().join("config-warning.yml");
55
+ fs::write(
56
+ &warn_cfg,
57
+ "rules:\n document-start: disable\n new-line-at-end-of-file:\n level: warning\n",
58
+ )
59
+ .unwrap();
60
+ let (warn_code, warn_stdout, warn_stderr) = run(Command::new(exe)
61
+ .arg("--format")
62
+ .arg("parsable")
63
+ .arg("-c")
64
+ .arg(&warn_cfg)
65
+ .arg(&file));
66
+ assert_eq!(warn_code, 0, "warning-level parsable format should exit 0");
67
+ assert!(warn_stdout.is_empty(), "warnings should emit on stderr");
68
+ assert!(
69
+ warn_stderr.contains("[warning]"),
70
+ "expected warning line: {warn_stderr}"
71
+ );
72
+ }
73
+
74
+ #[test]
75
+ fn parsable_format_omits_rule_suffix_for_syntax_errors() {
76
+ let dir = tempdir().unwrap();
77
+ let cfg = disable_doc_start_config(dir.path());
78
+ let file = dir.path().join("invalid.yaml");
79
+ fs::write(&file, "foo: [1, 2\n").unwrap();
80
+
81
+ let exe = env!("CARGO_BIN_EXE_ryl");
82
+ let (code, stdout, stderr) = run(Command::new(exe)
83
+ .arg("--format")
84
+ .arg("parsable")
85
+ .arg("-c")
86
+ .arg(&cfg)
87
+ .arg(&file));
88
+ assert_eq!(code, 1, "syntax errors should exit 1");
89
+ assert!(
90
+ stdout.is_empty(),
91
+ "syntax diagnostics should print to stderr"
92
+ );
93
+ let lines: Vec<&str> = stderr.lines().collect();
94
+ assert_eq!(lines.len(), 1, "expected single diagnostic line: {stderr}");
95
+ let diagnostic = lines[0];
96
+ assert!(
97
+ diagnostic.contains("[error]"),
98
+ "syntax diagnostic must report an error: {diagnostic}"
99
+ );
100
+ assert!(
101
+ diagnostic.contains("(syntax)"),
102
+ "missing syntax marker: {diagnostic}"
103
+ );
104
+ assert!(
105
+ !diagnostic.contains("(syntax) ("),
106
+ "syntax diagnostics must not include rule suffix: {diagnostic}"
107
+ );
108
+ }
109
+
110
+ #[test]
111
+ fn github_format_emits_workflow_commands() {
112
+ let dir = tempdir().unwrap();
113
+ let cfg = disable_doc_start_config(dir.path());
114
+ let file = dir.path().join("missing.yaml");
115
+ fs::write(&file, "key: value").unwrap();
116
+
117
+ let exe = env!("CARGO_BIN_EXE_ryl");
118
+ let (code, stdout, stderr) = run(Command::new(exe)
119
+ .arg("--format")
120
+ .arg("github")
121
+ .arg("-c")
122
+ .arg(&cfg)
123
+ .arg(&file));
124
+ assert_eq!(code, 1, "github format should keep error exit");
125
+ assert!(stdout.is_empty(), "github format writes to stderr");
126
+ assert!(
127
+ stderr.contains("::group::"),
128
+ "missing GitHub group: {stderr}"
129
+ );
130
+ assert!(
131
+ stderr.contains("::error file="),
132
+ "missing GitHub error command: {stderr}"
133
+ );
134
+ assert!(
135
+ stderr.contains("::endgroup::"),
136
+ "missing GitHub endgroup: {stderr}"
137
+ );
138
+ }
139
+
140
+ #[test]
141
+ fn colored_format_uses_ansi_sequences() {
142
+ let dir = tempdir().unwrap();
143
+ let cfg = disable_doc_start_config(dir.path());
144
+ let file = dir.path().join("missing.yaml");
145
+ fs::write(&file, "key: value").unwrap();
146
+
147
+ let exe = env!("CARGO_BIN_EXE_ryl");
148
+ let (code, stdout, stderr) = run(Command::new(exe)
149
+ .arg("--format")
150
+ .arg("colored")
151
+ .arg("-c")
152
+ .arg(&cfg)
153
+ .arg(&file));
154
+ assert_eq!(code, 1, "colored format should keep error exit");
155
+ assert!(stdout.is_empty(), "colored format writes to stderr");
156
+ assert!(
157
+ stderr.contains("\u{001b}[4m") && stderr.contains("\u{001b}[31m"),
158
+ "expected ANSI sequences in colored output: {stderr}"
159
+ );
160
+
161
+ let warn_cfg = dir.path().join("config-warning.yml");
162
+ fs::write(
163
+ &warn_cfg,
164
+ "rules:\n document-start: disable\n new-line-at-end-of-file:\n level: warning\n",
165
+ )
166
+ .unwrap();
167
+ let (warn_code, warn_stdout, warn_stderr) = run(Command::new(exe)
168
+ .arg("--format")
169
+ .arg("colored")
170
+ .arg("-c")
171
+ .arg(&warn_cfg)
172
+ .arg(&file));
173
+ assert_eq!(warn_code, 0, "warning-level colored output should exit 0");
174
+ assert!(warn_stdout.is_empty(), "warnings should emit on stderr");
175
+ assert!(
176
+ warn_stderr.contains("\u{001b}[33mwarning")
177
+ && warn_stderr.contains("(new-line-at-end-of-file)"),
178
+ "expected colored warning payload: {warn_stderr}"
179
+ );
180
+ }
181
+
182
+ #[test]
183
+ fn colored_format_omits_rule_suffix_for_syntax_errors() {
184
+ let dir = tempdir().unwrap();
185
+ let cfg = disable_doc_start_config(dir.path());
186
+ let file = dir.path().join("syntax.yaml");
187
+ fs::write(&file, "foo: [1, 2\n").unwrap();
188
+
189
+ let exe = env!("CARGO_BIN_EXE_ryl");
190
+ let (code, stdout, stderr) = run(Command::new(exe)
191
+ .arg("--format")
192
+ .arg("colored")
193
+ .arg("-c")
194
+ .arg(&cfg)
195
+ .arg(&file));
196
+ assert_eq!(code, 1, "syntax errors should exit 1");
197
+ assert!(
198
+ stdout.is_empty(),
199
+ "syntax diagnostics should print to stderr"
200
+ );
201
+ let lines: Vec<&str> = stderr
202
+ .lines()
203
+ .filter(|line| !line.trim().is_empty())
204
+ .collect();
205
+ assert!(
206
+ lines.len() >= 2,
207
+ "expected path and diagnostic lines: {stderr}"
208
+ );
209
+ let diagnostic = lines[1];
210
+ assert!(
211
+ diagnostic.contains("(syntax)"),
212
+ "missing syntax marker: {diagnostic}"
213
+ );
214
+ assert!(
215
+ !diagnostic.contains(" \u{001b}[2m("),
216
+ "syntax diagnostics must not include colored rule suffix: {diagnostic}"
217
+ );
218
+ }
219
+
220
+ #[test]
221
+ fn colored_format_matches_reference_layout() {
222
+ let dir = tempdir().unwrap();
223
+ let file = dir.path().join("layout.yaml");
224
+ fs::write(&file, "list: [1,2]\n").unwrap();
225
+
226
+ let exe = env!("CARGO_BIN_EXE_ryl");
227
+ let (code, stdout, stderr) =
228
+ run(Command::new(exe).arg("--format").arg("colored").arg(&file));
229
+ assert_eq!(code, 1, "colored format should exit 1 when errors occur");
230
+ assert!(
231
+ stdout.is_empty(),
232
+ "colored format diagnostics must print on stderr"
233
+ );
234
+ let expected = format!(
235
+ "\u{001b}[4m{path}\u{001b}[0m\n \u{001b}[2m1:1\u{001b}[0m \u{001b}[33mwarning\u{001b}[0m missing document start \"---\" \u{001b}[2m(document-start)\u{001b}[0m\n \u{001b}[2m1:10\u{001b}[0m \u{001b}[31merror\u{001b}[0m too few spaces after comma \u{001b}[2m(commas)\u{001b}[0m\n\n",
236
+ path = file.display()
237
+ );
238
+ assert_eq!(stderr, expected, "colored diagnostic payload mismatch");
239
+ }
240
+
241
+ #[test]
242
+ fn standard_format_remains_plain_text() {
243
+ let dir = tempdir().unwrap();
244
+ let cfg = disable_doc_start_config(dir.path());
245
+ let file = dir.path().join("missing.yaml");
246
+ fs::write(&file, "key: value").unwrap();
247
+
248
+ let exe = env!("CARGO_BIN_EXE_ryl");
249
+ let (code, stdout, stderr) = run(Command::new(exe)
250
+ .arg("--format")
251
+ .arg("standard")
252
+ .arg("-c")
253
+ .arg(&cfg)
254
+ .arg(&file));
255
+ assert_eq!(code, 1, "standard format should keep error exit");
256
+ assert!(stdout.is_empty(), "standard format writes to stderr");
257
+ assert!(
258
+ !stderr.contains("\u{001b}"),
259
+ "standard format should not use ANSI: {stderr}"
260
+ );
261
+ assert!(
262
+ !stderr.contains("::group::"),
263
+ "standard format should not emit GitHub commands: {stderr}"
264
+ );
265
+ }
266
+
267
+ #[test]
268
+ fn auto_format_honors_force_color_env() {
269
+ let dir = tempdir().unwrap();
270
+ let cfg = disable_doc_start_config(dir.path());
271
+ let file = dir.path().join("missing.yaml");
272
+ fs::write(&file, "key: value").unwrap();
273
+
274
+ let exe = env!("CARGO_BIN_EXE_ryl");
275
+ let (code, stdout, stderr) = run(Command::new(exe)
276
+ .env("FORCE_COLOR", "1")
277
+ .env_remove("NO_COLOR")
278
+ .env_remove("GITHUB_ACTIONS")
279
+ .env_remove("GITHUB_WORKFLOW")
280
+ .arg("-c")
281
+ .arg(&cfg)
282
+ .arg(&file));
283
+ assert_eq!(code, 1, "auto format should keep error exit");
284
+ assert!(
285
+ stdout.is_empty(),
286
+ "auto format writes diagnostics to stderr"
287
+ );
288
+ assert!(
289
+ stderr.contains("\u{001b}[4m") && stderr.contains("\u{001b}[31m"),
290
+ "force color should enable colored output: {stderr}"
291
+ );
292
+ }
293
+
294
+ #[test]
295
+ fn auto_format_respects_no_color_env() {
296
+ let dir = tempdir().unwrap();
297
+ let cfg = disable_doc_start_config(dir.path());
298
+ let file = dir.path().join("missing.yaml");
299
+ fs::write(&file, "key: value").unwrap();
300
+
301
+ let exe = env!("CARGO_BIN_EXE_ryl");
302
+ let (code, stdout, stderr) = run(Command::new(exe)
303
+ .env("FORCE_COLOR", "1")
304
+ .env("NO_COLOR", "1")
305
+ .env_remove("GITHUB_ACTIONS")
306
+ .env_remove("GITHUB_WORKFLOW")
307
+ .arg("-c")
308
+ .arg(&cfg)
309
+ .arg(&file));
310
+ assert_eq!(code, 1, "auto format with NO_COLOR keeps error exit");
311
+ assert!(stdout.is_empty(), "diagnostics should be on stderr");
312
+ assert!(
313
+ !stderr.contains("\u{001b}"),
314
+ "NO_COLOR should disable ANSI sequences: {stderr}"
315
+ );
316
+ }
@@ -0,0 +1,20 @@
1
+ use std::fs;
2
+ use std::process::Command;
3
+
4
+ use tempfile::tempdir;
5
+
6
+ #[test]
7
+ fn cli_inline_preset_expands_relaxed() {
8
+ let td = tempdir().unwrap();
9
+ let yaml = td.path().join("file.yaml");
10
+ fs::write(&yaml, "key: value\n").unwrap();
11
+ let exe = env!("CARGO_BIN_EXE_ryl");
12
+ let output = Command::new(exe)
13
+ .arg("--list-files")
14
+ .arg("-d")
15
+ .arg("relaxed")
16
+ .arg(td.path())
17
+ .output()
18
+ .expect("run cli");
19
+ assert_eq!(output.status.code(), Some(0));
20
+ }