Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support loading custom rules by their name instead of FQN #781

Merged
merged 2 commits into from
Aug 3, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ object RuleDecoderOps {
val legacyRuleClass: Class[v0.Rule] =
classOf[scalafix.rule.Rule]
def toRule(cls: Class[_]): v1.Rule = {
assertNotOutdatedScalafixRule(cls)
if (legacySemanticRuleClass.isAssignableFrom(cls)) {
val fn: v0.SemanticdbIndex => v0.Rule = { index =>
val ctor = cls.getDeclaredConstructor(classOf[v0.SemanticdbIndex])
Expand All @@ -43,6 +44,18 @@ object RuleDecoderOps {
}
}

def assertNotOutdatedScalafixRule(cls: Class[_]): Unit = {
cls.getSuperclass.getName match {
case "scalafix.rule.Rule" | "scalafix.rule.SemanticRule" =>
// Custom rule is using 0.5 API that is not supported here.
throw new IllegalArgumentException(
"Outdated Scalafix rule, please upgrade to the latest Scalafix version"
)
case _ =>
()
}
}

def tryClassload(classloader: ClassLoader, fqn: String): Option[v1.Rule] = {
try {
Some(toRule(classloader.loadClass(fqn)))
Expand Down
95 changes: 56 additions & 39 deletions scalafix-reflect/src/main/scala/scalafix/v1/RuleDecoder.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package scalafix.v1

import java.net.URLClassLoader
import java.util.ServiceLoader
import metaconfig.Conf
import metaconfig.ConfDecoder
import metaconfig.ConfError
Expand All @@ -17,6 +18,7 @@ import scalafix.internal.util.ClassloadRule
import scalafix.internal.v1.Rules
import scalafix.patch.TreePatch
import scalafix.v1
import scala.collection.JavaConverters._

/** One-stop shop for loading scalafix rules from strings. */
object RuleDecoder {
Expand All @@ -34,7 +36,6 @@ object RuleDecoder {
rule: String,
settings: Settings
): List[Configured[v1.Rule]] = {
val FromSource = new FromSourceRule(settings.cwd)
lazy val classloader =
if (settings.toolClasspath.isEmpty) ClassloadRule.defaultClassloader
else {
Expand All @@ -43,47 +44,63 @@ object RuleDecoder {
ClassloadRule.defaultClassloader
)
}
Rules.defaults.find(_.name.matches(rule)) match {
case Some(r) => Configured.ok(r) :: Nil
case _ =>
Conf.Str(rule) match {
// Patch.replaceSymbols(from , to)
case UriRuleString("replace", replace @ SlashSeparated(from, to)) =>
val constant = parseReplaceSymbol(from, to)
.map(TreePatch.ReplaceSymbol.tupled)
.map(p => scalafix.v1.SemanticRule.constant(replace, p))
constant :: Nil
// Classload rule from classloader
case UriRuleString("scala" | "class", fqn) =>
tryClassload(classloader, fqn) match {
case Some(r) =>
Configured.ok(r) :: Nil
case _ =>
ConfError.message(s"Class not found: $fqn").notOk :: Nil
}
// Compile rules from source with file/github/http protocols
case FromSource(input) =>
input match {
val customRules =
ServiceLoader.load(classOf[v1.Rule], classloader)
val allRules =
Iterator(Rules.defaults.iterator, customRules.iterator().asScala).flatten
allRules.find(_.name.matches(rule)) match {
case Some(r) =>
Configured.ok(r) :: Nil
case None =>
fromStringURI(rule, classloader, settings)
}
}

// Attempts to load a rule as if it was a URI, for example 'class:FQN' or 'github:org/repo/v1'
private def fromStringURI(
rule: String,
classloader: ClassLoader,
settings: Settings
): List[Configured[v1.Rule]] = {
val FromSource = new FromSourceRule(settings.cwd)
Conf.Str(rule) match {
// Patch.replaceSymbols(from, to)
case UriRuleString("replace", replace @ SlashSeparated(from, to)) =>
val constant = parseReplaceSymbol(from, to)
.map(TreePatch.ReplaceSymbol.tupled)
.map(p => scalafix.v1.SemanticRule.constant(replace, p))
constant :: Nil
// Classload rule from classloader
case UriRuleString("scala" | "class", fqn) =>
tryClassload(classloader, fqn) match {
case Some(r) =>
Configured.ok(r) :: Nil
case _ =>
ConfError.message(s"Class not found: $fqn").notOk :: Nil
}
// Compile rules from source with file/github/http protocols
case FromSource(input) =>
input match {
case Configured.NotOk(err) => err.notOk :: Nil
case Configured.Ok(code) =>
ScalafixToolbox.getRule(code, settings.toolClasspath) match {
case Configured.NotOk(err) => err.notOk :: Nil
case Configured.Ok(code) =>
ScalafixToolbox.getRule(code, settings.toolClasspath) match {
case Configured.NotOk(err) => err.notOk :: Nil
case Configured.Ok(CompiledRules(loader, names)) =>
val x = names.iterator.map { fqn =>
tryClassload(loader, fqn) match {
case Some(r) =>
Configured.ok(r)
case _ =>
ConfError
.message(s"Failed to classload rule $fqn")
.notOk
}
}.toList
x
}
case Configured.Ok(CompiledRules(loader, names)) =>
val x = names.iterator.map { fqn =>
tryClassload(loader, fqn) match {
case Some(r) =>
Configured.ok(r)
case _ =>
ConfError
.message(s"Failed to classload rule $fqn")
.notOk
}
}.toList
x
}
}

case _ =>
Configured.error(s"Unknown rule '$rule'") :: Nil
}
}

Expand Down
9 changes: 9 additions & 0 deletions scalafix-tests/input/src/main/scala/test/ServiceLoaders.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
rules = [
SemanticRuleV1
SyntacticRuleV1
]
*/
package test

object ServiceLoaders
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package test

object ServiceLoaders

object SemanticRuleV1
object SyntacticRuleV1
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
banana.rule.SemanticRuleV1
banana.rule.SyntacticRuleV1