use std::collections::HashSet;
use rlsp_yaml_parser::Span;
use rlsp_yaml_parser::node::Node;
pub(super) fn dedup_key_str(key: &Node<Span>) -> Option<String> {
match key {
Node::Scalar { value, .. } => Some(value.clone()),
Node::Alias { name, .. } => Some(format!("*{name}")),
Node::Mapping { .. } | Node::Sequence { .. } => None,
}
}
pub(super) fn dedup_mapping_keys(node: &mut Node<Span>) {
match node {
Node::Mapping { entries, .. } => {
let mut seen: HashSet<String> = HashSet::new();
let keep: Vec<bool> = entries
.iter()
.rev()
.map(|(key, _)| {
dedup_key_str(key).is_none_or(|k| seen.insert(k))
})
.collect::<Vec<_>>()
.into_iter()
.rev()
.collect();
let old = std::mem::take(entries);
*entries = old
.into_iter()
.zip(keep)
.filter_map(|(entry, k)| if k { Some(entry) } else { None })
.collect();
for (_, value) in entries.iter_mut() {
dedup_mapping_keys(value);
}
}
Node::Sequence { items, .. } => {
for item in items.iter_mut() {
dedup_mapping_keys(item);
}
}
Node::Scalar { .. } | Node::Alias { .. } => {}
}
}
#[cfg(test)]
mod tests {
use super::super::{YamlFormatOptions, format_yaml};
fn default_opts() -> YamlFormatOptions {
YamlFormatOptions::default()
}
fn dedup_opts() -> YamlFormatOptions {
YamlFormatOptions {
format_remove_duplicate_keys: true,
..default_opts()
}
}
#[test]
fn dedup_disabled_does_not_remove_duplicate_keys() {
let input = "key: 1\nkey: 2\n";
let result = format_yaml(input, &default_opts());
let count = result.matches("key:").count();
assert!(
count >= 2,
"both keys should remain when dedup disabled: {result:?}"
);
}
#[test]
fn dedup_single_duplicate_keeps_last() {
let result = format_yaml("key: 1\nkey: 2\n", &dedup_opts());
assert!(
result.contains("key: 2"),
"last occurrence missing: {result:?}"
);
assert!(
!result.contains("key: 1"),
"first occurrence should be removed: {result:?}"
);
}
#[test]
fn dedup_three_occurrences_keeps_only_last() {
let result = format_yaml("key: a\nkey: b\nkey: c\n", &dedup_opts());
assert!(
result.contains("key: c"),
"last occurrence missing: {result:?}"
);
assert!(
!result.contains("key: a"),
"first occurrence should be removed: {result:?}"
);
assert!(
!result.contains("key: b"),
"middle occurrence should be removed: {result:?}"
);
}
#[test]
fn dedup_unique_keys_unchanged() {
let result = format_yaml("a: 1\nb: 2\n", &dedup_opts());
assert!(result.contains("a: 1"), "a:1 missing: {result:?}");
assert!(result.contains("b: 2"), "b:2 missing: {result:?}");
}
#[test]
fn dedup_mixed_unique_and_duplicate() {
let result = format_yaml("a: 1\nb: 2\na: 3\n", &dedup_opts());
assert!(
result.contains("a: 3"),
"last a: should be present: {result:?}"
);
assert!(
result.contains("b: 2"),
"unique b: should be present: {result:?}"
);
assert!(
!result.contains("a: 1"),
"first a: should be removed: {result:?}"
);
}
#[test]
fn dedup_empty_mapping_unchanged() {
let result = format_yaml("map: {}\n", &dedup_opts());
assert!(
result.contains("{}"),
"empty mapping should be preserved: {result:?}"
);
}
#[test]
fn dedup_single_entry_mapping_unchanged() {
let result = format_yaml("key: value\n", &dedup_opts());
assert!(
result.contains("key: value"),
"single entry should be preserved: {result:?}"
);
}
#[test]
fn dedup_alias_key_duplicate_keeps_last() {
let input = "? *ref\n: value1\n? *ref\n: value2\n";
let result = format_yaml(input, &dedup_opts());
assert!(
result.contains("value2"),
"last alias-keyed value missing: {result:?}"
);
assert!(
!result.contains("value1"),
"first alias-keyed value should be removed: {result:?}"
);
}
#[test]
fn dedup_complex_mapping_key_no_panic() {
let input = "? {a: 1}\n: value\n";
let result = format_yaml(input, &dedup_opts());
let _ = result;
}
#[test]
fn dedup_complex_sequence_key_no_panic() {
let input = "? [1, 2]\n: value\n";
let result = format_yaml(input, &dedup_opts());
let _ = result;
}
#[test]
fn dedup_case_sensitive_keys_both_kept() {
let result = format_yaml("Key: 1\nkey: 2\n", &dedup_opts());
assert!(result.contains("Key: 1"), "Key:1 missing: {result:?}");
assert!(result.contains("key: 2"), "key:2 missing: {result:?}");
}
#[test]
fn dedup_nested_mapping_removes_inner_duplicates() {
let input = "outer:\n inner: 1\n inner: 2\n";
let result = format_yaml(input, &dedup_opts());
assert!(result.contains("outer:"), "outer key missing: {result:?}");
assert!(
result.contains("inner: 2"),
"last inner should be kept: {result:?}"
);
assert!(
!result.contains("inner: 1"),
"first inner should be removed: {result:?}"
);
}
#[test]
fn dedup_recurses_into_sequence_items() {
let input = "items:\n - key: 1\n key: 2\n - key: 3\n key: 4\n";
let result = format_yaml(input, &dedup_opts());
assert!(
result.contains("key: 2"),
"last key in first item missing: {result:?}"
);
assert!(
result.contains("key: 4"),
"last key in second item missing: {result:?}"
);
assert!(
!result.contains("key: 1"),
"first key in first item should be removed: {result:?}"
);
assert!(
!result.contains("key: 3"),
"first key in second item should be removed: {result:?}"
);
}
#[test]
fn dedup_deeply_nested_removes_innermost_duplicates() {
let input = "a:\n b:\n c: 1\n c: 2\n";
let result = format_yaml(input, &dedup_opts());
assert!(result.contains("a:"), "a: missing: {result:?}");
assert!(result.contains("b:"), "b: missing: {result:?}");
assert!(
result.contains("c: 2"),
"last c: should be kept: {result:?}"
);
assert!(
!result.contains("c: 1"),
"first c: should be removed: {result:?}"
);
}
#[test]
fn dedup_flow_mapping_removes_duplicate() {
let result = format_yaml("{key: 1, key: 2}\n", &dedup_opts());
assert!(
result.contains("key: 2"),
"last occurrence missing: {result:?}"
);
assert!(
!result.contains("key: 1"),
"first occurrence should be removed: {result:?}"
);
}
#[test]
fn dedup_removed_entry_with_trailing_comment_no_crash() {
let input = "key: 1 # this gets removed\nkey: 2\n";
let result = format_yaml(input, &dedup_opts());
assert!(
result.contains("key: 2"),
"last occurrence missing: {result:?}"
);
assert!(
!result.contains("key: 1"),
"first occurrence should be removed: {result:?}"
);
}
#[test]
fn dedup_surviving_entry_leading_comment_preserved() {
let input = "key: 1\n# keep this\nkey: 2\n";
let result = format_yaml(input, &dedup_opts());
assert!(
result.contains("key: 2"),
"last occurrence missing: {result:?}"
);
assert!(
result.contains("# keep this"),
"leading comment should be preserved: {result:?}"
);
}
#[test]
fn dedup_multi_document_per_document() {
let input = "key: 1\nkey: 2\n---\nkey: 3\nkey: 4\n";
let result = format_yaml(input, &dedup_opts());
assert!(
result.contains("key: 2"),
"last key in doc1 missing: {result:?}"
);
assert!(
result.contains("key: 4"),
"last key in doc2 missing: {result:?}"
);
assert!(
result.contains("---"),
"document separator missing: {result:?}"
);
assert!(
!result.contains("key: 1"),
"first key in doc1 should be removed: {result:?}"
);
assert!(
!result.contains("key: 3"),
"first key in doc2 should be removed: {result:?}"
);
}
#[test]
fn dedup_idempotent() {
let input = "key: 1\nkey: 2\n";
let first = format_yaml(input, &dedup_opts());
let second = format_yaml(&first, &dedup_opts());
assert_eq!(first, second, "dedup not idempotent: {first:?}");
}
}