diff --git a/config/src/main/java/com/typesafe/config/impl/ConfigConcatenation.java b/config/src/main/java/com/typesafe/config/impl/ConfigConcatenation.java index ae0bae1d..00c58df5 100644 --- a/config/src/main/java/com/typesafe/config/impl/ConfigConcatenation.java +++ b/config/src/main/java/com/typesafe/config/impl/ConfigConcatenation.java @@ -193,10 +193,12 @@ ResolveResult resolveSubstitutions(ResolveContext } } - // Right now there's no reason to pushParent here because the - // content of ConfigConcatenation should not need to replaceChild, - // but if it did we'd have to do this. - ResolveSource sourceWithParent = source; // .pushParent(this); + // Pieces can contain containers with unresolved substitutions (e.g. a + // delayed merge inside a list), and resolving those replaces them via + // ResolveSource.replaceWithinCurrentParent(); for the replacement to + // walk back up to the root, this concatenation must be on the parent + // chain. + ResolveSource sourceWithParent = source.pushParent(this); ResolveContext newContext = context; List resolved = new ArrayList(pieces.size()); diff --git a/config/src/test/scala/com/typesafe/config/impl/ConcatenationTest.scala b/config/src/test/scala/com/typesafe/config/impl/ConcatenationTest.scala index 76e9709b..9e64b428 100644 --- a/config/src/test/scala/com/typesafe/config/impl/ConcatenationTest.scala +++ b/config/src/test/scala/com/typesafe/config/impl/ConcatenationTest.scala @@ -656,4 +656,49 @@ class ConcatenationTest extends TestUtils { parseConfig("""foo = [1], bar = [2], x = ${foo}" "${bar}""").resolve() } } + + // https://github.com/lightbend/config/issues/751 + // A delayed merge (a duplicate key whose later value is a substitution) + // inside a list that is a piece of a value concatenation used to throw + // BugOrBroken "SimpleConfigObject.replaceChild did not find ...": the + // concatenation was not on the substitution source's parent chain, so the + // resolver walked past it and could not find the list in the parent. + @Test + def delayedMergeInConcatenatedListPiece(): Unit = { + val conf = parseConfig(""" + |tail = [ 2 ] + |front = [ + | { + | foo = 1 + | foo = ${?bar} + | } + |] ${tail} + |""".stripMargin).resolve() + + val front = conf.getList("front") + assertEquals(2, front.size()) + assertEquals(1, front.get(0).asInstanceOf[AbstractConfigObject].toConfig.getInt("foo")) + assertEquals(2, front.get(1).unwrapped()) + } + + // Same shape as delayedMergeInConcatenatedListPiece, but the substitution + // is strict and defined, so it takes part in the delayed merge. + @Test + def definedSubstitutionInConcatenatedListPiece(): Unit = { + val conf = parseConfig(""" + |bar = 7 + |tail = [ 2 ] + |front = [ + | { + | foo = 1 + | foo = ${bar} + | } + |] ${tail} + |""".stripMargin).resolve() + + val front = conf.getList("front") + assertEquals(2, front.size()) + assertEquals(7, front.get(0).asInstanceOf[AbstractConfigObject].toConfig.getInt("foo")) + assertEquals(2, front.get(1).unwrapped()) + } }