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

make scalafixEnable less aggressive & more future-proof #292

Merged
merged 3 commits into from
Mar 25, 2022
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
25 changes: 25 additions & 0 deletions src/main/scala/scalafix/internal/sbt/Implicits.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package scalafix.internal.sbt

import sbt.librarymanagement._
import scalafix.sbt.InvalidArgument

trait Implicits {
implicit class XtensionModuleID(m: ModuleID) {
def asCoursierCoordinates: String = {
m.crossVersion match {
case _: Disabled =>
s"${m.organization}:${m.name}:${m.revision}"
case _: CrossVersion.Binary =>
s"${m.organization}::${m.name}:${m.revision}"
case _: CrossVersion.Full =>
s"${m.organization}:::${m.name}:${m.revision}"
case other =>
throw new InvalidArgument(
s"Unsupported crossVersion $other for dependency $m"
)
}
}
}
}

object Implicits extends Implicits
20 changes: 3 additions & 17 deletions src/main/scala/scalafix/internal/sbt/ScalafixInterface.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,29 +22,15 @@ object Arg {
repositories: Seq[Repository]
) extends Arg
with CacheKey {

import scalafix.internal.sbt.Implicits._

override def apply(sa: ScalafixArguments): ScalafixArguments =
sa.withToolClasspath(
customURIs.map(_.toURL).asJava,
customDependencies.map(_.asCoursierCoordinates).asJava,
repositories.asJava
)

private implicit class XtensionModuleID(m: ModuleID) {
def asCoursierCoordinates: String = {
m.crossVersion match {
case _: Disabled =>
s"${m.organization}:${m.name}:${m.revision}"
case _: CrossVersion.Binary =>
s"${m.organization}::${m.name}:${m.revision}"
case _: CrossVersion.Full =>
s"${m.organization}:::${m.name}:${m.revision}"
case other =>
throw new InvalidArgument(
s"Unsupported crossVersion $other for dependency $m"
)
}
}
}
}

case class Rules(rules: Seq[String]) extends Arg with CacheKey {
Expand Down
191 changes: 156 additions & 35 deletions src/main/scala/scalafix/sbt/ScalafixEnable.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,66 +2,187 @@ package scalafix.sbt

import sbt._
import sbt.Keys._
import sbt.VersionNumber.SemVer

import ScalafixPlugin.autoImport.scalafixResolvers

import collection.JavaConverters._
import scala.util._
import coursierapi.Repository

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

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

/** 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.
/** If the provided Scala binary version is supported, return the latest scala
* full version for which the recommended semanticdb-scalac is available, or
* None if semanticdb support is built-in in the compiler
*/
private lazy val maybeSemanticdbScalacFullScalaVersion
: PartialFunction[(Long, Long), Option[String]] =
semanticdbScalacFullScalaVersion.andThen(Some.apply).orElse {
private lazy val maybeRecommendedSemanticdbScalacScalaVersion
: PartialFunction[(Long, Long), Option[VersionNumber]] =
recommendedSemanticdbScalacScalaVersion.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 <- (p / scalaVersion).get(extracted.structure.data).toList
partialVersion <- CrossVersion.partialVersion(version).toList
res <- pf.lift(partialVersion).toList
} yield p -> res
/** Collect compatible projects across the entire build */
private def collectProjects(extracted: Extracted): Seq[CompatibleProject] =
for {
p <- extracted.structure.allProjectRefs
scalaV <- (p / scalaVersion).get(extracted.structure.data).toList
partialVersion <- CrossVersion.partialVersion(scalaV).toList
maybeRecommendedSemanticdbScalacV <-
maybeRecommendedSemanticdbScalacScalaVersion.lift(partialVersion).toList
scalafixResolvers0 <- (p / scalafixResolvers)
Copy link
Collaborator Author

@bjaglin bjaglin Mar 24, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is currently looked up at the ThisBuild level elsewhere, but since usage of custom resolvers is not really tested, I chose to be overly defensive

.get(extracted.structure.data)
.toList
semanticdbCompilerPlugin0 <- (p / semanticdbCompilerPlugin)
.get(extracted.structure.data)
.toList
} yield CompatibleProject(
p,
VersionNumber(scalaV),
semanticdbCompilerPlugin0,
scalafixResolvers0,
maybeRecommendedSemanticdbScalacV
)

private case class CompatibleProject(
ref: ProjectRef,
scalaVersion0: VersionNumber,
semanticdbCompilerPlugin0: ModuleID,
scalafixResolvers0: Seq[Repository],
maybeRecommendedSemanticdbScalacScalaV: Option[VersionNumber]
)

lazy val command = Command.command(
"scalafixEnable",
briefHelp = "Configure SemanticdbPlugin for scalafix.",
detail = """1. set semanticdbEnabled & semanticdbVersion
|2. conditionally sets scalaVersion when support is not built-in in the compiler""".stripMargin
briefHelp =
"Configure SemanticdbPlugin for scalafix on supported projects.",
detail = """1. set semanticdbEnabled := true
|2. for scala 2.x,
| - set semanticdbCompilerPlugin to the scalameta version tracked by scalafix if available for scalaVersion,
| - otherwise set semanticdbCompilerPlugin to a compatible version available for scalaVersion,
| - otherwise force scalaVersion to the latest version supported by the scalameta version tracked by scalafix.""".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.map { fullVersion =>
scalaVersion := fullVersion,
} :+ (semanticdbEnabled := true)
project <- collectProjects(extracted)
enableSemanticdbPlugin <-
project.maybeRecommendedSemanticdbScalacScalaV.toList
.flatMap { recommendedSemanticdbScalacScalaV =>

import scalafix.internal.sbt.Implicits._
val semanticdbScalacModule =
coursierapi.Dependency
.parse(
project.semanticdbCompilerPlugin0.asCoursierCoordinates,
coursierapi.ScalaVersion.of(project.scalaVersion0.toString)
)
.getModule
val recommendedSemanticdbV =
VersionNumber(BuildInfo.scalametaVersion)
val compatibleSemanticdbVs = Try(
coursierapi.Versions.create
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To my knowledge, this is the best Java coursier API to achieve something like suggested in scalacenter/scalafix#1146 (comment)

.withRepositories(project.scalafixResolvers0: _*)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That part is untested, so there is a risk that this fails on corporate environments. The failure fallback keeps the current behavior though, so apart from an added latency it shouldn't be a problem.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this is out of topic in this PR but we could use the csrConfiguration key in sbt to configure coursier with authentication and credentials.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I looked at that in the context of scalacenter/scalafix#1571, but I am not sure it would help as scalafix-core uses the Java coursier API directly, not via what lm/lm-coursier.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's true, but it may not be so hard to translate the sbt csrConfiguration into the Java API with something similar to ToCoursier given that both csrConfiguration and the coursier Java API are stable.

.withModule(semanticdbScalacModule)
.versions()
.getMergedListings
.getAvailable
.asScala
.map(VersionNumber.apply)
// don't use snapshots
.filter(_.extras == Nil)
// https://github.com/scalameta/scalameta/blob/main/COMPATIBILITY.md
.filter(SemVer.isCompatible(_, recommendedSemanticdbV))
.toList
)

compatibleSemanticdbVs match {
case Success(Nil) | Failure(_) =>
Seq(
scalaVersion := {
val v = recommendedSemanticdbScalacScalaV.toString
sLog.value.warn(
s"Forcing scalaVersion to $v in project " +
s"${project.ref.project} since no semanticdb-scalac " +
s"version binary-compatible with $recommendedSemanticdbV " +
s"and cross-published for scala " +
s"${project.scalaVersion0.toString} was found - " +
s"consider bumping scala"
)
v
},
semanticdbVersion := recommendedSemanticdbV.toString
)
case Success(available)
if available.contains(recommendedSemanticdbV) =>
Seq(
semanticdbVersion := recommendedSemanticdbV.toString
)
case Success(earliestAvailable :: tail) =>
val futureVersion =
SemanticSelector.apply(s">${recommendedSemanticdbV}")

if (earliestAvailable.matchesSemVer(futureVersion)) {
Seq(
semanticdbVersion := {
val v = earliestAvailable.toString
sLog.value.info(
s"Setting semanticdbVersion to $v in project " +
s"${project.ref.project} since the version " +
s"${recommendedSemanticdbV} tracked by scalafix " +
s"${BuildInfo.scalafixVersion} will not be " +
s"published for scala " +
s"${project.scalaVersion0.toString} - " +
s"consider upgrading sbt-scalafix"
)
v
}
)
} else {
val latestAvailable =
tail.lastOption.getOrElse(earliestAvailable)
Seq(
semanticdbVersion := {
val v = latestAvailable.toString
sLog.value.info(
s"Setting semanticdbVersion to $v in project " +
s"${project.ref.project} since the version " +
s"${recommendedSemanticdbV} tracked by scalafix " +
s"${BuildInfo.scalafixVersion} is no longer " +
s"published for scala " +
s"${project.scalaVersion0.toString} - " +
s"consider bumping scala"
)
v
}
)
}
}
} ++ Seq(
semanticdbEnabled := true,
// support sbt 1.3.[0-3] which does not contain
// https://github.com/sbt/sbt/pull/5202
(semanticdbCompilerPlugin := semanticdbCompilerPlugin.value
.withRevision((semanticdbVersion).value))
)
settings <-
inScope(ThisScope.copy(project = Select(p)))(
inScope(ThisScope.copy(project = Select(project.ref)))(
scalacOptionsSettings ++ enableSemanticdbPlugin
) :+ (Global / semanticdbVersion := BuildInfo.scalametaVersion)
)
} yield settings
extracted.appendWithoutSession(settings, s)
}
Expand Down
42 changes: 28 additions & 14 deletions src/sbt-test/sbt-scalafix/scalafixEnable/build.sbt
Original file line number Diff line number Diff line change
@@ -1,54 +1,68 @@
val V = _root_.scalafix.sbt.BuildInfo

// 2.10 is not supported, scalafix is not enabled
// 2.10 is not supported
lazy val scala210 = project.settings(
scalaVersion := "2.10.4",
libraryDependencies := Nil,
scalacOptions := Nil
)

// 2.12.0 is supported but the version is overidden
lazy val overridesSettings = project.settings(
scalaVersion := "2.12.0",
libraryDependencies := Nil,
scalacOptions := Nil
// 2.11.x is supported
lazy val scala211_old = project.settings(
// semanticdb-scalac_2.11.0 was never available
scalaVersion := "2.11.0"
)

// 2.11.x is supported
lazy val scala211 = project.settings(
scalaVersion := V.scala211
// semanticdb-scalac_2.11.11 no longer available after 4.1.9
scalaVersion := "2.11.11"
)

// 2.12.x is supported
lazy val scala212 = project.settings(
scalaVersion := V.scala212
// semanticdb-scalac_2.12.15 not yet available in 4.4.10, became available as of 4.4.28
scalaVersion := "2.12.15"
)

// 2.13.x is supported
lazy val scala213 = project.settings(
scalaVersion := V.scala213
// semanticdb-scalac_2.13.4 available in 4.4.10, became available as of 4.4.0
scalaVersion := "2.13.4"
)

TaskKey[Unit]("check") := {
// nothing should change for the 2.10 project
assert((scala210 / semanticdbEnabled).value == false)
assert((scala210 / scalaVersion).value == "2.10.4")
assert((scala210 / Compile / compile / scalacOptions).value.isEmpty)

// 2.12.0 should be overidden to 2.12.X
assert((overridesSettings / scalaVersion).value == V.scala212)
assert((scala211_old / semanticdbEnabled).value == true)
assert((scala211_old / scalaVersion).value == "2.11.12")
assert((scala211_old / semanticdbCompilerPlugin).value.revision == "4.4.10")
assert(
(overridesSettings / Compile / compile / scalacOptions).value
.contains("-Yrangepos")
(scala211_old / Compile / compile / scalacOptions).value
.count(_ == "-Yrangepos") == 1
)

assert((scala211 / semanticdbEnabled).value == true)
assert((scala211 / scalaVersion).value == "2.11.11")
assert((scala211 / semanticdbCompilerPlugin).value.revision == "4.1.9")
assert(
(scala211 / Compile / compile / scalacOptions).value
.count(_ == "-Yrangepos") == 1
)

assert((scala212 / semanticdbEnabled).value == true)
assert((scala212 / scalaVersion).value == "2.12.15")
assert((scala212 / semanticdbCompilerPlugin).value.revision == "4.4.28")
assert(
(scala212 / Compile / compile / scalacOptions).value
.count(_ == "-Yrangepos") == 1
)

assert((scala213 / semanticdbEnabled).value == true)
assert((scala213 / scalaVersion).value == "2.13.4")
assert((scala213 / semanticdbCompilerPlugin).value.revision == "4.4.10")
assert(
(scala213 / Test / compile / scalacOptions).value
.count(_ == "-Yrangepos") == 1
Expand Down
3 changes: 3 additions & 0 deletions src/sbt-test/sbt-scalafix/scalafixEnable/project/plugins.sbt
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
resolvers += Resolver.sonatypeRepo("public")
addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % sys.props("plugin.version"))

// https://github.com/scalameta/scalameta/blob/v4.4.10/project/Versions.scala
dependencyOverrides += "ch.epfl.scala" % "scalafix-interfaces" % "0.9.27" // scala-steward:off