Skip to content

Commit 949f6e1

Browse files
committed
Add conversion for v1 YAML structs to v2
1 parent 7fa3a31 commit 949f6e1

2 files changed

Lines changed: 352 additions & 0 deletions

File tree

crates/static-analysis-kernel/src/config.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@
33
// Copyright 2026 Datadog, Inc.
44

55
pub mod common;
6+
mod conversion;
67
pub mod file_v1;
78
pub mod file_v2;
Lines changed: 351 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,351 @@
1+
// Unless explicitly stated otherwise all files in this repository are licensed under the Apache License, Version 2.0.
2+
// This product includes software developed at Datadog (https://www.datadoghq.com/).
3+
// Copyright 2026 Datadog, Inc.
4+
5+
use crate::config::common::YamlSchemaVersion;
6+
use crate::config::file_v1::UniqueKeyMap;
7+
use crate::config::{file_v1, file_v2};
8+
use indexmap::IndexMap;
9+
10+
impl From<file_v1::YamlConfigFile> for file_v2::YamlConfigFile {
11+
fn from(value: file_v1::YamlConfigFile) -> Self {
12+
let mut combined_ignores = value.paths.ignore;
13+
if let Some(paths) = value.ignore_paths {
14+
combined_ignores.extend(paths)
15+
}
16+
17+
let mut use_rulesets = Vec::<String>::with_capacity(value.rulesets.0.len());
18+
let mut ruleset_configs = IndexMap::<String, file_v2::YamlRulesetConfig>::new();
19+
20+
for v1_ruleset_cfg in value.rulesets.0 {
21+
let ruleset_cfg: file_v2::YamlRulesetConfig = v1_ruleset_cfg.cfg.into();
22+
if ruleset_cfg != file_v2::YamlRulesetConfig::default() {
23+
let _ = ruleset_configs.insert(v1_ruleset_cfg.name.clone(), ruleset_cfg);
24+
}
25+
use_rulesets.push(v1_ruleset_cfg.name);
26+
}
27+
28+
let global_config = file_v2::YamlGlobalConfig {
29+
path_config: file_v2::YamlPathConfig {
30+
only_paths: value.paths.only,
31+
ignore_paths: (!combined_ignores.is_empty()).then_some(combined_ignores),
32+
},
33+
// v2 `use_gitignore` defaults to true and is logically equivalent to !(v1 `ignore_gitignore`).
34+
// Thus, it should only be Some if the v1 `ignore_gitignore` is non-default (i.e. "false").
35+
//
36+
// Thus, only set to Some if v1 `ignore_gitignore` is true.
37+
use_gitignore: (value.ignore_gitignore == Some(true)).then_some(false),
38+
ignore_generated_files: value.ignore_generated_files,
39+
max_file_size_kb: value.max_file_size_kb,
40+
};
41+
42+
Self {
43+
schema_version: YamlSchemaVersion::V2,
44+
// (Going from v1 -> v2 always implies an explicit disabling of default rulesets)
45+
use_default_rulesets: Some(false),
46+
use_rulesets: (!use_rulesets.is_empty()).then_some(use_rulesets),
47+
ignore_rulesets: None,
48+
// (ruleset_configs came from file_v1::YamlRulesetList, which enforces key uniqueness, so
49+
// we can manually construct UniqueKeyMap without validation).
50+
ruleset_configs: (!ruleset_configs.is_empty()).then_some(UniqueKeyMap(ruleset_configs)),
51+
global_config: (global_config != file_v2::YamlGlobalConfig::default())
52+
.then_some(global_config),
53+
}
54+
}
55+
}
56+
57+
impl From<file_v1::YamlPathConfig> for file_v2::YamlPathConfig {
58+
fn from(value: file_v1::YamlPathConfig) -> Self {
59+
Self {
60+
only_paths: value.only,
61+
ignore_paths: (!value.ignore.is_empty()).then_some(value.ignore),
62+
}
63+
}
64+
}
65+
66+
impl From<file_v1::YamlRuleConfig> for file_v2::YamlRuleConfig {
67+
fn from(value: file_v1::YamlRuleConfig) -> Self {
68+
Self {
69+
path_config: value.paths.into(),
70+
arguments: (!value.arguments.0.is_empty()).then_some(value.arguments),
71+
severity: value.severity,
72+
category: value.category,
73+
}
74+
}
75+
}
76+
77+
impl From<file_v1::YamlRulesetConfig> for file_v2::YamlRulesetConfig {
78+
fn from(value: file_v1::YamlRulesetConfig) -> Self {
79+
Self {
80+
path_config: value.paths.into(),
81+
rule_configs: (!value.rules.0.is_empty()).then(|| {
82+
UniqueKeyMap(
83+
value
84+
.rules
85+
.0
86+
.into_iter()
87+
.map(|(name, config)| (name, config.into()))
88+
.collect::<IndexMap<_, _>>(),
89+
)
90+
}),
91+
}
92+
}
93+
}
94+
95+
#[cfg(test)]
96+
mod tests {
97+
use crate::config::common::YamlSchemaVersion;
98+
use crate::config::{file_v1, file_v2};
99+
use crate::model::rule::{RuleCategory, RuleSeverity};
100+
use indexmap::IndexMap;
101+
102+
/// Returns a v1 schema with `rulesets: [java-security]` followed by the provided content.
103+
fn v1_template(content: &str) -> String {
104+
format!(
105+
"\
106+
schema-version: v1
107+
rulesets:
108+
- java-security
109+
{content}
110+
"
111+
)
112+
}
113+
114+
/// Shorthand to deserialize a valid v1 config string into a v2 YamlConfigFile
115+
fn to_v2(cfg: impl AsRef<str>) -> file_v2::YamlConfigFile {
116+
serde_yaml::from_str::<file_v1::YamlConfigFile>(cfg.as_ref())
117+
.unwrap()
118+
.into()
119+
}
120+
121+
#[test]
122+
fn yaml_path_config_from() {
123+
let v2 = file_v2::YamlPathConfig::from(file_v1::YamlPathConfig {
124+
only: Some(vec!["src/a".to_string()]),
125+
ignore: vec!["src/a/z".to_string()],
126+
});
127+
assert_eq!(
128+
v2,
129+
file_v2::YamlPathConfig {
130+
only_paths: Some(vec!["src/a".to_string()]),
131+
ignore_paths: Some(vec!["src/a/z".to_string()]),
132+
}
133+
);
134+
// Empty vec is translated to None.
135+
let v2 = file_v2::YamlPathConfig::from(file_v1::YamlPathConfig {
136+
only: Some(vec!["src/a".to_string()]),
137+
ignore: vec![],
138+
});
139+
assert!(v2.ignore_paths.is_none());
140+
}
141+
142+
#[test]
143+
fn yaml_rule_config_from() {
144+
#[rustfmt::skip]
145+
let argument_map = file_v1::UniqueKeyMap(IndexMap::from([
146+
("src/a".to_string(), file_v1::YamlBySubtree(IndexMap::from([("arg_name".to_string(), file_v1::AnyAsString::Str("some_value".to_string()))]))),
147+
]));
148+
#[rustfmt::skip]
149+
let severity_map = file_v1::YamlBySubtree(IndexMap::<String, RuleSeverity>::from([
150+
("src/a".to_string(), RuleSeverity::Error)
151+
]));
152+
let category = file_v1::YamlRuleCategory(RuleCategory::Security);
153+
let v2 = file_v2::YamlRuleConfig::from(file_v1::YamlRuleConfig {
154+
paths: Default::default(),
155+
arguments: argument_map.clone(),
156+
severity: Some(severity_map.clone()),
157+
category: Some(category),
158+
});
159+
assert_eq!(v2.arguments, Some(argument_map));
160+
assert_eq!(v2.severity, Some(severity_map));
161+
assert_eq!(v2.category, Some(category));
162+
163+
// Empty map is translated to None.
164+
let argument_map = file_v1::UniqueKeyMap(IndexMap::default());
165+
let v2 = file_v2::YamlRuleConfig::from(file_v1::YamlRuleConfig {
166+
paths: Default::default(),
167+
arguments: argument_map,
168+
severity: None,
169+
category: None,
170+
});
171+
assert!(v2.arguments.is_none());
172+
}
173+
174+
#[test]
175+
fn yaml_ruleset_config_from() {
176+
// Empty map is translated to None.
177+
let v2 = file_v2::YamlRulesetConfig::from(file_v1::YamlRulesetConfig {
178+
paths: Default::default(),
179+
rules: file_v1::UniqueKeyMap(IndexMap::default()),
180+
});
181+
assert!(v2.rule_configs.is_none());
182+
}
183+
184+
/// Baseline conversion:
185+
/// * `schema-version` is always v2
186+
/// * `use-default-rulesets` is always false.
187+
#[test]
188+
fn baseline() {
189+
let cfg = to_v2(v1_template(""));
190+
assert_eq!(cfg.schema_version, YamlSchemaVersion::V2);
191+
assert_eq!(cfg.use_default_rulesets, Some(false));
192+
}
193+
194+
/// v1 `ignore` and `ignore-paths` are concatenated, if present.
195+
#[test]
196+
fn ignore_paths_concat() {
197+
let cfg = to_v2(v1_template(""));
198+
assert!(cfg.global_config.is_none());
199+
200+
let cfg = to_v2(v1_template(
201+
// language=yaml
202+
"
203+
ignore:
204+
- src/a
205+
",
206+
));
207+
assert_eq!(
208+
cfg.global_config.unwrap().path_config.ignore_paths.unwrap(),
209+
vec!["src/a"]
210+
);
211+
212+
let cfg = to_v2(v1_template(
213+
// language=yaml
214+
"
215+
ignore-paths:
216+
- src/b
217+
",
218+
));
219+
assert_eq!(
220+
cfg.global_config.unwrap().path_config.ignore_paths.unwrap(),
221+
vec!["src/b"]
222+
);
223+
224+
let cfg = to_v2(v1_template(
225+
// language=yaml
226+
"
227+
ignore:
228+
- src/a
229+
ignore-paths:
230+
- src/b
231+
",
232+
));
233+
assert_eq!(
234+
cfg.global_config.unwrap().path_config.ignore_paths.unwrap(),
235+
vec!["src/a", "src/b"]
236+
);
237+
}
238+
239+
/// v2 `use-gitignore` is only present if v1 `ignore-gitignore` was true.
240+
#[test]
241+
fn gitignore_semantics() {
242+
let cfg = to_v2(v1_template(""));
243+
assert!(cfg.global_config.is_none());
244+
245+
let cfg = to_v2(v1_template(
246+
// language=yaml
247+
"
248+
ignore-gitignore: false
249+
",
250+
));
251+
assert!(cfg.global_config.is_none());
252+
253+
let cfg = to_v2(v1_template(
254+
// language=yaml
255+
"
256+
ignore-gitignore: true
257+
",
258+
));
259+
assert_eq!(cfg.global_config.unwrap().use_gitignore, Some(false));
260+
}
261+
262+
#[test]
263+
fn global_config() {
264+
let cfg = to_v2(v1_template(""));
265+
assert!(cfg.global_config.is_none());
266+
267+
let cfg = to_v2(v1_template(
268+
// language=yaml
269+
"
270+
only:
271+
- src/a
272+
ignore:
273+
- src/a/z
274+
ignore-generated-files: true
275+
max-file-size-kb: 500
276+
",
277+
));
278+
let global_config = cfg.global_config.unwrap();
279+
assert_eq!(global_config.path_config.only_paths.unwrap(), vec!["src/a"]);
280+
assert_eq!(
281+
global_config.path_config.ignore_paths.unwrap(),
282+
vec!["src/a/z"]
283+
);
284+
assert_eq!(global_config.ignore_generated_files, Some(true));
285+
assert_eq!(global_config.max_file_size_kb, Some(500));
286+
}
287+
288+
/// v2 `use-rulesets` and `ruleset-configs` are constructed correctly
289+
#[test]
290+
fn ruleset_configs_use_rulesets_split() {
291+
let cfg = to_v2(
292+
// language=yaml
293+
"
294+
schema-version: v1
295+
rulesets:
296+
- java-security
297+
- python-security
298+
",
299+
);
300+
assert_eq!(
301+
cfg.use_rulesets.unwrap(),
302+
vec!["java-security", "python-security"]
303+
);
304+
assert!(cfg.ruleset_configs.is_none());
305+
306+
let cfg = to_v2(
307+
// language=yaml
308+
"
309+
schema-version: v1
310+
rulesets:
311+
- java-security
312+
# (Note the colon, indicating an empty config)
313+
- python-security:
314+
",
315+
);
316+
assert_eq!(
317+
cfg.use_rulesets.unwrap(),
318+
vec!["java-security", "python-security"]
319+
);
320+
assert!(cfg.ruleset_configs.is_none());
321+
322+
let cfg = to_v2(
323+
// language=yaml
324+
"
325+
schema-version: v1
326+
rulesets:
327+
- java-security
328+
- python-security:
329+
only:
330+
- src/a
331+
",
332+
);
333+
assert_eq!(
334+
cfg.use_rulesets.unwrap(),
335+
vec!["java-security", "python-security"]
336+
);
337+
assert_eq!(
338+
cfg.ruleset_configs.unwrap().0,
339+
IndexMap::from([(
340+
"python-security".to_string(),
341+
file_v2::YamlRulesetConfig {
342+
path_config: file_v2::YamlPathConfig {
343+
only_paths: Some(vec!["src/a".to_string()]),
344+
ignore_paths: None,
345+
},
346+
rule_configs: None,
347+
}
348+
)])
349+
)
350+
}
351+
}

0 commit comments

Comments
 (0)