Allow unquoted strings consisting entirely of number chars

Previously, a string which was an invalid number (such as
"1.0.0" or "1e3e3") but consisted entirely of chars allowed
in numbers would be an error and require quoting. Now, we
allow such strings to be unquoted.
This commit is contained in:
Havoc Pennington 2013-12-06 12:20:43 -05:00
parent 41f3de8261
commit 7ac81ecb5e
5 changed files with 37 additions and 15 deletions
HOCON.md
config/src
main/java/com/typesafe/config/impl
test/scala/com/typesafe/config/impl

View File

@ -441,6 +441,7 @@ number-to-string library function).
be a two-element path with `foo10` and `0` as the elements.
- `foo"10.0"` is an unquoted then a quoted string which are
concatenated, so this is a single-element path.
- `1.2.3` is the three-element path with `1`,`2`,`3`
Unlike value concatenations, path expressions are _always_
converted to a string, even if they are just a single value.

View File

@ -355,7 +355,15 @@ final class Tokenizer {
return Tokens.newLong(lineOrigin, Long.parseLong(s), s);
}
} catch (NumberFormatException e) {
throw problem(s, "Invalid number: '" + s + "'", true /* suggestQuotes */, e);
// not a number after all, see if it's an unquoted string.
for (char u : s.toCharArray()) {
if (notInUnquotedText.indexOf(u) >= 0)
throw problem(asString(u), "Reserved character '" + asString(u)
+ "' is not allowed outside quotes", true /* suggestQuotes */);
}
// no evil chars so we just decide this was a string and
// not a number.
return Tokens.newUnquotedText(lineOrigin, s);
}
}

View File

@ -124,6 +124,7 @@ class ConfParserTest extends TestUtils {
assertEquals(path("a b"), parsePath(" a b"))
assertEquals(path("a", "b.c", "d"), parsePath("a.\"b.c\".d"))
assertEquals(path("3", "14"), parsePath("3.14"))
assertEquals(path("3", "14", "159"), parsePath("3.14.159"))
assertEquals(path("a3", "14"), parsePath("a3.14"))
assertEquals(path(""), parsePath("\"\""))
assertEquals(path("a", "", "b"), parsePath("a.\"\".b"))
@ -133,6 +134,9 @@ class ConfParserTest extends TestUtils {
assertEquals(path("a-c"), parsePath("a-c"))
assertEquals(path("a_c"), parsePath("a_c"))
assertEquals(path("-"), parsePath("\"-\""))
assertEquals(path("-"), parsePath("-"))
assertEquals(path("-foo"), parsePath("-foo"))
assertEquals(path("-10"), parsePath("-10"))
// here 10.0 is part of an unquoted string
assertEquals(path("foo10", "0"), parsePath("foo10.0"))
@ -140,6 +144,8 @@ class ConfParserTest extends TestUtils {
assertEquals(path("10", "0foo"), parsePath("10.0foo"))
// just a number
assertEquals(path("10", "0"), parsePath("10.0"))
// multiple-decimal number
assertEquals(path("1", "2", "3", "4"), parsePath("1.2.3.4"))
for (invalid <- Seq("", " ", " \n \n ", "a.", ".b", "a..b", "a${b}c", "\"\".", ".\"\"")) {
try {
@ -152,11 +158,6 @@ class ConfParserTest extends TestUtils {
throw e;
}
}
intercept[ConfigException.Parse] {
// this gets parsed as a number since it starts with '-'
parsePath("-")
}
}
@Test
@ -339,11 +340,6 @@ class ConfParserTest extends TestUtils {
lineNumberTest(2, "\n\"foo\"")
lineNumberTest(3, "\n\n\"foo\"")
// newline in middle of number uses the line the number was on
lineNumberTest(1, "1e\n")
lineNumberTest(2, "\n1e\n")
lineNumberTest(3, "\n\n1e\n")
// newlines in triple-quoted string should not hose up the numbering
lineNumberTest(1, "a : \"\"\"foo\"\"\"}")
lineNumberTest(2, "a : \"\"\"foo\n\"\"\"}")
@ -813,4 +809,14 @@ class ConfParserTest extends TestUtils {
val conf = ConfigFactory.parseString("foo= \uFEFFbar\uFEFF")
assertEquals("bar", conf.getString("foo"))
}
@Test
def acceptMultiPeriodNumericPath() {
val conf1 = ConfigFactory.parseString("0.1.2.3=foobar1")
assertEquals("foobar1", conf1.getString("0.1.2.3"))
val conf2 = ConfigFactory.parseString("0.1.2.3.ABC=foobar2")
assertEquals("foobar2", conf2.getString("0.1.2.3.ABC"))
val conf3 = ConfigFactory.parseString("ABC.0.1.2.3=foobar3")
assertEquals("foobar3", conf3.getString("ABC.0.1.2.3"))
}
}

View File

@ -67,6 +67,9 @@ class PathTest extends TestUtils {
RenderTest("\" foo \"", path(" foo ")),
// trailing space only
RenderTest("\"foo \"", path("foo ")))
// numbers with decimal points
RenderTest("1.2", path("1", "2"))
RenderTest("1.2.3.4", path("1", "2", "3", "4"))
for (t <- tests) {
assertEquals(t.expected, t.path.render())

View File

@ -342,8 +342,6 @@ abstract trait TestUtils {
// these two problems are ignored by the lift tokenizer
"[:\"foo\", \"bar\"]", // colon in an array; lift doesn't throw (tokenizer erases it)
"[\"foo\" : \"bar\"]", // colon in an array another way, lift ignores (tokenizer erases it)
"[ 10e3e3 ]", // two exponents. ideally this might parse to a number plus string "e3" but it's hard to implement.
"[ 1-e3 ]", // malformed number but all chars can appear in a number
"[ \"hello ]", // unterminated string
ParseTest(true, "{ \"foo\" , true }"), // comma instead of colon, lift is fine with this
ParseTest(true, "{ \"foo\" : true \"bar\" : false }"), // missing comma between fields, lift fine with this
@ -380,6 +378,7 @@ abstract trait TestUtils {
"[ += ]",
"+= 10",
"10 +=",
"[ 10e+3e ]", // "+" not allowed in unquoted strings, and not a valid number
ParseTest(true, "[ \"foo\nbar\" ]"), // unescaped newline in quoted string, lift doesn't care
"[ # comment ]",
"${ #comment }",
@ -412,7 +411,8 @@ abstract trait TestUtils {
"[ \"//comment\" ]", // quoted // comment
// this long one is mostly to test rendering
"""{ "foo" : { "bar" : "baz", "woo" : "w00t" }, "baz" : { "bar" : "baz", "woo" : [1,2,3,4], "w00t" : true, "a" : false, "b" : 3.14, "c" : null } }""",
"{}")
"{}",
ParseTest(true, "[ 10e+3 ]")) // "+" in a number (lift doesn't handle)
private val validConfInvalidJson = List[ParseTest]("", // empty document
" ", // empty document single space
@ -504,7 +504,11 @@ abstract trait TestUtils {
"a = [], a += b", // += operator with previous init
"{ a = [], a += 10 }", // += in braces object with previous init
"a += b", // += operator without previous init
"{ a += 10 }") // += in braces object without previous init
"{ a += 10 }", // += in braces object without previous init
"[ 10e3e3 ]", // two exponents. this should parse to a number plus string "e3"
"[ 1-e3 ]", // malformed number should end up as a string instead
"[ 1.0.0 ]", // two decimals, should end up as a string
"[ 1.0. ]") // trailing decimal should end up as a string
protected val invalidJson = validConfInvalidJson ++ invalidJsonInvalidConf;