Fix semantics of references from _inside_ a field value to that field

vs. the value itself being a reference to the field.

So this looks "backward":

  a : ${a}

vs. this does not:

  a : { b : ${a} }
This commit is contained in:
Havoc Pennington 2012-04-05 14:16:42 -04:00
parent 06ed4d24e1
commit b696dbee38
10 changed files with 263 additions and 186 deletions

View File

@ -156,19 +156,9 @@ abstract class AbstractConfigObject extends AbstractConfigValue implements Confi
}
@Override
protected final AbstractConfigObject mergedWithTheUnmergeable(Unmergeable fallback) {
if (ignoresFallbacks())
throw new ConfigException.BugOrBroken("should not be reached");
List<AbstractConfigValue> stack = new ArrayList<AbstractConfigValue>();
if (this instanceof Unmergeable) {
stack.addAll(((Unmergeable) this).unmergedValues());
} else {
stack.add(this);
}
stack.addAll(fallback.unmergedValues());
return new ConfigDelayedMergeObject(mergeOrigins(stack), stack,
((AbstractConfigValue) fallback).ignoresFallbacks());
protected AbstractConfigObject constructDelayedMerge(ConfigOrigin origin,
List<AbstractConfigValue> stack) {
return new ConfigDelayedMergeObject(origin, stack);
}
@Override

View File

@ -4,6 +4,10 @@
package com.typesafe.config.impl;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import com.typesafe.config.ConfigException;
import com.typesafe.config.ConfigMergeable;
@ -124,32 +128,91 @@ abstract class AbstractConfigValue implements ConfigValue, MergeableValue, Seria
// really need to store the boolean, and they may be able to pack it
// with another boolean to save space.
protected boolean ignoresFallbacks() {
return true;
// if we are not resolved, then somewhere in this value there's
// a substitution that may need to look at the fallbacks.
return resolveStatus() == ResolveStatus.RESOLVED;
}
private ConfigException badMergeException() {
// the withFallback() implementation is supposed to avoid calling
// mergedWith* if we're ignoring fallbacks.
protected final void requireNotIgnoringFallbacks() {
if (ignoresFallbacks())
throw new ConfigException.BugOrBroken(
"method should not have been called with ignoresFallbacks=true"
+ getClass().getSimpleName());
else
throw new ConfigException.BugOrBroken("should override this in "
"method should not have been called with ignoresFallbacks=true "
+ getClass().getSimpleName());
}
protected AbstractConfigValue mergedWithTheUnmergeable(Unmergeable fallback) {
throw badMergeException();
protected AbstractConfigValue constructDelayedMerge(ConfigOrigin origin,
List<AbstractConfigValue> stack) {
return new ConfigDelayedMerge(origin, stack);
}
protected AbstractConfigValue mergedWithObject(AbstractConfigObject fallback) {
throw badMergeException();
protected final AbstractConfigValue mergedWithTheUnmergeable(
Collection<AbstractConfigValue> stack, Unmergeable fallback) {
requireNotIgnoringFallbacks();
// if we turn out to be an object, and the fallback also does,
// then a merge may be required; delay until we resolve.
List<AbstractConfigValue> newStack = new ArrayList<AbstractConfigValue>();
newStack.addAll(stack);
newStack.addAll(fallback.unmergedValues());
return constructDelayedMerge(AbstractConfigObject.mergeOrigins(newStack), newStack);
}
protected AbstractConfigValue mergedWithNonObject(AbstractConfigValue fallback) {
private final AbstractConfigValue delayMerge(Collection<AbstractConfigValue> stack,
AbstractConfigValue fallback) {
// if we turn out to be an object, and the fallback also does,
// then a merge may be required.
// if we contain a substitution, resolving it may need to look
// back to the fallback.
List<AbstractConfigValue> newStack = new ArrayList<AbstractConfigValue>();
newStack.addAll(stack);
newStack.add(fallback);
return constructDelayedMerge(AbstractConfigObject.mergeOrigins(newStack), newStack);
}
protected final AbstractConfigValue mergedWithObject(Collection<AbstractConfigValue> stack,
AbstractConfigObject fallback) {
requireNotIgnoringFallbacks();
if (this instanceof AbstractConfigObject)
throw new ConfigException.BugOrBroken("Objects must reimplement mergedWithObject");
return mergedWithNonObject(stack, fallback);
}
protected final AbstractConfigValue mergedWithNonObject(Collection<AbstractConfigValue> stack,
AbstractConfigValue fallback) {
requireNotIgnoringFallbacks();
if (resolveStatus() == ResolveStatus.RESOLVED) {
// falling back to a non-object doesn't merge anything, and also
// prohibits merging any objects that we fall back to later.
// so we have to switch to ignoresFallbacks mode.
return newCopy(true /* ignoresFallbacks */, origin);
} else {
// if unresolved, we may have to look back to fallbacks as part of
// the resolution process, so always delay
return delayMerge(stack, fallback);
}
}
protected AbstractConfigValue mergedWithTheUnmergeable(Unmergeable fallback) {
requireNotIgnoringFallbacks();
return mergedWithTheUnmergeable(Collections.singletonList(this), fallback);
}
protected AbstractConfigValue mergedWithObject(AbstractConfigObject fallback) {
requireNotIgnoringFallbacks();
return mergedWithObject(Collections.singletonList(this), fallback);
}
protected AbstractConfigValue mergedWithNonObject(AbstractConfigValue fallback) {
requireNotIgnoringFallbacks();
return mergedWithNonObject(Collections.singletonList(this), fallback);
}
public AbstractConfigValue withOrigin(ConfigOrigin origin) {
@ -159,6 +222,7 @@ abstract class AbstractConfigValue implements ConfigValue, MergeableValue, Seria
return newCopy(ignoresFallbacks(), origin);
}
// this is only overridden to change the return type
@Override
public AbstractConfigValue withFallback(ConfigMergeable mergeable) {
if (ignoresFallbacks()) {
@ -169,15 +233,7 @@ abstract class AbstractConfigValue implements ConfigValue, MergeableValue, Seria
if (other instanceof Unmergeable) {
return mergedWithTheUnmergeable((Unmergeable) other);
} else if (other instanceof AbstractConfigObject) {
AbstractConfigObject fallback = (AbstractConfigObject) other;
if (fallback.resolveStatus() == ResolveStatus.RESOLVED && fallback.isEmpty()) {
if (fallback.ignoresFallbacks())
return newCopy(true /* ignoresFallbacks */, origin);
else
return this;
} else {
return mergedWithObject((AbstractConfigObject) other);
}
} else {
return mergedWithNonObject((AbstractConfigValue) other);
}

View File

@ -60,43 +60,6 @@ final class ConfigConcatenation extends AbstractConfigValue implements Unmergeab
return false;
}
@Override
protected AbstractConfigValue mergedWithTheUnmergeable(Unmergeable fallback) {
// if we turn out to be an object, and the fallback also does,
// then a merge may be required; delay until we resolve.
List<AbstractConfigValue> newStack = new ArrayList<AbstractConfigValue>();
newStack.add(this);
newStack.addAll(fallback.unmergedValues());
return new ConfigDelayedMerge(AbstractConfigObject.mergeOrigins(newStack), newStack,
((AbstractConfigValue) fallback).ignoresFallbacks());
}
protected AbstractConfigValue mergedLater(AbstractConfigValue fallback) {
List<AbstractConfigValue> newStack = new ArrayList<AbstractConfigValue>();
newStack.add(this);
newStack.add(fallback);
return new ConfigDelayedMerge(AbstractConfigObject.mergeOrigins(newStack), newStack,
fallback.ignoresFallbacks());
}
@Override
protected AbstractConfigValue mergedWithObject(AbstractConfigObject fallback) {
// if we turn out to be an object, and the fallback also does,
// then a merge may be required; delay until we resolve.
return mergedLater(fallback);
}
@Override
protected AbstractConfigValue mergedWithNonObject(AbstractConfigValue fallback) {
// We may need the fallback if we contain a self-referential
// ConfigReference.
//
// we can't easily detect the self-referential case since the cycle
// may involve more than one step, so we have to wait and
// merge later when resolving.
return mergedLater(fallback);
}
@Override
public Collection<ConfigConcatenation> unmergedValues() {
return Collections.singleton(this);

View File

@ -28,13 +28,15 @@ final class ConfigDelayedMerge extends AbstractConfigValue implements Unmergeabl
// earlier items in the stack win
final private List<AbstractConfigValue> stack;
final private boolean ignoresFallbacks;
// this is just here for serialization compat; whether we ignore is purely
// a function of the bottom of the merge stack
@SuppressWarnings("unused")
@Deprecated
final private boolean ignoresFallbacks = false;
ConfigDelayedMerge(ConfigOrigin origin, List<AbstractConfigValue> stack,
boolean ignoresFallbacks) {
ConfigDelayedMerge(ConfigOrigin origin, List<AbstractConfigValue> stack) {
super(origin);
this.stack = stack;
this.ignoresFallbacks = ignoresFallbacks;
if (stack.isEmpty())
throw new ConfigException.BugOrBroken(
"creating empty delayed merge value");
@ -46,10 +48,6 @@ final class ConfigDelayedMerge extends AbstractConfigValue implements Unmergeabl
}
}
ConfigDelayedMerge(ConfigOrigin origin, List<AbstractConfigValue> stack) {
this(origin, stack, false /* ignoresFallbacks */);
}
@Override
public ConfigValueType valueType() {
throw new ConfigException.NotResolved(
@ -80,10 +78,9 @@ final class ConfigDelayedMerge extends AbstractConfigValue implements Unmergeabl
AbstractConfigValue merged = null;
for (AbstractConfigValue v : stack) {
boolean replaced = false;
// checking for RESOLVED already is just an optimization
// to avoid creating the replacer when it can't possibly
// be needed.
if (v.resolveStatus() != ResolveStatus.RESOLVED) {
// we only replace if we have a substitution, or
// value-concatenation containing one
if (v instanceof Unmergeable) {
// If, while resolving 'v' we come back to the same
// merge stack, we only want to look _below_ 'v'
// in the stack. So we arrange to replace the
@ -158,50 +155,38 @@ final class ConfigDelayedMerge extends AbstractConfigValue implements Unmergeabl
for (AbstractConfigValue o : stack) {
newStack.add(o.relativized(prefix));
}
return new ConfigDelayedMerge(origin(), newStack, ignoresFallbacks);
return new ConfigDelayedMerge(origin(), newStack);
}
// static utility shared with ConfigDelayedMergeObject
static boolean stackIgnoresFallbacks(List<AbstractConfigValue> stack) {
AbstractConfigValue last = stack.get(stack.size() - 1);
return last.ignoresFallbacks();
}
@Override
protected boolean ignoresFallbacks() {
return ignoresFallbacks;
return stackIgnoresFallbacks(stack);
}
@Override
protected AbstractConfigValue newCopy(boolean newIgnoresFallbacks, ConfigOrigin newOrigin) {
return new ConfigDelayedMerge(newOrigin, stack, newIgnoresFallbacks);
return new ConfigDelayedMerge(newOrigin, stack);
}
@Override
protected final ConfigDelayedMerge mergedWithTheUnmergeable(Unmergeable fallback) {
if (ignoresFallbacks)
throw new ConfigException.BugOrBroken("should not be reached");
// if we turn out to be an object, and the fallback also does,
// then a merge may be required; delay until we resolve.
List<AbstractConfigValue> newStack = new ArrayList<AbstractConfigValue>();
newStack.addAll(stack);
newStack.addAll(fallback.unmergedValues());
return new ConfigDelayedMerge(AbstractConfigObject.mergeOrigins(newStack), newStack,
((AbstractConfigValue) fallback).ignoresFallbacks());
return (ConfigDelayedMerge) mergedWithTheUnmergeable(stack, fallback);
}
@Override
protected final ConfigDelayedMerge mergedWithObject(AbstractConfigObject fallback) {
return mergedWithNonObject(fallback);
return (ConfigDelayedMerge) mergedWithObject(stack, fallback);
}
@Override
protected ConfigDelayedMerge mergedWithNonObject(AbstractConfigValue fallback) {
if (ignoresFallbacks)
throw new ConfigException.BugOrBroken("should not be reached");
// if we turn out to be an object, and the fallback also does,
// then a merge may be required; delay until we resolve.
List<AbstractConfigValue> newStack = new ArrayList<AbstractConfigValue>();
newStack.addAll(stack);
newStack.add(fallback);
return new ConfigDelayedMerge(AbstractConfigObject.mergeOrigins(newStack), newStack,
fallback.ignoresFallbacks());
return (ConfigDelayedMerge) mergedWithNonObject(stack, fallback);
}
@Override
@ -300,6 +285,6 @@ final class ConfigDelayedMerge extends AbstractConfigValue implements Unmergeabl
private Object writeReplace() throws ObjectStreamException {
// switch to LinkedList
return new ConfigDelayedMerge(origin(),
new java.util.LinkedList<AbstractConfigValue>(stack), ignoresFallbacks);
new java.util.LinkedList<AbstractConfigValue>(stack));
}
}

View File

@ -11,6 +11,7 @@ import java.util.Map;
import java.util.Set;
import com.typesafe.config.ConfigException;
import com.typesafe.config.ConfigList;
import com.typesafe.config.ConfigMergeable;
import com.typesafe.config.ConfigOrigin;
import com.typesafe.config.ConfigValue;
@ -23,18 +24,16 @@ final class ConfigDelayedMergeObject extends AbstractConfigObject implements Unm
private static final long serialVersionUID = 1L;
final private List<AbstractConfigValue> stack;
final private boolean ignoresFallbacks;
ConfigDelayedMergeObject(ConfigOrigin origin,
List<AbstractConfigValue> stack) {
this(origin, stack, false /* ignoresFallbacks */);
}
// this is just here for serialization compat; whether we ignore is purely
// a function of the bottom of the merge stack
@SuppressWarnings("unused")
@Deprecated
final private boolean ignoresFallbacks = false;
ConfigDelayedMergeObject(ConfigOrigin origin, List<AbstractConfigValue> stack,
boolean ignoresFallbacks) {
ConfigDelayedMergeObject(ConfigOrigin origin, List<AbstractConfigValue> stack) {
super(origin);
this.stack = stack;
this.ignoresFallbacks = ignoresFallbacks;
if (stack.isEmpty())
throw new ConfigException.BugOrBroken(
@ -56,7 +55,7 @@ final class ConfigDelayedMergeObject extends AbstractConfigObject implements Unm
if (status != resolveStatus())
throw new ConfigException.BugOrBroken(
"attempt to create resolved ConfigDelayedMergeObject");
return new ConfigDelayedMergeObject(origin, stack, ignoresFallbacks);
return new ConfigDelayedMergeObject(origin, stack);
}
@Override
@ -93,30 +92,31 @@ final class ConfigDelayedMergeObject extends AbstractConfigObject implements Unm
for (AbstractConfigValue o : stack) {
newStack.add(o.relativized(prefix));
}
return new ConfigDelayedMergeObject(origin(), newStack,
ignoresFallbacks);
return new ConfigDelayedMergeObject(origin(), newStack);
}
@Override
protected boolean ignoresFallbacks() {
return ignoresFallbacks;
return ConfigDelayedMerge.stackIgnoresFallbacks(stack);
}
@Override
protected ConfigDelayedMergeObject mergedWithObject(AbstractConfigObject fallback) {
protected final ConfigDelayedMergeObject mergedWithTheUnmergeable(Unmergeable fallback) {
requireNotIgnoringFallbacks();
return (ConfigDelayedMergeObject) mergedWithTheUnmergeable(stack, fallback);
}
@Override
protected final ConfigDelayedMergeObject mergedWithObject(AbstractConfigObject fallback) {
return mergedWithNonObject(fallback);
}
@Override
protected ConfigDelayedMergeObject mergedWithNonObject(AbstractConfigValue fallback) {
if (ignoresFallbacks)
throw new ConfigException.BugOrBroken("should not be reached");
protected final ConfigDelayedMergeObject mergedWithNonObject(AbstractConfigValue fallback) {
requireNotIgnoringFallbacks();
List<AbstractConfigValue> newStack = new ArrayList<AbstractConfigValue>();
newStack.addAll(stack);
newStack.add(fallback);
return new ConfigDelayedMergeObject(AbstractConfigObject.mergeOrigins(newStack), newStack,
fallback.ignoresFallbacks());
return (ConfigDelayedMergeObject) mergedWithNonObject(stack, fallback);
}
@Override
@ -249,10 +249,6 @@ final class ConfigDelayedMergeObject extends AbstractConfigObject implements Unm
// to touch the exact key that isn't resolved, so this is in that
// spirit.
// this function should never return null; if we know a value doesn't
// exist, then there would be no reason for the merge to be delayed
// (i.e. as long as some stuff is unmerged, the value may be non-null).
// we'll be able to return a key if we have a value that ignores
// fallbacks, prior to any unmergeable values.
for (AbstractConfigValue layer : stack) {
@ -289,17 +285,33 @@ final class ConfigDelayedMergeObject extends AbstractConfigObject implements Unm
throw new ConfigException.NotResolved("Key '" + key + "' is not available at '"
+ origin().description() + "' because value at '"
+ layer.origin().description()
+ "' has not been resolved and may turn out to contain '" + key + "'."
+ "' has not been resolved and may turn out to contain or hide '" + key
+ "'."
+ " Be sure to Config#resolve() before using a config object.");
} else if (layer.resolveStatus() == ResolveStatus.UNRESOLVED) {
// if the layer is not an object, and not a substitution or
// merge,
// then it's something that's unresolved because it _contains_
// an unresolved object... i.e. it's an array
if (!(layer instanceof ConfigList))
throw new ConfigException.BugOrBroken("Expecting a list here, not " + layer);
// all later objects will be hidden so we can say we won't find
// the key
return null;
} else {
// non-object, but not unresolved, like an integer or something.
// non-object, but resolved, like an integer or something.
// has no children so the one we're after won't be in it.
// this should always be overridden by an object though so
// ideally we never build a stack that would have this in it.
continue;
// we would only have this in the stack in case something
// else "looks back" to it due to a cycle.
// anyway at this point we know we can't find the key anymore.
if (!layer.ignoresFallbacks()) {
throw new ConfigException.BugOrBroken(
"resolved non-object should ignore fallbacks");
}
return null;
}
}
// If we get here, then we never found an unmergeable which means
// If we get here, then we never found anything unresolved which means
// the ConfigDelayedMergeObject should not have existed. some
// invariant was violated.
throw new ConfigException.BugOrBroken(
@ -314,6 +326,6 @@ final class ConfigDelayedMergeObject extends AbstractConfigObject implements Unm
private Object writeReplace() throws ObjectStreamException {
// switch to LinkedList
return new ConfigDelayedMergeObject(origin(),
new java.util.LinkedList<AbstractConfigValue>(stack), ignoresFallbacks);
new java.util.LinkedList<AbstractConfigValue>(stack));
}
}

View File

@ -1,9 +1,7 @@
package com.typesafe.config.impl;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import com.typesafe.config.ConfigException;
import com.typesafe.config.ConfigOrigin;
@ -59,45 +57,6 @@ final class ConfigReference extends AbstractConfigValue implements Unmergeable {
return false;
}
@Override
protected AbstractConfigValue mergedWithTheUnmergeable(Unmergeable fallback) {
// if we turn out to be an object, and the fallback also does,
// then a merge may be required; delay until we resolve.
List<AbstractConfigValue> newStack = new ArrayList<AbstractConfigValue>();
newStack.add(this);
newStack.addAll(fallback.unmergedValues());
return new ConfigDelayedMerge(AbstractConfigObject.mergeOrigins(newStack), newStack,
((AbstractConfigValue) fallback).ignoresFallbacks());
}
protected AbstractConfigValue mergedLater(AbstractConfigValue fallback) {
List<AbstractConfigValue> newStack = new ArrayList<AbstractConfigValue>();
newStack.add(this);
newStack.add(fallback);
return new ConfigDelayedMerge(AbstractConfigObject.mergeOrigins(newStack), newStack,
fallback.ignoresFallbacks());
}
@Override
protected AbstractConfigValue mergedWithObject(AbstractConfigObject fallback) {
// if we turn out to be an object, and the fallback also does,
// then a merge may be required; delay until we resolve.
return mergedLater(fallback);
}
@Override
protected AbstractConfigValue mergedWithNonObject(AbstractConfigValue fallback) {
// We may need the fallback for two reasons:
// 1. if an optional substitution ends up getting deleted
// because it is not defined
// 2. if the substitution is self-referential
//
// we can't easily detect the self-referential case since the cycle
// may involve more than one step, so we have to wait and
// merge later when resolving.
return mergedLater(fallback);
}
@Override
public Collection<ConfigReference> unmergedValues() {
return Collections.singleton(this);

View File

@ -93,7 +93,6 @@ final class ResolveContext {
}
AbstractConfigValue resolve(AbstractConfigValue original) throws NotPossibleToResolve {
// a fully-resolved (no restrictToChild) object can satisfy a
// request for a restricted object, so always check that first.
final MemoKey fullKey = new MemoKey(original, null);

View File

@ -159,8 +159,7 @@ final class SimpleConfigObject extends AbstractConfigObject {
@Override
protected SimpleConfigObject mergedWithObject(AbstractConfigObject abstractFallback) {
if (ignoresFallbacks())
throw new ConfigException.BugOrBroken("should not be reached");
requireNotIgnoringFallbacks();
if (!(abstractFallback instanceof SimpleConfigObject)) {
throw new ConfigException.BugOrBroken(

View File

@ -1039,6 +1039,20 @@ class ConfigSubstitutionTest extends TestUtils {
assertEquals("optional self reference disappears", 0, resolved.root.size)
}
@Test
def substSelfReferenceAlongPath() {
val obj = parseObject("""a.b=1, a.b=${a.b}""")
val resolved = resolve(obj)
assertEquals(1, resolved.getInt("a.b"))
}
@Test
def substSelfReferenceAlongLongerPath() {
val obj = parseObject("""a.b.c=1, a.b.c=${a.b.c}""")
val resolved = resolve(obj)
assertEquals(1, resolved.getInt("a.b.c"))
}
@Test
def substSelfReferenceIndirect() {
val obj = parseObject("""a=1, b=${a}, a=${b}""")
@ -1077,6 +1091,13 @@ class ConfigSubstitutionTest extends TestUtils {
assertEquals(5, resolved.getInt("a.b"))
}
@Test
def substSelfReferenceObjectAlongPath() {
val obj = parseObject("""a.b={c=5}, a.b=${a.b}""")
val resolved = resolve(obj)
assertEquals(5, resolved.getInt("a.b.c"))
}
@Test
def substSelfReferenceInConcat() {
val obj = parseObject("""a=1, a=${a}foo""")
@ -1174,6 +1195,15 @@ class ConfigSubstitutionTest extends TestUtils {
assertEquals(5, resolved.getInt("a.c"))
}
@Test
def substSelfReferenceAlongAPathInsideObject() {
// if the ${a.b} is _inside_ a field value instead of
// _being_ the field value, it does not look backward.
val obj = parseObject("""a={b={c=5}}, a={ x : ${a.b} }, a={b=2}""")
val resolved = resolve(obj)
assertEquals(2, resolved.getInt("a.x"))
}
@Test
def substInChildFieldNotASelfReference1() {
// here, ${bar.foo} is not a self reference because
@ -1220,6 +1250,37 @@ class ConfigSubstitutionTest extends TestUtils {
assertEquals(42, resolved.getInt("bar.foo"))
}
@Test
def substInChildFieldNotASelfReference4() {
// checking that having bar set to non-object earlier
// doesn't break the behavior.
val obj = parseObject("""
bar : 101
bar : { foo : 42,
baz : ${bar.foo}
}
""")
val resolved = resolve(obj)
assertEquals(42, resolved.getInt("bar.baz"))
assertEquals(42, resolved.getInt("bar.foo"))
}
@Test
def substInChildFieldNotASelfReference5() {
// checking that having bar set to unresolved array earlier
// doesn't break the behavior.
val obj = parseObject("""
x : 0
bar : [ ${x}, 1, 2, 3 ]
bar : { foo : 42,
baz : ${bar.foo}
}
""")
val resolved = resolve(obj)
assertEquals(42, resolved.getInt("bar.baz"))
assertEquals(42, resolved.getInt("bar.foo"))
}
@Test
def mutuallyReferringNotASelfReference() {
val obj = parseObject("""
@ -1248,4 +1309,42 @@ class ConfigSubstitutionTest extends TestUtils {
val resolved = resolve(obj)
assertEquals("1xyz", resolved.getString("a"))
}
@Test
def substSelfReferenceInArray() {
// never "look back" from "inside" an array
val obj = parseObject("""a=1,a=[${a}, 2]""")
val e = intercept[ConfigException.UnresolvedSubstitution] {
resolve(obj)
}
assertTrue("wrong exception: " + e.getMessage,
e.getMessage.contains("cycle") && e.getMessage.contains("${a}"))
}
@Test
def substSelfReferenceInObject() {
// never "look back" from "inside" an object
val obj = parseObject("""a=1,a={ x : ${a} }""")
val e = intercept[ConfigException.UnresolvedSubstitution] {
resolve(obj)
}
assertTrue("wrong exception: " + e.getMessage,
e.getMessage.contains("cycle") && e.getMessage.contains("${a}"))
}
@Test
def selfReferentialObjectNotAffectedByOverriding() {
// this is testing that we can still refer to another
// field in the same object, even though we are overriding
// an earlier object.
val obj = parseObject("""a={ x : 42, y : ${a.x} }""")
val resolved = resolve(obj)
assertEquals(parseObject("{ x : 42, y : 42 }"), resolved.getConfig("a").root)
// this is expected because if adding "a=1" here affects the outcome,
// it would be flat-out bizarre.
val obj2 = parseObject("""a=1, a={ x : 42, y : ${a.x} }""")
val resolved2 = resolve(obj2)
assertEquals(parseObject("{ x : 42, y : 42 }"), resolved2.getConfig("a").root)
}
}

View File

@ -79,7 +79,8 @@ class ConfigTest extends TestUtils {
if (!trees(0).equals(tree))
throw new AssertionError("Merge was not associative, " +
"verify that it should not be, then don't use associativeMerge " +
"for this one.\none: " + trees(0) + "\ntwo: " + tree)
"for this one. two results were: \none: " + trees(0) + "\ntwo: " +
tree + "\noriginal list: " + allObjects)
}
for (tree <- trees) {
@ -503,6 +504,20 @@ class ConfigTest extends TestUtils {
testIgnoredMergesDoNothing(conf)
}
@Test
def testNoMergeAcrossArray() {
val conf = parseConfig("a: {b:1}, a: [2,3], a:{c:4}")
assertFalse("a.b found in: " + conf, conf.hasPath("a.b"))
assertTrue("a.c not found in: " + conf, conf.hasPath("a.c"))
}
@Test
def testNoMergeAcrossUnresolvedArray() {
val conf = parseConfig("a: {b:1}, a: [2,${x}], a:{c:4}, x: 42")
assertFalse("a.b found in: " + conf, conf.hasPath("a.b"))
assertTrue("a.c not found in: " + conf, conf.hasPath("a.c"))
}
@Test
def integerRangeChecks() {
val conf = parseConfig("{ tooNegative: " + (Integer.MIN_VALUE - 1L) + ", tooPositive: " + (Integer.MAX_VALUE + 1L) + "}")