Skip to content

Commit 90d6b30

Browse files
committed
Complete pretty printer.
This should likely be moved upstream to metaconfig
1 parent c8212ed commit 90d6b30

File tree

6 files changed

+172
-46
lines changed

6 files changed

+172
-46
lines changed

build.sbt

+1
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ lazy val tests = project
152152
// Test dependencies
153153
"com.googlecode.java-diff-utils" % "diffutils" % "1.3.0",
154154
"com.lihaoyi" %% "scalatags" % "0.6.3",
155+
"org.typelevel" %% "paiges-core" % "0.2.0",
155156
scalametaTestkit
156157
)
157158
)

readme/src/main/scala/org/scalafmt/readme/Readme.scala

+4-4
Original file line numberDiff line numberDiff line change
@@ -117,12 +117,12 @@ object Readme {
117117
// a.b = 1
118118
// a.c = 2
119119
// which means a simple line-by-line string diffing is sufficient for now.
120-
val defaultStrs = Config.toHocon(ScalafmtConfig.default).toSet
121-
val configStrs = Config.toHocon(style).toSet
120+
val defaultStrs = Config.toHocon(ScalafmtConfig.default).render(40).lines.toSet
121+
val configStrs = Config.toHocon(style).render(40).lines.toSet
122122
configStrs.diff(defaultStrs).mkString("\n")
123123
}
124124

125-
def allOptions = hl.scala.apply(Config.toHocon(ScalafmtConfig.default))
125+
def allOptions = hl.scala.apply(Config.toHocon(ScalafmtConfig.default).render(40))
126126

127127
def configurationBlock(style: ScalafmtConfig) = {
128128
div(
@@ -247,7 +247,7 @@ object Readme {
247247
)
248248

249249
val VerticalMultilineDefultConfigStr = "verticalMultiline = {\n" +
250-
Config.toHocon(ScalafmtConfig.default.verticalMultiline) + "}"
250+
Config.toHocon(ScalafmtConfig.default.verticalMultiline).render(40) + "}"
251251

252252
val verticalAlignImplicitBefore = ScalafmtConfig.default.copy(
253253
maxColumn = 60,

scalafmt-core/shared/src/main/scala/org/scalafmt/config/Align.scala

+8-1
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,14 @@ object Align {
9696
implicit lazy val surface: Surface[Align] = generic.deriveSurface[Align]
9797
implicit lazy val encoder: ConfEncoder[Align] = generic.deriveEncoder
9898
// TODO: metaconfig should handle iterables
99-
implicit def encoderSet[T:ConfEncoder]: ConfEncoder[Set[T]] = implicitly[ConfEncoder[Seq[T]]].contramap(_.toSeq)
99+
implicit def encoderSet[T: ConfEncoder]: ConfEncoder[Set[T]] =
100+
implicitly[ConfEncoder[Seq[T]]].contramap(_.toSeq)
101+
implicit val mapEncoder: ConfEncoder[Map[String, String]] =
102+
ConfEncoder.instance[Map[String, String]] { m =>
103+
Conf.Obj(m.iterator.map {
104+
case (k, v) => k -> Conf.fromString(v)
105+
}.toList)
106+
}
100107

101108
// only for the truest vertical aligners, this setting is open for changes,
102109
// please open PR addding more stuff to it if you like.

scalafmt-core/shared/src/main/scala/org/scalafmt/config/Config.scala

+105-38
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
11
package org.scalafmt.config
22

33
import scala.language.reflectiveCalls
4-
54
import metaconfig._
6-
75
import java.io.File
8-
96
import metaconfig.Conf
107
import metaconfig.ConfError
118
import metaconfig.Configured
129
import metaconfig.Configured.Ok
1310
import org.scalafmt.config.PlatformConfig._
11+
import org.typelevel.paiges.Doc._
12+
import org.typelevel.paiges.Doc
1413

1514
object Config {
1615

@@ -22,45 +21,113 @@ object Config {
2221
}
2322
}
2423

25-
def toHocon[T: ConfEncoder](value: T): String = {
26-
val conf = ConfEncoder[T].write(value)
27-
val out = new StringBuilder
28-
def loop(c: Conf): Unit = c match {
29-
case Conf.Null() => out.append("null").append("\n")
30-
case Conf.Num(num) => out.append(num).append("\n")
31-
case Conf.Str(str) => out.append('"').append(str).append('"').append("\n")
32-
case Conf.Bool(bool) => out.append(bool).append("\n")
33-
case Conf.Lst(lst) =>
34-
out.append("[\n")
35-
lst.foreach { elem =>
36-
out.append(" ")
37-
loop(elem)
38-
}
39-
out.append("]\n")
40-
case Conf.Obj(obj) =>
41-
obj.foreach {
42-
case (key, value) =>
43-
out.append(key).append(" = ")
44-
loop(value)
45-
}
46-
}
24+
def quote(key: String): String =
25+
if (key.indexOf('.') < 0) key
26+
else "\"" + key + "\""
27+
28+
val isForbiddenCharacter = Set[Char](
29+
'$', '"', '{', '}', '[', ']', ':', '=', ',', '+', '#', '`', '^', '?', '!',
30+
'@', '*', '&', ' ', '\\'
31+
)
32+
val quote = char('"')
33+
def quoteKey(key: String): Doc =
34+
if (key.indexOf('.') < 0) text(key)
35+
else quote + text(key) + quote
36+
37+
// Spec is here:
38+
// https://github.com/lightbend/config/blob/master/HOCON.md#unquoted-strings
39+
// but this method is conservative and quotes if the string contains non-letter characters
40+
def needsQuote(str: String): Boolean =
41+
str.isEmpty ||
42+
str.startsWith("true") ||
43+
str.startsWith("false") ||
44+
str.startsWith("null") ||
45+
str.exists(!_.isLetter)
46+
47+
def quoteString(str: String): Doc =
48+
if (needsQuote(str)) quote + text(str) + quote
49+
else text(str)
50+
51+
def wrap(open: Char, close: Char, doc: Doc): Doc = {
52+
(char(open) + line + doc).nested(2) + line + char(close)
53+
}
4754

48-
def flatten(c: Conf): Conf = c match {
49-
case Conf.Obj(obj) =>
50-
val flattened = obj.flatMap {
51-
case (key, Conf.Obj(nested)) =>
52-
nested.map {
53-
case (k, v) =>
54-
s"$key.$k" -> flatten(v)
55-
}
56-
case x => x :: Nil
57-
}
58-
Conf.Obj(flattened)
59-
case x => x
55+
def toHocon[T: ConfEncoder](value: T): Doc = {
56+
toHocon(ConfEncoder[T].write(value))
57+
}
58+
def toHocon(conf: Conf): Doc = {
59+
def loop(c: Conf): Doc = {
60+
c match {
61+
case Conf.Null() => text("null")
62+
case Conf.Num(num) => str(num)
63+
case Conf.Str(str) => quoteString(str)
64+
case Conf.Bool(bool) => str(bool)
65+
case Conf.Lst(lst) =>
66+
if (lst.isEmpty) text("[]")
67+
else {
68+
val parts = intercalate(line, lst.map {
69+
case c: Conf.Obj =>
70+
wrap('{', '}', loop(c))
71+
case x => loop(x)
72+
})
73+
wrap('[', ']', parts)
74+
}
75+
case Conf.Obj(obj) =>
76+
intercalate(line, obj.map {
77+
case (k, v) =>
78+
text(k) + text(" = ") + loop(v)
79+
})
80+
// "[" +: (parts :+ " ]").nested(2)
81+
// out.append("[\n")
82+
// lst.foreach { elem =>
83+
// indent(nesting + 2)
84+
// elem match {
85+
// case _: Conf.Obj =>
86+
// out.append('{')
87+
// newline()
88+
// loop(elem, nesting + 4)
89+
// indent(nesting + 2)
90+
// out.append('}')
91+
// case _ =>
92+
// loop(elem, nesting + 2)
93+
// }
94+
// newline()
95+
// }
96+
// out.append("]")
97+
// }
98+
// case Conf.Obj(obj) =>
99+
// if (obj.isEmpty) out.append("{}")
100+
// else {
101+
// obj.foreach {
102+
// case (key, value) =>
103+
// indent(nesting)
104+
// out.append(key).append(" = ")
105+
// loop(value, nesting + 2)
106+
// newline()
107+
// }
108+
// }
109+
}
60110
}
111+
61112
loop(flatten(conf))
113+
}
62114

63-
out.toString()
115+
def flatten(c: Conf): Conf = c match {
116+
case Conf.Obj(obj) =>
117+
val flattened = obj.map {
118+
case (k, v) => (k, flatten(v))
119+
}
120+
val next = flattened.flatMap {
121+
case (key, Conf.Obj(nested)) =>
122+
nested.map {
123+
case (k, v) => s"${quote(key)}.$k" -> v
124+
}
125+
case (key, value) => (quote(key), value) :: Nil
126+
}
127+
Conf.Obj(next)
128+
case Conf.Lst(lst) =>
129+
Conf.Lst(lst.map(flatten))
130+
case x => x
64131
}
65132

66133
def fromHoconString(

scalafmt-core/shared/src/main/scala/org/scalafmt/config/ScalafmtRunner.scala

+3-3
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,11 @@ object ScalafmtRunner {
4646
implicit lazy val encoder: ConfEncoder[ScalafmtRunner] =
4747
generic.deriveEncoder
4848
implicit lazy val formatEventEncoder: ConfEncoder[FormatEvent => Unit] =
49-
ConfEncoder.StringEncoder.contramap(_ => "FormatEvent => Unit")
49+
ConfEncoder.StringEncoder.contramap(_ => "<FormatEvent => Unit>")
5050
implicit lazy val parseEncoder: ConfEncoder[Parse[_ <: Tree]] =
51-
ConfEncoder.StringEncoder.contramap(_ => "Parse[Tree]")
51+
ConfEncoder.StringEncoder.contramap(_ => "<Parse[Tree]>")
5252
implicit lazy val dialectEncoder: ConfEncoder[Dialect] =
53-
ConfEncoder.StringEncoder.contramap(_.toString())
53+
ConfEncoder.StringEncoder.contramap(_ => "<Dialect>")
5454
val defaultDialect = Scala211.copy(
5555
// Are `&` intersection types supported by this dialect?
5656
allowAndTypes = true,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package org.scalafmt
2+
3+
import metaconfig.Conf
4+
import metaconfig.ConfEncoder
5+
import org.scalafmt.config.Config
6+
import org.scalafmt.util.DiffAssertions
7+
import org.scalatest.FunSuite
8+
9+
10+
class ToHoconSuite extends FunSuite with DiffAssertions {
11+
implicit val encoder = ConfEncoder.instance[Conf](identity)
12+
def check(original: Conf, expected: String): Unit = {
13+
test(original.toString()) {
14+
val obtained = Config.toHocon(original).render(10)
15+
println(obtained)
16+
assertNoDiff(obtained, expected)
17+
}
18+
}
19+
check(
20+
Conf.Obj(
21+
"a" -> Conf.Bool(true),
22+
"b" -> Conf.Null(),
23+
"c" -> Conf.Num(1),
24+
"d" -> Conf.Lst(Conf.Str("2"), Conf.Str("")),
25+
"e" -> Conf.Obj("f" -> Conf.Num(3)),
26+
"f.g" -> Conf.Num(2),
27+
),
28+
"""|a = true
29+
|b = null
30+
|c = 1
31+
|d = [
32+
| "2"
33+
| ""
34+
|]
35+
|e.f = 3
36+
|"f.g" = 2
37+
|""".stripMargin.trim
38+
)
39+
40+
check(
41+
Conf.Obj(
42+
"a" -> Conf.Lst(Conf.Str("b.c"))
43+
),
44+
"""
45+
|a = [
46+
| "b.c"
47+
|]
48+
""".stripMargin.trim
49+
)
50+
51+
}

0 commit comments

Comments
 (0)