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 @Override
protected final AbstractConfigObject mergedWithTheUnmergeable(Unmergeable fallback) { protected AbstractConfigObject constructDelayedMerge(ConfigOrigin origin,
if (ignoresFallbacks()) List<AbstractConfigValue> stack) {
throw new ConfigException.BugOrBroken("should not be reached"); return new ConfigDelayedMergeObject(origin, stack);
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());
} }
@Override @Override

View File

@ -4,6 +4,10 @@
package com.typesafe.config.impl; package com.typesafe.config.impl;
import java.io.Serializable; 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.ConfigException;
import com.typesafe.config.ConfigMergeable; 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 // really need to store the boolean, and they may be able to pack it
// with another boolean to save space. // with another boolean to save space.
protected boolean ignoresFallbacks() { 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()) if (ignoresFallbacks())
throw new ConfigException.BugOrBroken( throw new ConfigException.BugOrBroken(
"method should not have been called with ignoresFallbacks=true" "method should not have been called with ignoresFallbacks=true "
+ getClass().getSimpleName()); + getClass().getSimpleName());
else }
throw new ConfigException.BugOrBroken("should override this in "
+ getClass().getSimpleName()); protected AbstractConfigValue constructDelayedMerge(ConfigOrigin origin,
List<AbstractConfigValue> stack) {
return new ConfigDelayedMerge(origin, stack);
}
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);
}
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) { protected AbstractConfigValue mergedWithTheUnmergeable(Unmergeable fallback) {
throw badMergeException(); requireNotIgnoringFallbacks();
return mergedWithTheUnmergeable(Collections.singletonList(this), fallback);
} }
protected AbstractConfigValue mergedWithObject(AbstractConfigObject fallback) { protected AbstractConfigValue mergedWithObject(AbstractConfigObject fallback) {
throw badMergeException(); requireNotIgnoringFallbacks();
return mergedWithObject(Collections.singletonList(this), fallback);
} }
protected AbstractConfigValue mergedWithNonObject(AbstractConfigValue fallback) { protected AbstractConfigValue mergedWithNonObject(AbstractConfigValue fallback) {
// falling back to a non-object doesn't merge anything, and also requireNotIgnoringFallbacks();
// prohibits merging any objects that we fall back to later.
// so we have to switch to ignoresFallbacks mode. return mergedWithNonObject(Collections.singletonList(this), fallback);
return newCopy(true /* ignoresFallbacks */, origin);
} }
public AbstractConfigValue withOrigin(ConfigOrigin origin) { public AbstractConfigValue withOrigin(ConfigOrigin origin) {
@ -159,6 +222,7 @@ abstract class AbstractConfigValue implements ConfigValue, MergeableValue, Seria
return newCopy(ignoresFallbacks(), origin); return newCopy(ignoresFallbacks(), origin);
} }
// this is only overridden to change the return type
@Override @Override
public AbstractConfigValue withFallback(ConfigMergeable mergeable) { public AbstractConfigValue withFallback(ConfigMergeable mergeable) {
if (ignoresFallbacks()) { if (ignoresFallbacks()) {
@ -169,15 +233,7 @@ abstract class AbstractConfigValue implements ConfigValue, MergeableValue, Seria
if (other instanceof Unmergeable) { if (other instanceof Unmergeable) {
return mergedWithTheUnmergeable((Unmergeable) other); return mergedWithTheUnmergeable((Unmergeable) other);
} else if (other instanceof AbstractConfigObject) { } else if (other instanceof AbstractConfigObject) {
AbstractConfigObject fallback = (AbstractConfigObject) other; return mergedWithObject((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 { } else {
return mergedWithNonObject((AbstractConfigValue) other); return mergedWithNonObject((AbstractConfigValue) other);
} }

View File

@ -60,43 +60,6 @@ final class ConfigConcatenation extends AbstractConfigValue implements Unmergeab
return false; 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 @Override
public Collection<ConfigConcatenation> unmergedValues() { public Collection<ConfigConcatenation> unmergedValues() {
return Collections.singleton(this); return Collections.singleton(this);

View File

@ -28,13 +28,15 @@ final class ConfigDelayedMerge extends AbstractConfigValue implements Unmergeabl
// earlier items in the stack win // earlier items in the stack win
final private List<AbstractConfigValue> stack; 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, ConfigDelayedMerge(ConfigOrigin origin, List<AbstractConfigValue> stack) {
boolean ignoresFallbacks) {
super(origin); super(origin);
this.stack = stack; this.stack = stack;
this.ignoresFallbacks = ignoresFallbacks;
if (stack.isEmpty()) if (stack.isEmpty())
throw new ConfigException.BugOrBroken( throw new ConfigException.BugOrBroken(
"creating empty delayed merge value"); "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 @Override
public ConfigValueType valueType() { public ConfigValueType valueType() {
throw new ConfigException.NotResolved( throw new ConfigException.NotResolved(
@ -80,10 +78,9 @@ final class ConfigDelayedMerge extends AbstractConfigValue implements Unmergeabl
AbstractConfigValue merged = null; AbstractConfigValue merged = null;
for (AbstractConfigValue v : stack) { for (AbstractConfigValue v : stack) {
boolean replaced = false; boolean replaced = false;
// checking for RESOLVED already is just an optimization // we only replace if we have a substitution, or
// to avoid creating the replacer when it can't possibly // value-concatenation containing one
// be needed. if (v instanceof Unmergeable) {
if (v.resolveStatus() != ResolveStatus.RESOLVED) {
// If, while resolving 'v' we come back to the same // If, while resolving 'v' we come back to the same
// merge stack, we only want to look _below_ 'v' // merge stack, we only want to look _below_ 'v'
// in the stack. So we arrange to replace the // in the stack. So we arrange to replace the
@ -158,50 +155,38 @@ final class ConfigDelayedMerge extends AbstractConfigValue implements Unmergeabl
for (AbstractConfigValue o : stack) { for (AbstractConfigValue o : stack) {
newStack.add(o.relativized(prefix)); 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 @Override
protected boolean ignoresFallbacks() { protected boolean ignoresFallbacks() {
return ignoresFallbacks; return stackIgnoresFallbacks(stack);
} }
@Override @Override
protected AbstractConfigValue newCopy(boolean newIgnoresFallbacks, ConfigOrigin newOrigin) { protected AbstractConfigValue newCopy(boolean newIgnoresFallbacks, ConfigOrigin newOrigin) {
return new ConfigDelayedMerge(newOrigin, stack, newIgnoresFallbacks); return new ConfigDelayedMerge(newOrigin, stack);
} }
@Override @Override
protected final ConfigDelayedMerge mergedWithTheUnmergeable(Unmergeable fallback) { protected final ConfigDelayedMerge mergedWithTheUnmergeable(Unmergeable fallback) {
if (ignoresFallbacks) return (ConfigDelayedMerge) mergedWithTheUnmergeable(stack, fallback);
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());
} }
@Override @Override
protected final ConfigDelayedMerge mergedWithObject(AbstractConfigObject fallback) { protected final ConfigDelayedMerge mergedWithObject(AbstractConfigObject fallback) {
return mergedWithNonObject(fallback); return (ConfigDelayedMerge) mergedWithObject(stack, fallback);
} }
@Override @Override
protected ConfigDelayedMerge mergedWithNonObject(AbstractConfigValue fallback) { protected ConfigDelayedMerge mergedWithNonObject(AbstractConfigValue fallback) {
if (ignoresFallbacks) return (ConfigDelayedMerge) mergedWithNonObject(stack, fallback);
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());
} }
@Override @Override
@ -300,6 +285,6 @@ final class ConfigDelayedMerge extends AbstractConfigValue implements Unmergeabl
private Object writeReplace() throws ObjectStreamException { private Object writeReplace() throws ObjectStreamException {
// switch to LinkedList // switch to LinkedList
return new ConfigDelayedMerge(origin(), 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 java.util.Set;
import com.typesafe.config.ConfigException; import com.typesafe.config.ConfigException;
import com.typesafe.config.ConfigList;
import com.typesafe.config.ConfigMergeable; import com.typesafe.config.ConfigMergeable;
import com.typesafe.config.ConfigOrigin; import com.typesafe.config.ConfigOrigin;
import com.typesafe.config.ConfigValue; import com.typesafe.config.ConfigValue;
@ -23,18 +24,16 @@ final class ConfigDelayedMergeObject extends AbstractConfigObject implements Unm
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
final private List<AbstractConfigValue> stack; final private List<AbstractConfigValue> stack;
final private boolean ignoresFallbacks;
ConfigDelayedMergeObject(ConfigOrigin origin, // this is just here for serialization compat; whether we ignore is purely
List<AbstractConfigValue> stack) { // a function of the bottom of the merge stack
this(origin, stack, false /* ignoresFallbacks */); @SuppressWarnings("unused")
} @Deprecated
final private boolean ignoresFallbacks = false;
ConfigDelayedMergeObject(ConfigOrigin origin, List<AbstractConfigValue> stack, ConfigDelayedMergeObject(ConfigOrigin origin, List<AbstractConfigValue> stack) {
boolean ignoresFallbacks) {
super(origin); super(origin);
this.stack = stack; this.stack = stack;
this.ignoresFallbacks = ignoresFallbacks;
if (stack.isEmpty()) if (stack.isEmpty())
throw new ConfigException.BugOrBroken( throw new ConfigException.BugOrBroken(
@ -56,7 +55,7 @@ final class ConfigDelayedMergeObject extends AbstractConfigObject implements Unm
if (status != resolveStatus()) if (status != resolveStatus())
throw new ConfigException.BugOrBroken( throw new ConfigException.BugOrBroken(
"attempt to create resolved ConfigDelayedMergeObject"); "attempt to create resolved ConfigDelayedMergeObject");
return new ConfigDelayedMergeObject(origin, stack, ignoresFallbacks); return new ConfigDelayedMergeObject(origin, stack);
} }
@Override @Override
@ -93,30 +92,31 @@ final class ConfigDelayedMergeObject extends AbstractConfigObject implements Unm
for (AbstractConfigValue o : stack) { for (AbstractConfigValue o : stack) {
newStack.add(o.relativized(prefix)); newStack.add(o.relativized(prefix));
} }
return new ConfigDelayedMergeObject(origin(), newStack, return new ConfigDelayedMergeObject(origin(), newStack);
ignoresFallbacks);
} }
@Override @Override
protected boolean ignoresFallbacks() { protected boolean ignoresFallbacks() {
return ignoresFallbacks; return ConfigDelayedMerge.stackIgnoresFallbacks(stack);
} }
@Override @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); return mergedWithNonObject(fallback);
} }
@Override @Override
protected ConfigDelayedMergeObject mergedWithNonObject(AbstractConfigValue fallback) { protected final ConfigDelayedMergeObject mergedWithNonObject(AbstractConfigValue fallback) {
if (ignoresFallbacks) requireNotIgnoringFallbacks();
throw new ConfigException.BugOrBroken("should not be reached");
List<AbstractConfigValue> newStack = new ArrayList<AbstractConfigValue>(); return (ConfigDelayedMergeObject) mergedWithNonObject(stack, fallback);
newStack.addAll(stack);
newStack.add(fallback);
return new ConfigDelayedMergeObject(AbstractConfigObject.mergeOrigins(newStack), newStack,
fallback.ignoresFallbacks());
} }
@Override @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 // to touch the exact key that isn't resolved, so this is in that
// spirit. // 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 // we'll be able to return a key if we have a value that ignores
// fallbacks, prior to any unmergeable values. // fallbacks, prior to any unmergeable values.
for (AbstractConfigValue layer : stack) { 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 '" throw new ConfigException.NotResolved("Key '" + key + "' is not available at '"
+ origin().description() + "' because value at '" + origin().description() + "' because value at '"
+ layer.origin().description() + 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."); + " 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 { } 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. // has no children so the one we're after won't be in it.
// this should always be overridden by an object though so // we would only have this in the stack in case something
// ideally we never build a stack that would have this in it. // else "looks back" to it due to a cycle.
continue; // 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 // the ConfigDelayedMergeObject should not have existed. some
// invariant was violated. // invariant was violated.
throw new ConfigException.BugOrBroken( throw new ConfigException.BugOrBroken(
@ -314,6 +326,6 @@ final class ConfigDelayedMergeObject extends AbstractConfigObject implements Unm
private Object writeReplace() throws ObjectStreamException { private Object writeReplace() throws ObjectStreamException {
// switch to LinkedList // switch to LinkedList
return new ConfigDelayedMergeObject(origin(), 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; package com.typesafe.config.impl;
import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.List;
import com.typesafe.config.ConfigException; import com.typesafe.config.ConfigException;
import com.typesafe.config.ConfigOrigin; import com.typesafe.config.ConfigOrigin;
@ -59,45 +57,6 @@ final class ConfigReference extends AbstractConfigValue implements Unmergeable {
return false; 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 @Override
public Collection<ConfigReference> unmergedValues() { public Collection<ConfigReference> unmergedValues() {
return Collections.singleton(this); return Collections.singleton(this);

View File

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

View File

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

View File

@ -1039,6 +1039,20 @@ class ConfigSubstitutionTest extends TestUtils {
assertEquals("optional self reference disappears", 0, resolved.root.size) 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 @Test
def substSelfReferenceIndirect() { def substSelfReferenceIndirect() {
val obj = parseObject("""a=1, b=${a}, a=${b}""") val obj = parseObject("""a=1, b=${a}, a=${b}""")
@ -1077,6 +1091,13 @@ class ConfigSubstitutionTest extends TestUtils {
assertEquals(5, resolved.getInt("a.b")) 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 @Test
def substSelfReferenceInConcat() { def substSelfReferenceInConcat() {
val obj = parseObject("""a=1, a=${a}foo""") val obj = parseObject("""a=1, a=${a}foo""")
@ -1174,6 +1195,15 @@ class ConfigSubstitutionTest extends TestUtils {
assertEquals(5, resolved.getInt("a.c")) 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 @Test
def substInChildFieldNotASelfReference1() { def substInChildFieldNotASelfReference1() {
// here, ${bar.foo} is not a self reference because // here, ${bar.foo} is not a self reference because
@ -1220,6 +1250,37 @@ class ConfigSubstitutionTest extends TestUtils {
assertEquals(42, resolved.getInt("bar.foo")) 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 @Test
def mutuallyReferringNotASelfReference() { def mutuallyReferringNotASelfReference() {
val obj = parseObject(""" val obj = parseObject("""
@ -1248,4 +1309,42 @@ class ConfigSubstitutionTest extends TestUtils {
val resolved = resolve(obj) val resolved = resolve(obj)
assertEquals("1xyz", resolved.getString("a")) 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)) if (!trees(0).equals(tree))
throw new AssertionError("Merge was not associative, " + throw new AssertionError("Merge was not associative, " +
"verify that it should not be, then don't use associativeMerge " + "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) { for (tree <- trees) {
@ -503,6 +504,20 @@ class ConfigTest extends TestUtils {
testIgnoredMergesDoNothing(conf) 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 @Test
def integerRangeChecks() { def integerRangeChecks() {
val conf = parseConfig("{ tooNegative: " + (Integer.MIN_VALUE - 1L) + ", tooPositive: " + (Integer.MAX_VALUE + 1L) + "}") val conf = parseConfig("{ tooNegative: " + (Integer.MIN_VALUE - 1L) + ", tooPositive: " + (Integer.MAX_VALUE + 1L) + "}")