diff --git a/build.sbt b/build.sbt index 22c6e9387f..46c7736458 100644 --- a/build.sbt +++ b/build.sbt @@ -38,6 +38,7 @@ libraryDependencies ++= Seq( "ch.qos.logback" % "logback-classic" % "1.1.3", "org.scalameta" %% "scalameta" % "0.1.0-SNAPSHOT", "com.googlecode.java-diff-utils" % "diffutils" % "1.3.0" % "test", + "com.lihaoyi" %% "scalatags" % "0.5.4" % "test", "org.scalatest" %% "scalatest" % "2.2.1" % "test" ) diff --git a/src/main/scala/org/scalafmt/Debug.scala b/src/main/scala/org/scalafmt/Debug.scala index f5eeff28ba..33c343503b 100644 --- a/src/main/scala/org/scalafmt/Debug.scala +++ b/src/main/scala/org/scalafmt/Debug.scala @@ -2,31 +2,47 @@ package org.scalafmt import scala.collection.mutable import scala.meta.Tree +import scala.meta.tokens.Token object Debug extends ScalaFmtLogger { val treeExplored = mutable.Map.empty[Tree, Int] - val tokenExplored = mutable.Map.empty[FormatToken, Int] + val tokenExplored = mutable.Map.empty[Token, Int] + val formatTokenExplored = mutable.Map.empty[FormatToken, Int] var explored = 0 + var state = State.start + var toks = Array.empty[FormatToken] def clear(): Unit = { treeExplored.clear() tokenExplored.clear() } - def visit(token: FormatToken): Unit = { + def visit(token: Token): Unit = { val visits = tokenExplored.getOrElse(token, 0) + 1 tokenExplored += token -> visits } - def reportTokens = { - tokenExplored.toSeq.sortBy(_._1.right.end).foreach { + def visit(token: FormatToken): Unit = { + visit(token.left) + visit(token.right) + val visits = formatTokenExplored.getOrElse(token, 0) + 1 + formatTokenExplored += token -> visits + } + + def visit(tree: Tree): Unit = { + val visits = treeExplored.getOrElse(tree, 0) + 1 + treeExplored += tree -> visits + } + + def reportTokens() = { + formatTokenExplored.toSeq.sortBy(_._1.right.end).foreach { case (code, count) => logger.debug( f"""$count%-5s $code""".stripMargin) } } - def reportTrees = { + def reportTrees() = { Debug.treeExplored.toSeq.sortBy(_._2).foreach { case (code, count) => logger.debug( f"""$count%-5s @@ -34,8 +50,4 @@ object Debug extends ScalaFmtLogger { } } - def visit(tree: Tree): Unit = { - val visits = treeExplored.getOrElse(tree, 0) + 1 - treeExplored += tree -> visits - } } diff --git a/src/main/scala/org/scalafmt/ScalaFmt.scala b/src/main/scala/org/scalafmt/ScalaFmt.scala index 76f6692c9d..16f4e00895 100644 --- a/src/main/scala/org/scalafmt/ScalaFmt.scala +++ b/src/main/scala/org/scalafmt/ScalaFmt.scala @@ -99,6 +99,8 @@ class ScalaFmt(val style: ScalaStyle) extends ScalaFmtLogger { logger.warn("UNABLE TO FORMAT") } Debug.explored += explored + Debug.state = state + Debug.toks = toks state.reconstructPath(toks, style) } diff --git a/src/main/scala/org/scalafmt/State.scala b/src/main/scala/org/scalafmt/State.scala index 47b8110c2b..8786c9be0c 100644 --- a/src/main/scala/org/scalafmt/State.scala +++ b/src/main/scala/org/scalafmt/State.scala @@ -33,7 +33,8 @@ case class State(cost: Int, * Returns formatted output from FormatTokens and Splits. */ def reconstructPath(toks: Array[FormatToken], - style: ScalaStyle): String = { + style: ScalaStyle, + f: FormatToken => String = _.left.code): String = { // require(splits.length - start.splits.length == toks.length, // s"${toks.toVector} $splits") val sb = new StringBuilder() @@ -42,7 +43,7 @@ case class State(cost: Int, case (tok, split) => // logger.debug(s"${log(tok.left)} $split ${state.indents}") state = state.next(style, split, tok) - sb.append(tok.left.code) + sb.append(f(tok)) val ws = split.modification match { case Space => sb.append(" ") diff --git a/src/test/resources/TypeArgument.test b/src/test/resources/TypeArgument.test index 2038abd8f8..62d417bab5 100644 --- a/src/test/resources/TypeArgument.test +++ b/src/test/resources/TypeArgument.test @@ -64,6 +64,16 @@ object a { function[function[function[a, b], function[c, d]]] } +<<< SKIP Break on higher level of nesting 3x. +object a { + function[function[function[function[a, b], function[c, d]]]] +} +>>> +object a { + function[function[ + function[function[a, b], + function[c, d]]]] +} <<< SKIP Type args and args different policy. object Object { val x = function[a1234567, b1234567](a) diff --git a/src/test/scala/org/scalafmt/FilesUtil.scala b/src/test/scala/org/scalafmt/FilesUtil.scala index cd7d49cb02..09610e3a29 100644 --- a/src/test/scala/org/scalafmt/FilesUtil.scala +++ b/src/test/scala/org/scalafmt/FilesUtil.scala @@ -17,4 +17,9 @@ object FilesUtil { def readFile(filename: String): String = new String( java.nio.file.Files.readAllBytes(java.nio.file.Paths.get(filename))) + def writeFile(filename: String, content: String): Unit = { + val path = java.nio.file.Paths.get(filename) + java.nio.file.Files.write(path, content.getBytes) + } + } diff --git a/src/test/scala/org/scalafmt/FormatTest.scala b/src/test/scala/org/scalafmt/FormatTest.scala index 6af4cd4cd6..30eb36f568 100644 --- a/src/test/scala/org/scalafmt/FormatTest.scala +++ b/src/test/scala/org/scalafmt/FormatTest.scala @@ -7,8 +7,16 @@ import org.scalatest.FunSuite import org.scalatest.concurrent.Timeouts import org.scalatest.time.SpanSugar._ +import scala.collection.mutable + case class DiffTest(spec: String, name: String, original: String, expected: String) +case class Result(test: DiffTest, + obtained: String, + obtainedHtml: String, + redness: Int, + time: Long) + trait FormatTest extends FunSuite with Timeouts with ScalaFmtLogger with BeforeAndAfterAll { @@ -21,6 +29,14 @@ trait FormatTest val fmt = new ScalaFmt(style) lazy val onlyOne = tests.exists(_.name.startsWith("ONLY")) + val reports = mutable.ArrayBuilder.make[Result] + + def red(token: FormatToken): Int = { + val max = 10 + val i = Math.min(max, Debug.formatTokenExplored(token)) + val k = (i.toDouble / max.toDouble * 256).toInt + Math.min(256, 270 - k) + } tests.sortWith { case (left, right) => @@ -31,22 +47,40 @@ trait FormatTest !t.name.startsWith("SKIP") && (!onlyOne || t.name.startsWith("ONLY")) }.foreach { - case DiffTest(spec, name, original, expected) => + case t@DiffTest(spec, name, original, expected) => val testName = s"$spec: $name" test(f"$testName%-50s|") { failAfter(10 seconds) { Debug.clear() val before = Debug.explored - val result = fmt.format(original) + val start = System.currentTimeMillis() + val obtained = fmt.format(original) logger.debug(f"${Debug.explored - before}%-4s $testName") + var maxTok = 0 + val obtainedHtml = + Debug.state.reconstructPath(Debug.toks, style, { tok => + import scalatags.Text.all._ + val color = red(tok) + maxTok = Math.max(Debug.formatTokenExplored(tok), maxTok) + span(background := s"rgb(256, $color, $color)", tok.left.code).render + }) + reports += Result(t, + obtained, + obtainedHtml, + maxTok, + System.currentTimeMillis() - start) + if (name.startsWith("ONLY")) - Debug.reportTokens - assert(result diff expected) + Debug.reportTokens() + assert(obtained diff expected) } } } override def afterAll(configMap: ConfigMap): Unit = { logger.debug(s"Total explored: ${Debug.explored}") + val report = Report.generate(reports.result()) + val filename = "target/index.html" + FilesUtil.writeFile(filename, report) } } diff --git a/src/test/scala/org/scalafmt/Report.scala b/src/test/scala/org/scalafmt/Report.scala new file mode 100644 index 0000000000..8864003bd7 --- /dev/null +++ b/src/test/scala/org/scalafmt/Report.scala @@ -0,0 +1,29 @@ +package org.scalafmt + +import scalatags.Text.all._ + +object Report { + def generate(results: Seq[Result]): String = { + html( + head( + ), + body( + div( + h1(id := "title", "Heatmap"), + for (result <- results.sortBy(-_.redness) + if result.test.name != "Warmup") yield { + div( + h2(result.test.name), + pre( + code( + raw(result.obtainedHtml) + ) + ) + ) + } + ) + ) + ).render + } + +}