Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -193,21 +193,22 @@ ResolveResult<? extends AbstractConfigValue> 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);
// Track the concatenation so delayed merges in its pieces can replace their parents.
ResolveSource sourceWithParent = source.pushParent(this);
ResolveContext newContext = context;

List<AbstractConfigValue> resolved = new ArrayList<AbstractConfigValue>(pieces.size());
AbstractConfigValue wasMergeStackPiece = context.mergeStackPiece();
for (AbstractConfigValue p : pieces) {
// to concat into a string we have to do a full resolve,
// so unrestrict the context, then put restriction back afterward
Path restriction = newContext.restrictToChild();
ResolveResult<? extends AbstractConfigValue> result = newContext.unrestricted()
// Preserve the restriction so object lookups do not resolve unrelated fields.
if (p instanceof SimpleConfigObject) {
// Mark object pieces so self-referential fields can look back.
newContext = newContext.withMergeStackPiece(p);
}
ResolveResult<? extends AbstractConfigValue> result = newContext
.resolve(p, sourceWithParent);
AbstractConfigValue r = result.value;
newContext = result.context.restrict(restriction);
newContext = result.context.withMergeStackPiece(wasMergeStackPiece);
if (ConfigImpl.traceSubstitutionsEnabled())
ConfigImpl.trace(context.depth(), "resolved concat piece to " + r);
if (r == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ static ResolveResult<? extends AbstractConfigValue> resolveSubstitutions(Replace
ResolveContext newContext = context;
int count = 0;
AbstractConfigValue merged = null;
AbstractConfigValue wasMergeStackPiece = context.mergeStackPiece();
for (AbstractConfigValue end : stack) {
// Per the HOCON spec, a substitution hidden by a value that
// cannot be merged with it is never evaluated. If merged already
Expand Down Expand Up @@ -146,6 +147,11 @@ && allKeysShadowed((SimpleConfigObject) end, (AbstractConfigObject) merged)) {
count += 1;
continue;
}

// mark the piece so self-referential fields directly inside
// it are dropped (looking back at the pieces below) when a
// lookup re-enters them while this piece is still resolving
newContext = newContext.withMergeStackPiece(end);
}

if (ConfigImpl.traceSubstitutionsEnabled()) {
Expand All @@ -157,7 +163,7 @@ && allKeysShadowed((SimpleConfigObject) end, (AbstractConfigObject) merged)) {
+ " against " + sourceForEnd + " endWasRemoved=" + (source != sourceForEnd));
ResolveResult<? extends AbstractConfigValue> result = newContext.resolve(end, sourceForEnd);
AbstractConfigValue resolvedEnd = result.value;
newContext = result.context;
newContext = result.context.withMergeStackPiece(wasMergeStackPiece);

if (resolvedEnd != null) {
if (merged == null) {
Expand Down
61 changes: 50 additions & 11 deletions config/src/main/java/com/typesafe/config/impl/ResolveContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,14 @@ final class ResolveContext {

final private Set<AbstractConfigValue> cycleMarkers;

// the delayed merge stack piece currently being resolved, or null. A
// self-referential field directly inside the piece is dropped in favor of
// earlier pieces when a lookup re-enters it while the piece resolves.
final private AbstractConfigValue mergeStackPiece;

ResolveContext(ResolveMemos memos, ConfigResolveOptions options, Path restrictToChild,
List<AbstractConfigValue> resolveStack, Set<AbstractConfigValue> cycleMarkers) {
List<AbstractConfigValue> resolveStack, Set<AbstractConfigValue> cycleMarkers,
AbstractConfigValue mergeStackPiece) {
this.memos = memos;
this.options = options;
this.restrictToChild = restrictToChild;
Expand All @@ -38,6 +44,7 @@ final class ResolveContext {
// up shared between multiple ResolveContext.
this.resolveStack = resolveStack;
this.cycleMarkers = cycleMarkers;
this.mergeStackPiece = mergeStackPiece;
}

private static Set<AbstractConfigValue> newCycleMarkers() {
Expand All @@ -47,7 +54,8 @@ private static Set<AbstractConfigValue> newCycleMarkers() {
ResolveContext(ConfigResolveOptions options, Path restrictToChild) {
// LinkedHashSet keeps the traversal order which is at least useful
// in error messages if nothing else
this(new ResolveMemos(), options, restrictToChild, new ArrayList<AbstractConfigValue>(), newCycleMarkers());
this(new ResolveMemos(), options, restrictToChild, new ArrayList<AbstractConfigValue>(), newCycleMarkers(),
null);
if (ConfigImpl.traceSubstitutionsEnabled())
ConfigImpl.trace(depth(), "ResolveContext restrict to child " + restrictToChild);
}
Expand All @@ -60,7 +68,7 @@ ResolveContext addCycleMarker(AbstractConfigValue value) {
Set<AbstractConfigValue> copy = newCycleMarkers();
copy.addAll(cycleMarkers);
copy.add(value);
return new ResolveContext(memos, options, restrictToChild, resolveStack, copy);
return new ResolveContext(memos, options, restrictToChild, resolveStack, copy, mergeStackPiece);
}

ResolveContext removeCycleMarker(AbstractConfigValue value) {
Expand All @@ -70,12 +78,12 @@ ResolveContext removeCycleMarker(AbstractConfigValue value) {
Set<AbstractConfigValue> copy = newCycleMarkers();
copy.addAll(cycleMarkers);
copy.remove(value);
return new ResolveContext(memos, options, restrictToChild, resolveStack, copy);
return new ResolveContext(memos, options, restrictToChild, resolveStack, copy, mergeStackPiece);
}

private ResolveContext memoize(MemoKey key, AbstractConfigValue value) {
ResolveMemos changed = memos.put(key, value);
return new ResolveContext(changed, options, restrictToChild, resolveStack, cycleMarkers);
return new ResolveContext(changed, options, restrictToChild, resolveStack, cycleMarkers, mergeStackPiece);
}

ConfigResolveOptions options() {
Expand All @@ -95,13 +103,35 @@ ResolveContext restrict(Path restrictTo) {
if (restrictTo == restrictToChild)
return this;
else
return new ResolveContext(memos, options, restrictTo, resolveStack, cycleMarkers);
return new ResolveContext(memos, options, restrictTo, resolveStack, cycleMarkers, mergeStackPiece);
}

ResolveContext unrestricted() {
return restrict(null);
}

ResolveContext withMergeStackPiece(AbstractConfigValue mergeStackPiece) {
if (mergeStackPiece == this.mergeStackPiece)
return this;
else
return new ResolveContext(memos, options, restrictToChild, resolveStack, cycleMarkers, mergeStackPiece);
}

AbstractConfigValue mergeStackPiece() {
return mergeStackPiece;
}

// identity check whether the value is already being resolved somewhere
// else on the resolve stack; the current resolution itself is always on
// top of the stack, so it is not counted
boolean isResolvingElsewhere(AbstractConfigValue value) {
for (int i = 0; i < resolveStack.size() - 1; i++) {
if (resolveStack.get(i) == value)
return true;
}
return false;
}

String traceString() {
String separator = ", ";
StringBuilder sb = new StringBuilder();
Expand All @@ -121,15 +151,15 @@ private ResolveContext pushTrace(AbstractConfigValue value) {
ConfigImpl.trace(depth(), "pushing trace " + value);
List<AbstractConfigValue> copy = new ArrayList<AbstractConfigValue>(resolveStack);
copy.add(value);
return new ResolveContext(memos, options, restrictToChild, copy, cycleMarkers);
return new ResolveContext(memos, options, restrictToChild, copy, cycleMarkers, mergeStackPiece);
}

ResolveContext popTrace() {
List<AbstractConfigValue> copy = new ArrayList<AbstractConfigValue>(resolveStack);
AbstractConfigValue old = copy.remove(resolveStack.size() - 1);
if (ConfigImpl.traceSubstitutionsEnabled())
ConfigImpl.trace(depth() - 1, "popped trace " + old);
return new ResolveContext(memos, options, restrictToChild, copy, cycleMarkers);
return new ResolveContext(memos, options, restrictToChild, copy, cycleMarkers, mergeStackPiece);
}

int depth() {
Expand Down Expand Up @@ -189,6 +219,12 @@ private ResolveResult<? extends AbstractConfigValue> realResolve(AbstractConfigV

ResolveContext withMemo = result.context;

// a resolution re-entered while the same value is still being
// resolved further out depends on that in-flight state (for
// example a self-referential field looking back inside a merge
// stack), so its result must not be cached
boolean reentering = mergeStackPiece != null && isResolvingElsewhere(original);

if (resolved == null || resolved.resolveStatus() == ResolveStatus.RESOLVED) {
// if the resolved object is fully resolved by resolving
// only the restrictToChildOrNull, then it can be cached
Expand All @@ -197,7 +233,8 @@ private ResolveResult<? extends AbstractConfigValue> realResolve(AbstractConfigV
if (ConfigImpl.traceSubstitutionsEnabled())
ConfigImpl.trace(depth(), "caching " + fullKey + " result " + resolved);

withMemo = withMemo.memoize(fullKey, resolved);
if (!reentering)
withMemo = withMemo.memoize(fullKey, resolved);
} else {
// if we have an unresolved object then either we did a
// partial resolve restricted to a certain child, or we are
Expand All @@ -210,12 +247,14 @@ private ResolveResult<? extends AbstractConfigValue> realResolve(AbstractConfigV
if (ConfigImpl.traceSubstitutionsEnabled())
ConfigImpl.trace(depth(), "caching " + restrictedKey + " result " + resolved);

withMemo = withMemo.memoize(restrictedKey, resolved);
if (!reentering)
withMemo = withMemo.memoize(restrictedKey, resolved);
} else if (options().getAllowUnresolved()) {
if (ConfigImpl.traceSubstitutionsEnabled())
ConfigImpl.trace(depth(), "caching " + fullKey + " result " + resolved);

withMemo = withMemo.memoize(fullKey, resolved);
if (!reentering)
withMemo = withMemo.memoize(fullKey, resolved);
} else {
throw new ConfigException.BugOrBroken(
"resolveSubstitutions() did not give us a resolved object");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -435,15 +435,25 @@ private SimpleConfigObject modifyMayThrow(Modifier modifier) throws Exception {
}
}

// HOCON.md: a self-referential field has a substitution, or a value
// concatenation containing a substitution, as its value
private static boolean potentiallySelfReferential(AbstractConfigValue v) {
return v instanceof ConfigReference || v instanceof ConfigConcatenation;
}

private static final class ResolveModifier implements Modifier {

final Path originalRestrict;
// Allow a self-referential leaf to look back within a merge piece,
// including when nested in one of its object fields.
final boolean dropInFlightChildren;
ResolveContext context;
final ResolveSource source;

ResolveModifier(ResolveContext context, ResolveSource source) {
ResolveModifier(ResolveContext context, ResolveSource source, boolean dropInFlightChildren) {
this.context = context;
this.source = source;
this.dropInFlightChildren = dropInFlightChildren;
originalRestrict = context.restrictToChild();
}

Expand All @@ -458,7 +468,12 @@ public AbstractConfigValue modifyChildMayThrow(String key, AbstractConfigValue v
context = result.context.unrestricted().restrict(originalRestrict);
return result.value;
} else {
// we don't want to resolve the leaf child.
// Drop an in-flight self-referential leaf to look back through the merge.
// Object and array cycles remain unbreakable under HOCON.md.
if (dropInFlightChildren && potentiallySelfReferential(v)
&& context.isResolvingElsewhere(v)) {
return null;
}
return v;
}
} else {
Expand All @@ -484,7 +499,12 @@ ResolveResult<? extends AbstractConfigObject> resolveSubstitutions(ResolveContex
final ResolveSource sourceWithParent = source.pushParent(this);

try {
ResolveModifier modifier = new ResolveModifier(context, sourceWithParent);
boolean inMergeStackPiece = context.mergeStackPiece() == this;
for (ResolveSource.Node<Container> node = sourceWithParent.pathFromRoot;
!inMergeStackPiece && node != null; node = node.tail()) {
inMergeStackPiece = node.head() == context.mergeStackPiece();
}
ResolveModifier modifier = new ResolveModifier(context, sourceWithParent, inMergeStackPiece);

AbstractConfigValue value = modifyMayThrow(modifier);
return ResolveResult.make(modifier.context, value).asObjectResult();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
}
Loading
Loading