@@ -4,7 +4,7 @@ import java.util
44import sjsonnet .Expr .Member .Visibility
55import sjsonnet .Expr .Params
66
7- import scala .annotation .tailrec
7+ import scala .annotation .{ nowarn , tailrec }
88import scala .collection .mutable
99import scala .collection .mutable .ArrayBuffer
1010import 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 ) {
0 commit comments