Skip to content

Commit

Permalink
cats.data.Xor/XorT to Either/EitherT rewrite (+7 squashed commits)
Browse files Browse the repository at this point in the history
Squashed commits:
[b5aaec5] Pushing a few experiments for feedback.
[445b2cd] Experiment using desugared to rewrite xor
[13935bf] WIP
[40caaa3] Pick up configuration in sbt plugin.
[ab34355] Use build-info for friendlier intellij setup
[a5b666a] Setup hocon configuration.
[c94fef1] Cross-build to 2.12 (#28)

* Upgrade scalameta and cross-build to 2.12

* Gracefully handle 2.11 and 2.12 in sbt plugin.

* Don't rely on sbt.ivy.home.
  • Loading branch information
olafurpg authored and ShaneDelmore committed Dec 26, 2016
1 parent efdbe17 commit 5d9bf0d
Show file tree
Hide file tree
Showing 38 changed files with 689 additions and 163 deletions.
21 changes: 20 additions & 1 deletion .drone.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,26 @@ build:
environment:
- COURSIER_CACHE=/drone/cache/coursier
commands:
- ./bin/testAll.sh
- export SBT_OPTS="-Xmx24G -XX:MaxPermSize=4G -Xss4M"
# configuring ivy.home doesn't seem to work. Maybe related:
# https://github.com/sbt/sbt/issues/1894
# After 10+ experiments I've given up on trying to use sbt.ivy.yhome and
# copy the files myself instead, as recommended here:
# http://readme.drone.io/usage/caching/
- test -d /drone/.sbt && cp -a /drone/.sbt /root
- rm -rf /drone/.sbt

- test -d /drone/.ivy2 && cp -a /drone/.ivy2 /root
- rm -rf /drone/.ivy2

- sbt "very publishLocal" # necessary for 2.11/2.12 cross publish.
- sbt clean test scripted

- cp -a /root/.ivy2 /drone
- cp -a /root/.sbt /drone
- sbt clean test scripted
cache:
mount:
- /drone/.sbt
- /drone/.ivy2
- /drone/cache
4 changes: 0 additions & 4 deletions bin/testAll.sh

This file was deleted.

82 changes: 53 additions & 29 deletions build.sbt
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import sbt.ScriptedPlugin
import sbt.ScriptedPlugin._
import sbtbuildinfo.BuildInfoKey.Entry
organization in ThisBuild := "ch.epfl.scala"
version in ThisBuild := scalafix.Versions.nightly
version in ThisBuild := "0.2.0-SNAPSHOT"

lazy val crossVersions = Seq("2.11.8", "2.12.1")

lazy val buildSettings = Seq(
assemblyJarName in assembly := "scalafix.jar",
// See core/src/main/scala/ch/epfl/scala/Versions.scala
scalaVersion := scalafix.Versions.scala,
scalaVersion := "2.11.8",
updateOptions := updateOptions.value.withCachedResolution(true)
)

Expand Down Expand Up @@ -70,14 +72,28 @@ lazy val noPublish = Seq(
publishLocal := {}
)

lazy val allSettings = commonSettings ++ buildSettings ++ publishSettings
lazy val buildInfoSettings: Seq[Def.Setting[_]] = Seq(
buildInfoKeys := Seq[BuildInfoKey](
name,
version,
scalaVersion,
sbtVersion
),
buildInfoPackage := "scalafix",
buildInfoObject := "Versions"
)

lazy val allSettings =
commonSettings ++
buildSettings ++
publishSettings

lazy val root = project
lazy val `scalafix-root` = project
.in(file("."))
.settings(moduleName := "scalafix")
.settings(allSettings)
.settings(noPublish)
.settings(
moduleName := "scalafix",
allSettings,
noPublish,
initialCommands in console :=
"""
|import scala.meta._
Expand All @@ -88,32 +104,37 @@ lazy val root = project
`scalafix-nsc`,
`scalafix-tests`,
core,
cli,
// cli, // superseded by sbt plugin
readme,
`scalafix-sbt`
)
.dependsOn(core)

lazy val core = project
.settings(allSettings)
.settings(
allSettings,
buildInfoSettings,
crossScalaVersions := crossVersions,
moduleName := "scalafix-core",
addCompilerPlugin(
"org.scalamacros" % "paradise" % "2.1.0" cross CrossVersion.full),
libraryDependencies ++= Seq(
"com.lihaoyi" %% "sourcecode" % "0.1.2",
"com.typesafe" % "config" % "1.3.1",
"com.lihaoyi" %% "sourcecode" % "0.1.3",
"org.scalameta" %% "scalameta" % Build.metaV,
"org.scala-lang" % "scala-reflect" % scalaVersion.value,
// Test dependencies
"org.scalatest" %% "scalatest" % Build.testV % "test",
"com.googlecode.java-diff-utils" % "diffutils" % "1.3.0" % "test"
)
)
.enablePlugins(BuildInfoPlugin)

lazy val `scalafix-nsc` = project
.settings(
allSettings,
scalaVersion := "2.11.8",
crossScalaVersions := crossVersions,
libraryDependencies ++= Seq(
"org.scala-lang" % "scala-compiler" % scalaVersion.value,
"org.scalameta" %% "scalameta" % Build.metaV % "provided",
Expand Down Expand Up @@ -176,24 +197,27 @@ lazy val publishedArtifacts = Seq(
publishLocal in core
)

lazy val `scalafix-sbt` = project.settings(
allSettings,
ScriptedPlugin.scriptedSettings,
sbtPlugin := true,
scripted := scripted.dependsOn(publishedArtifacts: _*).evaluated,
scalaVersion := "2.10.5",
moduleName := "sbt-scalafix",
sources in Compile +=
baseDirectory.value / "../core/src/main/scala/scalafix/Versions.scala",
scriptedLaunchOpts := Seq(
"-Dplugin.version=" + version.value,
// .jvmopts is ignored, simulate here
"-XX:MaxPermSize=256m",
"-Xmx2g",
"-Xss2m"
),
scriptedBufferLog := false
)
lazy val `scalafix-sbt` = project
.settings(
allSettings,
buildInfoSettings,
ScriptedPlugin.scriptedSettings,
sbtPlugin := true,
// Doesn't work because we need to publish 2.11 and 2.12.
// scripted := scripted.dependsOn(publishedArtifacts: _*).evaluated,
scalaVersion := "2.10.5",
crossScalaVersions := Seq(scalaVersion.value),
moduleName := "sbt-scalafix",
scriptedLaunchOpts ++= Seq(
"-Dplugin.version=" + version.value,
// .jvmopts is ignored, simulate here
"-XX:MaxPermSize=256m",
"-Xmx2g",
"-Xss2m"
),
scriptedBufferLog := false
)
.enablePlugins(BuildInfoPlugin)

lazy val `scalafix-tests` = project
.settings(
Expand Down
2 changes: 1 addition & 1 deletion cli/src/main/scala/scalafix/cli/Cli.scala
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ case class CommonOptions(
)

@AppName("scalafix")
@AppVersion(scalafix.Versions.nightly)
@AppVersion(scalafix.Versions.version)
@ProgName("scalafix")
case class ScalafixOptions(
@HelpMessage(
Expand Down
29 changes: 27 additions & 2 deletions core/src/main/scala/scalafix/ScalafixConfig.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,42 @@ import scala.meta.Dialect
import scala.meta.Tree
import scala.meta.dialects.Scala211
import scala.meta.parsers.Parse
import scala.util.control.NonFatal
import scalafix.rewrite.Rewrite

import java.io.File

import com.typesafe.config.Config
import com.typesafe.config.ConfigFactory

case class ScalafixConfig(
rewrites: Seq[Rewrite] = Rewrite.allRewrites,
parser: Parse[_ <: Tree] = Parse.parseSource,
dialect: Dialect = Scala211
)

object ScalafixConfig {
def fromNames(names: List[String]): Either[String, ScalafixConfig] = {
private def saferThanTypesafe(
op: () => Config): Either[String, ScalafixConfig] =
try fromConfig(op())
catch {
case NonFatal(e) => Left(e.getMessage)
}

def fromFile(file: File): Either[String, ScalafixConfig] =
saferThanTypesafe(() => ConfigFactory.parseFile(file))

def fromString(contents: String): Either[String, ScalafixConfig] =
saferThanTypesafe(() => ConfigFactory.parseString(contents))

def fromConfig(config: Config): Either[String, ScalafixConfig] = {
import scala.collection.JavaConverters._
if (config.hasPath("rewrites"))
fromNames(config.getStringList("rewrites").asScala.toList)
else Right(ScalafixConfig())
}

def fromNames(names: List[String]): Either[String, ScalafixConfig] =
names match {
case "all" :: Nil =>
Right(ScalafixConfig(rewrites = Rewrite.allRewrites))
Expand All @@ -29,5 +55,4 @@ object ScalafixConfig {
Right(ScalafixConfig(rewrites = rewrites))
}
}
}
}
7 changes: 0 additions & 7 deletions core/src/main/scala/scalafix/Versions.scala

This file was deleted.

2 changes: 1 addition & 1 deletion core/src/main/scala/scalafix/rewrite/Rewrite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ object Rewrite {
}

val syntaxRewrites: Seq[Rewrite] = Seq(ProcedureSyntax, VolatileLazyVal)
val semanticRewrites: Seq[Rewrite] = Seq(ExplicitImplicit)
val semanticRewrites: Seq[Rewrite] = Seq(ExplicitImplicit, Xor2Either)
val allRewrites: Seq[Rewrite] = syntaxRewrites ++ semanticRewrites
val name2rewrite: Map[String, Rewrite] =
allRewrites.map(x => x.toString -> x).toMap
Expand Down
13 changes: 11 additions & 2 deletions core/src/main/scala/scalafix/rewrite/SemanticApi.scala
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package scalafix.rewrite

import scala.meta.Defn
import scala.meta.Type
import scala.meta.{Defn, Term, Tree, Type}
import scala.meta.parsers.Parse

/** A custom semantic api for scalafix rewrites.
*
Expand All @@ -18,4 +18,13 @@ trait SemanticApi {

/** Returns the type annotation for given val/def. */
def typeSignature(defn: Defn): Option[Type]

def desugared[A <: Tree](tree: A)(implicit parse: Parse[A]): Option[A]

class Desugared[T <: Tree: Parse] {
def unapply(original: T): Option[T] = desugared(original)
}

object DType extends Desugared[Type]
object DTerm extends Desugared[Term]
}
53 changes: 53 additions & 0 deletions core/src/main/scala/scalafix/rewrite/Xor2Either.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package scalafix.rewrite

import scala.collection.immutable.Seq
import scala.{meta => m}
import scalafix.util._
import scala.meta._

case object Xor2Either extends Rewrite {
override def rewrite(ast: m.Tree, ctx: RewriteCtx): Seq[Patch] = {
implicit val semanticApi: SemanticApi = getSemanticApi(ctx)

//Create a sequence of type replacements
val replacementTypes = List(
ReplaceType(t"cats.data.XorT", t"cats.data.EitherT", "EitherT"),
ReplaceType(t"cats.data.Xor", t"scala.util.Either", "Either"),
ReplaceType(t"cats.data.Xor.Left", t"scala.util.Left", "Left"),
ReplaceType(t"cats.data.Xor.Right", t"scala.util.Either.Right", "Right")
)

//Add in some method replacements
val replacementTerms = List(
ReplaceTerm(q"cats.data.Xor.Right.apply",
q"scala.util.Right.apply",
q"scala.util"),
ReplaceTerm(q"cats.data.Xor.Left.apply",
q"scala.util.Left.apply",
q"scala.util")
)

//Then add needed imports.
//todo - derive this from patches created, types
//and terms replaced
//Only add if they are not already imported
val additionalImports = List(
"cats.data.EitherT",
"cats.implicits._",
"scala.util.Either"
)

val typeReplacements =
new ChangeType(ast).gatherPatches(replacementTypes)

val termReplacements =
new ChangeMethod(ast).gatherPatches(replacementTerms)

//Make this additional imports, beyond what can be derived from the types
val addedImports =
if (typeReplacements.isEmpty && termReplacements.isEmpty) Seq[Patch]()
else new AddImport(ast).gatherPatches(additionalImports)

addedImports ++ typeReplacements ++ termReplacements
}
}
37 changes: 37 additions & 0 deletions core/src/main/scala/scalafix/util/AddImport.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package scalafix.util

import scala.collection.immutable.Seq
import scala.{meta => m}
import scala.meta._
import scalafix.rewrite._

class AddImport(ast: m.Tree)(implicit sApi: SemanticApi) {
val allImports = ast.collect {
case t @ q"import ..$importersnel" => t -> importersnel
}

val firstImport = allImports.headOption
val firstImportFirstToken = firstImport.flatMap {
case (importStatement, _) => importStatement.tokens.headOption
}
val tokenBeforeFirstImport = firstImportFirstToken.flatMap { stopAt =>
ast.tokens.takeWhile(_ != stopAt).lastOption
}

//This is currently a very dumb implementation.
//It does no checking for existing imports and makes
//no attempt to consolidate imports
def addedImports(importString: String): Seq[Patch] =
tokenBeforeFirstImport
.map(
beginImportsLocation =>
Patch
.insertAfter(beginImportsLocation, importString)
)
.toList

def gatherPatches(imports: Seq[String]): Seq[Patch] = {
val importStrings = imports.map("import " + _).mkString("\n", "\n", "\n")
addedImports(importStrings)
}
}
Loading

0 comments on commit 5d9bf0d

Please sign in to comment.