@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,255 @@
1
+ use saphyr_parser::{Event, Parser, Span, SpannedEventReceiver};
2
+
3
+ use crate::config::YamlLintConfig;
4
+
5
+ pub const ID: &str = "empty-values";
6
+
7
+ const BLOCK_MAPPING_MESSAGE: &str = "empty value in block mapping";
8
+ const FLOW_MAPPING_MESSAGE: &str = "empty value in flow mapping";
9
+ const BLOCK_SEQUENCE_MESSAGE: &str = "empty value in block sequence";
10
+
11
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
12
+ pub struct Config {
13
+ forbid_block_mappings: bool,
14
+ forbid_flow_mappings: bool,
15
+ forbid_block_sequences: bool,
16
+ }
17
+
18
+ impl Config {
19
+ #[must_use]
20
+ pub fn resolve(cfg: &YamlLintConfig) -> Self {
21
+ let forbid_block_mappings = cfg
22
+ .rule_option(ID, "forbid-in-block-mappings")
23
+ .and_then(saphyr::YamlOwned::as_bool)
24
+ .unwrap_or(true);
25
+
26
+ let forbid_flow_mappings = cfg
27
+ .rule_option(ID, "forbid-in-flow-mappings")
28
+ .and_then(saphyr::YamlOwned::as_bool)
29
+ .unwrap_or(true);
30
+
31
+ let forbid_block_sequences = cfg
32
+ .rule_option(ID, "forbid-in-block-sequences")
33
+ .and_then(saphyr::YamlOwned::as_bool)
34
+ .unwrap_or(true);
35
+
36
+ Self {
37
+ forbid_block_mappings,
38
+ forbid_flow_mappings,
39
+ forbid_block_sequences,
40
+ }
41
+ }
42
+ }
43
+
44
+ #[derive(Debug, Clone, PartialEq, Eq)]
45
+ pub struct Violation {
46
+ pub line: usize,
47
+ pub column: usize,
48
+ pub message: String,
49
+ }
50
+
51
+ #[must_use]
52
+ pub fn check(buffer: &str, cfg: &Config) -> Vec<Violation> {
53
+ let mut parser = Parser::new_from_str(buffer);
54
+ let mut receiver = EmptyValuesReceiver::new(cfg);
55
+ let _ = parser.load(&mut receiver, true);
56
+ receiver.diagnostics
57
+ }
58
+
59
+ #[derive(Clone, Copy, Debug, PartialEq, Eq)]
60
+ enum MappingStyle {
61
+ Block,
62
+ Flow,
63
+ }
64
+
65
+ #[derive(Clone, Copy, Debug, PartialEq, Eq)]
66
+ enum SequenceStyle {
67
+ Block,
68
+ Flow,
69
+ }
70
+
71
+ #[derive(Clone, Copy, Debug, PartialEq, Eq)]
72
+ struct NodeContext {
73
+ mapping: Option<(MappingStyle, NodePosition)>,
74
+ in_block_sequence: bool,
75
+ }
76
+
77
+ #[derive(Clone, Copy, Debug, PartialEq, Eq)]
78
+ enum NodePosition {
79
+ Key,
80
+ Value,
81
+ }
82
+
83
+ struct EmptyValuesReceiver<'cfg> {
84
+ config: &'cfg Config,
85
+ containers: Vec<ContainerState>,
86
+ diagnostics: Vec<Violation>,
87
+ }
88
+
89
+ impl<'cfg> EmptyValuesReceiver<'cfg> {
90
+ const fn new(config: &'cfg Config) -> Self {
91
+ Self {
92
+ config,
93
+ containers: Vec::new(),
94
+ diagnostics: Vec::new(),
95
+ }
96
+ }
97
+
98
+ fn reset(&mut self) {
99
+ self.containers.clear();
100
+ }
101
+
102
+ fn begin_node(&mut self) -> NodeContext {
103
+ let mapping = if let Some(ContainerState::Mapping { style, expect_key }) =
104
+ self.containers.last_mut()
105
+ {
106
+ if *expect_key {
107
+ *expect_key = false;
108
+ Some((*style, NodePosition::Key))
109
+ } else {
110
+ *expect_key = true;
111
+ Some((*style, NodePosition::Value))
112
+ }
113
+ } else {
114
+ None
115
+ };
116
+
117
+ let in_block_sequence = matches!(
118
+ self.containers.last(),
119
+ Some(ContainerState::Sequence {
120
+ style: SequenceStyle::Block,
121
+ })
122
+ );
123
+
124
+ NodeContext {
125
+ mapping,
126
+ in_block_sequence,
127
+ }
128
+ }
129
+
130
+ fn push_mapping(&mut self, span: Span) {
131
+ let style = if span.is_empty() {
132
+ MappingStyle::Block
133
+ } else {
134
+ MappingStyle::Flow
135
+ };
136
+ self.containers.push(ContainerState::Mapping {
137
+ style,
138
+ expect_key: true,
139
+ });
140
+ }
141
+
142
+ fn push_sequence(&mut self, span: Span) {
143
+ let style = if span.is_empty() {
144
+ SequenceStyle::Block
145
+ } else {
146
+ SequenceStyle::Flow
147
+ };
148
+ self.containers.push(ContainerState::Sequence { style });
149
+ }
150
+
151
+ fn push_container(&mut self, span: Span, kind: ContainerKind) {
152
+ match kind {
153
+ ContainerKind::Mapping => self.push_mapping(span),
154
+ ContainerKind::Sequence => self.push_sequence(span),
155
+ }
156
+ }
157
+
158
+ fn pop_container(&mut self) {
159
+ let _ = self.containers.pop();
160
+ }
161
+
162
+ fn handle_scalar(&mut self, span: Span, ctx: NodeContext) {
163
+ if span.is_empty() {
164
+ if let Some((style, NodePosition::Value)) = ctx.mapping {
165
+ match style {
166
+ MappingStyle::Block if self.config.forbid_block_mappings => {
167
+ self.record(span, BLOCK_MAPPING_MESSAGE);
168
+ }
169
+ MappingStyle::Flow if self.config.forbid_flow_mappings => {
170
+ self.record(span, FLOW_MAPPING_MESSAGE);
171
+ }
172
+ _ => {}
173
+ }
174
+ return;
175
+ }
176
+
177
+ if ctx.in_block_sequence && self.config.forbid_block_sequences {
178
+ self.record(span, BLOCK_SEQUENCE_MESSAGE);
179
+ }
180
+ }
181
+ }
182
+
183
+ fn record(&mut self, span: Span, message: &str) {
184
+ let line = span.start.line();
185
+ let column =
186
+ if let Some(ContainerState::Mapping { .. }) = self.containers.last() {
187
+ span.start.col() + 2
188
+ } else {
189
+ span.start.col() + 1
190
+ };
191
+ self.diagnostics.push(Violation {
192
+ line,
193
+ column,
194
+ message: message.to_string(),
195
+ });
196
+ }
197
+ }
198
+
199
+ #[derive(Clone, Copy, Debug, PartialEq, Eq)]
200
+ enum ContainerKind {
201
+ Mapping,
202
+ Sequence,
203
+ }
204
+
205
+ enum ContainerState {
206
+ Mapping {
207
+ style: MappingStyle,
208
+ expect_key: bool,
209
+ },
210
+ Sequence {
211
+ style: SequenceStyle,
212
+ },
213
+ }
214
+
215
+ impl SpannedEventReceiver<'_> for EmptyValuesReceiver<'_> {
216
+ fn on_event(&mut self, event: Event<'_>, span: Span) {
217
+ match event {
218
+ Event::StreamStart
219
+ | Event::DocumentStart(_)
220
+ | Event::StreamEnd
221
+ | Event::DocumentEnd => {
222
+ self.reset();
223
+ }
224
+ Event::MappingStart(_, _) => {
225
+ self.begin_node();
226
+ self.push_container(span, ContainerKind::Mapping);
227
+ }
228
+ Event::SequenceStart(_, _) => {
229
+ self.begin_node();
230
+ self.push_container(span, ContainerKind::Sequence);
231
+ }
232
+ Event::MappingEnd | Event::SequenceEnd => {
233
+ self.pop_container();
234
+ }
235
+ Event::Scalar(_, _, _, _) => {
236
+ let ctx = self.begin_node();
237
+ self.handle_scalar(span, ctx);
238
+ }
239
+ Event::Alias(_) => {
240
+ self.begin_node();
241
+ }
242
+ Event::Nothing => {}
243
+ }
244
+ }
245
+ }
246
+
247
+ #[allow(dead_code)]
248
+ pub fn coverage_touch_nothing_branch() {
249
+ use saphyr_parser::{Marker, Span};
250
+
251
+ let cfg_struct = crate::config::YamlLintConfig::default();
252
+ let config = Config::resolve(&cfg_struct);
253
+ let mut receiver = EmptyValuesReceiver::new(&config);
254
+ receiver.on_event(Event::Nothing, Span::empty(Marker::default()));
255
+ }
@@ -0,0 +1,259 @@
1
+ use saphyr_parser::{Event, Parser, ScalarStyle, Span, SpannedEventReceiver};
2
+
3
+ use crate::config::YamlLintConfig;
4
+ use crate::rules::span_utils::span_char_index_to_byte;
5
+
6
+ pub const ID: &str = "float-values";
7
+
8
+ #[derive(Debug, Clone)]
9
+ pub struct Config {
10
+ flags: u8,
11
+ }
12
+
13
+ const REQUIRE_NUMERAL_FLAG: u8 = 1 << 0;
14
+ const FORBID_SCIENTIFIC_FLAG: u8 = 1 << 1;
15
+ const FORBID_NAN_FLAG: u8 = 1 << 2;
16
+ const FORBID_INF_FLAG: u8 = 1 << 3;
17
+
18
+ impl Config {
19
+ #[must_use]
20
+ pub fn resolve(cfg: &YamlLintConfig) -> Self {
21
+ let require_numeral_before_decimal = cfg
22
+ .rule_option(ID, "require-numeral-before-decimal")
23
+ .and_then(saphyr::YamlOwned::as_bool)
24
+ .unwrap_or(false);
25
+
26
+ let forbid_scientific_notation = cfg
27
+ .rule_option(ID, "forbid-scientific-notation")
28
+ .and_then(saphyr::YamlOwned::as_bool)
29
+ .unwrap_or(false);
30
+
31
+ let forbid_nan = cfg
32
+ .rule_option(ID, "forbid-nan")
33
+ .and_then(saphyr::YamlOwned::as_bool)
34
+ .unwrap_or(false);
35
+
36
+ let forbid_inf = cfg
37
+ .rule_option(ID, "forbid-inf")
38
+ .and_then(saphyr::YamlOwned::as_bool)
39
+ .unwrap_or(false);
40
+
41
+ let mut flags = 0u8;
42
+ if require_numeral_before_decimal {
43
+ flags |= REQUIRE_NUMERAL_FLAG;
44
+ }
45
+ if forbid_scientific_notation {
46
+ flags |= FORBID_SCIENTIFIC_FLAG;
47
+ }
48
+ if forbid_nan {
49
+ flags |= FORBID_NAN_FLAG;
50
+ }
51
+ if forbid_inf {
52
+ flags |= FORBID_INF_FLAG;
53
+ }
54
+
55
+ Self { flags }
56
+ }
57
+
58
+ const fn require_numeral_before_decimal(&self) -> bool {
59
+ (self.flags & REQUIRE_NUMERAL_FLAG) != 0
60
+ }
61
+
62
+ const fn forbid_scientific_notation(&self) -> bool {
63
+ (self.flags & FORBID_SCIENTIFIC_FLAG) != 0
64
+ }
65
+
66
+ const fn forbid_nan(&self) -> bool {
67
+ (self.flags & FORBID_NAN_FLAG) != 0
68
+ }
69
+
70
+ const fn forbid_inf(&self) -> bool {
71
+ (self.flags & FORBID_INF_FLAG) != 0
72
+ }
73
+ }
74
+
75
+ #[derive(Debug, Clone, PartialEq, Eq)]
76
+ pub struct Violation {
77
+ pub line: usize,
78
+ pub column: usize,
79
+ pub message: String,
80
+ }
81
+
82
+ #[must_use]
83
+ pub fn check(buffer: &str, cfg: &Config) -> Vec<Violation> {
84
+ let mut parser = Parser::new_from_str(buffer);
85
+ let mut receiver = FloatValuesReceiver::new(cfg, buffer);
86
+ let _ = parser.load(&mut receiver, true);
87
+ receiver.diagnostics
88
+ }
89
+
90
+ struct FloatValuesReceiver<'cfg, 'input> {
91
+ config: &'cfg Config,
92
+ buffer: &'input str,
93
+ chars: Vec<(usize, char)>,
94
+ buffer_len: usize,
95
+ diagnostics: Vec<Violation>,
96
+ }
97
+
98
+ impl<'cfg, 'input> FloatValuesReceiver<'cfg, 'input> {
99
+ fn new(config: &'cfg Config, buffer: &'input str) -> Self {
100
+ Self {
101
+ config,
102
+ buffer,
103
+ chars: buffer.char_indices().collect(),
104
+ buffer_len: buffer.len(),
105
+ diagnostics: Vec::new(),
106
+ }
107
+ }
108
+
109
+ fn handle_scalar(&mut self, value: &str, span: Span) {
110
+ let line = span.start.line();
111
+ let column = span.start.col() + 1;
112
+
113
+ if self.config.forbid_nan() && is_nan(value) {
114
+ self.diagnostics.push(Violation {
115
+ line,
116
+ column,
117
+ message: format!(
118
+ "forbidden not a number value \"{}\"",
119
+ self.original_scalar(span, value)
120
+ ),
121
+ });
122
+ }
123
+
124
+ if self.config.forbid_inf() && is_inf(value) {
125
+ self.diagnostics.push(Violation {
126
+ line,
127
+ column,
128
+ message: format!(
129
+ "forbidden infinite value \"{}\"",
130
+ self.original_scalar(span, value)
131
+ ),
132
+ });
133
+ }
134
+
135
+ if self.config.forbid_scientific_notation() && is_scientific_notation(value) {
136
+ self.diagnostics.push(Violation {
137
+ line,
138
+ column,
139
+ message: format!(
140
+ "forbidden scientific notation \"{}\"",
141
+ self.original_scalar(span, value)
142
+ ),
143
+ });
144
+ }
145
+
146
+ if self.config.require_numeral_before_decimal()
147
+ && is_missing_numeral_before_decimal(value)
148
+ {
149
+ self.diagnostics.push(Violation {
150
+ line,
151
+ column,
152
+ message: format!(
153
+ "forbidden decimal missing 0 prefix \"{}\"",
154
+ self.original_scalar(span, value)
155
+ ),
156
+ });
157
+ }
158
+ }
159
+
160
+ fn original_scalar<'a>(&'a self, span: Span, fallback: &'a str) -> &'a str {
161
+ let start_char = span.start.index();
162
+ let end_char = span.end.index();
163
+ let start = span_char_index_to_byte(&self.chars, start_char, self.buffer_len);
164
+ let end = span_char_index_to_byte(&self.chars, end_char, self.buffer_len);
165
+ let range_start = start.min(end);
166
+ let range_end = start.max(end);
167
+ self.buffer.get(range_start..range_end).unwrap_or(fallback)
168
+ }
169
+ }
170
+
171
+ impl<'input> SpannedEventReceiver<'input> for FloatValuesReceiver<'_, 'input> {
172
+ fn on_event(&mut self, event: Event<'input>, span: Span) {
173
+ if let Event::Scalar(value, style, _, tag) = event {
174
+ if tag.is_some() || !matches!(style, ScalarStyle::Plain) {
175
+ return;
176
+ }
177
+ self.handle_scalar(value.as_ref(), span);
178
+ }
179
+ }
180
+ }
181
+
182
+ fn is_nan(value: &str) -> bool {
183
+ matches!(value, ".nan" | ".NaN" | ".NAN")
184
+ }
185
+
186
+ fn is_inf(value: &str) -> bool {
187
+ let trimmed = without_sign(value);
188
+ matches!(trimmed, ".inf" | ".Inf" | ".INF")
189
+ }
190
+
191
+ fn is_scientific_notation(value: &str) -> bool {
192
+ let trimmed = without_sign(value);
193
+ let Some((mantissa, exponent)) = split_exponent(trimmed) else {
194
+ return false;
195
+ };
196
+ if !is_valid_exponent(exponent) {
197
+ return false;
198
+ }
199
+ if let Some(digits) = mantissa.strip_prefix('.') {
200
+ return !digits.is_empty() && digits.chars().all(|c| c.is_ascii_digit());
201
+ }
202
+
203
+ if mantissa.is_empty() {
204
+ return false;
205
+ }
206
+
207
+ let mut parts = mantissa.splitn(2, '.');
208
+ let int_part = parts.next().unwrap();
209
+ if int_part.is_empty() || !int_part.chars().all(|c| c.is_ascii_digit()) {
210
+ return false;
211
+ }
212
+ if let Some(frac_part) = parts.next() {
213
+ return frac_part.chars().all(|c| c.is_ascii_digit());
214
+ }
215
+ true
216
+ }
217
+
218
+ fn is_missing_numeral_before_decimal(value: &str) -> bool {
219
+ let trimmed = without_sign(value);
220
+ if !trimmed.starts_with('.') {
221
+ return false;
222
+ }
223
+ let after_dot = &trimmed[1..];
224
+ if after_dot.is_empty() {
225
+ return false;
226
+ }
227
+
228
+ let (digits, exponent) = match split_exponent(after_dot) {
229
+ Some((mantissa, exponent)) => (mantissa, Some(exponent)),
230
+ None => (after_dot, None),
231
+ };
232
+
233
+ if digits.is_empty() || !digits.chars().all(|c| c.is_ascii_digit()) {
234
+ return false;
235
+ }
236
+
237
+ exponent.is_none_or(is_valid_exponent)
238
+ }
239
+
240
+ fn split_exponent(value: &str) -> Option<(&str, &str)> {
241
+ let idx = value.find(['e', 'E'])?;
242
+ Some(value.split_at(idx))
243
+ }
244
+
245
+ fn is_valid_exponent(exponent: &str) -> bool {
246
+ let rest = exponent
247
+ .strip_prefix('e')
248
+ .or_else(|| exponent.strip_prefix('E'))
249
+ .expect("exponent fragment must start with e/E");
250
+ let rest = without_sign(rest);
251
+ !rest.is_empty() && rest.chars().all(|c| c.is_ascii_digit())
252
+ }
253
+
254
+ fn without_sign(value: &str) -> &str {
255
+ value
256
+ .strip_prefix('+')
257
+ .or_else(|| value.strip_prefix('-'))
258
+ .unwrap_or(value)
259
+ }