mirror of
https://github.com/lightbend/config.git
synced 2025-01-15 23:01:05 +08:00
Track the comments preceding a setting in the ConfigOrigin for the value.
The rule is that any block of comments uninterrupted by another token or blank line, goes with the following array element or object field. The comments are shown in the output of ConfigValue.render(). They are also available in the ConfigOrigin API for custom use. Comments that don't precede an array element of object field get discarded, they are not available anywhere.
This commit is contained in:
parent
59a2d8d0df
commit
516b38f44a
@ -4,6 +4,7 @@
|
||||
package com.typesafe.config;
|
||||
|
||||
import java.net.URL;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
/**
|
||||
@ -12,13 +13,13 @@ import java.net.URL;
|
||||
* with {@link ConfigValue#origin}. Exceptions may have an origin, see
|
||||
* {@link ConfigException#origin}, but be careful because
|
||||
* <code>ConfigException.origin()</code> may return null.
|
||||
*
|
||||
*
|
||||
* <p>
|
||||
* It's best to use this interface only for debugging; its accuracy is
|
||||
* "best effort" rather than guaranteed, and a potentially-noticeable amount of
|
||||
* memory could probably be saved if origins were not kept around, so in the
|
||||
* future there might be some option to discard origins.
|
||||
*
|
||||
*
|
||||
* <p>
|
||||
* <em>Do not implement this interface</em>; it should only be implemented by
|
||||
* the config library. Arbitrary implementations will not work because the
|
||||
@ -66,4 +67,16 @@ public interface ConfigOrigin {
|
||||
* @return line number or -1 if none is available
|
||||
*/
|
||||
public int lineNumber();
|
||||
|
||||
/**
|
||||
* Returns any comments that appeared to "go with" this place in the file.
|
||||
* Often an empty list, but never null. The details of this are subject to
|
||||
* change, but at the moment comments that are immediately before an array
|
||||
* element or object field, with no blank line after the comment, "go with"
|
||||
* that element or field.
|
||||
*
|
||||
* @return any comments that seemed to "go with" this origin, empty list if
|
||||
* none
|
||||
*/
|
||||
public List<String> comments();
|
||||
}
|
||||
|
@ -111,12 +111,12 @@ abstract class AbstractConfigObject extends AbstractConfigValue implements
|
||||
return ConfigValueType.OBJECT;
|
||||
}
|
||||
|
||||
protected abstract AbstractConfigObject newCopy(ResolveStatus status,
|
||||
boolean ignoresFallbacks);
|
||||
protected abstract AbstractConfigObject newCopy(ResolveStatus status, boolean ignoresFallbacks,
|
||||
ConfigOrigin origin);
|
||||
|
||||
@Override
|
||||
protected AbstractConfigObject newCopy(boolean ignoresFallbacks) {
|
||||
return newCopy(resolveStatus(), ignoresFallbacks);
|
||||
protected AbstractConfigObject newCopy(boolean ignoresFallbacks, ConfigOrigin origin) {
|
||||
return newCopy(resolveStatus(), ignoresFallbacks, origin);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -173,7 +173,7 @@ abstract class AbstractConfigObject extends AbstractConfigValue implements
|
||||
return new SimpleConfigObject(mergeOrigins(this, fallback), merged, newResolveStatus,
|
||||
newIgnoresFallbacks);
|
||||
else if (newResolveStatus != resolveStatus() || newIgnoresFallbacks != ignoresFallbacks())
|
||||
return newCopy(newResolveStatus, newIgnoresFallbacks);
|
||||
return newCopy(newResolveStatus, newIgnoresFallbacks, origin());
|
||||
else
|
||||
return this;
|
||||
}
|
||||
@ -234,7 +234,7 @@ abstract class AbstractConfigObject extends AbstractConfigValue implements
|
||||
}
|
||||
}
|
||||
if (changes == null) {
|
||||
return newCopy(newResolveStatus, ignoresFallbacks());
|
||||
return newCopy(newResolveStatus, ignoresFallbacks(), origin());
|
||||
} else {
|
||||
Map<String, AbstractConfigValue> modified = new HashMap<String, AbstractConfigValue>();
|
||||
for (String k : keySet()) {
|
||||
@ -306,6 +306,12 @@ abstract class AbstractConfigObject extends AbstractConfigValue implements
|
||||
sb.append("# ");
|
||||
sb.append(v.origin().description());
|
||||
sb.append("\n");
|
||||
for (String comment : v.origin().comments()) {
|
||||
indent(sb, indent + 1);
|
||||
sb.append("# ");
|
||||
sb.append(comment);
|
||||
sb.append("\n");
|
||||
}
|
||||
indent(sb, indent + 1);
|
||||
}
|
||||
v.render(sb, indent + 1, k, formatted);
|
||||
|
@ -18,14 +18,14 @@ import com.typesafe.config.ConfigValue;
|
||||
*/
|
||||
abstract class AbstractConfigValue implements ConfigValue, MergeableValue {
|
||||
|
||||
final private ConfigOrigin origin;
|
||||
final private SimpleConfigOrigin origin;
|
||||
|
||||
AbstractConfigValue(ConfigOrigin origin) {
|
||||
this.origin = origin;
|
||||
this.origin = (SimpleConfigOrigin) origin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ConfigOrigin origin() {
|
||||
public SimpleConfigOrigin origin() {
|
||||
return this.origin;
|
||||
}
|
||||
|
||||
@ -76,9 +76,7 @@ abstract class AbstractConfigValue implements ConfigValue, MergeableValue {
|
||||
return this;
|
||||
}
|
||||
|
||||
protected AbstractConfigValue newCopy(boolean ignoresFallbacks) {
|
||||
return this;
|
||||
}
|
||||
protected abstract AbstractConfigValue newCopy(boolean ignoresFallbacks, ConfigOrigin origin);
|
||||
|
||||
// this is virtualized rather than a field because only some subclasses
|
||||
// really need to store the boolean, and they may be able to pack it
|
||||
@ -105,6 +103,13 @@ abstract class AbstractConfigValue implements ConfigValue, MergeableValue {
|
||||
throw badMergeException();
|
||||
}
|
||||
|
||||
public AbstractConfigValue withOrigin(ConfigOrigin origin) {
|
||||
if (this.origin == origin)
|
||||
return this;
|
||||
else
|
||||
return newCopy(ignoresFallbacks(), origin);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AbstractConfigValue withFallback(ConfigMergeable mergeable) {
|
||||
if (ignoresFallbacks()) {
|
||||
@ -118,7 +123,7 @@ abstract class AbstractConfigValue implements ConfigValue, MergeableValue {
|
||||
AbstractConfigObject fallback = (AbstractConfigObject) other;
|
||||
if (fallback.resolveStatus() == ResolveStatus.RESOLVED && fallback.isEmpty()) {
|
||||
if (fallback.ignoresFallbacks())
|
||||
return newCopy(true /* ignoresFallbacks */);
|
||||
return newCopy(true /* ignoresFallbacks */, origin);
|
||||
else
|
||||
return this;
|
||||
} else {
|
||||
@ -128,7 +133,7 @@ abstract class AbstractConfigValue implements ConfigValue, MergeableValue {
|
||||
// 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 */);
|
||||
return newCopy(true /* ignoresFallbacks */, origin);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -29,4 +29,9 @@ final class ConfigBoolean extends AbstractConfigValue {
|
||||
String transformToString() {
|
||||
return value ? "true" : "false";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ConfigBoolean newCopy(boolean ignoresFallbacks, ConfigOrigin origin) {
|
||||
return new ConfigBoolean(origin, value);
|
||||
}
|
||||
}
|
||||
|
@ -107,6 +107,11 @@ final class ConfigDelayedMerge extends AbstractConfigValue implements
|
||||
return ignoresFallbacks;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AbstractConfigValue newCopy(boolean newIgnoresFallbacks, ConfigOrigin newOrigin) {
|
||||
return new ConfigDelayedMerge(newOrigin, stack, newIgnoresFallbacks);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected final ConfigDelayedMerge mergedWithTheUnmergeable(Unmergeable fallback) {
|
||||
if (ignoresFallbacks)
|
||||
@ -196,6 +201,12 @@ final class ConfigDelayedMerge extends AbstractConfigValue implements
|
||||
i += 1;
|
||||
sb.append(v.origin().description());
|
||||
sb.append("\n");
|
||||
for (String comment : v.origin().comments()) {
|
||||
indent(sb, indent);
|
||||
sb.append("# ");
|
||||
sb.append(comment);
|
||||
sb.append("\n");
|
||||
}
|
||||
indent(sb, indent);
|
||||
}
|
||||
|
||||
|
@ -49,12 +49,12 @@ class ConfigDelayedMergeObject extends AbstractConfigObject implements
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ConfigDelayedMergeObject newCopy(ResolveStatus status,
|
||||
boolean ignoresFallbacks) {
|
||||
protected ConfigDelayedMergeObject newCopy(ResolveStatus status, boolean ignoresFallbacks,
|
||||
ConfigOrigin origin) {
|
||||
if (status != resolveStatus())
|
||||
throw new ConfigException.BugOrBroken(
|
||||
"attempt to create resolved ConfigDelayedMergeObject");
|
||||
return new ConfigDelayedMergeObject(origin(), stack, ignoresFallbacks);
|
||||
return new ConfigDelayedMergeObject(origin, stack, ignoresFallbacks);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -43,4 +43,9 @@ final class ConfigDouble extends ConfigNumber {
|
||||
protected double doubleValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ConfigDouble newCopy(boolean ignoresFallbacks, ConfigOrigin origin) {
|
||||
return new ConfigDouble(origin, value, originalText);
|
||||
}
|
||||
}
|
||||
|
@ -43,4 +43,9 @@ final class ConfigInt extends ConfigNumber {
|
||||
protected double doubleValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ConfigInt newCopy(boolean ignoresFallbacks, ConfigOrigin origin) {
|
||||
return new ConfigInt(origin, value, originalText);
|
||||
}
|
||||
}
|
||||
|
@ -43,4 +43,9 @@ final class ConfigLong extends ConfigNumber {
|
||||
protected double doubleValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ConfigLong newCopy(boolean ignoresFallbacks, ConfigOrigin origin) {
|
||||
return new ConfigLong(origin, value, originalText);
|
||||
}
|
||||
}
|
||||
|
@ -39,4 +39,9 @@ final class ConfigNull extends AbstractConfigValue {
|
||||
protected void render(StringBuilder sb, int indent, boolean formatted) {
|
||||
sb.append("null");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ConfigNull newCopy(boolean ignoresFallbacks, ConfigOrigin origin) {
|
||||
return new ConfigNull(origin);
|
||||
}
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ abstract class ConfigNumber extends AbstractConfigValue {
|
||||
// a sentence) we always have it exactly as the person typed it into the
|
||||
// config file. It's purely cosmetic; equals/hashCode don't consider this
|
||||
// for example.
|
||||
final private String originalText;
|
||||
final protected String originalText;
|
||||
|
||||
protected ConfigNumber(ConfigOrigin origin, String originalText) {
|
||||
super(origin);
|
||||
|
@ -34,4 +34,9 @@ final class ConfigString extends AbstractConfigValue {
|
||||
protected void render(StringBuilder sb, int indent, boolean formatted) {
|
||||
sb.append(ConfigImplUtil.renderJsonString(value));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ConfigString newCopy(boolean ignoresFallbacks, ConfigOrigin origin) {
|
||||
return new ConfigString(origin, value);
|
||||
}
|
||||
}
|
||||
|
@ -61,8 +61,8 @@ final class ConfigSubstitution extends AbstractConfigValue implements
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ConfigSubstitution newCopy(boolean ignoresFallbacks) {
|
||||
return new ConfigSubstitution(origin(), pieces, prefixLength, ignoresFallbacks);
|
||||
protected ConfigSubstitution newCopy(boolean ignoresFallbacks, ConfigOrigin newOrigin) {
|
||||
return new ConfigSubstitution(newOrigin, pieces, prefixLength, ignoresFallbacks);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -32,9 +32,53 @@ final class Parser {
|
||||
return context.parse();
|
||||
}
|
||||
|
||||
static private final class TokenWithComments {
|
||||
final Token token;
|
||||
final List<Token> comments;
|
||||
|
||||
TokenWithComments(Token token, List<Token> comments) {
|
||||
this.token = token;
|
||||
this.comments = comments;
|
||||
}
|
||||
|
||||
TokenWithComments(Token token) {
|
||||
this(token, Collections.<Token> emptyList());
|
||||
}
|
||||
|
||||
TokenWithComments prepend(List<Token> earlier) {
|
||||
if (this.comments.isEmpty()) {
|
||||
return new TokenWithComments(token, earlier);
|
||||
} else {
|
||||
List<Token> merged = new ArrayList<Token>();
|
||||
merged.addAll(earlier);
|
||||
merged.addAll(comments);
|
||||
return new TokenWithComments(token, merged);
|
||||
}
|
||||
}
|
||||
|
||||
SimpleConfigOrigin setComments(SimpleConfigOrigin origin) {
|
||||
if (comments.isEmpty()) {
|
||||
return origin;
|
||||
} else {
|
||||
List<String> newComments = new ArrayList<String>();
|
||||
for (Token c : comments) {
|
||||
newComments.add(Tokens.getCommentText(c));
|
||||
}
|
||||
return origin.setComments(newComments);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
// this ends up in user-visible error messages, so we don't want the
|
||||
// comments
|
||||
return token.toString();
|
||||
}
|
||||
}
|
||||
|
||||
static private final class ParseContext {
|
||||
private int lineNumber;
|
||||
final private Stack<Token> buffer;
|
||||
final private Stack<TokenWithComments> buffer;
|
||||
final private Iterator<Token> tokens;
|
||||
final private ConfigIncluder includer;
|
||||
final private ConfigIncludeContext includeContext;
|
||||
@ -50,7 +94,7 @@ final class Parser {
|
||||
Iterator<Token> tokens, ConfigIncluder includer,
|
||||
ConfigIncludeContext includeContext) {
|
||||
lineNumber = 1;
|
||||
buffer = new Stack<Token>();
|
||||
buffer = new Stack<TokenWithComments>();
|
||||
this.tokens = tokens;
|
||||
this.flavor = flavor;
|
||||
this.baseOrigin = origin;
|
||||
@ -60,14 +104,67 @@ final class Parser {
|
||||
this.equalsCount = 0;
|
||||
}
|
||||
|
||||
private Token nextToken() {
|
||||
Token t = null;
|
||||
if (buffer.isEmpty()) {
|
||||
t = tokens.next();
|
||||
} else {
|
||||
t = buffer.pop();
|
||||
private void consolidateCommentBlock(Token commentToken) {
|
||||
// a comment block "goes with" the following token
|
||||
// unless it's separated from it by a blank line.
|
||||
// we want to build a list of newline tokens followed
|
||||
// by a non-newline non-comment token; with all comments
|
||||
// associated with that final non-newline non-comment token.
|
||||
List<Token> newlines = new ArrayList<Token>();
|
||||
List<Token> comments = new ArrayList<Token>();
|
||||
|
||||
Token previous = null;
|
||||
Token next = commentToken;
|
||||
while (true) {
|
||||
if (Tokens.isNewline(next)) {
|
||||
if (previous != null && Tokens.isNewline(previous)) {
|
||||
// blank line; drop all comments to this point and
|
||||
// start a new comment block
|
||||
comments.clear();
|
||||
}
|
||||
newlines.add(next);
|
||||
} else if (Tokens.isComment(next)) {
|
||||
comments.add(next);
|
||||
} else {
|
||||
// a non-newline non-comment token
|
||||
break;
|
||||
}
|
||||
|
||||
previous = next;
|
||||
next = tokens.next();
|
||||
}
|
||||
|
||||
// put our concluding token in the queue with all the comments
|
||||
// attached
|
||||
buffer.push(new TokenWithComments(next, comments));
|
||||
|
||||
// now put all the newlines back in front of it
|
||||
ListIterator<Token> li = newlines.listIterator(newlines.size());
|
||||
while (li.hasPrevious()) {
|
||||
buffer.push(new TokenWithComments(li.previous()));
|
||||
}
|
||||
}
|
||||
|
||||
private TokenWithComments popToken() {
|
||||
if (buffer.isEmpty()) {
|
||||
Token t = tokens.next();
|
||||
if (Tokens.isComment(t)) {
|
||||
consolidateCommentBlock(t);
|
||||
return buffer.pop();
|
||||
} else {
|
||||
return new TokenWithComments(t);
|
||||
}
|
||||
} else {
|
||||
return buffer.pop();
|
||||
}
|
||||
}
|
||||
|
||||
private TokenWithComments nextToken() {
|
||||
TokenWithComments withComments = null;
|
||||
|
||||
withComments = popToken();
|
||||
Token t = withComments.token;
|
||||
|
||||
if (Tokens.isProblem(t)) {
|
||||
ConfigOrigin origin = t.origin();
|
||||
String message = Tokens.getProblemMessage(t);
|
||||
@ -79,32 +176,35 @@ final class Parser {
|
||||
message = addKeyName(message);
|
||||
}
|
||||
throw new ConfigException.Parse(origin, message, cause);
|
||||
}
|
||||
|
||||
if (flavor == ConfigSyntax.JSON) {
|
||||
if (Tokens.isUnquotedText(t)) {
|
||||
throw parseError(addKeyName("Token not allowed in valid JSON: '"
|
||||
+ Tokens.getUnquotedText(t) + "'"));
|
||||
} else if (Tokens.isSubstitution(t)) {
|
||||
throw parseError(addKeyName("Substitutions (${} syntax) not allowed in JSON"));
|
||||
} else {
|
||||
if (flavor == ConfigSyntax.JSON) {
|
||||
if (Tokens.isUnquotedText(t)) {
|
||||
throw parseError(addKeyName("Token not allowed in valid JSON: '"
|
||||
+ Tokens.getUnquotedText(t) + "'"));
|
||||
} else if (Tokens.isSubstitution(t)) {
|
||||
throw parseError(addKeyName("Substitutions (${} syntax) not allowed in JSON"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return t;
|
||||
return withComments;
|
||||
}
|
||||
}
|
||||
|
||||
private void putBack(Token token) {
|
||||
private void putBack(TokenWithComments token) {
|
||||
buffer.push(token);
|
||||
}
|
||||
|
||||
private Token nextTokenIgnoringNewline() {
|
||||
Token t = nextToken();
|
||||
while (Tokens.isNewline(t)) {
|
||||
private TokenWithComments nextTokenIgnoringNewline() {
|
||||
TokenWithComments t = nextToken();
|
||||
|
||||
while (Tokens.isNewline(t.token)) {
|
||||
// line number tokens have the line that was _ended_ by the
|
||||
// newline, so we have to add one.
|
||||
lineNumber = t.lineNumber() + 1;
|
||||
lineNumber = t.token.lineNumber() + 1;
|
||||
|
||||
t = nextToken();
|
||||
}
|
||||
|
||||
return t;
|
||||
}
|
||||
|
||||
@ -116,8 +216,8 @@ final class Parser {
|
||||
// is left just after the comma or the newline.
|
||||
private boolean checkElementSeparator() {
|
||||
if (flavor == ConfigSyntax.JSON) {
|
||||
Token t = nextTokenIgnoringNewline();
|
||||
if (t == Tokens.COMMA) {
|
||||
TokenWithComments t = nextTokenIgnoringNewline();
|
||||
if (t.token == Tokens.COMMA) {
|
||||
return true;
|
||||
} else {
|
||||
putBack(t);
|
||||
@ -125,15 +225,16 @@ final class Parser {
|
||||
}
|
||||
} else {
|
||||
boolean sawSeparatorOrNewline = false;
|
||||
Token t = nextToken();
|
||||
TokenWithComments t = nextToken();
|
||||
while (true) {
|
||||
if (Tokens.isNewline(t)) {
|
||||
if (Tokens.isNewline(t.token)) {
|
||||
// newline number is the line just ended, so add one
|
||||
lineNumber = t.lineNumber() + 1;
|
||||
lineNumber = t.token.lineNumber() + 1;
|
||||
sawSeparatorOrNewline = true;
|
||||
|
||||
// we want to continue to also eat
|
||||
// a comma if there is one.
|
||||
} else if (t == Tokens.COMMA) {
|
||||
} else if (t.token == Tokens.COMMA) {
|
||||
return true;
|
||||
} else {
|
||||
// non-newline-or-comma
|
||||
@ -154,12 +255,17 @@ final class Parser {
|
||||
return;
|
||||
|
||||
List<Token> values = null; // create only if we have value tokens
|
||||
Token t = nextTokenIgnoringNewline(); // ignore a newline up front
|
||||
while (Tokens.isValue(t) || Tokens.isUnquotedText(t)
|
||||
|| Tokens.isSubstitution(t)) {
|
||||
if (values == null)
|
||||
TokenWithComments firstValueWithComments = null;
|
||||
TokenWithComments t = nextTokenIgnoringNewline(); // ignore a
|
||||
// newline up
|
||||
// front
|
||||
while (Tokens.isValue(t.token) || Tokens.isUnquotedText(t.token)
|
||||
|| Tokens.isSubstitution(t.token)) {
|
||||
if (values == null) {
|
||||
values = new ArrayList<Token>();
|
||||
values.add(t);
|
||||
firstValueWithComments = t;
|
||||
}
|
||||
values.add(t.token);
|
||||
t = nextToken(); // but don't consolidate across a newline
|
||||
}
|
||||
// the last one wasn't a value token
|
||||
@ -168,9 +274,9 @@ final class Parser {
|
||||
if (values == null)
|
||||
return;
|
||||
|
||||
if (values.size() == 1 && Tokens.isValue(values.get(0))) {
|
||||
if (values.size() == 1 && Tokens.isValue(firstValueWithComments.token)) {
|
||||
// a single value token requires no consolidation
|
||||
putBack(values.get(0));
|
||||
putBack(firstValueWithComments);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -235,7 +341,7 @@ final class Parser {
|
||||
firstOrigin, minimized));
|
||||
}
|
||||
|
||||
putBack(consolidated);
|
||||
putBack(new TokenWithComments(consolidated, firstValueWithComments.comments));
|
||||
}
|
||||
|
||||
private ConfigOrigin lineOrigin() {
|
||||
@ -309,17 +415,23 @@ final class Parser {
|
||||
return part + ")";
|
||||
}
|
||||
|
||||
private AbstractConfigValue parseValue(Token token) {
|
||||
if (Tokens.isValue(token)) {
|
||||
return Tokens.getValue(token);
|
||||
} else if (token == Tokens.OPEN_CURLY) {
|
||||
return parseObject(true);
|
||||
} else if (token == Tokens.OPEN_SQUARE) {
|
||||
return parseArray();
|
||||
private AbstractConfigValue parseValue(TokenWithComments t) {
|
||||
AbstractConfigValue v;
|
||||
|
||||
if (Tokens.isValue(t.token)) {
|
||||
v = Tokens.getValue(t.token);
|
||||
} else if (t.token == Tokens.OPEN_CURLY) {
|
||||
v = parseObject(true);
|
||||
} else if (t.token == Tokens.OPEN_SQUARE) {
|
||||
v = parseArray();
|
||||
} else {
|
||||
throw parseError(addQuoteSuggestion(token.toString(),
|
||||
"Expecting a value but got wrong token: " + token));
|
||||
throw parseError(addQuoteSuggestion(t.token.toString(),
|
||||
"Expecting a value but got wrong token: " + t.token));
|
||||
}
|
||||
|
||||
v = v.withOrigin(t.setComments(v.origin()));
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
private static AbstractConfigObject createValueUnderPath(Path path,
|
||||
@ -339,24 +451,29 @@ final class Parser {
|
||||
remaining = remaining.remainder();
|
||||
}
|
||||
}
|
||||
|
||||
// the setComments(null) is to ensure comments are only
|
||||
// on the exact leaf node they apply to.
|
||||
// a comment before "foo.bar" applies to the full setting
|
||||
// "foo.bar" not also to "foo"
|
||||
ListIterator<String> i = keys.listIterator(keys.size());
|
||||
String deepest = i.previous();
|
||||
AbstractConfigObject o = new SimpleConfigObject(value.origin(),
|
||||
AbstractConfigObject o = new SimpleConfigObject(value.origin().setComments(null),
|
||||
Collections.<String, AbstractConfigValue> singletonMap(
|
||||
deepest, value));
|
||||
while (i.hasPrevious()) {
|
||||
Map<String, AbstractConfigValue> m = Collections.<String, AbstractConfigValue> singletonMap(
|
||||
i.previous(), o);
|
||||
o = new SimpleConfigObject(value.origin(), m);
|
||||
o = new SimpleConfigObject(value.origin().setComments(null), m);
|
||||
}
|
||||
|
||||
return o;
|
||||
}
|
||||
|
||||
private Path parseKey(Token token) {
|
||||
private Path parseKey(TokenWithComments token) {
|
||||
if (flavor == ConfigSyntax.JSON) {
|
||||
if (Tokens.isValueWithType(token, ConfigValueType.STRING)) {
|
||||
String key = (String) Tokens.getValue(token).unwrapped();
|
||||
if (Tokens.isValueWithType(token.token, ConfigValueType.STRING)) {
|
||||
String key = (String) Tokens.getValue(token.token).unwrapped();
|
||||
return Path.newKey(key);
|
||||
} else {
|
||||
throw parseError(addKeyName("Expecting close brace } or a field name here, got "
|
||||
@ -364,9 +481,9 @@ final class Parser {
|
||||
}
|
||||
} else {
|
||||
List<Token> expression = new ArrayList<Token>();
|
||||
Token t = token;
|
||||
while (Tokens.isValue(t) || Tokens.isUnquotedText(t)) {
|
||||
expression.add(t);
|
||||
TokenWithComments t = token;
|
||||
while (Tokens.isValue(t.token) || Tokens.isUnquotedText(t.token)) {
|
||||
expression.add(t.token);
|
||||
t = nextToken(); // note: don't cross a newline
|
||||
}
|
||||
|
||||
@ -400,13 +517,13 @@ final class Parser {
|
||||
}
|
||||
|
||||
private void parseInclude(Map<String, AbstractConfigValue> values) {
|
||||
Token t = nextTokenIgnoringNewline();
|
||||
while (isUnquotedWhitespace(t)) {
|
||||
TokenWithComments t = nextTokenIgnoringNewline();
|
||||
while (isUnquotedWhitespace(t.token)) {
|
||||
t = nextTokenIgnoringNewline();
|
||||
}
|
||||
|
||||
if (Tokens.isValueWithType(t, ConfigValueType.STRING)) {
|
||||
String name = (String) Tokens.getValue(t).unwrapped();
|
||||
if (Tokens.isValueWithType(t.token, ConfigValueType.STRING)) {
|
||||
String name = (String) Tokens.getValue(t.token).unwrapped();
|
||||
AbstractConfigObject obj = (AbstractConfigObject) includer
|
||||
.include(includeContext, name);
|
||||
|
||||
@ -448,8 +565,8 @@ final class Parser {
|
||||
boolean lastInsideEquals = false;
|
||||
|
||||
while (true) {
|
||||
Token t = nextTokenIgnoringNewline();
|
||||
if (t == Tokens.CLOSE_CURLY) {
|
||||
TokenWithComments t = nextTokenIgnoringNewline();
|
||||
if (t.token == Tokens.CLOSE_CURLY) {
|
||||
if (flavor == ConfigSyntax.JSON && afterComma) {
|
||||
throw parseError(addQuoteSuggestion(t.toString(),
|
||||
"expecting a field name after a comma, got a close brace } instead"));
|
||||
@ -458,45 +575,45 @@ final class Parser {
|
||||
"unbalanced close brace '}' with no open brace"));
|
||||
}
|
||||
break;
|
||||
} else if (t == Tokens.END && !hadOpenCurly) {
|
||||
} else if (t.token == Tokens.END && !hadOpenCurly) {
|
||||
putBack(t);
|
||||
break;
|
||||
} else if (flavor != ConfigSyntax.JSON && isIncludeKeyword(t)) {
|
||||
} else if (flavor != ConfigSyntax.JSON && isIncludeKeyword(t.token)) {
|
||||
parseInclude(values);
|
||||
|
||||
afterComma = false;
|
||||
} else {
|
||||
Path path = parseKey(t);
|
||||
Token afterKey = nextTokenIgnoringNewline();
|
||||
TokenWithComments keyToken = t;
|
||||
Path path = parseKey(keyToken);
|
||||
TokenWithComments afterKey = nextTokenIgnoringNewline();
|
||||
boolean insideEquals = false;
|
||||
|
||||
// path must be on-stack while we parse the value
|
||||
pathStack.push(path);
|
||||
|
||||
Token valueToken;
|
||||
TokenWithComments valueToken;
|
||||
AbstractConfigValue newValue;
|
||||
if (flavor == ConfigSyntax.CONF
|
||||
&& afterKey == Tokens.OPEN_CURLY) {
|
||||
if (flavor == ConfigSyntax.CONF && afterKey.token == Tokens.OPEN_CURLY) {
|
||||
// can omit the ':' or '=' before an object value
|
||||
valueToken = afterKey;
|
||||
newValue = parseObject(true);
|
||||
} else {
|
||||
if (!isKeyValueSeparatorToken(afterKey)) {
|
||||
if (!isKeyValueSeparatorToken(afterKey.token)) {
|
||||
throw parseError(addQuoteSuggestion(afterKey.toString(),
|
||||
"Key '" + path.render() + "' may not be followed by token: "
|
||||
+ afterKey));
|
||||
}
|
||||
|
||||
if (afterKey == Tokens.EQUALS) {
|
||||
if (afterKey.token == Tokens.EQUALS) {
|
||||
insideEquals = true;
|
||||
equalsCount += 1;
|
||||
}
|
||||
|
||||
consolidateValueTokens();
|
||||
valueToken = nextTokenIgnoringNewline();
|
||||
newValue = parseValue(valueToken);
|
||||
}
|
||||
|
||||
newValue = parseValue(valueToken.prepend(keyToken.comments));
|
||||
|
||||
lastPath = pathStack.pop();
|
||||
if (insideEquals) {
|
||||
equalsCount -= 1;
|
||||
@ -547,7 +664,7 @@ final class Parser {
|
||||
afterComma = true;
|
||||
} else {
|
||||
t = nextTokenIgnoringNewline();
|
||||
if (t == Tokens.CLOSE_CURLY) {
|
||||
if (t.token == Tokens.CLOSE_CURLY) {
|
||||
if (!hadOpenCurly) {
|
||||
throw parseError(addQuoteSuggestion(lastPath, lastInsideEquals,
|
||||
t.toString(), "unbalanced close brace '}' with no open brace"));
|
||||
@ -557,7 +674,7 @@ final class Parser {
|
||||
throw parseError(addQuoteSuggestion(lastPath, lastInsideEquals,
|
||||
t.toString(), "Expecting close brace } or a comma, got " + t));
|
||||
} else {
|
||||
if (t == Tokens.END) {
|
||||
if (t.token == Tokens.END) {
|
||||
putBack(t);
|
||||
break;
|
||||
} else {
|
||||
@ -567,6 +684,7 @@ final class Parser {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new SimpleConfigObject(objectOrigin, values);
|
||||
}
|
||||
|
||||
@ -577,18 +695,15 @@ final class Parser {
|
||||
|
||||
consolidateValueTokens();
|
||||
|
||||
Token t = nextTokenIgnoringNewline();
|
||||
TokenWithComments t = nextTokenIgnoringNewline();
|
||||
|
||||
// special-case the first element
|
||||
if (t == Tokens.CLOSE_SQUARE) {
|
||||
if (t.token == Tokens.CLOSE_SQUARE) {
|
||||
return new SimpleConfigList(arrayOrigin,
|
||||
Collections.<AbstractConfigValue> emptyList());
|
||||
} else if (Tokens.isValue(t)) {
|
||||
} else if (Tokens.isValue(t.token) || t.token == Tokens.OPEN_CURLY
|
||||
|| t.token == Tokens.OPEN_SQUARE) {
|
||||
values.add(parseValue(t));
|
||||
} else if (t == Tokens.OPEN_CURLY) {
|
||||
values.add(parseObject(true));
|
||||
} else if (t == Tokens.OPEN_SQUARE) {
|
||||
values.add(parseArray());
|
||||
} else {
|
||||
throw parseError(addKeyName("List should have ] or a first element after the open [, instead had token: "
|
||||
+ t
|
||||
@ -604,7 +719,7 @@ final class Parser {
|
||||
// comma (or newline equivalent) consumed
|
||||
} else {
|
||||
t = nextTokenIgnoringNewline();
|
||||
if (t == Tokens.CLOSE_SQUARE) {
|
||||
if (t.token == Tokens.CLOSE_SQUARE) {
|
||||
return new SimpleConfigList(arrayOrigin, values);
|
||||
} else {
|
||||
throw parseError(addKeyName("List should have ended with ] or had a comma, instead had token: "
|
||||
@ -619,14 +734,10 @@ final class Parser {
|
||||
consolidateValueTokens();
|
||||
|
||||
t = nextTokenIgnoringNewline();
|
||||
if (Tokens.isValue(t)) {
|
||||
if (Tokens.isValue(t.token) || t.token == Tokens.OPEN_CURLY
|
||||
|| t.token == Tokens.OPEN_SQUARE) {
|
||||
values.add(parseValue(t));
|
||||
} else if (t == Tokens.OPEN_CURLY) {
|
||||
values.add(parseObject(true));
|
||||
} else if (t == Tokens.OPEN_SQUARE) {
|
||||
values.add(parseArray());
|
||||
} else if (flavor != ConfigSyntax.JSON
|
||||
&& t == Tokens.CLOSE_SQUARE) {
|
||||
} else if (flavor != ConfigSyntax.JSON && t.token == Tokens.CLOSE_SQUARE) {
|
||||
// we allow one trailing comma
|
||||
putBack(t);
|
||||
} else {
|
||||
@ -640,8 +751,8 @@ final class Parser {
|
||||
}
|
||||
|
||||
AbstractConfigValue parse() {
|
||||
Token t = nextTokenIgnoringNewline();
|
||||
if (t == Tokens.START) {
|
||||
TokenWithComments t = nextTokenIgnoringNewline();
|
||||
if (t.token == Tokens.START) {
|
||||
// OK
|
||||
} else {
|
||||
throw new ConfigException.BugOrBroken(
|
||||
@ -650,13 +761,11 @@ final class Parser {
|
||||
|
||||
t = nextTokenIgnoringNewline();
|
||||
AbstractConfigValue result = null;
|
||||
if (t == Tokens.OPEN_CURLY) {
|
||||
result = parseObject(true);
|
||||
} else if (t == Tokens.OPEN_SQUARE) {
|
||||
result = parseArray();
|
||||
if (t.token == Tokens.OPEN_CURLY || t.token == Tokens.OPEN_SQUARE) {
|
||||
result = parseValue(t);
|
||||
} else {
|
||||
if (flavor == ConfigSyntax.JSON) {
|
||||
if (t == Tokens.END) {
|
||||
if (t.token == Tokens.END) {
|
||||
throw parseError("Empty document");
|
||||
} else {
|
||||
throw parseError("Document must have an object or array at root, unexpected token: "
|
||||
@ -668,11 +777,14 @@ final class Parser {
|
||||
// of it, so put it back.
|
||||
putBack(t);
|
||||
result = parseObject(false);
|
||||
// in this case we don't try to use commentsStack comments
|
||||
// since they would all presumably apply to fields not the
|
||||
// root object
|
||||
}
|
||||
}
|
||||
|
||||
t = nextTokenIgnoringNewline();
|
||||
if (t == Tokens.END) {
|
||||
if (t.token == Tokens.END) {
|
||||
return result;
|
||||
} else {
|
||||
throw parseError("Document has trailing tokens after first object or array: "
|
||||
|
@ -145,6 +145,14 @@ final class SimpleConfigList extends AbstractConfigValue implements ConfigList {
|
||||
sb.append("# ");
|
||||
sb.append(v.origin().description());
|
||||
sb.append("\n");
|
||||
|
||||
for (String comment : v.origin().comments()) {
|
||||
indent(sb, indent + 1);
|
||||
sb.append("# ");
|
||||
sb.append(comment);
|
||||
sb.append("\n");
|
||||
}
|
||||
|
||||
indent(sb, indent + 1);
|
||||
}
|
||||
v.render(sb, indent + 1, formatted);
|
||||
@ -353,4 +361,9 @@ final class SimpleConfigList extends AbstractConfigValue implements ConfigList {
|
||||
public ConfigValue set(int index, ConfigValue element) {
|
||||
throw weAreImmutable("set");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SimpleConfigList newCopy(boolean ignoresFallbacks, ConfigOrigin newOrigin) {
|
||||
return new SimpleConfigList(newOrigin, value);
|
||||
}
|
||||
}
|
||||
|
@ -45,8 +45,9 @@ final class SimpleConfigObject extends AbstractConfigObject {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SimpleConfigObject newCopy(ResolveStatus newStatus, boolean newIgnoresFallbacks) {
|
||||
return new SimpleConfigObject(origin(), value, newStatus, newIgnoresFallbacks);
|
||||
protected SimpleConfigObject newCopy(ResolveStatus newStatus, boolean newIgnoresFallbacks,
|
||||
ConfigOrigin newOrigin) {
|
||||
return new SimpleConfigObject(newOrigin, value, newStatus, newIgnoresFallbacks);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -8,6 +8,7 @@ import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
@ -22,19 +23,21 @@ final class SimpleConfigOrigin implements ConfigOrigin {
|
||||
final private int endLineNumber;
|
||||
final private OriginType originType;
|
||||
final private String urlOrNull;
|
||||
final private List<String> commentsOrNull;
|
||||
|
||||
protected SimpleConfigOrigin(String description, int lineNumber, int endLineNumber,
|
||||
OriginType originType,
|
||||
String urlOrNull) {
|
||||
String urlOrNull, List<String> commentsOrNull) {
|
||||
this.description = description;
|
||||
this.lineNumber = lineNumber;
|
||||
this.endLineNumber = endLineNumber;
|
||||
this.originType = originType;
|
||||
this.urlOrNull = urlOrNull;
|
||||
this.commentsOrNull = commentsOrNull;
|
||||
}
|
||||
|
||||
static SimpleConfigOrigin newSimple(String description) {
|
||||
return new SimpleConfigOrigin(description, -1, -1, OriginType.GENERIC, null);
|
||||
return new SimpleConfigOrigin(description, -1, -1, OriginType.GENERIC, null, null);
|
||||
}
|
||||
|
||||
static SimpleConfigOrigin newFile(String filename) {
|
||||
@ -44,17 +47,17 @@ final class SimpleConfigOrigin implements ConfigOrigin {
|
||||
} catch (MalformedURLException e) {
|
||||
url = null;
|
||||
}
|
||||
return new SimpleConfigOrigin(filename, -1, -1, OriginType.FILE, url);
|
||||
return new SimpleConfigOrigin(filename, -1, -1, OriginType.FILE, url, null);
|
||||
}
|
||||
|
||||
static SimpleConfigOrigin newURL(URL url) {
|
||||
String u = url.toExternalForm();
|
||||
return new SimpleConfigOrigin(u, -1, -1, OriginType.URL, u);
|
||||
return new SimpleConfigOrigin(u, -1, -1, OriginType.URL, u, null);
|
||||
}
|
||||
|
||||
static SimpleConfigOrigin newResource(String resource, URL url) {
|
||||
return new SimpleConfigOrigin(resource, -1, -1, OriginType.RESOURCE,
|
||||
url != null ? url.toExternalForm() : null);
|
||||
url != null ? url.toExternalForm() : null, null);
|
||||
}
|
||||
|
||||
static SimpleConfigOrigin newResource(String resource) {
|
||||
@ -66,13 +69,22 @@ final class SimpleConfigOrigin implements ConfigOrigin {
|
||||
return this;
|
||||
} else {
|
||||
return new SimpleConfigOrigin(this.description, lineNumber, lineNumber,
|
||||
this.originType, this.urlOrNull);
|
||||
this.originType, this.urlOrNull, this.commentsOrNull);
|
||||
}
|
||||
}
|
||||
|
||||
SimpleConfigOrigin addURL(URL url) {
|
||||
return new SimpleConfigOrigin(this.description, this.lineNumber, this.endLineNumber, this.originType,
|
||||
url != null ? url.toExternalForm() : null);
|
||||
return new SimpleConfigOrigin(this.description, this.lineNumber, this.endLineNumber,
|
||||
this.originType, url != null ? url.toExternalForm() : null, this.commentsOrNull);
|
||||
}
|
||||
|
||||
SimpleConfigOrigin setComments(List<String> comments) {
|
||||
if (ConfigImplUtil.equalsHandlingNull(comments, this.commentsOrNull)) {
|
||||
return this;
|
||||
} else {
|
||||
return new SimpleConfigOrigin(this.description, this.lineNumber, this.endLineNumber,
|
||||
this.originType, this.urlOrNull, comments);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -172,12 +184,22 @@ final class SimpleConfigOrigin implements ConfigOrigin {
|
||||
return lineNumber;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> comments() {
|
||||
if (commentsOrNull != null) {
|
||||
return commentsOrNull;
|
||||
} else {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
static final String MERGE_OF_PREFIX = "merge of ";
|
||||
|
||||
private static SimpleConfigOrigin mergeTwo(SimpleConfigOrigin a, SimpleConfigOrigin b) {
|
||||
String mergedDesc;
|
||||
int mergedStartLine;
|
||||
int mergedEndLine;
|
||||
List<String> mergedComments;
|
||||
|
||||
OriginType mergedType;
|
||||
if (a.originType == b.originType) {
|
||||
@ -233,8 +255,18 @@ final class SimpleConfigOrigin implements ConfigOrigin {
|
||||
mergedURL = null;
|
||||
}
|
||||
|
||||
if (ConfigImplUtil.equalsHandlingNull(a.commentsOrNull, b.commentsOrNull)) {
|
||||
mergedComments = a.commentsOrNull;
|
||||
} else {
|
||||
mergedComments = new ArrayList<String>();
|
||||
if (a.commentsOrNull != null)
|
||||
mergedComments.addAll(a.commentsOrNull);
|
||||
if (b.commentsOrNull != null)
|
||||
mergedComments.addAll(b.commentsOrNull);
|
||||
}
|
||||
|
||||
return new SimpleConfigOrigin(mergedDesc, mergedStartLine, mergedEndLine, mergedType,
|
||||
mergedURL);
|
||||
mergedURL, mergedComments);
|
||||
}
|
||||
|
||||
private static int similarity(SimpleConfigOrigin a, SimpleConfigOrigin b) {
|
||||
|
@ -17,5 +17,6 @@ enum TokenType {
|
||||
NEWLINE,
|
||||
UNQUOTED_TEXT,
|
||||
SUBSTITUTION,
|
||||
PROBLEM;
|
||||
PROBLEM,
|
||||
COMMENT;
|
||||
}
|
||||
|
@ -168,40 +168,27 @@ final class Tokenizer {
|
||||
return c != '\n' && ConfigImplUtil.isWhitespace(c);
|
||||
}
|
||||
|
||||
private int slurpComment() {
|
||||
for (;;) {
|
||||
int c = nextCharRaw();
|
||||
if (c == -1 || c == '\n') {
|
||||
return c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// get next char, skipping comments
|
||||
private int nextCharSkippingComments() {
|
||||
for (;;) {
|
||||
int c = nextCharRaw();
|
||||
|
||||
if (c == -1) {
|
||||
return -1;
|
||||
} else {
|
||||
if (allowComments) {
|
||||
if (c == '#') {
|
||||
return slurpComment();
|
||||
} else if (c == '/') {
|
||||
int maybeSecondSlash = nextCharRaw();
|
||||
if (maybeSecondSlash == '/') {
|
||||
return slurpComment();
|
||||
} else {
|
||||
putBack(maybeSecondSlash);
|
||||
return c;
|
||||
}
|
||||
private boolean startOfComment(int c) {
|
||||
if (c == -1) {
|
||||
return false;
|
||||
} else {
|
||||
if (allowComments) {
|
||||
if (c == '#') {
|
||||
return true;
|
||||
} else if (c == '/') {
|
||||
int maybeSecondSlash = nextCharRaw();
|
||||
// we want to predictably NOT consume any chars
|
||||
putBack(maybeSecondSlash);
|
||||
if (maybeSecondSlash == '/') {
|
||||
return true;
|
||||
} else {
|
||||
return c;
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return c;
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -209,7 +196,7 @@ final class Tokenizer {
|
||||
// get next char, skipping non-newline whitespace
|
||||
private int nextCharAfterWhitespace(WhitespaceSaver saver) {
|
||||
for (;;) {
|
||||
int c = nextCharSkippingComments();
|
||||
int c = nextCharRaw();
|
||||
|
||||
if (c == -1) {
|
||||
return -1;
|
||||
@ -269,6 +256,27 @@ final class Tokenizer {
|
||||
return ((SimpleConfigOrigin) baseOrigin).setLineNumber(lineNumber);
|
||||
}
|
||||
|
||||
// ONE char has always been consumed, either the # or the first /, but
|
||||
// not both slashes
|
||||
private Token pullComment(int firstChar) {
|
||||
if (firstChar == '/') {
|
||||
int discard = nextCharRaw();
|
||||
if (discard != '/')
|
||||
throw new ConfigException.BugOrBroken("called pullComment but // not seen");
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (;;) {
|
||||
int c = nextCharRaw();
|
||||
if (c == -1 || c == '\n') {
|
||||
putBack(c);
|
||||
return Tokens.newComment(lineOrigin, sb.toString());
|
||||
} else {
|
||||
sb.appendCodePoint(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// chars JSON allows a number to start with
|
||||
static final String firstNumberChars = "0123456789-";
|
||||
// chars JSON allows to be part of a number
|
||||
@ -283,7 +291,7 @@ final class Tokenizer {
|
||||
private Token pullUnquotedText() {
|
||||
ConfigOrigin origin = lineOrigin;
|
||||
StringBuilder sb = new StringBuilder();
|
||||
int c = nextCharSkippingComments();
|
||||
int c = nextCharRaw();
|
||||
while (true) {
|
||||
if (c == -1) {
|
||||
break;
|
||||
@ -291,6 +299,8 @@ final class Tokenizer {
|
||||
break;
|
||||
} else if (isWhitespace(c)) {
|
||||
break;
|
||||
} else if (startOfComment(c)) {
|
||||
break;
|
||||
} else {
|
||||
sb.appendCodePoint(c);
|
||||
}
|
||||
@ -310,7 +320,7 @@ final class Tokenizer {
|
||||
return Tokens.newBoolean(origin, false);
|
||||
}
|
||||
|
||||
c = nextCharSkippingComments();
|
||||
c = nextCharRaw();
|
||||
}
|
||||
|
||||
// put back the char that ended the unquoted text
|
||||
@ -324,12 +334,12 @@ final class Tokenizer {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.appendCodePoint(firstChar);
|
||||
boolean containedDecimalOrE = false;
|
||||
int c = nextCharSkippingComments();
|
||||
int c = nextCharRaw();
|
||||
while (c != -1 && numberChars.indexOf(c) >= 0) {
|
||||
if (c == '.' || c == 'e' || c == 'E')
|
||||
containedDecimalOrE = true;
|
||||
sb.appendCodePoint(c);
|
||||
c = nextCharSkippingComments();
|
||||
c = nextCharRaw();
|
||||
}
|
||||
// the last character we looked at wasn't part of the number, put it
|
||||
// back
|
||||
@ -382,7 +392,7 @@ final class Tokenizer {
|
||||
// kind of absurdly slow, but screw it for now
|
||||
char[] a = new char[4];
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
int c = nextCharSkippingComments();
|
||||
int c = nextCharRaw();
|
||||
if (c == -1)
|
||||
throw problem("End of input but expecting 4 hex digits for \\uXXXX escape");
|
||||
a[i] = (char) c;
|
||||
@ -431,14 +441,14 @@ final class Tokenizer {
|
||||
private Token pullSubstitution() throws ProblemException {
|
||||
// the initial '$' has already been consumed
|
||||
ConfigOrigin origin = lineOrigin;
|
||||
int c = nextCharSkippingComments();
|
||||
int c = nextCharRaw();
|
||||
if (c != '{') {
|
||||
throw problem(asString(c), "'$' not followed by {, '" + asString(c)
|
||||
+ "' not allowed after '$'", true /* suggestQuotes */);
|
||||
}
|
||||
|
||||
boolean optional = false;
|
||||
c = nextCharSkippingComments();
|
||||
c = nextCharRaw();
|
||||
if (c == '?') {
|
||||
optional = true;
|
||||
} else {
|
||||
@ -465,6 +475,8 @@ final class Tokenizer {
|
||||
Token whitespace = saver.check(t, origin, lineNumber);
|
||||
if (whitespace != null)
|
||||
expression.add(whitespace);
|
||||
// we don't add comments here though; they can't happen in
|
||||
// valid syntax anyway.
|
||||
expression.add(t);
|
||||
}
|
||||
} while (true);
|
||||
@ -484,45 +496,49 @@ final class Tokenizer {
|
||||
return line;
|
||||
} else {
|
||||
Token t = null;
|
||||
switch (c) {
|
||||
case '"':
|
||||
t = pullQuotedString();
|
||||
break;
|
||||
case '$':
|
||||
t = pullSubstitution();
|
||||
break;
|
||||
case ':':
|
||||
t = Tokens.COLON;
|
||||
break;
|
||||
case ',':
|
||||
t = Tokens.COMMA;
|
||||
break;
|
||||
case '=':
|
||||
t = Tokens.EQUALS;
|
||||
break;
|
||||
case '{':
|
||||
t = Tokens.OPEN_CURLY;
|
||||
break;
|
||||
case '}':
|
||||
t = Tokens.CLOSE_CURLY;
|
||||
break;
|
||||
case '[':
|
||||
t = Tokens.OPEN_SQUARE;
|
||||
break;
|
||||
case ']':
|
||||
t = Tokens.CLOSE_SQUARE;
|
||||
break;
|
||||
}
|
||||
if (startOfComment(c)) {
|
||||
t = pullComment(c);
|
||||
} else {
|
||||
switch (c) {
|
||||
case '"':
|
||||
t = pullQuotedString();
|
||||
break;
|
||||
case '$':
|
||||
t = pullSubstitution();
|
||||
break;
|
||||
case ':':
|
||||
t = Tokens.COLON;
|
||||
break;
|
||||
case ',':
|
||||
t = Tokens.COMMA;
|
||||
break;
|
||||
case '=':
|
||||
t = Tokens.EQUALS;
|
||||
break;
|
||||
case '{':
|
||||
t = Tokens.OPEN_CURLY;
|
||||
break;
|
||||
case '}':
|
||||
t = Tokens.CLOSE_CURLY;
|
||||
break;
|
||||
case '[':
|
||||
t = Tokens.OPEN_SQUARE;
|
||||
break;
|
||||
case ']':
|
||||
t = Tokens.CLOSE_SQUARE;
|
||||
break;
|
||||
}
|
||||
|
||||
if (t == null) {
|
||||
if (firstNumberChars.indexOf(c) >= 0) {
|
||||
t = pullNumber(c);
|
||||
} else if (notInUnquotedText.indexOf(c) >= 0) {
|
||||
throw problem(asString(c), "Reserved character '" + asString(c)
|
||||
+ "' is not allowed outside quotes", true /* suggestQuotes */);
|
||||
} else {
|
||||
putBack(c);
|
||||
t = pullUnquotedText();
|
||||
if (t == null) {
|
||||
if (firstNumberChars.indexOf(c) >= 0) {
|
||||
t = pullNumber(c);
|
||||
} else if (notInUnquotedText.indexOf(c) >= 0) {
|
||||
throw problem(asString(c), "Reserved character '" + asString(c)
|
||||
+ "' is not allowed outside quotes", true /* suggestQuotes */);
|
||||
} else {
|
||||
putBack(c);
|
||||
t = pullUnquotedText();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -548,6 +564,7 @@ final class Tokenizer {
|
||||
Token whitespace = whitespaceSaver.check(t, origin, lineNumber);
|
||||
if (whitespace != null)
|
||||
tokens.add(whitespace);
|
||||
|
||||
tokens.add(t);
|
||||
}
|
||||
|
||||
|
@ -52,7 +52,7 @@ final class Tokens {
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "'\n'@" + lineNumber();
|
||||
return "'\\n'@" + lineNumber();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -167,6 +167,45 @@ final class Tokens {
|
||||
}
|
||||
}
|
||||
|
||||
static private class Comment extends Token {
|
||||
final private String text;
|
||||
|
||||
Comment(ConfigOrigin origin, String text) {
|
||||
super(TokenType.COMMENT, origin);
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
String text() {
|
||||
return text;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("'#");
|
||||
sb.append(text);
|
||||
sb.append("' (COMMENT)");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean canEqual(Object other) {
|
||||
return other instanceof Comment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
return super.equals(other) && ((Comment) other).text.equals(text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int h = 41 * (41 + super.hashCode());
|
||||
h = 41 * (h + text.hashCode());
|
||||
return h;
|
||||
}
|
||||
}
|
||||
|
||||
// This is not a Value, because it requires special processing
|
||||
static private class Substitution extends Token {
|
||||
final private boolean optional;
|
||||
@ -262,6 +301,18 @@ final class Tokens {
|
||||
}
|
||||
}
|
||||
|
||||
static boolean isComment(Token token) {
|
||||
return token instanceof Comment;
|
||||
}
|
||||
|
||||
static String getCommentText(Token token) {
|
||||
if (token instanceof Comment) {
|
||||
return ((Comment) token).text();
|
||||
} else {
|
||||
throw new ConfigException.BugOrBroken("tried to get comment text from " + token);
|
||||
}
|
||||
}
|
||||
|
||||
static boolean isUnquotedText(Token token) {
|
||||
return token instanceof UnquotedText;
|
||||
}
|
||||
@ -316,6 +367,10 @@ final class Tokens {
|
||||
return new Problem(origin, what, message, suggestQuotes, cause);
|
||||
}
|
||||
|
||||
static Token newComment(ConfigOrigin origin, String text) {
|
||||
return new Comment(origin, text);
|
||||
}
|
||||
|
||||
static Token newUnquotedText(ConfigOrigin origin, String s) {
|
||||
return new UnquotedText(origin, s);
|
||||
}
|
||||
|
@ -21,4 +21,5 @@ object RenderExample extends App {
|
||||
|
||||
render("test01")
|
||||
render("test06")
|
||||
render("test05")
|
||||
}
|
||||
|
@ -360,4 +360,135 @@ class ConfParserTest extends TestUtils {
|
||||
Parseable.newProperties(new Properties(), options).toString
|
||||
Parseable.newReader(new StringReader("{}"), options).toString
|
||||
}
|
||||
|
||||
private def assertComments(comments: Seq[String], conf: Config, path: String) {
|
||||
assertEquals(comments, conf.getValue(path).origin().comments().asScala.toSeq)
|
||||
}
|
||||
|
||||
private def assertComments(comments: Seq[String], conf: Config, path: String, index: Int) {
|
||||
val v = conf.getList(path).get(index)
|
||||
assertEquals(comments, v.origin().comments().asScala.toSeq)
|
||||
}
|
||||
|
||||
@Test
|
||||
def trackCommentsForFields() {
|
||||
// comment in front of a field is used
|
||||
val conf1 = parseConfig("""
|
||||
{ # Hello
|
||||
foo=10 }
|
||||
""")
|
||||
assertComments(Seq(" Hello"), conf1, "foo")
|
||||
|
||||
// comment with a blank line after is dropped
|
||||
val conf2 = parseConfig("""
|
||||
{ # Hello
|
||||
|
||||
foo=10 }
|
||||
""")
|
||||
assertComments(Seq(), conf2, "foo")
|
||||
|
||||
// comment in front of a field is used with no root {}
|
||||
val conf3 = parseConfig("""
|
||||
# Hello
|
||||
foo=10
|
||||
""")
|
||||
assertComments(Seq(" Hello"), conf3, "foo")
|
||||
|
||||
// comment with a blank line after is dropped with no root {}
|
||||
val conf4 = parseConfig("""
|
||||
# Hello
|
||||
|
||||
foo=10
|
||||
""")
|
||||
assertComments(Seq(), conf4, "foo")
|
||||
|
||||
// nested objects
|
||||
val conf5 = parseConfig("""
|
||||
# Outside
|
||||
bar {
|
||||
# Ignore me
|
||||
|
||||
# Middle
|
||||
# two lines
|
||||
baz {
|
||||
# Inner
|
||||
foo=10 # should be ignored
|
||||
# This should be ignored too
|
||||
} ## not used
|
||||
# ignored
|
||||
}
|
||||
# ignored!
|
||||
""")
|
||||
assertComments(Seq(" Inner"), conf5, "bar.baz.foo")
|
||||
assertComments(Seq(" Middle", " two lines"), conf5, "bar.baz")
|
||||
assertComments(Seq(" Outside"), conf5, "bar")
|
||||
|
||||
// multiple fields
|
||||
val conf6 = parseConfig("""{
|
||||
# this is not with a field
|
||||
|
||||
# this is field A
|
||||
a : 10
|
||||
# this is field B
|
||||
b : 12 # goes with field C
|
||||
# this is field C
|
||||
c : 14,
|
||||
|
||||
# this is not used
|
||||
# nor is this
|
||||
# multi-line block
|
||||
|
||||
# this is with field D
|
||||
# this is with field D also
|
||||
d : 16
|
||||
|
||||
# this is after the fields
|
||||
}""")
|
||||
assertComments(Seq(" this is field A"), conf6, "a")
|
||||
assertComments(Seq(" this is field B"), conf6, "b")
|
||||
assertComments(Seq(" goes with field C", " this is field C"), conf6, "c")
|
||||
assertComments(Seq(" this is with field D", " this is with field D also"), conf6, "d")
|
||||
|
||||
// array
|
||||
val conf7 = parseConfig("""
|
||||
array = [
|
||||
# goes with 0
|
||||
0,
|
||||
# goes with 1
|
||||
1, # with 2
|
||||
# goes with 2
|
||||
2
|
||||
# not with anything
|
||||
]
|
||||
""")
|
||||
assertComments(Seq(" goes with 0"), conf7, "array", 0)
|
||||
assertComments(Seq(" goes with 1"), conf7, "array", 1)
|
||||
assertComments(Seq(" with 2", " goes with 2"), conf7, "array", 2)
|
||||
|
||||
// properties-like syntax
|
||||
val conf8 = parseConfig("""
|
||||
# ignored comment
|
||||
|
||||
# x.y comment
|
||||
x.y = 10
|
||||
# x.z comment
|
||||
x.z = 11
|
||||
# x.a comment
|
||||
x.a = 12
|
||||
# a.b comment
|
||||
a.b = 14
|
||||
a.c = 15
|
||||
# ignored comment
|
||||
""")
|
||||
|
||||
assertComments(Seq(" x.y comment"), conf8, "x.y")
|
||||
assertComments(Seq(" x.z comment"), conf8, "x.z")
|
||||
assertComments(Seq(" x.a comment"), conf8, "x.a")
|
||||
assertComments(Seq(" a.b comment"), conf8, "a.b")
|
||||
assertComments(Seq(), conf8, "a.c")
|
||||
// here we're concerned that comments apply only to leaf
|
||||
// nodes, not to parent objects.
|
||||
assertComments(Seq(), conf8, "x")
|
||||
assertComments(Seq(), conf8, "a")
|
||||
}
|
||||
}
|
||||
|
@ -389,6 +389,7 @@ abstract trait TestUtils {
|
||||
def tokenInt(i: Int) = Tokens.newInt(fakeOrigin(), i, null)
|
||||
def tokenLong(l: Long) = Tokens.newLong(fakeOrigin(), l, null)
|
||||
def tokenLine(line: Int) = Tokens.newLine(fakeOrigin.setLineNumber(line))
|
||||
def tokenComment(text: String) = Tokens.newComment(fakeOrigin(), text)
|
||||
|
||||
private def tokenMaybeOptionalSubstitution(optional: Boolean, expression: Token*) = {
|
||||
val l = new java.util.ArrayList[Token]
|
||||
|
@ -108,7 +108,7 @@ class TokenizerTest extends TestUtils {
|
||||
tokenizerTest(List(tokenUnquoted("a/b/c/")), "a/b/c/")
|
||||
tokenizerTest(List(tokenUnquoted("/")), "/")
|
||||
tokenizerTest(List(tokenUnquoted("/"), tokenUnquoted(" "), tokenUnquoted("/")), "/ /")
|
||||
tokenizerTest(Nil, "//")
|
||||
tokenizerTest(List(tokenComment("")), "//")
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -205,15 +205,18 @@ class TokenizerTest extends TestUtils {
|
||||
}
|
||||
|
||||
@Test
|
||||
def commentsIgnoredInVariousContext() {
|
||||
def commentsHandledInVariousContexts() {
|
||||
tokenizerTest(List(tokenString("//bar")), "\"//bar\"")
|
||||
tokenizerTest(List(tokenString("#bar")), "\"#bar\"")
|
||||
tokenizerTest(List(tokenUnquoted("bar")), "bar//comment")
|
||||
tokenizerTest(List(tokenUnquoted("bar")), "bar#comment")
|
||||
tokenizerTest(List(tokenInt(10)), "10//comment")
|
||||
tokenizerTest(List(tokenInt(10)), "10#comment")
|
||||
tokenizerTest(List(tokenDouble(3.14)), "3.14//comment")
|
||||
tokenizerTest(List(tokenDouble(3.14)), "3.14#comment")
|
||||
tokenizerTest(List(tokenUnquoted("bar"), tokenComment("comment")), "bar//comment")
|
||||
tokenizerTest(List(tokenUnquoted("bar"), tokenComment("comment")), "bar#comment")
|
||||
tokenizerTest(List(tokenInt(10), tokenComment("comment")), "10//comment")
|
||||
tokenizerTest(List(tokenInt(10), tokenComment("comment")), "10#comment")
|
||||
tokenizerTest(List(tokenDouble(3.14), tokenComment("comment")), "3.14//comment")
|
||||
tokenizerTest(List(tokenDouble(3.14), tokenComment("comment")), "3.14#comment")
|
||||
// be sure we keep the newline
|
||||
tokenizerTest(List(tokenInt(10), tokenComment("comment"), tokenLine(1), tokenInt(12)), "10//comment\n12")
|
||||
tokenizerTest(List(tokenInt(10), tokenComment("comment"), tokenLine(1), tokenInt(12)), "10#comment\n12")
|
||||
}
|
||||
|
||||
@Test
|
||||
|
Loading…
Reference in New Issue
Block a user