Skip to content

Commit d847581

Browse files
fix: objectRemoveKey exclusions leaking across + inheritance boundaries (#659)
objectRemoveKey's excludedKeys mechanism suppressed re-added keys in both directions of object inheritance via +. Two fixes in Val.Obj: 1. gatherKeys: replaced global exclusion set with incremental processing during root-first iteration. Each object's excludedKeys removes keys from lower levels, but higher-level objects can re-introduce them. Fixes: removeKey(obj, k) + {k: v} 2. addSuper: when the RHS has excludedKeys and the LHS provides those keys as visible, inject synthetic members that delegate to the LHS and clear the exclusion. Fixes: {k: v} + removeKey(obj, k)
1 parent b75b159 commit d847581

2 files changed

Lines changed: 113 additions & 24 deletions

File tree

sjsonnet/src/sjsonnet/Val.scala

Lines changed: 38 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -425,12 +425,39 @@ object Val {
425425
var i = chain.length - 1
426426
while (i >= 0) {
427427
val s = chain(i)
428-
val filteredExcludedKeys = if (s.excludedKeys != null) {
428+
var members = s.getValue0
429+
var filteredExcludedKeys = if (s.excludedKeys != null) {
429430
Util.intersect(s.excludedKeys, keysInThisChain)
430431
} else null
432+
433+
// If this object has excluded keys that the LHS provides as visible,
434+
// re-introduce them with synthetic members that delegate to the LHS.
435+
// This ensures `lhs + removeKey(rhs, k)` preserves lhs's visible key `k`.
436+
if (filteredExcludedKeys != null) {
437+
val iter = filteredExcludedKeys.iterator()
438+
while (iter.hasNext) {
439+
val key = iter.next()
440+
if (lhs.containsVisibleKey(key)) {
441+
if (members eq s.getValue0) {
442+
members = new util.LinkedHashMap[String, Obj.Member](s.getValue0)
443+
}
444+
val capturedKey = key
445+
members.put(
446+
key,
447+
new Obj.Member(false, Visibility.Normal, deprecatedSkipAsserts = true) {
448+
def invoke(self: Val.Obj, sup: Val.Obj, fs: FileScope, ev: EvalScope): Val =
449+
lhs.value(capturedKey, pos, self)(ev)
450+
}
451+
)
452+
iter.remove()
453+
}
454+
}
455+
if (filteredExcludedKeys.isEmpty) filteredExcludedKeys = null
456+
}
457+
431458
current = new Val.Obj(
432459
s.pos,
433-
s.getValue0,
460+
members,
434461
false,
435462
s.triggerAsserts,
436463
current,
@@ -498,30 +525,17 @@ object Val {
498525
val chain = builder.result()
499526
val chainLength = chain.length
500527

501-
// Collect all excluded keys, reusing the set directly when only one source has exclusions
502-
var exclusionSet: java.util.Set[String] = null
503-
var multipleExclusions = false
504-
for (s <- chain) {
505-
val keys = s.excludedKeys
506-
if (Util.isNotEmpty(keys)) {
507-
if (exclusionSet == null) {
508-
exclusionSet = keys
509-
} else {
510-
if (!multipleExclusions) {
511-
val merged = new util.HashSet[String](exclusionSet.size + keys.size)
512-
merged.addAll(exclusionSet)
513-
exclusionSet = merged
514-
multipleExclusions = true
515-
}
516-
exclusionSet.asInstanceOf[util.HashSet[String]].addAll(keys)
517-
}
518-
}
519-
}
520-
521-
// Iterate root-first (reverse of collection order) and populate the mapping
528+
// Iterate root-first (reverse of collection order) and populate the mapping.
529+
// Each object's excludedKeys removes keys gathered from lower levels (closer to root),
530+
// but objects above it in the chain can re-introduce those keys via their own members.
522531
var i = chainLength - 1
523532
while (i >= 0) {
524-
gatherKeysForSingle(chain(i), exclusionSet, mapping)
533+
val s = chain(i)
534+
if (Util.isNotEmpty(s.excludedKeys)) {
535+
val iter = s.excludedKeys.iterator()
536+
while (iter.hasNext) mapping.remove(iter.next())
537+
}
538+
gatherKeysForSingle(s, null, mapping)
525539
i -= 1
526540
}
527541
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package sjsonnet
2+
3+
import utest._
4+
import TestUtils.eval
5+
6+
object StdObjectRemoveKeyTests extends TestSuite {
7+
8+
def tests: Tests = Tests {
9+
10+
test("basic removal") {
11+
eval("std.objectRemoveKey({a: 1, b: 2}, 'a')") ==> ujson.Obj("b" -> 2)
12+
eval("std.objectRemoveKey({a: 1, b: 2}, 'b')") ==> ujson.Obj("a" -> 1)
13+
eval("std.objectRemoveKey({a: 1}, 'a')") ==> ujson.Obj()
14+
}
15+
16+
test("removing non-existent key is a no-op") {
17+
eval("std.objectRemoveKey({a: 1}, 'b')") ==> ujson.Obj("a" -> 1)
18+
}
19+
20+
test("re-adding removed key via object inheritance") {
21+
eval("""std.objectRemoveKey({foo: "bar"}, "foo") + {foo: "bar2"}""") ==>
22+
ujson.Obj("foo" -> "bar2")
23+
24+
eval("""std.objectRemoveKey({a: 1, b: 2}, "a") + {a: 3}""") ==>
25+
ujson.Obj("b" -> 2, "a" -> 3)
26+
27+
eval("""std.objectRemoveKey({a: 1, b: 2, c: 3}, "b") + {b: 99, d: 4}""") ==>
28+
ujson.Obj("a" -> 1, "c" -> 3, "b" -> 99, "d" -> 4)
29+
}
30+
31+
test("double removal then re-add") {
32+
eval(
33+
"""std.objectRemoveKey(std.objectRemoveKey({a: 1, b: 2, c: 3}, "a"), "b") + {a: 10}"""
34+
) ==> ujson.Obj("c" -> 3, "a" -> 10)
35+
}
36+
37+
test("remove then re-add then remove again") {
38+
eval(
39+
"""std.objectRemoveKey(std.objectRemoveKey({foo: 1, bar: 2}, "foo") + {foo: 3}, "foo")"""
40+
) ==> ujson.Obj("bar" -> 2)
41+
}
42+
43+
test("internal super chain preserved after removal") {
44+
eval("std.objectRemoveKey({a: 1} + {b: super.a}, 'a')") ==> ujson.Obj("b" -> 1)
45+
}
46+
47+
test("external inheritance preserved after removal") {
48+
eval("{a: 1} + std.objectRemoveKey({b: super.a}, 'a')") ==>
49+
ujson.Obj("a" -> 1, "b" -> 1)
50+
}
51+
52+
test("LHS key preserved when RHS removes it") {
53+
eval("""{a: 1} + std.objectRemoveKey({a: 2}, "a")""") ==>
54+
ujson.Obj("a" -> 1)
55+
56+
eval("""{a: 1} + std.objectRemoveKey({a: 2, b: 3}, "a")""") ==>
57+
ujson.Obj("a" -> 1, "b" -> 3)
58+
59+
eval("""{a: 10} + std.objectRemoveKey({a: 1} + {b: super.a}, "a")""") ==>
60+
ujson.Obj("a" -> 10, "b" -> 1)
61+
}
62+
63+
test("containsKey and objectFields reflect re-added key") {
64+
eval(
65+
"""local r = std.objectRemoveKey({a: 1}, "a") + {a: 2};
66+
|std.objectHas(r, "a")""".stripMargin
67+
) ==> ujson.True
68+
69+
eval(
70+
"""local r = std.objectRemoveKey({a: 1}, "a") + {a: 2};
71+
|std.objectFields(r)""".stripMargin
72+
) ==> ujson.Arr("a")
73+
}
74+
}
75+
}

0 commit comments

Comments
 (0)