Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Format using shortest path search. #1

Merged
merged 1 commit into from
Jan 13, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
triggeredMessage in ThisBuild := Watched.clearWhenTriggered

lazy val scalafmt = project.in(file("scalafmt")).settings(
name := "scalafmt",
organization := "org.scalafmt",
version := "0.0.1-SNAPSHOT",
scalaVersion := "2.11.7",
resolvers += "Sonatype OSS Snapshots" at "https://oss.sonatype.org/content/repositories/snapshots",
libraryDependencies ++= Seq(
"com.googlecode.java-diff-utils" % "diffutils" % "1.3.0",
"com.typesafe.scala-logging" %% "scala-logging" % "3.1.0",
"ch.qos.logback" % "logback-classic" % "1.1.3",
"org.scalameta" %% "scalameta" % "0.1.0-SNAPSHOT",
"org.scalatest" %% "scalatest" % "2.2.1" % "test"
)
)


2 changes: 1 addition & 1 deletion project/build.properties
Original file line number Diff line number Diff line change
@@ -1 +1 @@
sbt.version=0.13.7
sbt.version=0.13.9
21 changes: 21 additions & 0 deletions scalafmt/src/main/resources/logback.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<withJansi>true</withJansi>
<encoder>
<pattern>[%highlight(%-5level)] %-25(%file:%line) %msg%n</pattern>
</encoder>
</appender>

<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<file>/tmp/test.log</file>
<append>true</append>
<encoder>
<pattern>%d{HH:mm:ss.SSS} TKD [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>

<root level="debug">
<appender-ref ref="STDOUT" />
<appender-ref ref="FILE" />
</root>
</configuration>
204 changes: 204 additions & 0 deletions scalafmt/src/main/scala/org/scalafmt/ScalaFmt.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
package org.scalafmt

import scala.collection.mutable
import scala.meta._
import scala.meta.tokens.Token._

trait Split

case object NoSplit extends Split

case object Space extends Split

case object Newline extends Split

/**
* A state represents one potential solution to reach token at index,
* @param cost The penalty for using path
* @param index The index of the current token.
* @param path The splits/decicions made to reach here.
*/
case class State(cost: Int,
index: Int,
path: List[Split]) extends Ordered[State] {

import scala.math.Ordered.orderingToOrdered

def compare(that: State): Int =
(-this.cost, this.index) compare(-that.cost, that.index)
}

class ScalaFmt(style: ScalaStyle) extends ScalaFmtLogger {

/**
* Pretty-prints Scala code.
*/
def format(code: String): String = {
val source = code.parse[Source]
val realTokens = source.tokens.filter(!_.isInstanceOf[Whitespace])
val path = shortestPath(source, realTokens)
val sb = new StringBuilder()
realTokens.zip(path).foreach {
case (tok, split) =>
sb.append(tok.code)
split match {
case Space =>
sb.append(" ")
case Newline =>
sb.append("\n")
case NoSplit =>
}
}
sb.toString()
}

/**
* Runs Dijstra's shortest path algorithm to find lowest penalty split.
*/
def shortestPath(source: Source, realTokens: Tokens): List[Split] = {
val owners = getOwners(source)
val Q = new mutable.PriorityQueue[State]()
var explored = 0
// First state.
Q += State(0, 0, Nil)
while (Q.nonEmpty) {
val curr = Q.dequeue()
explored += 1
if (explored % 100000 == 0)
println(explored)
val tokens = realTokens
.drop(curr.index)
.dropWhile(_.isInstanceOf[Whitespace])
val left = tokens.head
if (left.isInstanceOf[EOF])
return curr.path.reverse
val right = tokens.tail
.find(!_.isInstanceOf[Whitespace])
.getOrElse(tokens.last)
val between = tokens.drop(1).takeWhile(_.isInstanceOf[Whitespace])
val splits = splitPenalty(owners, left, between, right)
splits.foreach {
case (split, cost) =>
Q.enqueue(State(curr.cost + cost, curr.index + 1, split :: curr.path))
}
}
// Could not find path to final token.
???
}

/**
* Assigns cost of splitting between two non-whitespace tokens.
*/
def splitPenalty(owners: Map[Token, Tree],
left: Token,
between: Tokens,
right: Token): List[(Split, Int)] = {
(left, right) match {
case (_: BOF, _) => List(
NoSplit -> 0
)
case (_, _: EOF) => List(
NoSplit -> 0
)
case (_, _) if left.name.startsWith("xml") &&
right.name.startsWith("xml") => List(
NoSplit -> 0
)
case (_, _: `,`) => List(
NoSplit -> 0
)
case (_: `,`, _) => List(
Space -> 0,
Newline -> 1
)
case (_: `{`, _) => List(
Space -> 0,
Newline -> 0
)
case (_, _: `{`) => List(
Space -> 0
)
case (_, _: `}`) => List(
Space -> 0,
Newline -> 1
)
case (_, _: `:`) => List(
NoSplit -> 0
)
case (_, _: `=`) => List(
Space -> 0
)
case (_: `:` | _: `=`, _) => List(
Space -> 0
)
case (_, _: `@`) => List(
Newline -> 0
)
case (_: `@`, _) => List(
NoSplit -> 0
)
case (_: Ident, _: `.` | _: `#`) => List(
NoSplit -> 0
)
case (_: `.` | _: `#`, _: Ident) => List(
NoSplit -> 0
)
case (_: Ident | _: Literal, _: Ident | _: Literal) => List(
Space -> 0
)
case (_, _: `)` | _: `]`) => List(
NoSplit -> 0
)
case (_, _: `(` | _: `[`) => List(
NoSplit -> 0
)
case (_: `(` | _: `[`, _) => List(
NoSplit -> 0,
Newline -> 1
)
case (_, _: `val`) => List(
Space -> 0,
Newline -> 1
)
case (_: Keyword | _: Modifier, _) => List(
Space -> 1,
Newline -> 2
)
case (_, _: Keyword) => List(
Space -> 0,
Newline -> 1
)
case (_, c: Comment) => List(
Space -> 0
)
case (c: Comment, _) =>
if (c.code.startsWith("//")) List(Newline -> 0)
else List(Space -> 0, Newline -> 1)
case (_, _: Delim) => List(
Space -> 0
)
case (_: Delim, _) => List(
Space -> 0
)
case _ =>
logger.debug(s"60 ===========\n${log(left)}\n${log(between)}\n${log(right)}")
???
}
}

/**
* Creates lookup table from token to its closest scala.meta contains tree.
*/
def getOwners(source: Source): Map[Token, Tree] = {
val result = mutable.Map.empty[Token, Tree]
def loop(x: Tree): Unit = {
x.tokens
.foreach { tok =>
result += tok -> x
}
x.children.foreach(loop)
}
loop(source)
result.toMap
}
}
42 changes: 42 additions & 0 deletions scalafmt/src/main/scala/org/scalafmt/ScalaFmtLogger.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package org.scalafmt

import com.typesafe.scalalogging.Logger
import org.slf4j.LoggerFactory

import scala.meta.Tree
import scala.meta.prettyprinters.Structure
import scala.meta.tokens.Token
import scala.meta.tokens.Tokens

trait ScalaFmtLogger {
val logger = Logger(LoggerFactory.getLogger(this.getClass))

private def getTokenClass(token: Token) =
token.getClass.getName.stripPrefix("scala.meta.tokens.Token$")

def log(token: Token): String = f"$token%30s ${getTokenClass(token)}"
def log(tokens: Token*): String = tokens.map(log).mkString("\n")
def log(tokens: Tokens): String = tokens.map(log).mkString("\n")

def header[T](t: T): String = {
val line = s"=" * (t.toString.length + 3)
s"$line\n=> $t\n$line"
}

def reveal(s: String): String =
s.replaceAll("\n", "¶")
.replaceAll(" ", "∙")



def log(t: Tree, line: Int): Unit = {
logger.debug(
s"""${header(line)}
|TYPE: ${t.getClass.getName.stripPrefix("scala.meta.")}
|SOURCE: $t
|STRUCTURE: ${t.show[Structure]}
|TOKENS: ${t.tokens.map(x => reveal(x.code)).mkString(",")}
|""".stripMargin)
}
}

4 changes: 4 additions & 0 deletions scalafmt/src/main/scala/org/scalafmt/ScalaStyle.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package org.scalafmt

trait ScalaStyle
case object Standard extends ScalaStyle
45 changes: 45 additions & 0 deletions scalafmt/src/test/resources/basic.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
40 columns |
<<< Object definition fits in one line
@foobar object a {val x:Int=1}
>>>
@foobar object a { val x: Int = 1 }
<<< Pathological case
@ foobar("annot", {
val x = 2
val y = 2 // y=2
x + y
})
object
a extends b with c {
def
foo[T:Int#Double#Triple,
R <% String](
@annot1
x
: Int @annot2 = 2
, y: Int = 3): Int = {
"match" match {
case 1 | 2 =>
3
case <A>2</A> => 2
}
}
}
>>>
@foobar("annot", {
val x = 2
val y = 2 // y=2
x + y
})
object a extends b with c {
def foo[
T:Int#Double#Triple,
R <% String](
@annot1 x : Int @annot2 = 2,
y: Int = 3): Int = {
"match" match {
case 1 | 2 => 3
case <A>2</A> => 2
}
}
}
49 changes: 49 additions & 0 deletions scalafmt/src/test/scala/org/scalafmt/DiffUtil.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package org.scalafmt

import java.io.File
import java.text.SimpleDateFormat
import java.util.Date
import java.util.TimeZone

import org.scalatest.exceptions.TestFailedException

object DiffUtil extends ScalaFmtLogger {

implicit class DiffExtension(obtained: String) {
def diff(expected: String): Boolean = {
val result = compareContents(obtained, expected)
if (result.isEmpty) true
else throw new TestFailedException(
s"""
|${header("Obtained")}
|$obtained
|
|${header("Diff")}
|$result
""".stripMargin, 1)
}
}

def compareContents(original: String, revised: String): String = {
compareContents(original.split("\n"), revised.split("\n"))
}

def compareContents(original: Seq[String],
revised: Seq[String]): String = {
import collection.JavaConverters._
val diff = difflib.DiffUtils.diff(original.asJava, revised.asJava)
if (diff.getDeltas.isEmpty) ""
else difflib.DiffUtils.generateUnifiedDiff(
"original", "revised",original.asJava, diff, 1).asScala.drop(3).mkString("\n")
}

def fileModificationTimeOrEpoch(file: File): String = {
val format = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss Z")
if (file.exists)
format.format(new Date(file.lastModified()))
else {
format.setTimeZone(TimeZone.getTimeZone("UTC"))
format.format(new Date(0L))
}
}
}
Loading