diff --git a/HOCON.md b/HOCON.md index dfe46799..e85f1b4b 100644 --- a/HOCON.md +++ b/HOCON.md @@ -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. diff --git a/config/src/main/java/com/typesafe/config/impl/Tokenizer.java b/config/src/main/java/com/typesafe/config/impl/Tokenizer.java index 4332aa7b..0da23070 100644 --- a/config/src/main/java/com/typesafe/config/impl/Tokenizer.java +++ b/config/src/main/java/com/typesafe/config/impl/Tokenizer.java @@ -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); } } diff --git a/config/src/test/scala/com/typesafe/config/impl/ConfParserTest.scala b/config/src/test/scala/com/typesafe/config/impl/ConfParserTest.scala index c865c651..566d50a7 100644 --- a/config/src/test/scala/com/typesafe/config/impl/ConfParserTest.scala +++ b/config/src/test/scala/com/typesafe/config/impl/ConfParserTest.scala @@ -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")) + } } diff --git a/config/src/test/scala/com/typesafe/config/impl/PathTest.scala b/config/src/test/scala/com/typesafe/config/impl/PathTest.scala index b8a28aea..ccf05a94 100644 --- a/config/src/test/scala/com/typesafe/config/impl/PathTest.scala +++ b/config/src/test/scala/com/typesafe/config/impl/PathTest.scala @@ -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()) diff --git a/config/src/test/scala/com/typesafe/config/impl/TestUtils.scala b/config/src/test/scala/com/typesafe/config/impl/TestUtils.scala index 440851df..eea01ba8 100644 --- a/config/src/test/scala/com/typesafe/config/impl/TestUtils.scala +++ b/config/src/test/scala/com/typesafe/config/impl/TestUtils.scala @@ -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;