Skip to content

Commit

Permalink
initial support for running rules on Scala 3 sources
Browse files Browse the repository at this point in the history
- use sbt 1.3+ SemanticdbPlugin to offload the choice of the right
  compiler options to get semanticdb output from the compiler or the
  compiler plugin (with the right semanticdb and Scala full version)
  depending on the Scala binary version
- extend SemanticRuleValidator to detect Scala 3 compiler flag injected
  by SemanticdbPlugin
- always pass the scala version for scalafix to parse with the right
  dialect
  • Loading branch information
github-brice-jaglin committed May 11, 2021
1 parent dcaed38 commit 5e14eb5
Show file tree
Hide file tree
Showing 17 changed files with 177 additions and 36 deletions.
1 change: 1 addition & 0 deletions project/Dependencies.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import sbt._

object Dependencies {
val x = List(1) // scalafix:ok
// when bumping remove dep on SNAPSHOT in sbt-1.5 scripted tests
def scalafixVersion: String = "0.9.27"
val all = List(
"org.eclipse.jgit" % "org.eclipse.jgit" % "5.11.0.202103091610-r",
Expand Down
23 changes: 11 additions & 12 deletions src/main/scala/scalafix/internal/sbt/SemanticRuleValidator.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,23 @@ package scalafix.internal.sbt

import java.nio.file.Path

import sbt.{CrossVersion, ModuleID}
import sbt.ModuleID

import scala.collection.mutable.ListBuffer

class SemanticRuleValidator(ifNotFound: SemanticdbNotFound) {
def findErrors(
files: Seq[Path],
dependencies: Seq[ModuleID],
scalacOpts: Seq[String],
interface: ScalafixInterface
): Seq[String] = {
if (files.isEmpty) Nil
else {
val errors = ListBuffer.empty[String]
val hasSemanticdb =
dependencies.exists(_.name.startsWith("semanticdb-scalac"))
dependencies.exists(_.name.startsWith("semanticdb-scalac")) ||
scalacOpts.contains("-Xsemanticdb")
if (!hasSemanticdb)
errors += ifNotFound.message
val invalidArguments = interface.validate()
Expand All @@ -30,27 +32,24 @@ class SemanticRuleValidator(ifNotFound: SemanticdbNotFound) {

class SemanticdbNotFound(
ruleNames: Seq[String],
scalaVersion: String,
sbtVersion: String
scalaVersion: String
) {
def message: String = {
val names = ruleNames.mkString(", ")

val recommendedSetting = CrossVersion.partialVersion(sbtVersion) match {
case Some((1, n)) if n < 3 => atMostSbt12
case Some((0, _)) => atMostSbt12
case _ => atLeastSbt13(scalaVersion)
}
val recommendedSetting =
if (SemanticdbPlugin.available) semanticdbPluginSetup(scalaVersion)
else manualSetup

s"""|The semanticdb-scalac compiler plugin is required to run semantic rules like $names.
s"""|The scalac compiler should produce semanticdb files to run semantic rules like $names.
|To fix this problem for this sbt shell session, run `scalafixEnable` and try again.
|To fix this problem permanently for your build, add the following settings to build.sbt:
|
|$recommendedSetting
|""".stripMargin
}

private def atLeastSbt13(scalaVersion: String) =
private def semanticdbPluginSetup(scalaVersion: String) =
s"""inThisBuild(
| List(
| scalaVersion := "$scalaVersion",
Expand All @@ -60,7 +59,7 @@ class SemanticdbNotFound(
|)
|""".stripMargin

private val atMostSbt12 =
private val manualSetup =
s"""addCompilerPlugin(scalafixSemanticdb)
|scalacOptions += "-Yrangepos"
|""".stripMargin
Expand Down
19 changes: 19 additions & 0 deletions src/main/scala/scalafix/internal/sbt/SemanticdbPlugin.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package scalafix.internal.sbt

import sbt._

import scala.util.Try

/** Helper to use sbt 1.3+ SemanticdbPlugin features when available */
object SemanticdbPlugin {

// Copied from https://github.com/sbt/sbt/blob/v1.3.0/main/src/main/scala/sbt/Keys.scala#L190-L195
val semanticdbEnabled = settingKey[Boolean]("")
val semanticdbVersion = settingKey[String]("")

lazy val available = Try {
Class.forName("sbt.plugins.SemanticdbPlugin")
true
}.getOrElse(false)

}
87 changes: 69 additions & 18 deletions src/main/scala/scalafix/sbt/ScalafixEnable.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ package scalafix.sbt
import sbt._
import sbt.Keys._
import sbt.internal.sbtscalafix.Compat
import scalafix.internal.sbt.SemanticdbPlugin

/** Command to automatically enable semanticdb-scalac for shell session */
/** Command to automatically enable semanticdb compiler output for shell session
*/
object ScalafixEnable {

/** sbt 1.0 and 0.13 compatible implementation of partialVersion */
Expand All @@ -13,24 +15,73 @@ object ScalafixEnable {
(a.toLong, b.toLong)
}

lazy val partialToFullScalaVersion: Map[(Long, Long), String] = (for {
/** If the provided Scala binary version is supported, return the full version
* required for running semanticdb-scalac.
*/
private lazy val semanticdbScalacFullScalaVersion
: PartialFunction[(Long, Long), String] = (for {
v <- BuildInfo.supportedScalaVersions
p <- partialVersion(v).toList
} yield p -> v).toMap

def projectsWithMatchingScalaVersion(
state: State
): Seq[(ProjectRef, String)] = {
val extracted = Project.extract(state)
for {
p <- extracted.structure.allProjectRefs
version <- scalaVersion.in(p).get(extracted.structure.data).toList
partialVersion <- partialVersion(version).toList
fullVersion <- partialToFullScalaVersion.get(partialVersion).toList
} yield p -> fullVersion
/** If the provided Scala binary version is supported, return the full version
* required for running semanticdb-scalac or None if support is built-in in
* the compiler and the full version does not need to be adjusted.
*/
private lazy val maybeSemanticdbScalacFullScalaVersion
: PartialFunction[(Long, Long), Option[String]] =
semanticdbScalacFullScalaVersion.andThen(Some.apply).orElse {
// semanticdb is built-in in the Scala 3 compiler
case (major, _) if major == 3 => None
}

/** Collect projects across the entire build, using the partial function
* accepting a Scala binary version
*/
private def collectProjects[U](
extracted: Extracted,
pf: PartialFunction[(Long, Long), U]
): Seq[(ProjectRef, U)] = for {
p <- extracted.structure.allProjectRefs
version <- scalaVersion.in(p).get(extracted.structure.data).toList
partialVersion <- partialVersion(version).toList
res <- pf.lift(partialVersion).toList
} yield p -> res

lazy val command =
if (SemanticdbPlugin.available) withSemanticdbPlugin
else withSemanticdbScalac

private lazy val withSemanticdbPlugin = Command.command(
"scalafixEnable",
briefHelp = "Configure SemanticdbPlugin for scalafix.",
detail = """1. set semanticdbEnabled where supported
|2. conditionally sets semanticdbVersion & scalaVersion when support is not built-in in the compiler""".stripMargin
) { s =>
val extracted = Project.extract(s)
val scalacOptionsSettings = Seq(Compile, Test).flatMap(
inConfig(_)(ScalafixPlugin.relaxScalacOptionsConfigSettings)
)
val settings = for {
(p, maybeFullVersion) <- collectProjects(
extracted,
maybeSemanticdbScalacFullScalaVersion
)
enableSemanticdbPlugin <- maybeFullVersion.toList.flatMap { fullVersion =>
List(
scalaVersion := fullVersion,
SemanticdbPlugin.semanticdbVersion := BuildInfo.scalametaVersion
)
} :+ (SemanticdbPlugin.semanticdbEnabled := true)
settings <-
inScope(ThisScope.in(p))(
scalacOptionsSettings ++ enableSemanticdbPlugin
)
} yield settings
Compat.append(extracted, settings, s)
}

lazy val command = Command.command(
private lazy val withSemanticdbScalac = Command.command(
"scalafixEnable",
briefHelp =
"Configure libraryDependencies, scalaVersion and scalacOptions for scalafix.",
Expand All @@ -46,7 +97,10 @@ object ScalafixEnable {
)
)
val settings: Seq[Setting[_]] = for {
(p, fullVersion) <- projectsWithMatchingScalaVersion(s)
(p, fullVersion) <- collectProjects(
extracted,
semanticdbScalacFullScalaVersion
)
isSemanticdbEnabled =
libraryDependencies
.in(p)
Expand All @@ -62,10 +116,7 @@ object ScalafixEnable {
inScope(ThisScope.in(p))(scalacOptionsSettings) ++
(if (!isSemanticdbEnabled) addSemanticdbCompilerPlugin else List())
} yield settings

val scalafixReady = Compat.append(extracted, settings, s)

scalafixReady
Compat.append(extracted, settings, s)
}

private val semanticdbConfigSettings: Seq[Def.Setting[_]] =
Expand Down
11 changes: 5 additions & 6 deletions src/main/scala/scalafix/sbt/ScalafixPlugin.scala
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,7 @@ object ScalafixPlugin extends AutoPlugin {
Arg.PrintStream(errorLogger),
Arg.Config(scalafixConf),
Arg.Rules(shell.rules),
Arg.ScalaVersion(scalaVersion.value),
Arg.ParsedArgs(shell.extra)
)
val rulesThatWillRun = mainInterface.rulesThatWillRun()
Expand Down Expand Up @@ -429,13 +430,11 @@ object ScalafixPlugin extends AutoPlugin {
Def.taskDyn {
val dependencies = allDependencies.in(config).value
val files = filesToFix(shellArgs, config).value
val withScalaInterface = mainArgs.withArgs(
Arg.ScalaVersion(scalaVersion.value),
Arg.ScalacOptions(scalacOptions.in(config, compile).value)
)
val scalacOpts = scalacOptions.in(config, compile).value
val withScalaInterface = mainArgs.withArgs(Arg.ScalacOptions(scalacOpts))
val errors = new SemanticRuleValidator(
new SemanticdbNotFound(ruleNames, scalaVersion.value, sbtVersion.value)
).findErrors(files, dependencies, withScalaInterface)
new SemanticdbNotFound(ruleNames, scalaVersion.value)
).findErrors(files, dependencies, scalacOpts, withScalaInterface)
if (errors.isEmpty) {
val task = Def.task {
// don't use fullClasspath as it results in a cyclic dependency via compile when scalafixOnCompile := true
Expand Down
1 change: 1 addition & 0 deletions src/sbt-test/sbt-1.5/scala-3/build.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
scalaVersion := "3.0.0-RC3"
1 change: 1 addition & 0 deletions src/sbt-test/sbt-1.5/scala-3/project/build.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
sbt.version=1.5.2
6 changes: 6 additions & 0 deletions src/sbt-test/sbt-1.5/scala-3/project/plugins.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
resolvers += Resolver.sonatypeRepo("public")
addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % sys.props("plugin.version"))

//FIXME: remove when scalafixVersion is bumped to 0.9.28
resolvers += Resolver.sonatypeRepo("snapshots")
dependencyOverrides += "ch.epfl.scala" % "scalafix-interfaces" % "0.9.27+52-6c9eeec9-SNAPSHOT"
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
object SignificantIndentation:
implicit class XtensionVal(val str: String) extends AnyVal:
def doubled: String = str + str
4 changes: 4 additions & 0 deletions src/sbt-test/sbt-1.5/scala-3/test
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# check that we can run a syntactic rule against a Scala 3 dialect source file
-> scalafix --check LeakingImplicitClassVal
> scalafix LeakingImplicitClassVal
> scalafix --check LeakingImplicitClassVal
19 changes: 19 additions & 0 deletions src/sbt-test/sbt-1.5/scalafixEnable/build.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
ThisBuild / scalafixDependencies += "ch.epfl.scala" %% "example-scalafix-rule" % "1.6.0"

lazy val scala210 = project
.in(file("scala210"))
.settings(
scalaVersion := "2.10.7" // unsupported by semanticdb-scalac
)

lazy val scala211 = project
.in(file("scala211"))
.settings(
scalaVersion := "2.11.12" // supported by semanticdb-scalac
)

lazy val scala3 = project
.in(file("scala3"))
.settings(
scalaVersion := "3.0.0-RC3" // built-in support for semanticdb
)
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
sbt.version=1.5.2
6 changes: 6 additions & 0 deletions src/sbt-test/sbt-1.5/scalafixEnable/project/plugins.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
resolvers += Resolver.sonatypeRepo("public")
addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % sys.props("plugin.version"))

//FIXME: remove when scalafixVersion is bumped to 0.9.28
resolvers += Resolver.sonatypeRepo("snapshots")
dependencyOverrides += "ch.epfl.scala" % "scalafix-interfaces" % "0.9.27+52-6c9eeec9-SNAPSHOT"
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
object Main {
def foo(a: (Int, String)) = a
foo(1, "str")
def main(args: Array[String]) {
println(1)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
object Main {
def foo(a: (Int, String)) = a
foo(1, "str")
def main(args: Array[String]) {
println(1)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
object SignificantIndentation:
val hello = "world"
15 changes: 15 additions & 0 deletions src/sbt-test/sbt-1.5/scalafixEnable/test
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# enable semanticdb output where supported
> scalafixEnable

# check that projects not supported by semanticdb-scalac can still compile
> scala210/compile

# check that we can run a semantic rule against a Scala 2.11 dialect source file
-> scala211/scalafix --check SemanticRule
> scala211/scalafix SemanticRule
> scala211/scalafix --check SemanticRule

# check that we can run a semantic rule against a Scala 3 dialect source file
-> scala3/scalafix --check SemanticRule
> scala3/scalafix SemanticRule
> scala3/scalafix --check SemanticRule

0 comments on commit 5e14eb5

Please sign in to comment.