diff --git a/core/src/main/scala/scalafix/config/ScalafixMetaconfigReaders.scala b/core/src/main/scala/scalafix/config/ScalafixMetaconfigReaders.scala index 58cdb234b..923d6be8d 100644 --- a/core/src/main/scala/scalafix/config/ScalafixMetaconfigReaders.scala +++ b/core/src/main/scala/scalafix/config/ScalafixMetaconfigReaders.scala @@ -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 @@ -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) } diff --git a/core/src/main/scala/scalafix/rewrite/Rewrite.scala b/core/src/main/scala/scalafix/rewrite/Rewrite.scala index 03df9a13d..a0caeca7b 100644 --- a/core/src/main/scala/scalafix/rewrite/Rewrite.scala +++ b/core/src/main/scala/scalafix/rewrite/Rewrite.scala @@ -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 { diff --git a/core/src/main/scala/scalafix/rewrite/RewriteCtx.scala b/core/src/main/scala/scalafix/rewrite/RewriteCtx.scala index 14e922598..c79d706d9 100644 --- a/core/src/main/scala/scalafix/rewrite/RewriteCtx.scala +++ b/core/src/main/scala/scalafix/rewrite/RewriteCtx.scala @@ -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] = diff --git a/core/src/main/scala/scalafix/util/FileOps.scala b/core/src/main/scala/scalafix/util/FileOps.scala index ae18d3787..1c992aafd 100644 --- a/core/src/main/scala/scalafix/util/FileOps.scala +++ b/core/src/main/scala/scalafix/util/FileOps.scala @@ -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 { @@ -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)) } diff --git a/core/src/main/scala/scalafix/util/Replacer.scala b/core/src/main/scala/scalafix/util/Replacer.scala index 1c62abb93..19db3a434 100644 --- a/core/src/main/scala/scalafix/util/Replacer.scala +++ b/core/src/main/scala/scalafix/util/Replacer.scala @@ -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) => diff --git a/core/src/main/scala/scalafix/util/RewriteInstrumentation.scala b/core/src/main/scala/scalafix/util/RewriteInstrumentation.scala new file mode 100644 index 000000000..10ea5b409 --- /dev/null +++ b/core/src/main/scala/scalafix/util/RewriteInstrumentation.scala @@ -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 + } + } +} diff --git a/core/src/main/scala/scalafix/util/ScalafixToolbox.scala b/core/src/main/scala/scalafix/util/ScalafixToolbox.scala new file mode 100644 index 000000000..4266eda22 --- /dev/null +++ b/core/src/main/scala/scalafix/util/ScalafixToolbox.scala @@ -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))) + } +} diff --git a/rewrites/MyRewrite.scala b/rewrites/MyRewrite.scala new file mode 100644 index 000000000..046248ac4 --- /dev/null +++ b/rewrites/MyRewrite.scala @@ -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") + ) + } +} +} diff --git a/rewrites/MyRewrite2.scala b/rewrites/MyRewrite2.scala new file mode 100644 index 000000000..4f6d1a2bd --- /dev/null +++ b/rewrites/MyRewrite2.scala @@ -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") + ) + } +} diff --git a/scalafix-nsc/src/test/resources/checkSyntax/FileRewrite.source b/scalafix-nsc/src/test/resources/checkSyntax/FileRewrite.source new file mode 100644 index 000000000..dd6972ee0 --- /dev/null +++ b/scalafix-nsc/src/test/resources/checkSyntax/FileRewrite.source @@ -0,0 +1,9 @@ +rewrites = [ + "file:rewrites/MyRewrite.scala" +] +<<< NOWRAP basic +object Name +>>> +import scala.collection.immutable.Seq +object Name1 + diff --git a/scalafix-nsc/src/test/resources/checkSyntax/FileRewrite2.source b/scalafix-nsc/src/test/resources/checkSyntax/FileRewrite2.source new file mode 100644 index 000000000..e143211d3 --- /dev/null +++ b/scalafix-nsc/src/test/resources/checkSyntax/FileRewrite2.source @@ -0,0 +1,9 @@ +rewrites = [ + "file:rewrites/MyRewrite2.scala" +] +<<< NOWRAP basic +object Name +>>> +import scala.collection.immutable.Seq +object Name1 + diff --git a/scalafix-nsc/src/test/resources/checkSyntax/FqnRewrite.source b/scalafix-nsc/src/test/resources/checkSyntax/FqnRewrite.source index f6fbc5f59..7d6fa869b 100644 --- a/scalafix-nsc/src/test/resources/checkSyntax/FqnRewrite.source +++ b/scalafix-nsc/src/test/resources/checkSyntax/FqnRewrite.source @@ -1,5 +1,5 @@ rewrites = [ - _root_.scalafix.test.FqnRewrite + "scala:scalafix.test.FqnRewrite" ] <<< NOWRAP add import import scala._ diff --git a/scalafix-nsc/src/test/resources/checkSyntax/UrlRewrite.source b/scalafix-nsc/src/test/resources/checkSyntax/UrlRewrite.source new file mode 100644 index 000000000..58811ef6d --- /dev/null +++ b/scalafix-nsc/src/test/resources/checkSyntax/UrlRewrite.source @@ -0,0 +1,7 @@ +rewrites = [ + "https://gist.githubusercontent.com/olafurpg/2a4b1ee14c831fb7cab556b4b471c976/raw/e42891cd705eed6b1411b92a71f9f4f4a399a4f1/MyRewrite.scala" +] +<<< NOWRAP basic +object Name +>>> +object Name1 diff --git a/scalafix-nsc/src/test/scala/scalafix/SemanticTests.scala b/scalafix-nsc/src/test/scala/scalafix/SemanticTests.scala index 037171b40..3aeff889b 100644 --- a/scalafix-nsc/src/test/scala/scalafix/SemanticTests.scala +++ b/scalafix-nsc/src/test/scala/scalafix/SemanticTests.scala @@ -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 @@ -215,3 +217,4 @@ class SemanticTests extends FunSuite with DiffAssertions { self => } } } +