From a99e24b7ebe69d5559a04233e4d8db75fb85b1aa Mon Sep 17 00:00:00 2001 From: Matthew Orford Date: Tue, 2 Jun 2026 21:15:30 -0400 Subject: [PATCH 1/4] test case from issue --- .../typesafe/config/impl/ConfParserTest.scala | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) 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 f32b7b3b..4e4379b1 100644 --- a/config/src/test/scala/com/typesafe/config/impl/ConfParserTest.scala +++ b/config/src/test/scala/com/typesafe/config/impl/ConfParserTest.scala @@ -893,6 +893,42 @@ class ConfParserTest extends TestUtils { assertEquals(2, arrayAfterEquals.getList("a").origin().lineNumber()) } + @Test + def valuesAfterMultilineStringHaveCorrectOriginLine() { + val tripleQuote = "\"\"\"" + val conf = parseConfig(Seq( + "transformations {", + " trim-xml-leading-whitespace = " + tripleQuote, + " stringTransformation {", + " println(\"Original message: '$it'\")", + " it.trimStart { it.isWhitespace() || it == '\\uFEFF' }.also { println(\"Trimmed leading whitespace: '${it}'\") }", + " }", + " " + tripleQuote, + "}", + "", + "pipelines {", + " my-pipeline {", + " from {", + " type = GENERATOR", + " count = 1", + " message = \"foo\"", + " }", + " error-strategy = SHUTDOWN", + " processing {", + " transformation = trim-xml-leading-whitespace", + " xml-to-json = { attribute-prefix = \"@\" }", + " }", + " to {", + " type = LOGGER", + " }", + " }", + "}" + ).mkString("\n")) + + assertEquals(19, conf.getValue("pipelines.my-pipeline.processing.transformation").origin().lineNumber()) + assertEquals(20, conf.getObject("pipelines.my-pipeline.processing.xml-to-json").origin().lineNumber()) + } + @Test def acceptMultiPeriodNumericPath() { val conf1 = ConfigFactory.parseString("0.1.2.3=foobar1") From 4e17f975f078f35c91e1a3015e0ca2c21f956892 Mon Sep 17 00:00:00 2001 From: Matthew Orford Date: Tue, 2 Jun 2026 23:35:40 -0400 Subject: [PATCH 2/4] capture at tokenization time --- .../typesafe/config/impl/ConfigDocumentParser.java | 8 +++++--- .../com/typesafe/config/impl/ConfigNodeArray.java | 8 +++++++- .../typesafe/config/impl/ConfigNodeComplexValue.java | 12 ++++++++++++ .../com/typesafe/config/impl/ConfigNodeObject.java | 7 ++++++- .../java/com/typesafe/config/impl/ConfigParser.java | 12 ++++++++++-- 5 files changed, 40 insertions(+), 7 deletions(-) diff --git a/config/src/main/java/com/typesafe/config/impl/ConfigDocumentParser.java b/config/src/main/java/com/typesafe/config/impl/ConfigDocumentParser.java index 29ffc70a..6ded92fc 100644 --- a/config/src/main/java/com/typesafe/config/impl/ConfigDocumentParser.java +++ b/config/src/main/java/com/typesafe/config/impl/ConfigDocumentParser.java @@ -418,6 +418,7 @@ private ConfigNodeComplexValue parseObject(boolean hadOpenCurly) { boolean afterComma = false; Path lastPath = null; boolean lastInsideEquals = false; + ConfigOrigin objectOrigin = baseOrigin.withLineNumber(lineNumber); ArrayList objectNodes = new ArrayList(); ArrayList keyValueNodes; HashMap keys = new HashMap(); @@ -539,11 +540,12 @@ private ConfigNodeComplexValue parseObject(boolean hadOpenCurly) { } } - return new ConfigNodeObject(objectNodes); + return new ConfigNodeObject(objectNodes, objectOrigin); } private ConfigNodeComplexValue parseArray() { ArrayList children = new ArrayList(); + ConfigOrigin arrayOrigin = baseOrigin.withLineNumber(lineNumber); children.add(new ConfigNodeSingleToken(Tokens.OPEN_SQUARE)); // invoked just after the OPEN_SQUARE Token t; @@ -557,7 +559,7 @@ private ConfigNodeComplexValue parseArray() { // special-case the first element if (t == Tokens.CLOSE_SQUARE) { children.add(new ConfigNodeSingleToken(t)); - return new ConfigNodeArray(children); + return new ConfigNodeArray(children, arrayOrigin); } else if (Tokens.isValue(t) || t == Tokens.OPEN_CURLY || t == Tokens.OPEN_SQUARE || Tokens.isUnquotedText(t) || Tokens.isSubstitution(t)) { @@ -581,7 +583,7 @@ private ConfigNodeComplexValue parseArray() { t = nextTokenCollectingWhitespace(children); if (t == Tokens.CLOSE_SQUARE) { children.add(new ConfigNodeSingleToken(t)); - return new ConfigNodeArray(children); + return new ConfigNodeArray(children, arrayOrigin); } else { throw parseError("List should have ended with ] or had a comma, instead had token: " + t diff --git a/config/src/main/java/com/typesafe/config/impl/ConfigNodeArray.java b/config/src/main/java/com/typesafe/config/impl/ConfigNodeArray.java index 46500b52..5c5130e3 100644 --- a/config/src/main/java/com/typesafe/config/impl/ConfigNodeArray.java +++ b/config/src/main/java/com/typesafe/config/impl/ConfigNodeArray.java @@ -1,5 +1,7 @@ package com.typesafe.config.impl; +import com.typesafe.config.ConfigOrigin; + import java.util.Collection; final class ConfigNodeArray extends ConfigNodeComplexValue { @@ -7,8 +9,12 @@ final class ConfigNodeArray extends ConfigNodeComplexValue { super(children); } + ConfigNodeArray(Collection children, ConfigOrigin origin) { + super(children, origin); + } + @Override protected ConfigNodeArray newNode(Collection nodes) { - return new ConfigNodeArray(nodes); + return new ConfigNodeArray(nodes, origin()); } } diff --git a/config/src/main/java/com/typesafe/config/impl/ConfigNodeComplexValue.java b/config/src/main/java/com/typesafe/config/impl/ConfigNodeComplexValue.java index c899ee01..645782c7 100644 --- a/config/src/main/java/com/typesafe/config/impl/ConfigNodeComplexValue.java +++ b/config/src/main/java/com/typesafe/config/impl/ConfigNodeComplexValue.java @@ -3,19 +3,31 @@ */ package com.typesafe.config.impl; +import com.typesafe.config.ConfigOrigin; + import java.util.*; abstract class ConfigNodeComplexValue extends AbstractConfigNodeValue { final protected ArrayList children; + final private ConfigOrigin origin; ConfigNodeComplexValue(Collection children) { + this(children, null); + } + + ConfigNodeComplexValue(Collection children, ConfigOrigin origin) { this.children = new ArrayList(children); + this.origin = origin; } final public Collection children() { return children; } + final public ConfigOrigin origin() { + return origin; + } + @Override protected Collection tokens() { ArrayList tokens = new ArrayList(); diff --git a/config/src/main/java/com/typesafe/config/impl/ConfigNodeObject.java b/config/src/main/java/com/typesafe/config/impl/ConfigNodeObject.java index 66c65f06..684c40f0 100644 --- a/config/src/main/java/com/typesafe/config/impl/ConfigNodeObject.java +++ b/config/src/main/java/com/typesafe/config/impl/ConfigNodeObject.java @@ -1,5 +1,6 @@ package com.typesafe.config.impl; +import com.typesafe.config.ConfigOrigin; import com.typesafe.config.ConfigSyntax; import java.util.ArrayList; @@ -10,9 +11,13 @@ final class ConfigNodeObject extends ConfigNodeComplexValue { super(children); } + ConfigNodeObject(Collection children, ConfigOrigin origin) { + super(children, origin); + } + @Override protected ConfigNodeObject newNode(Collection nodes) { - return new ConfigNodeObject(nodes); + return new ConfigNodeObject(nodes, origin()); } public boolean hasValue(Path desiredPath) { diff --git a/config/src/main/java/com/typesafe/config/impl/ConfigParser.java b/config/src/main/java/com/typesafe/config/impl/ConfigParser.java index 90b5d5d9..c75292eb 100644 --- a/config/src/main/java/com/typesafe/config/impl/ConfigParser.java +++ b/config/src/main/java/com/typesafe/config/impl/ConfigParser.java @@ -76,6 +76,14 @@ private SimpleConfigOrigin lineOrigin() { return ((SimpleConfigOrigin) baseOrigin).withLineNumber(lineNumber); } + private SimpleConfigOrigin nodeOrigin(ConfigNodeComplexValue n) { + ConfigOrigin origin = n.origin(); + if (origin != null) + return (SimpleConfigOrigin) origin; + else + return lineOrigin(); + } + private ConfigException parseError(String message) { return parseError(message, null); } @@ -220,7 +228,7 @@ private void parseInclude(Map values, ConfigNodeInc private AbstractConfigObject parseObject(ConfigNodeObject n) { Map values = new HashMap(); - SimpleConfigOrigin objectOrigin = lineOrigin(); + SimpleConfigOrigin objectOrigin = nodeOrigin(n); boolean lastWasNewline = false; ArrayList nodes = new ArrayList(n.children()); @@ -357,7 +365,7 @@ private AbstractConfigObject parseObject(ConfigNodeObject n) { private SimpleConfigList parseArray(ConfigNodeArray n) { arrayCount += 1; - SimpleConfigOrigin arrayOrigin = lineOrigin(); + SimpleConfigOrigin arrayOrigin = nodeOrigin(n); List values = new ArrayList(); boolean lastWasNewLine = false; From 9f94dc914ad4f52dedf6a7ca506b3a591ef53bb7 Mon Sep 17 00:00:00 2001 From: Matthew Orford Date: Tue, 2 Jun 2026 23:48:58 -0400 Subject: [PATCH 3/4] thread root origin into super + commnet --- .../src/main/java/com/typesafe/config/impl/ConfigNodeRoot.java | 2 +- .../src/main/java/com/typesafe/config/impl/ConfigParser.java | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/config/src/main/java/com/typesafe/config/impl/ConfigNodeRoot.java b/config/src/main/java/com/typesafe/config/impl/ConfigNodeRoot.java index c84c16f5..f5e299dc 100644 --- a/config/src/main/java/com/typesafe/config/impl/ConfigNodeRoot.java +++ b/config/src/main/java/com/typesafe/config/impl/ConfigNodeRoot.java @@ -11,7 +11,7 @@ final class ConfigNodeRoot extends ConfigNodeComplexValue { final private ConfigOrigin origin; ConfigNodeRoot(Collection children, ConfigOrigin origin) { - super(children); + super(children, origin); this.origin = origin; } diff --git a/config/src/main/java/com/typesafe/config/impl/ConfigParser.java b/config/src/main/java/com/typesafe/config/impl/ConfigParser.java index c75292eb..ebde345c 100644 --- a/config/src/main/java/com/typesafe/config/impl/ConfigParser.java +++ b/config/src/main/java/com/typesafe/config/impl/ConfigParser.java @@ -76,6 +76,9 @@ private SimpleConfigOrigin lineOrigin() { return ((SimpleConfigOrigin) baseOrigin).withLineNumber(lineNumber); } + // prefer origin captured at tokenize time (should be correct even when + // following a multiline string); falls back to the line counter for + // nodes built without origin. private SimpleConfigOrigin nodeOrigin(ConfigNodeComplexValue n) { ConfigOrigin origin = n.origin(); if (origin != null) From 68ff13390bc580206bb54c2a988f944a409491c7 Mon Sep 17 00:00:00 2001 From: Matthew Orford Date: Wed, 3 Jun 2026 08:53:41 -0400 Subject: [PATCH 4/4] add array case to tests --- .../test/scala/com/typesafe/config/impl/ConfParserTest.scala | 2 ++ 1 file changed, 2 insertions(+) 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 4e4379b1..d429cabc 100644 --- a/config/src/test/scala/com/typesafe/config/impl/ConfParserTest.scala +++ b/config/src/test/scala/com/typesafe/config/impl/ConfParserTest.scala @@ -917,6 +917,7 @@ class ConfParserTest extends TestUtils { " processing {", " transformation = trim-xml-leading-whitespace", " xml-to-json = { attribute-prefix = \"@\" }", + " xml-to-json-list = [ { attribute-prefix = \"@\" } ]", " }", " to {", " type = LOGGER", @@ -927,6 +928,7 @@ class ConfParserTest extends TestUtils { assertEquals(19, conf.getValue("pipelines.my-pipeline.processing.transformation").origin().lineNumber()) assertEquals(20, conf.getObject("pipelines.my-pipeline.processing.xml-to-json").origin().lineNumber()) + assertEquals(21, conf.getList("pipelines.my-pipeline.processing.xml-to-json-list").origin().lineNumber()) } @Test