Skip to content

Commit 0d7af68

Browse files
authored
chore: Keep hidden field to std.objectRemoveKey and support inheritan… (#586)
port google/jsonnet#1269 refs: google/go-jsonnet#837 refs: google/go-jsonnet#830
1 parent ebd7d8f commit 0d7af68

11 files changed

Lines changed: 214 additions & 21 deletions

sjsonnet/src/sjsonnet/Util.scala

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package sjsonnet
22

33
object Util {
4+
// A special empty LinkedHashMap instance to avoid creating multiple empty maps
5+
private val EMPTY_LINKED_HASHMAP = new java.util.LinkedHashMap[Any, Any]()
46
private val hashMapDefaultLoadFactor = 0.75f
57
def prettyIndex(lineStarts: Array[Int], index: Int): String = {
68
// Returns (-insertionPoint - 1) when the value is not found, where
@@ -190,10 +192,26 @@ object Util {
190192
new java.util.LinkedHashMap[K, V](hashMapCapacity(expectedElems), hashMapDefaultLoadFactor)
191193
}
192194

195+
/**
196+
* Returns a shared empty Java HashMap instance, please do not modify it!
197+
*/
198+
def emptyJavaLinkedHashMap[K, V]: java.util.LinkedHashMap[K, V] =
199+
EMPTY_LINKED_HASHMAP.asInstanceOf[java.util.LinkedHashMap[K, V]]
200+
193201
def preSizedJavaHashMap[K, V](expectedElems: Int): java.util.HashMap[K, V] = {
194202
new java.util.HashMap[K, V](hashMapCapacity(expectedElems), hashMapDefaultLoadFactor)
195203
}
196204

205+
def preSizedJavaHashSet[E](expectedElems: Int): java.util.HashSet[E] = {
206+
new java.util.HashSet[E](hashMapCapacity(expectedElems), hashMapDefaultLoadFactor)
207+
}
208+
209+
def intersect[E](a: java.util.Set[E], b: java.util.Set[E]): java.util.Set[E] = {
210+
val result = new java.util.HashSet[E](a)
211+
result.retainAll(b)
212+
result
213+
}
214+
197215
private def hashMapCapacity(expectedElems: Int): Int = {
198216
if (expectedElems < 3) {
199217
expectedElems + 1
@@ -204,4 +222,8 @@ object Util {
204222
(expectedElems / hashMapDefaultLoadFactor).toInt + 1
205223
}
206224
}
225+
226+
def isNotEmpty(collection: java.util.Collection[_]): Boolean = {
227+
(collection ne null) && !collection.isEmpty
228+
}
207229
}

sjsonnet/src/sjsonnet/Val.scala

Lines changed: 129 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import java.util
44
import sjsonnet.Expr.Member.Visibility
55
import sjsonnet.Expr.Params
66

7-
import scala.annotation.tailrec
7+
import scala.annotation.{nowarn, tailrec}
88
import scala.collection.mutable
99
import scala.collection.mutable.ArrayBuffer
1010
import scala.reflect.ClassTag
@@ -271,7 +271,8 @@ object Val {
271271
private val triggerAsserts: (Val.Obj, Val.Obj) => Unit,
272272
`super`: Obj,
273273
valueCache: util.HashMap[Any, Val] = new util.HashMap[Any, Val](),
274-
private var allKeys: util.LinkedHashMap[String, java.lang.Boolean] = null)
274+
private var allKeys: util.LinkedHashMap[String, java.lang.Boolean] = null,
275+
private val excludedKeys: java.util.Set[String] = null)
275276
extends Literal
276277
with Expr.ObjBody {
277278
private var asserting: Boolean = false
@@ -314,36 +315,144 @@ object Val {
314315
}
315316

316317
def addSuper(pos: Position, lhs: Val.Obj): Val.Obj = {
317-
val objs = mutable.ArrayBuffer(this)
318+
// Single traversal: collect chain in this-first order
319+
val builder = new mutable.ArrayBuilder.ofRef[Val.Obj]
318320
var current = this
319-
while (current.getSuper != null) {
320-
objs += current.getSuper
321+
while (current != null) {
322+
builder += current
321323
current = current.getSuper
322324
}
325+
val chain = builder.result()
323326

327+
// Pre-collect all keys defined in this chain once (only needed if any obj has excludedKeys)
328+
lazy val keysInThisChain: java.util.Set[String] = {
329+
val set = Util.preSizedJavaHashSet[String](chain.length * 4)
330+
for (s <- chain) set.addAll(s.getValue0.keySet())
331+
set
332+
}
333+
334+
// Iterate root-first (reverse of collection order) to build the new super chain
324335
current = lhs
325-
for (s <- objs.reverse) {
326-
current = new Val.Obj(s.pos, s.getValue0, false, s.triggerAsserts, current)
336+
var i = chain.length - 1
337+
while (i >= 0) {
338+
val s = chain(i)
339+
val filteredExcludedKeys = if (s.excludedKeys != null) {
340+
Util.intersect(s.excludedKeys, keysInThisChain)
341+
} else null
342+
current = new Val.Obj(
343+
s.pos,
344+
s.getValue0,
345+
false,
346+
s.triggerAsserts,
347+
current,
348+
new util.HashMap[Any, Val](),
349+
null,
350+
filteredExcludedKeys
351+
)
352+
i -= 1
327353
}
328354
current
329355
}
330356

357+
/**
358+
* Create a new object that removes the specified keys from this object.
359+
*
360+
* The implementation preserves both internal and external inheritance:
361+
* 1. Internal: For `objectRemoveKey({ a: 1 } + { b: super.a }, 'a')`, the original object's
362+
* internal super chain is preserved, so `b: super.a` can still access `a`.
363+
* 2. External: For `{ a: 1 } + objectRemoveKey({ b: super.a }, 'a')`, the result can
364+
* participate in a new inheritance chain, where `super.a` accesses the new super.
365+
*
366+
* The approach is to create a thin wrapper object with the original object as super, and mark
367+
* the key as excluded via the excludedKeys set. The excluded key won't appear in
368+
* allKeyNames/visibleKeyNames, but super.key can still access the value.
369+
*/
370+
@nowarn("cat=deprecation")
371+
def removeKeys(pos: Position, keys: String*): Val.Obj = {
372+
val excluded =
373+
if (keys.length == 1)
374+
java.util.Collections.singleton(keys.head)
375+
else {
376+
import scala.collection.JavaConverters._
377+
new util.HashSet[String](keys.asJavaCollection)
378+
}
379+
380+
new Val.Obj(
381+
pos,
382+
Util.emptyJavaLinkedHashMap[String, Obj.Member],
383+
false,
384+
null, // No asserts in wrapper; original object's asserts are triggered via super chain
385+
this,
386+
new util.HashMap[Any, Val](), // NOTE: Must be a dedicated new value cache.
387+
null,
388+
excluded
389+
)
390+
}
391+
331392
def prettyName = "object"
332393
override def asObj: Val.Obj = this
333394

334395
private def gatherKeys(mapping: util.LinkedHashMap[String, java.lang.Boolean]): Unit = {
335-
val objs = mutable.ArrayBuffer(this)
396+
// Fast path: no super chain — just copy this object's keys directly
397+
if (this.getSuper == null) {
398+
gatherKeysForSingle(this, null, mapping)
399+
return
400+
}
401+
402+
// Single traversal: collect chain in this-first order using ArrayBuilder
403+
val builder = new mutable.ArrayBuilder.ofRef[Val.Obj]
336404
var current = this
337-
while (current.getSuper != null) {
338-
objs += current.getSuper
405+
while (current != null) {
406+
builder += current
339407
current = current.getSuper
340408
}
409+
val chain = builder.result()
410+
val chainLength = chain.length
411+
412+
// Collect all excluded keys, reusing the set directly when only one source has exclusions
413+
var exclusionSet: java.util.Set[String] = null
414+
var multipleExclusions = false
415+
for (s <- chain) {
416+
val keys = s.excludedKeys
417+
if (Util.isNotEmpty(keys)) {
418+
if (exclusionSet == null) {
419+
exclusionSet = keys
420+
} else {
421+
if (!multipleExclusions) {
422+
val merged = new util.HashSet[String](exclusionSet.size + keys.size)
423+
merged.addAll(exclusionSet)
424+
exclusionSet = merged
425+
multipleExclusions = true
426+
}
427+
exclusionSet.asInstanceOf[util.HashSet[String]].addAll(keys)
428+
}
429+
}
430+
}
341431

342-
for (s <- objs.reverse) {
343-
if (s.static) {
344-
mapping.putAll(s.allKeys)
345-
} else {
346-
s.getValue0.forEach { (k, m) =>
432+
// Iterate root-first (reverse of collection order) and populate the mapping
433+
var i = chainLength - 1
434+
while (i >= 0) {
435+
gatherKeysForSingle(chain(i), exclusionSet, mapping)
436+
i -= 1
437+
}
438+
}
439+
440+
/** Gather keys from a single object into the mapping, filtering by exclusions. */
441+
private def gatherKeysForSingle(
442+
obj: Val.Obj,
443+
exclusionSet: java.util.Set[String],
444+
mapping: util.LinkedHashMap[String, java.lang.Boolean]): Unit = {
445+
if (obj.static) {
446+
obj.allKeys
447+
.keySet()
448+
.forEach(key => {
449+
if (exclusionSet == null || !exclusionSet.contains(key)) {
450+
mapping.put(key, false)
451+
}
452+
})
453+
} else {
454+
obj.getValue0.forEach { (k, m) =>
455+
if (exclusionSet == null || !exclusionSet.contains(k)) {
347456
val vis = m.visibility
348457
if (!mapping.containsKey(k)) mapping.put(k, vis == Visibility.Hidden)
349458
else if (vis == Visibility.Hidden) mapping.put(k, true)
@@ -411,6 +520,11 @@ object Val {
411520
case x => x
412521
}
413522
} else {
523+
// Check if the key is excluded (used by objectRemoveKey)
524+
// When self != this, we need to check if the key exists in self's visible keys
525+
if ((self eq this) && excludedKeys != null && excludedKeys.contains(k)) {
526+
Error.fail("Field does not exist: " + k, pos)
527+
}
414528
val cacheKey = if (self eq this) k else (k, self)
415529
val cachedValue = valueCache.get(cacheKey)
416530
if (cachedValue != null) {

sjsonnet/src/sjsonnet/stdlib/ObjectModule.scala

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -200,11 +200,7 @@ object ObjectModule extends AbstractFunctionModule {
200200
)
201201
},
202202
builtin("objectRemoveKey", "obj", "key") { (pos, ev, o: Val.Obj, key: String) =>
203-
val bindings: Array[(String, Val.Obj.Member)] = for {
204-
k <- o.visibleKeyNames if k != key
205-
v = o.value(k, pos.fileScope.noOffsetPos)(ev)
206-
} yield (k, new Val.Obj.ConstMember(false, Visibility.Normal, v))
207-
Val.Obj.mk(pos, bindings)
203+
o.removeKeys(pos, key)
208204
},
209205
builtin("mergePatch", "target", "patch") { (pos, ev, target: Val, patch: Val) =>
210206
val mergePosition = pos
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
local o = std.objectRemoveKey({foo: 1, bar:: 2, baz: 3}, "foo");
2+
{
3+
fields: std.objectFields(o),
4+
all_fields: std.objectFieldsAll(o),
5+
bar: o.bar,
6+
object: o,
7+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"all_fields": [
3+
"bar",
4+
"baz"
5+
],
6+
"bar": 2,
7+
"fields": [
8+
"baz"
9+
],
10+
"object": {
11+
"baz": 3
12+
}
13+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
local o1 = {
2+
assert self.x : 'x',
3+
assert super.y : 'y',
4+
b: super.a,
5+
};
6+
{ a: 'begin' } + std.objectRemoveKey({ y: true } + o1 + { a: 'end', x: true }, 'y')
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"a": "end",
3+
"b": "begin",
4+
"x": true
5+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
local o1 = {
2+
assert self.x : 'x',
3+
assert super.y : 'y',
4+
b: super.a,
5+
};
6+
local o2 = {
7+
x: true,
8+
y: true,
9+
a: 'one',
10+
};
11+
{ a: 'begin' } + std.objectRemoveKey({ y: std.isString('yyy') } + o1 + o2, 'x')
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
sjsonnet.Error: Field does not exist: x
2+
at [Select x].(builtinObjectRemoveKey_super_assert.jsonnet:2:14)
3+

sjsonnet/test/resources/test_suite/stdlib.jsonnet

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1630,6 +1630,20 @@ std.assertEqual(std.remove([1, 2, 3], 2), [1, 3]) &&
16301630
std.assertEqual(std.removeAt([1, 2, 3], 1), [1, 3]) &&
16311631

16321632
std.assertEqual(std.objectRemoveKey({ foo: 1, bar: 2, baz: 3 }, 'foo'), { bar: 2, baz: 3 }) &&
1633+
// objectRemoveKey should retain hidden fields as hidden fields.
1634+
std.assertEqual(std.objectRemoveKey({ foo: 1, bar: 2, baz:: 3 }, 'foo').baz, 3) &&
1635+
// objectRemoveKey doesn't break inheritance within the provided object.
1636+
std.assertEqual(std.objectRemoveKey({ a: 1 } + { b: super.a }, 'a'), { b: 1 }) &&
1637+
// objectRemoveKey works with inheritance outside of the object.
1638+
std.assertEqual({ a: 1 } + std.objectRemoveKey({ b: super.a }, 'a'), { a: 1, b: 1 }) &&
1639+
// Referential transparency still works.
1640+
std.assertEqual(local o1 = { b: super.a }; std.objectRemoveKey({ a: 1 } + o1, 'a'), { b: 1 }) &&
1641+
std.assertEqual(local o1 = { b: super.a }; { a: 1 } + std.objectRemoveKey(o1, 'a'), { a: 1, b: 1 }) &&
1642+
// Hidden fields still work.
1643+
std.assertEqual(std.objectRemoveKey({ a: 1 } + { b:: super.a }, 'a'), {}) &&
1644+
std.assertEqual(std.objectRemoveKey({ a: 1 } + { b:: super.a }, 'a').b, 1) &&
1645+
std.assertEqual(({ a: 1 } + std.objectRemoveKey({ b:: super.a }, 'a')), { a: 1 }) &&
1646+
std.assertEqual(({ a: 1 } + std.objectRemoveKey({ b:: super.a }, 'a')).b, 1) &&
16331647

16341648
std.assertEqual(std.trim('already trimmed string'), 'already trimmed string') &&
16351649
std.assertEqual(std.trim(' string with spaces on both ends '), 'string with spaces on both ends') &&

0 commit comments

Comments
 (0)