Skip to content

Commit 7a4b33a

Browse files
committed
fix(configuration): Improve recursive dictionary merging and cloning
Ensures nested dictionaries merge correctly and incoming dictionaries are cloned to prevent unintended external mutations.
1 parent e3bf324 commit 7a4b33a

2 files changed

Lines changed: 103 additions & 42 deletions

File tree

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
namespace GitVersion.Configuration.Tests;
2+
3+
[TestFixture]
4+
public class ConfigurationHelperTests
5+
{
6+
[Test]
7+
public void Override_Replaces_Nested_Dictionary_With_Scalar_Value()
8+
{
9+
Dictionary<object, object?> original =
10+
new()
11+
{
12+
["key"] = new Dictionary<object, object?> { ["nested"] = "value" }
13+
};
14+
var helper = new ConfigurationHelper(original);
15+
16+
IReadOnlyDictionary<object, object?> source =
17+
new Dictionary<object, object?>
18+
{
19+
["key"] = "override"
20+
};
21+
22+
helper.Override(source);
23+
24+
helper.Dictionary["key"].ShouldBe("override");
25+
}
26+
27+
[Test]
28+
public void Override_Merges_Nested_Dictionaries_Recursively()
29+
{
30+
Dictionary<object, object?> original =
31+
new()
32+
{
33+
["key"] = new Dictionary<object, object?>
34+
{
35+
["a"] = 1,
36+
["b"] = 2
37+
}
38+
};
39+
var helper = new ConfigurationHelper(original);
40+
41+
IReadOnlyDictionary<object, object?> source =
42+
new Dictionary<object, object?>
43+
{
44+
["key"] = new Dictionary<object, object?>
45+
{
46+
["b"] = 3,
47+
["c"] = 4
48+
}
49+
};
50+
51+
helper.Override(source);
52+
53+
var nested = (IDictionary<object, object?>)helper.Dictionary["key"]!;
54+
nested["a"].ShouldBe(1);
55+
nested["b"].ShouldBe(3);
56+
nested["c"].ShouldBe(4);
57+
}
58+
59+
[Test]
60+
public void Override_Clones_New_Nested_Dictionaries()
61+
{
62+
Dictionary<object, object?> original = [];
63+
var helper = new ConfigurationHelper(original);
64+
65+
Dictionary<object, object?> sourceNested =
66+
new()
67+
{
68+
["a"] = 1
69+
};
70+
IReadOnlyDictionary<object, object?> source =
71+
new Dictionary<object, object?>
72+
{
73+
["key"] = sourceNested
74+
};
75+
76+
helper.Override(source);
77+
sourceNested["a"] = 2;
78+
79+
var nested = (IDictionary<object, object?>)helper.Dictionary["key"]!;
80+
nested["a"].ShouldBe(1);
81+
}
82+
}

src/GitVersion.Configuration/ConfigurationHelper.cs

Lines changed: 21 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -45,54 +45,33 @@ public void Override(IReadOnlyDictionary<object, object?> value)
4545

4646
private static void Merge(IDictionary<object, object?> dictionary, IReadOnlyDictionary<object, object?> anotherDictionary)
4747
{
48-
foreach (var item in dictionary)
48+
foreach (var (key, sourceValue) in anotherDictionary)
4949
{
50-
switch (item.Value)
50+
if (dictionary.TryGetValue(key, out var currentValue)
51+
&& currentValue is IDictionary<object, object?> currentDictionary
52+
&& sourceValue is IReadOnlyDictionary<object, object?> sourceDictionary)
5153
{
52-
case IDictionary<object, object?> anotherDictionaryValue:
53-
{
54-
if (anotherDictionary.TryGetValue(item.Key, out var value) && value is IReadOnlyDictionary<object, object?> dictionaryValue)
55-
{
56-
Merge(anotherDictionaryValue, dictionaryValue);
57-
}
58-
59-
break;
60-
}
61-
default:
62-
{
63-
if (anotherDictionary.TryGetValue(item.Key, out var value))
64-
{
65-
dictionary[item.Key] = value;
66-
}
67-
68-
break;
69-
}
54+
Merge(currentDictionary, sourceDictionary);
55+
continue;
7056
}
57+
58+
dictionary[key] = sourceValue is IReadOnlyDictionary<object, object?> nestedDictionary
59+
? CloneDictionary(nestedDictionary)
60+
: sourceValue;
7161
}
62+
}
7263

73-
foreach (var item in anotherDictionary)
74-
{
75-
switch (item.Value)
76-
{
77-
case IReadOnlyDictionary<object, object?> when dictionary.ContainsKey(item.Key):
78-
continue;
79-
case IReadOnlyDictionary<object, object?> dictionaryValue:
80-
{
81-
Dictionary<object, object?> anotherDictionaryValue = [];
82-
Merge(anotherDictionaryValue, dictionaryValue);
83-
dictionary.Add(item.Key, anotherDictionaryValue);
84-
break;
85-
}
86-
default:
87-
{
88-
if (!dictionary.ContainsKey(item.Key))
89-
{
90-
dictionary.Add(item.Key, item.Value);
91-
}
64+
private static Dictionary<object, object?> CloneDictionary(IReadOnlyDictionary<object, object?> dictionary)
65+
{
66+
Dictionary<object, object?> cloned = [];
9267

93-
break;
94-
}
95-
}
68+
foreach (var (key, value) in dictionary)
69+
{
70+
cloned[key] = value is IReadOnlyDictionary<object, object?> nestedDictionary
71+
? CloneDictionary(nestedDictionary)
72+
: value;
9673
}
74+
75+
return cloned;
9776
}
9877
}

0 commit comments

Comments
 (0)