Skip to content

Commit 51a76bd

Browse files
Preserve YAML parse offsets in generated YAML (#6164)
This opens the door to improving error line/column reporting, since we know where each YAML AST node comes from now --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent 5ac729d commit 51a76bd

File tree

1 file changed

+71
-27
lines changed

1 file changed

+71
-27
lines changed

core/internal/src/mill/internal/Util.scala

Lines changed: 71 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -81,37 +81,81 @@ private[mill] object Util {
8181
}
8282

8383
def parseYaml(fileName: String, headerData: String): Result[ujson.Value] =
84+
parseYaml0(fileName, headerData).map(upickle.core.BufferedValue.transform(_, ujson.Value))
85+
86+
def parseYaml0(fileName: String, headerData: String): Result[upickle.core.BufferedValue] =
8487
try Result.Success {
85-
import org.snakeyaml.engine.v2.api.{Load, LoadSettings}
86-
val loaded = new Load(LoadSettings.builder().build()).loadFromString(headerData)
87-
88-
// recursively convert java data structure to ujson.Value
89-
def rec(x: Any): ujson.Value = {
90-
x match {
91-
case d: java.util.Date => ujson.Str(d.toString)
92-
case s: String => ujson.Str(s)
93-
case d: Double => ujson.Num(d)
94-
case d: Int => ujson.Num(d)
95-
case d: Long => ujson.Num(d)
96-
case true => ujson.True
97-
case false => ujson.False
98-
case null => ujson.Null
99-
case m: java.util.Map[Object, Object] =>
100-
import scala.jdk.CollectionConverters._
101-
val scalaMap = m.asScala
102-
ujson.Obj.from(scalaMap.map { case (k, v) => (k.toString, rec(v)) })
103-
case l: java.util.List[Object] =>
104-
import scala.jdk.CollectionConverters._
105-
val scalaList: collection.Seq[Object] = l.asScala
106-
ujson.Arr.from(scalaList.map(rec))
88+
import org.snakeyaml.engine.v2.api.{LoadSettings}
89+
import org.snakeyaml.engine.v2.composer.Composer
90+
import org.snakeyaml.engine.v2.parser.ParserImpl
91+
import org.snakeyaml.engine.v2.scanner.StreamReader
92+
import org.snakeyaml.engine.v2.nodes._
93+
import scala.jdk.CollectionConverters._
94+
import scala.collection.mutable.ArrayBuffer
95+
96+
val settings = LoadSettings.builder().build()
97+
val reader = new StreamReader(settings, headerData)
98+
val parser = new ParserImpl(settings, reader)
99+
val composer = new Composer(settings, parser)
100+
101+
// recursively convert Node to upickle.core.BufferedValue, preserving character offsets
102+
def rec(node: Node): upickle.core.BufferedValue = {
103+
val index = node.getStartMark.map(_.getIndex.intValue()).orElse(0)
104+
105+
node match {
106+
case scalar: ScalarNode =>
107+
val value = scalar.getValue
108+
val tag = scalar.getTag.getValue
109+
tag match {
110+
case "tag:yaml.org,2002:null" => upickle.core.BufferedValue.Null(index)
111+
case "tag:yaml.org,2002:bool" =>
112+
if (value == "true") upickle.core.BufferedValue.True(index)
113+
else upickle.core.BufferedValue.False(index)
114+
case "tag:yaml.org,2002:int" =>
115+
upickle.core.BufferedValue.Num(value, -1, -1, index)
116+
case "tag:yaml.org,2002:float" =>
117+
upickle.core.BufferedValue.Num(value, -1, -1, index)
118+
case _ => upickle.core.BufferedValue.Str(value, index)
119+
}
120+
121+
case mapping: MappingNode =>
122+
val pairs = mapping.getValue.asScala.map { tuple =>
123+
val keyNode = tuple.getKeyNode
124+
val valueNode = tuple.getValueNode
125+
val key = keyNode match {
126+
case s: ScalarNode => upickle.core.BufferedValue.Str(
127+
s.getValue,
128+
keyNode.getStartMark.map(_.getIndex.intValue()).orElse(0)
129+
)
130+
case _ => upickle.core.BufferedValue.Str(
131+
keyNode.toString,
132+
keyNode.getStartMark.map(_.getIndex.intValue()).orElse(0)
133+
)
134+
}
135+
(key, rec(valueNode))
136+
}
137+
upickle.core.BufferedValue.Obj(ArrayBuffer.from(pairs), jsonableKeys = true, index)
138+
139+
case sequence: SequenceNode =>
140+
val items = sequence.getValue.asScala.map(rec)
141+
upickle.core.BufferedValue.Arr(ArrayBuffer.from(items), index)
107142
}
108143
}
109144

110-
// Treat a top-level `null` as an empty object, so that an empty YAML header
111-
// block is treated gracefully rather than blowing up with a NPE
112-
rec(loaded) match {
113-
case ujson.Null => ujson.Obj()
114-
case v => v
145+
// Treat a top-level `null` or empty document as an empty object
146+
if (composer.hasNext) {
147+
val node = composer.next()
148+
rec(node) match {
149+
case nullValue @ upickle.core.BufferedValue.Null(_) =>
150+
upickle.core.BufferedValue.Obj(
151+
ArrayBuffer.empty,
152+
jsonableKeys = true,
153+
nullValue.index
154+
)
155+
case v => v
156+
}
157+
} else {
158+
upickle.core.BufferedValue.Obj(ArrayBuffer.empty, jsonableKeys = true, 0)
115159
}
116160
}
117161
catch {

0 commit comments

Comments
 (0)