Skip to content

Commit

Permalink
feature: Allow to run all scalafix rules on a file
Browse files Browse the repository at this point in the history
Previously, it would not be possible to run sclafix inside metals aside from organize imports. Now, it's possible to run a list of predefined rules and ones added in settings.
  • Loading branch information
tgodzik committed Jun 5, 2022
1 parent fd67e71 commit 018907b
Show file tree
Hide file tree
Showing 6 changed files with 264 additions and 18 deletions.
12 changes: 5 additions & 7 deletions metals/src/main/scala/scala/meta/internal/metals/Embedded.scala
Expand Up @@ -281,13 +281,11 @@ object Embedded {
resolution = resolutionParams
)

def organizeImportRule(scalaBinaryVersion: String): List[Path] = {
val dep = Dependency.of(
"com.github.liancheng",
s"organize-imports_$scalaBinaryVersion",
BuildInfo.organizeImportVersion
)
downloadDependency(dep, scalaVersion = None)
def rulesClasspath(dependencies: List[Dependency]): List[Path] = {
for {
dep <- dependencies
path <- downloadDependency(dep, scalaVersion = None)
} yield path
}

def toClassLoader(
Expand Down
Expand Up @@ -1740,6 +1740,22 @@ class MetalsLanguageServer(
Option(params.uri).map(_.toAbsolutePath)
)
}.asJavaObject
case ServerCommands.RunScalafix(params) =>
val uri = params.getTextDocument().getUri()
scalafixProvider
.runAllRules(
uri.toAbsolutePath
)
.flatMap { edits =>
languageClient
.applyEdit(
new l.ApplyWorkspaceEditParams(
new l.WorkspaceEdit(Map(uri -> edits.asJava).asJava)
)
)
.asScala
}
.asJavaObject
case ServerCommands.ChooseClass(params) =>
fileDecoderProvider
.chooseClassFromFile(
Expand Down
Expand Up @@ -20,6 +20,8 @@ import scala.meta.internal.mtags.SemanticdbClasspath
import scala.meta.internal.semanticdb.TextDocuments
import scala.meta.io.AbsolutePath

import com.typesafe.config.ConfigFactory
import coursierapi.Dependency
import org.eclipse.lsp4j.MessageParams
import org.eclipse.lsp4j.MessageType
import org.eclipse.{lsp4j => l}
Expand All @@ -40,7 +42,7 @@ case class ScalafixProvider(
)(implicit ec: ExecutionContext) {
import ScalafixProvider._
private val scalafixCache = TrieMap.empty[ScalaBinaryVersion, Scalafix]
private val organizeImportRuleCache =
private val rulesClassloaderCache =
TrieMap.empty[ScalaBinaryVersion, URLClassLoader]

// Warms up the Scalafix instance so that the first organize imports request responds faster.
Expand All @@ -57,7 +59,13 @@ case class ScalafixProvider(
val contents = "object Main{}\n"
tmp.writeText(contents)
for (target <- targets)
scalafixEvaluate(tmp, target, contents, produceSemanticdb = true)
scalafixEvaluate(
tmp,
target,
contents,
produceSemanticdb = true,
List(organizeImportRuleName)
)

tmp.delete()
} catch {
Expand All @@ -69,9 +77,46 @@ case class ScalafixProvider(
}
}

def runAllRules(file: AbsolutePath): Future[List[l.TextEdit]] = {
val definedRules = rulesFromScalafixConf()
val result = for {
buildId <- buildTargets.inverseSources(file)
target <- buildTargets.scalaTarget(buildId)
runRules = ScalafixProvider
.knownRules(
target.scalaBinaryVersion,
target.scalaVersion,
userConfig()
)
.keySet
.intersect(definedRules)
.toList
if runRules.nonEmpty
} yield {
runScalafixRules(
file,
target,
runRules
)
}
result.getOrElse(Future.successful(Nil))
}

def organizeImports(
file: AbsolutePath,
scalaTarget: ScalaTarget
): Future[List[l.TextEdit]] = {
runScalafixRules(
file,
scalaTarget,
List(organizeImportRuleName)
)
}

def runScalafixRules(
file: AbsolutePath,
scalaTarget: ScalaTarget,
rules: List[String],
retried: Boolean = false
): Future[List[l.TextEdit]] = {
val fromDisk = file.toInput
Expand All @@ -83,7 +128,8 @@ case class ScalafixProvider(
file,
scalaTarget,
inBuffers.value,
retried || isUnsaved(inBuffers.text, fromDisk.text)
retried || isUnsaved(inBuffers.text, fromDisk.text),
rules
)

scalafixEvaluation match {
Expand Down Expand Up @@ -114,7 +160,7 @@ case class ScalafixProvider(
scribe.error(scalafixError, exception)
if (!retried && hasStaleSemanticdb(results)) {
// Retry, since the semanticdb might be stale
organizeImports(file, scalaTarget, retried = true)
runScalafixRules(file, scalaTarget, rules, retried = true)
} else {
Future.failed(exception)
}
Expand Down Expand Up @@ -240,6 +286,24 @@ case class ScalafixProvider(
}
}

private def rulesFromScalafixConf(): Set[String] = {
scalafixConf(false) match {
case None => Set.empty
case Some(configPath) =>
val conf = ConfigFactory.parseFile(configPath.toFile)
if (conf.hasPath("rules"))
conf
.getList("rules")
.map { item =>
item.unwrapped().toString()
}
.asScala
.toSet
else Set.empty
}

}

/**
* Tries to use the Scalafix rule to organize imports.
*
Expand All @@ -255,7 +319,8 @@ case class ScalafixProvider(
file: AbsolutePath,
scalaTarget: ScalaTarget,
inBuffers: String,
produceSemanticdb: Boolean
produceSemanticdb: Boolean,
rules: List[String]
): Try[ScalafixEvaluation] = {
val isScala3 = ScalaVersions.isScala3Version(scalaTarget.scalaVersion)
val scalaBinaryVersion =
Expand Down Expand Up @@ -293,6 +358,7 @@ case class ScalafixProvider(
api <- getScalafix(scalaBinaryVersion)
urlClassLoaderWithExternalRule <- getRuleClassLoader(
scalaBinaryVersion,
scalaVersion,
api.getClass.getClassLoader
)
} yield {
Expand All @@ -314,7 +380,7 @@ case class ScalafixProvider(
.withClasspath(classpath)
.withToolClasspath(urlClassLoaderWithExternalRule)
.withConfig(scalafixConf(isScala3).asJava)
.withRules(List(organizeImportRuleName).asJava)
.withRules(rules.asJava)
.withPaths(List(diskFilePath.toNIO).asJava)
.withSourceroot(sourceroot.toNIO)
.withScalacOptions(scalacOptions)
Expand Down Expand Up @@ -365,19 +431,29 @@ case class ScalafixProvider(

private def getRuleClassLoader(
scalaBinaryVersion: ScalaBinaryVersion,
scalaVersion: String,
scalafixClassLoader: ClassLoader
): Try[URLClassLoader] = {
organizeImportRuleCache.get(scalaBinaryVersion) match {
rulesClassloaderCache.get(scalaBinaryVersion) match {
case Some(value) => Success(value)
case None =>
statusBar.trackBlockingTask("Downloading organize import rule") {

val organizeImportRule =
Try(Embedded.organizeImportRule(scalaBinaryVersion)).map { paths =>
Try(
Embedded.rulesClasspath(
ScalafixProvider
.knownRules(scalaBinaryVersion, scalaVersion, userConfig())
.values
.flatten
.toList
)
).map { paths =>
val classloader = Embedded.toClassLoader(
Classpath(paths.map(AbsolutePath(_))),
scalafixClassLoader
)
organizeImportRuleCache.update(scalaBinaryVersion, classloader)
rulesClassloaderCache.update(scalaBinaryVersion, classloader)
classloader
}
organizeImportRule
Expand All @@ -403,5 +479,51 @@ object ScalafixProvider {
case class ScalafixRunException(msg: String) extends Exception(msg)

val organizeImportRuleName = "OrganizeImports"
val explicitResultTypesRuleName = "ExplicitResultTypes"

// TODO refresh cache if settings changed
// TODO if unknown rule encountered suggested adding to defaults or to config
// TODO only classload defined rules
def knownRules(
scalaBinaryVersion: String,
scalaVersion: String,
userConfig: UserConfiguration
): Map[String, Option[Dependency]] = {
val fromSettings = userConfig.scalafixRulesDependencies.flatMap {
case (name, dependencyString) =>
Try {
Dependency.parse(
dependencyString,
coursierapi.ScalaVersion.of(scalaVersion)
)
} match {
case Failure(exception) =>
scribe.warn(s"Could not download `${dependencyString}`", exception)
None
case Success(dep) =>
Some(name -> Some(dep))
}
}
val all = Map(
organizeImportRuleName -> Some(
Dependency.of(
"com.github.liancheng",
s"organize-imports_$scalaBinaryVersion",
BuildInfo.organizeImportVersion
)
),
explicitResultTypesRuleName -> None,
"NoAutoTupling" -> None,
"RemoveUnused" -> None,
"DisableSyntax" -> None,
"LeakingImplicitClassVal" -> None,
"NoValInForComprehension" -> None,
"ProcedureSyntax" -> None,
"RedundantSyntax" -> None
) ++ fromSettings
if (scalaBinaryVersion.startsWith("3"))
all.filter { case (key, _) => key != explicitResultTypesRuleName }
else all

}
}
Expand Up @@ -158,6 +158,17 @@ object ServerCommands {
|""".stripMargin
)

val RunScalafix = new ParametrizedCommand[TextDocumentPositionParams](
"scalafix-run",
"Run all Scalafix Rules",
"""|Run all the supported scalafix rules in your codebase.
|
|If the rules are missing please add them to user configuration `metals.scalafixRulesDependencies`.
|""".stripMargin,
"""|This command should be sent in with the LSP [`TextDocumentPositionParams`](https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentPositionParams)
|""".stripMargin
)

val CascadeCompile = new Command(
"compile-cascade",
"Cascade compile",
Expand Down Expand Up @@ -539,6 +550,7 @@ object ServerCommands {
ResetChoicePopup,
RestartBuildServer,
RunDoctor,
RunScalafix,
DecodeFile,
DisconnectBuildServer,
ListBuildTargets,
Expand Down
Expand Up @@ -50,7 +50,8 @@ case class UserConfiguration(
excludedPackages: Option[List[String]] = None,
fallbackScalaVersion: Option[String] = None,
testUserInterface: TestUserInterfaceKind = TestUserInterfaceKind.CodeLenses,
javaFormatConfig: Option[JavaFormatConfig] = None
javaFormatConfig: Option[JavaFormatConfig] = None,
scalafixRulesDependencies: Map[String, String] = Map.empty
) {

def currentBloopVersion: String =
Expand Down Expand Up @@ -490,6 +491,10 @@ object UserConfiguration {
)
)

val scalafixRulesDependencies =
getStringMap("scalafix-rules-dependencies")
.getOrElse(Map.empty)

if (errors.isEmpty) {
Right(
UserConfiguration(
Expand Down Expand Up @@ -517,7 +522,8 @@ object UserConfiguration {
excludedPackages,
defaultScalaVersion,
disableTestCodeLenses,
javaFormatConfig
javaFormatConfig,
scalafixRulesDependencies
)
)
} else {
Expand Down

0 comments on commit 018907b

Please sign in to comment.