Skip to content

Commit

Permalink
Load rules from source
Browse files Browse the repository at this point in the history
Implements `file:`/`github:`/...
  • Loading branch information
olafurpg committed May 25, 2018
1 parent 192d497 commit 77f1cfa
Show file tree
Hide file tree
Showing 17 changed files with 348 additions and 284 deletions.
19 changes: 6 additions & 13 deletions scalafix-cli/src/main/scala/scalafix/v1/Args.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package scalafix.v1
import java.io.File
import java.io.PrintStream
import java.net.URI
import java.net.URLClassLoader
import java.nio.charset.Charset
import java.nio.charset.StandardCharsets
import java.nio.file.FileSystems
Expand All @@ -27,7 +26,6 @@ import scalafix.internal.config.ScalafixConfig
import scalafix.internal.diff.DiffDisable
import scalafix.internal.jgit.JGitDiff
import scalafix.internal.reflect.ClasspathOps
import scalafix.internal.util.ClassloadRule
import scalafix.internal.util.SymbolTable
import scalafix.internal.v1.Rules

Expand Down Expand Up @@ -92,15 +90,6 @@ case class Args(
}
}

def getClassloader: ClassLoader =
if (toolClasspath.isEmpty) ClassloadRule.defaultClassloader
else {
new URLClassLoader(
toolClasspath.iterator.map(_.toURI.toURL).toArray,
ClassloadRule.defaultClassloader
)
}

def baseConfig: Configured[(Conf, ScalafixConfig)] = {
val toRead: Option[AbsolutePath] = config.orElse {
val defaultPath = cwd.resolve(".scalafix.conf")
Expand Down Expand Up @@ -139,8 +128,12 @@ case class Args(
} else {
Conf.Lst(rules.map(Conf.fromString))
}
val decoder =
RuleDecoder.decoder(scalafixConfig, getClassloader)
val decoderSettings = RuleDecoder
.Settings()
.withConfig(scalafixConfig)
.withToolClasspath(toolClasspath)
.withCwd(cwd)
val decoder = RuleDecoder.decoder(decoderSettings)
decoder.read(rulesConf).andThen { rules =>
if (rules.isEmpty) ConfError.message("No rules provided").notOk
else rules.withConfig(base)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package scalafix.internal.config

import scala.collection.immutable.Seq
import scala.{meta => m}
import metaconfig.Input
import metaconfig.Position
Expand All @@ -12,7 +11,7 @@ import metaconfig.internal.ConfGet

// TODO(olafur) contribute upstream to metaconfig.
object MetaconfigPendingUpstream {
def traverse[T](lst: List[Configured[T]]): Configured[List[T]] = {
def traverse[T](lst: Seq[Configured[T]]): Configured[List[T]] = {
val buf = List.newBuilder[T]
var err = List.newBuilder[ConfError]
lst.foreach {
Expand Down Expand Up @@ -58,8 +57,14 @@ object MetaconfigPendingUpstream {
def getField[T: ConfDecoder](e: sourcecode.Text[T]): Configured[T] =
conf.getOrElse(e.source)(e.value)
}
implicit class XtensionConfiguredScalafix(`_`: Configured.type) {
implicit class XtensionConfiguredCompanionScalafix(`_`: Configured.type) {
def fromEither[T](either: Either[String, T]): Configured[T] =
either.fold(ConfError.message(_).notOk, Configured.ok)
}
implicit class XtensionConfiguredScalafix[T](configured: Configured[T]) {
def foreach(f: T => Unit): Unit = configured match {
case Configured.Ok(value) => f(value)
case _ =>
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,9 @@ import scala.util.Try
import scala.util.matching.Regex
import scalafix.patch.TreePatch._
import scalafix.rule.ScalafixRules
import scalafix.internal.util.ClassloadRule
import java.io.OutputStream
import java.io.PrintStream
import java.net.URI
import java.net.URLClassLoader
import java.util.regex.Pattern
import java.util.regex.PatternSyntaxException
import scala.collection.immutable.Seq
Expand All @@ -26,7 +24,6 @@ import metaconfig.Configured
import metaconfig.Configured.Ok
import scalafix.internal.config.MetaconfigParser.{parser => hoconParser}
import scalafix.internal.rule.ConfigRule
import scalafix.patch.TreePatch

object ScalafixMetaconfigReaders extends ScalafixMetaconfigReaders
// A collection of metaconfig.Reader instances that are shared across
Expand Down Expand Up @@ -142,23 +139,7 @@ trait ScalafixMetaconfigReaders {
symbolGlobalReader.read(Conf.Str(to))

def classloadRuleDecoder(index: LazySemanticdbIndex): ConfDecoder[Rule] =
ConfDecoder.instance[Rule] {
case UriRuleString("scala" | "class", fqn) =>
val classloader =
if (index.toolClasspath.isEmpty) ClassloadRule.defaultClassloader
else {
val urls =
index.toolClasspath.iterator.map(_.toURI.toURL).toArray
new URLClassLoader(urls, ClassloadRule.defaultClassloader)
}
ClassloadRule(fqn, classloadRule(index), classloader)
case UriRuleString("replace", replace @ SlashSeparated(from, to)) =>
requireSemanticSemanticdbIndex(index, replace) { m =>
parseReplaceSymbol(from, to)
.map(TreePatch.ReplaceSymbol.tupled)
.map(p => Rule.constant(replace, p, m))
}
}
throw new UnsupportedOperationException

def baseSyntacticRuleDecoder: ConfDecoder[Rule] =
baseRuleDecoders(LazySemanticdbIndex.empty)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import scalafix.internal.rule._
import scalafix.internal.util.SuppressOps
import scalafix.lint.LintMessage
import scalafix.patch.Patch
import scalafix.rule.RuleName
import scalafix.rule.ScalafixRules
import scalafix.util.SemanticdbIndex
import scalafix.v1.Doc
Expand All @@ -19,6 +20,8 @@ import scalafix.v1.SemanticRule
import scalafix.v1.SyntacticRule

case class Rules(rules: List[Rule] = Nil) {

def name: RuleName = RuleName(rules.flatMap(_.name.identifiers))
def isEmpty: Boolean = rules.isEmpty
def isSemantic: Boolean = semanticRules.nonEmpty
def withConfig(conf: Conf): Configured[Rules] =
Expand Down
5 changes: 0 additions & 5 deletions scalafix-core/shared/src/main/scala/scalafix/rule/Rule.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,9 @@ package rule

import scala.meta._
import scalafix.internal.config.MetaconfigPendingUpstream
import scalafix.internal.config.ScalafixMetaconfigReaders
import scalafix.internal.config.ScalafixConfig
import scalafix.syntax._
import metaconfig.Conf
import metaconfig.ConfDecoder
import metaconfig.Configured

/** A Scalafix Rule.
Expand Down Expand Up @@ -146,9 +144,6 @@ object Rule {
}
.getOrElse(None)
}
val syntaxRuleConfDecoder: ConfDecoder[Rule] =
ScalafixMetaconfigReaders.ruleConfDecoderSyntactic(
ScalafixMetaconfigReaders.baseSyntacticRuleDecoder)
lazy val empty: Rule = new Rule(RuleName.empty) {}
def emptyConfigured: Configured[Rule] = Configured.Ok(empty)
def emptyFromSemanticdbIndexOpt(index: Option[SemanticdbIndex]): Rule =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package scalafix.internal.reflect

import java.net.URL
import metaconfig.Conf
import metaconfig.ConfError
import metaconfig.Configured
import metaconfig.Configured.Ok

object GitHubUrlRule {
private[this] val GitHubShorthand =
"""github:([^\/]+)\/([^\/]+)\/([^\/]+)""".r
private[this] val GitHubShorthandWithSha =
"""github:([^\/]+)\/([^\/]+)\/([^\/]+)\?sha=(.+)""".r
private[this] val GitHubFallback =
"""github:(.*)""".r

private[this] val alphanumerical = "[^a-zA-Z0-9]"

// approximates the "format=Camel" formatter in giter8.
// http://www.foundweekends.org/giter8/Combined+Pages.html#Formatting+template+fields
private[this] def CamelCase(string: String): String =
string.split(alphanumerical).mkString.capitalize

// approximates the "format=Snake" formatter in giter8.
private[this] def SnakeCase(string: String): String =
string.split(alphanumerical).map(_.toLowerCase).mkString("_")

private[this] def expandGitHubURL(
org: String,
repo: String,
version: String,
sha: String): URL = {
val fileName = s"${CamelCase(repo)}_${SnakeCase(version)}.scala"
new URL(
s"https://raw.githubusercontent.com/$org/$repo/$sha/scalafix/rules/src/main/scala/fix/$fileName")
}

def unapply(arg: Conf.Str): Option[Configured[URL]] = arg.value match {
case GitHubShorthandWithSha(org, repo, version, sha) =>
Option(Ok(expandGitHubURL(org, repo, version, sha)))
case GitHubShorthand(org, repo, version) =>
Option(Ok(expandGitHubURL(org, repo, version, "master")))
case GitHubFallback(invalid) =>
Some(
ConfError
.message(s"""Invalid url 'github:$invalid'. Valid formats are:
|- github:org/repo/version
|- github:org/repo/version?sha=branch""".stripMargin)
.notOk)
case _ => None
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package scalafix.internal.reflect

import java.io.FileNotFoundException
import java.net.URL
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import metaconfig.Conf
import metaconfig.Configured
import metaconfig.Configured.Ok
import metaconfig.Input
import scala.collection.concurrent.TrieMap
import scala.meta.io.AbsolutePath
import scalafix.SemanticdbIndex
import scalafix.internal.config.ScalafixMetaconfigReaders.UriRule
import scalafix.internal.util.FileOps
import scalafix.internal.v1.LegacySemanticRule
import scalafix.internal.v1.LegacySyntacticRule
import scalafix.rule.Rule
import scalafix.rule.SemanticRule
import scalafix.v0
import scalafix.v1

object RuleDecoderOps {

val legacySemanticRuleClass: Class[SemanticRule] =
classOf[scalafix.rule.SemanticRule]
val legacyRuleClass: Class[Rule] =
classOf[scalafix.rule.Rule]
def toRule(cls: Class[_]): v1.Rule = {
if (legacySemanticRuleClass.isAssignableFrom(cls)) {
val fn: SemanticdbIndex => v0.Rule = { index =>
val ctor = cls.getDeclaredConstructor(classOf[SemanticdbIndex])
ctor.setAccessible(true)
ctor.newInstance(index).asInstanceOf[v0.Rule]
}
new LegacySemanticRule(fn(SemanticdbIndex.empty).name, fn)
} else if (legacyRuleClass.isAssignableFrom(cls)) {
val ctor = cls.getDeclaredConstructor()
ctor.setAccessible(true)
new LegacySyntacticRule(ctor.newInstance().asInstanceOf[v0.Rule])
} else {
val ctor = cls.getDeclaredConstructor()
ctor.setAccessible(true)
cls.newInstance().asInstanceOf[v1.Rule]
}
}

def tryClassload(classloader: ClassLoader, fqn: String): Option[v1.Rule] = {
try {
Some(toRule(classloader.loadClass(fqn)))
} catch {
case _: ClassNotFoundException | _: NoSuchMethodException =>
try {
Some(toRule(classloader.loadClass(fqn + "$")))
} catch {
case _: ClassNotFoundException =>
None
}
}
}

object UrlRule {
def unapply(arg: Conf.Str): Option[Configured[URL]] = arg match {
case UriRule("http" | "https", uri) if uri.isAbsolute =>
Option(Ok(uri.toURL))
case GitHubUrlRule(url) => Option(url)
case _ => None
}
}

// Warning: evil global mutable state
val scalafixRoot: Path = Files.createTempDirectory("scalafix")
scalafixRoot.toFile.deleteOnExit()
val fileCache: TrieMap[Int, Path] =
scala.collection.concurrent.TrieMap.empty[Int, Path]

class FromSourceRule(cwd: AbsolutePath) {
private def getTempFile(url: URL, code: String): Path =
fileCache.getOrElseUpdate(
code.hashCode, {
val filename = Paths.get(url.getPath).getFileName.toString
val tmp = Files.createTempFile(scalafixRoot, filename, ".scala")
Files.write(tmp, code.getBytes)
tmp
}
)
def unapply(arg: Conf.Str): Option[Configured[Input]] = arg match {
case UriRule("file", uri) =>
val path = AbsolutePath(Paths.get(uri.getSchemeSpecificPart))(cwd)
Option(Ok(Input.File(path.toNIO)))
case UrlRule(Ok(url)) =>
try {
val code = FileOps.readURL(url)
val file = getTempFile(url, code)
Option(Ok(Input.File(file)))
} catch {
case _: FileNotFoundException =>
Option(Configured.error(s"404 - not found $url"))
}
case _ => None
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,19 @@ object RuleInstrumentation {
def getRuleFqn(code: Input): Configured[Seq[String]] = {
object ExtendsRule {
def unapply(templ: Template): Boolean = templ match {

// v0
case Template(_, init"Rewrite" :: _, _, _) => true
case Template(_, init"Rule($_)" :: _, _, _) => true
case Template(_, init"SemanticRewrite($_)" :: _, _, _) => true
case Template(_, init"SemanticRule($_, $_)" :: _, _, _) => true

// v1
case Template(_, init"SemanticRule($_)" :: _, _, _) => true
case Template(_, init"v1.SemanticRule($_)" :: _, _, _) => true
case Template(_, init"SyntacticRule($_)" :: _, _, _) => true
case Template(_, init"v1.SyntacticRule($_)" :: _, _, _) => true

case _ => false
}
}
Expand All @@ -42,12 +51,8 @@ object RuleInstrumentation {
stats.foreach(s => loop(prefix :+ ref.syntax, s))
case Defn.Object(_, name, ExtendsRule()) =>
add(prefix :+ name.syntax)
case Defn.Object(_, name, _) =>
tree.children.foreach(s => loop(prefix :+ name.syntax, s))
case Defn.Class(_, name, _, _, ExtendsRule()) =>
add(prefix :+ name.syntax)
case Defn.Val(_, Pat.Var(name) :: Nil, _, LambdaRule()) =>
add(prefix :+ name.syntax)
case _ =>
tree.children.foreach(s => loop(prefix, s))
}
Expand Down
Loading

0 comments on commit 77f1cfa

Please sign in to comment.