scalafmt Opinionated code formatter for Scala - Ólafur Geirsson, @olafurpg

Agenda Why?

Demo.

Scalariform.

How it works.

WHY?

Reason 1: We have better things to do

Reason 2: Refactoring

// Before UserRepo hasEmail me.id flatMap { user => AccountRepo hasStatement user.id flatMap { statement => BalanceRepo hasBalance statement.id } } // After for { user <- UserRepo.hasEmail(me.id) statement <- AccountRepo.hasStatement(user.id) balance <- BalanceRepo.hasBalance(statement.id) } yield balance

Wright et al., “Large-Scale Automated Refactoring Using ClangMR.”

Reason 3: It's tedious // Columns 80 | case class Split(modification: Modification, cost: Int, ignoreIf: Boolean = false, indents: Vector[Indent[Length]], policy: Policy = NoPolicy, penalty: Boolean = false, optimalAt: Option[OptimalToken] = None)

// Columns 80 | case class Split(modification: Modification, cost: Int, ignoreIf: Boolean = false, indents: Vector[Indent[Length]] = Vector.empty[Indent[Length]], policy: Policy = NoPolicy, penalty: Boolean = false, optimalAt: Option[OptimalToken] = None)(implicit val line: sourcecode.Line)

// Columns 80 | case class Split( modification: Modification, cost: Int, ignoreIf: Boolean = false, indents: Vector[Indent[Length]] = Vector.empty[Indent[Length]], policy: Policy = NoPolicy, penalty: Boolean = false, optimalAt: Option[OptimalToken] = None)(implicit val line: sourcecode.Line)

Reason 4: Coding styles are hard

"Any style guide written in English is either so brief that it’s ambiguous, or so long that no one reads it."

-- Bob Nystrom, Hardest Program I've Ever Written , Dart, Google.

Demo

Case-study 1: typelevel/cats

olafurpg/cats/pull/1 Scala files 295 Lines of code 17.493 Time to format 23s Diff +2,672 −2,372 Longest line 136 chars private[data] trait XorTMonadCombine[F[_], L] extends MonadCombine[XorT[F, L, ?]] with XorTMonadFilter[F, L] with XorTSemigroupK[F, L] { - scalafmt command: scalafmt --maxColumn 100 --continuationIndentCallSite 2 --javaDocs --files . -i

Before - implicit def constSemigroup[A: Semigroup, B]: Semigroup[Const[A, B]] = new Semigroup[Const[A, B]] { - ... - } After + implicit def constSemigroup[A : Semigroup, B]: Semigroup[Const[A, B]] = + new Semigroup[Const[A, B]] { + ... + }

Before - Arbitrary(Gen.oneOf( - getArbitrary[A].map(Eval.now(_)), - getArbitrary[A].map(Eval.later(_)), - getArbitrary[A].map(Eval.always(_)))) After + Arbitrary( + Gen.oneOf(getArbitrary[A].map(Eval.now(_)), + getArbitrary[A].map(Eval.later(_)), + getArbitrary[A].map(Eval.always(_))))

Before - /** - * Lift a function into the context of an Arrow - */ After + /** + * Lift a function into the context of an Arrow + */

Before - def traverse[A: Arbitrary, B: Arbitrary, C: Arbitrary, M: Arbitrary, X[_]: Applicative, Y[_]: Applicative](implicit - ArbFA: Arbitrary[F[A]], - ArbXB: Arbitrary[X[B]], - ArbYB: Arbitrary[Y[B]], - ArbYC: Arbitrary[Y[C]], - M: Monoid[M], - ... After + def traverse[A : Arbitrary, + B : Arbitrary, + C : Arbitrary, + M : Arbitrary, + X[_]: Applicative, + Y[_]: Applicative](implicit ArbFA: Arbitrary[F[A]], + ArbXB: Arbitrary[X[B]], + ArbYB: Arbitrary[Y[B]], + ArbYC: Arbitrary[Y[C]], + M: Monoid[M], + ...

Before trait AllInstances - extends FunctionInstances - with StringInstances - with EitherInstances - with ListInstances - with OptionInstances - with SetInstances - with StreamInstances After + extends FunctionInstances with StringInstances with EitherInstances with ListInstances + with OptionInstances with SetInstances with StreamInstances

Case-study 2: lichess.org

olafurpg/lila/pull/1 Scala files 743 Lines of code 63.228 Time to format 51s Diff +3,870 −3,174 Longest line 147 chars protected def FormFuResult[A, B: Writeable: ContentTypeOf](form: Form[A])(err: Form[A] => Fu[B])(op: A => Fu[Result])(implicit req: Request[_]) = - scalafmt command: scalafmt --maxColumn 100 --continuationIndentCallSite 2 --style defaultWithAlign --files . -i //

Before - protected def SocketOptionLimited[A: FrameFormatter](consumer: TokenBucket.Consumer, name: String)(f: Context => Fu[Option[(Iteratee[A, _], Enumerator[A])]]) = - After + protected def SocketOptionLimited[A : FrameFormatter]( + consumer: TokenBucket.Consumer, name: String)( + f: Context => Fu[Option[(Iteratee[A, _], Enumerator[A])]]) =

Before - WS.url(url).get().map(_.body).mon(_.security.proxy.request.time).flatMap { str => - ... - }.addEffects( ... ) After + WS.url(url) + .get() + .map(_.body) + .mon(_.security.proxy.request.time) + .flatMap { str => + ... + } + .addEffects( ... )

Before - case (((((((((((((nbUsers, ranks), nbPlaying), nbImported), crosstable), ratingChart), nbFollowing), nbFollowers), nbBlockers), nbPosts), isDonor), trophies), insightVisible), playTime) => - After + case (((((((((((((nbUsers, ranks), nbPlaying), nbImported), crosstable), + ratingChart), + nbFollowing), + nbFollowers), + nbBlockers), + nbPosts), + isDonor), + trophies), + insightVisible), + playTime) =>

Before - Ok.chunked(Enumerator.outputStream(env.pngExport(game))).withHeaders( - CONTENT_TYPE -> "image/png", - CACHE_CONTROL -> "max-age=7200") After + Ok.chunked(Enumerator.outputStream(env.pngExport(game))) + .withHeaders(CONTENT_TYPE -> "image/png", + CACHE_CONTROL -> "max-age=7200")

+Ok( + Json.obj( + "api" -> Json.obj("current" -> api.currentVersion, + "olds" -> api.oldVersions.map { old => + Json.obj("version" -> old.version, + "deprecatedAt" -> old.deprecatedAt, + "unsupportedAt" -> old.unsupportedAt) + }) + )) as JSON

What it should look like + Ok( + Json.obj( + "api" -> Json.obj("current" -> api.currentVersion, + "olds" -> api.oldVersions.map { old => + Json.obj("version" -> old.version, + "deprecatedAt" -> old.deprecatedAt, + "unsupportedAt" -> old.unsupportedAt) + }) + )) as JSON

Try it yourself SBT plugin

IntelliJ plugin

brew install olafurpg/scalafmt/scalafmt

Download scalafmt.jar via Github releases.

via Github releases. See documentation.

Where are we now? Can format almost any Scala code.

Formatting options: --style default, --style defaultWithAlign, --style scalaJs (experimental) --maxColumn 120 --javaDocs / --scalaDocs --continuationIndentCallSite 2



Scalariform?

--maxColumn

How does scalafmt work?

scala.meta Tokenizer: String => scala.meta.Tokens scala> import scala.meta._ scala> """object Main extends App { world => println(s"Hello $world!") } """.tokenize.get res3: Tokens = Tokens( BOF (0..0), object (0..6), (6..7), Main (7..11), (11..12), extends (12..19), (19..20), App (20..23), (23..24), { (24..25), (25..26), world (26..31), ...

scala.meta Parser: String => scala.meta.Tree scala> import scala.meta._ scala> """object Main extends App { world => println(s"Hello $world!") } """.parse[Stat].get.show[Structure] res9: String = """ Defn.Object(Nil, Term.Name("Main"), Template(Nil, Seq(Ctor.Ref.Name("App")), Term.Param(Nil, Term.Name("world"), None, None), Some(Seq(Term.Apply(Term.Name("println"), Seq(Term.Interpolate(Term.Name("s"), Seq(Lit("Hello "), Lit("!")), Seq(Term.Name("world"))))))))) """

TeX: wrapping text

Every line break has a penalty // 50 columns | object BestFirstSearch { // 1 penalty DBObject(User(Name("Martin", "Odersky"), // 5 penalty Language("Scala")), // 3 penalty Address("Lausanne", "Switzerland")) // 0 penalty } // 0 penalty //---------- // 9 total

Exceeding column limit is expensive // 50 columns | object BestFirstSearch { // 1 penalty DBObject(User(Name("Martin", "Odersky"), Language("Scala")), // 1002 penalty Address("Lausanne", "Switzerland")) // 0 penalty } // 0 penalty //------------- // 1002 total

Try all the combinations using best-first search // 50 columns | object BestFirstSearch { DBObject(User(Name("Martin", "Odersky"), Language("Scala")), Address("Lausanne", "Switzerland")) DBObject( User(Name("Martin", "Odersky"), Language("Scala")), Address("Lausanne", "Switzerland")) DBObject(User(Name("Martin", "Odersky"), Language("Scala")), Address("Lausanne", "Switzerland")) DBObject( User(Name("Martin", "Odersky"), Language("Scala")), Address("Lausanne", "Switzerland")) DBObject(User(Name("Martin", "Odersky"), Language("Scala")), Address("Lausanne", "Switzerland")) }

Testing?

Property 1: AST preservation ast(code) == ast(format(code))

Property 2: Idempotency format(code) == format(format(code)) See #192.

More important problems, vertical alignment object VerticalAlignment { def name = column[String]("name") def status = column[Int]("status") for { dao <- olafur \/> "Can't find olafur" user <- dao.user \/> "Join failed: no user object" } libraryDependencies ++= Seq( "org.scalameta" % "scalameta" % "0.1.0-RC4-M10", "com.lihaoyi" %% "sourcecode" % "0.1.1" ) }

Summary. Code formatting has many benefits.

Coding styles are hard.

scalafmt is out there.

scalafmt is still very young