Skip to content

Commit

Permalink
Support dynamic loading of rewrites from uri. (#90)
Browse files Browse the repository at this point in the history
This commit adds support to load rewrites using different protocols

- file:path/to/rewrite.scala
- https://github.com/...
- scala:fully.qualified.Name

We use the scala.reflect toolbox to compile and load rewrites from code.
We instrument the code to make it compile with scala.reflect Toolbox.
  • Loading branch information
olafurpg authored Mar 13, 2017
1 parent dccc042 commit 0bc4f19
Show file tree
Hide file tree
Showing 14 changed files with 226 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,21 @@ import scala.meta.Ref
import scala.meta.parsers.Parse
import scala.meta.semantic.v1.Symbol
import scala.reflect.ClassTag
import scala.util.Try
import scala.util.matching.Regex
import scalafix.Failure.UnknownRewrite
import scalafix.rewrite.ScalafixMirror
import scalafix.rewrite.ScalafixRewrite
import scalafix.rewrite.ScalafixRewrites
import scalafix.util.ClassloadObject
import scalafix.util.FileOps
import scalafix.util.ScalafixToolbox
import scalafix.util.TreePatch._

import java.io.File
import java.net.URI
import java.net.URL

import metaconfig.Reader
import org.scalameta.logger

Expand All @@ -26,11 +34,53 @@ trait ScalafixMetaconfigReaders {
ReaderUtil.oneOf[Dialect](Scala211, Sbt0137, Dotty, Paradise211)
}

object ClassloadRewrite {
def unapply(arg: String): Option[String] = arg match {
case UriRewrite("scala", uri) =>
val suffix = if (arg.endsWith("$")) "" else "$"
Option(uri.getSchemeSpecificPart + suffix)
case _ => None
}
}

object UriRewrite {
def unapply(arg: String): Option[(String, URI)] =
for {
uri <- Try(new URI(arg)).toOption
scheme <- Option(uri.getScheme)
} yield scheme -> uri
}

object UrlRewrite {
def unapply(arg: String): Option[URL] = arg match {
case UriRewrite("http" | "https", uri) if uri.isAbsolute =>
Option(uri.toURL)
case _ => None
}
}

object FileRewrite {
def unapply(arg: String): Option[File] = arg match {
case UriRewrite("file", uri) =>
Option(new File(uri.getSchemeSpecificPart).getAbsoluteFile)
case _ => None
}
}

object FromSourceRewrite {
def unapply(arg: String): Option[String] = arg match {
case FileRewrite(file) => Option(FileOps.readFile(file))
case UrlRewrite(url) => Option(FileOps.readURL(url))
case _ => None
}
}

implicit lazy val rewriteReader: Reader[ScalafixRewrite] =
Reader.instance[ScalafixRewrite] {
case fqn: String if fqn.startsWith("_root_.") =>
val suffix = if (fqn.endsWith("$")) "" else "$"
ClassloadObject[ScalafixRewrite](fqn.stripPrefix("_root_.") + suffix)
case ClassloadRewrite(fqn) =>
ClassloadObject[ScalafixRewrite](fqn)
case FromSourceRewrite(code) =>
ScalafixToolbox.getRewrite[ScalafixMirror](code)
case els =>
ReaderUtil.fromMap(ScalafixRewrites.name2rewrite).read(els)
}
Expand Down
3 changes: 3 additions & 0 deletions core/src/main/scala/scalafix/rewrite/Rewrite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ abstract class Rewrite[-A](implicit sourceName: sourcecode.Name) {
def name: String = sourceName.value
override def toString: String = name
def rewrite[B <: A](ctx: RewriteCtx[B]): Seq[Patch]

def andThen[B <: A](other: Rewrite[B]): Rewrite[B] =
Rewrite(ctx => this.rewrite(ctx) ++ other.rewrite(ctx))
}

object Rewrite {
Expand Down
10 changes: 7 additions & 3 deletions core/src/main/scala/scalafix/rewrite/RewriteCtx.scala
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,14 @@ class RewriteCtx[+A](implicit val tree: Tree,

object RewriteCtx {

/** Constructor for a syntactic rewrite. */
/** Constructor for a syntactic rewrite.
*
* Syntactic rewrite ctx is RewriteCtx[Null] because it is a subtype of any
* other rewritectx.
*/
def apply(tree: Tree,
config: ScalafixConfig = ScalafixConfig()): RewriteCtx[None.type] =
apply(tree, config, None)
config: ScalafixConfig = ScalafixConfig()): RewriteCtx[Null] =
apply(tree, config, null)

/** Constructor for a generic rewrite. */
def apply[T](tree: Tree, config: ScalafixConfig, mirror: T): RewriteCtx[T] =
Expand Down
8 changes: 7 additions & 1 deletion core/src/main/scala/scalafix/util/FileOps.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import java.io.BufferedReader
import java.io.File
import java.io.FileReader
import java.io.PrintWriter
import java.net.URI
import java.net.URL

object FileOps {

Expand All @@ -26,12 +28,16 @@ object FileOps {
}
}

def readURL(url: URL): String = {
scala.io.Source.fromURL(url)("UTF-8").getLines().mkString("\n")
}

/**
* Reads file from file system or from http url.
*/
def readFile(filename: String): String = {
if (filename matches "https?://.*") {
scala.io.Source.fromURL(filename)("UTF-8").getLines().mkString("\n")
readURL(new URL(filename))
} else {
readFile(new File(filename))
}
Expand Down
8 changes: 4 additions & 4 deletions core/src/main/scala/scalafix/util/Replacer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,10 @@ object Renamer {
implicit ctx: RewriteCtx[T]): Seq[TokenPatch] = {
object MatchingRename {
def unapply(arg: Name): Option[(Token, Name)] =
renames.find(_.from eq arg).map { rename =>
val tok = arg.tokens(ctx.config.dialect).head
tok -> rename.to
}
for {
rename <- renames.find(_.from eq arg)
tok <- arg.tokens.headOption
} yield tok -> rename.to
}
ctx.tree.collect {
case MatchingRename(tok, to) =>
Expand Down
49 changes: 49 additions & 0 deletions core/src/main/scala/scalafix/util/RewriteInstrumentation.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package scalafix.util

import scala.meta._
import scala.collection.immutable.Seq
import scalafix.util.TreeExtractors._

object RewriteInstrumentation {

// removes syntax that is not supported by toolbox
private def simplify(tree: Tree): Tree = tree.transform {
// packages are not supported by toolbox.
case Source(Seq(Pkg(ref, stats))) => Source(q"import $ref._" +: stats)
case Source(stats) =>
Source(stats.flatMap {
case Import(is) => is.map(i => Import(Seq(i)))
case x => Seq(x)
})
}

def instrument(code: String): String = {
code.parse[Source] match {
case parsers.Parsed.Success(ast) =>
val rewriteName: Seq[String] = ast.collect {
case Defn.Object(_,
name,
Template(_, ctor"Rewrite[$_]" :: _, _, _)) =>
name.value
case Defn.Val(_, Pat.Var.Term(name) :: Nil, _, q"Rewrite[$_]($_)")
`:WithParent:` (_: Template)
`:WithParent:` Defn.Object(_, parentName, _) =>
s"$parentName.$name"
}
rewriteName match {
case Nil =>
sys.error(s"Found no rewrite in code! \n $code")
case _ =>
val folded = rewriteName.tail.foldLeft(rewriteName.head)(
_ + ".andThen(" + _ + ")")
val transformed = simplify(ast)
s"""
|${transformed.syntax}
|
|$folded
|""".stripMargin
}
case _ => code
}
}
}
28 changes: 28 additions & 0 deletions core/src/main/scala/scalafix/util/ScalafixToolbox.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package scalafix.util

import scala.collection.immutable.Seq
import scala.collection.mutable
import scala.util.control.NonFatal
import scalafix.rewrite.Rewrite
import scalafix.util.TreeExtractors._

object ScalafixToolbox {
import scala.reflect.runtime.universe._
import scala.tools.reflect.ToolBox
private val tb = runtimeMirror(getClass.getClassLoader).mkToolBox()
private val rewriteCache: mutable.WeakHashMap[String, Any] =
mutable.WeakHashMap.empty

def getRewrite[T](code: String): Either[Throwable, Rewrite[T]] =
try {
Right(
compile(RewriteInstrumentation.instrument(code))
.asInstanceOf[Rewrite[T]])
} catch {
case NonFatal(e) => Left(e)
}

private def compile(code: String): Any = {
rewriteCache.getOrElseUpdate(code, tb.eval(tb.parse(code)))
}
}
25 changes: 25 additions & 0 deletions rewrites/MyRewrite.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package foo.bar {

import scalafix._
import scalafix.rewrite._
import scalafix.util._
import scalafix.util.TreePatch._
import scala.collection.immutable.Seq
import scala.meta._

case object MyRewrite extends Rewrite[Any] {
def rewrite[T](ctx: RewriteCtx[T]): Seq[Patch] = {
ctx.tree.collect {
case n: scala.meta.Name => Rename(n, Term.Name(n.syntax + "1"))
}
}
}

case object MyRewrite2 extends Rewrite[Any] {
def rewrite[T](ctx: RewriteCtx[T]): Seq[Patch] = {
Seq(
AddGlobalImport(importer"scala.collection.immutable.Seq")
)
}
}
}
21 changes: 21 additions & 0 deletions rewrites/MyRewrite2.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import scalafix._
import scalafix.rewrite._
import scalafix.util._
import scalafix.util.TreePatch._
import scala.collection.immutable.Seq
import scala.meta._
import scalafix.config.ScalafixConfig

object Rewrites {
val myRewrite = Rewrite[Any] { ctx =>
ctx.tree.collect {
case n: scala.meta.Name => Rename(n, Term.Name(n.syntax + "1"))
}
}

val myRewrite2 = Rewrite[Any] { ctx =>
Seq(
AddGlobalImport(importer"scala.collection.immutable.Seq")
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
rewrites = [
"file:rewrites/MyRewrite.scala"
]
<<< NOWRAP basic
object Name
>>>
import scala.collection.immutable.Seq
object Name1

Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
rewrites = [
"file:rewrites/MyRewrite2.scala"
]
<<< NOWRAP basic
object Name
>>>
import scala.collection.immutable.Seq
object Name1

Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
rewrites = [
_root_.scalafix.test.FqnRewrite
"scala:scalafix.test.FqnRewrite"
]
<<< NOWRAP add import
import scala._
Expand Down
7 changes: 7 additions & 0 deletions scalafix-nsc/src/test/resources/checkSyntax/UrlRewrite.source
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
rewrites = [
"https://gist.githubusercontent.com/olafurpg/2a4b1ee14c831fb7cab556b4b471c976/raw/e42891cd705eed6b1411b92a71f9f4f4a399a4f1/MyRewrite.scala"
]
<<< NOWRAP basic
object Name
>>>
object Name1
3 changes: 3 additions & 0 deletions scalafix-nsc/src/test/scala/scalafix/SemanticTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ import scalafix.config.ScalafixConfig
import scalafix.nsc.ScalafixNscPlugin
import scalafix.rewrite.ExplicitImplicit
import scalafix.rewrite.Rewrite
import scalafix.rewrite.RewriteCtx
import scalafix.rewrite.ScalafixRewrite
import scalafix.util.DiffAssertions
import scalafix.util.ScalafixToolbox
import scalafix.util.logger

import java.io.File
Expand Down Expand Up @@ -215,3 +217,4 @@ class SemanticTests extends FunSuite with DiffAssertions { self =>
}
}
}

0 comments on commit 0bc4f19

Please sign in to comment.