Skip to content

Commit

Permalink
improvement: Load exact version of the presentation compiler
Browse files Browse the repository at this point in the history
  • Loading branch information
tgodzik committed Jul 25, 2024
1 parent 8a54b93 commit 25384fa
Show file tree
Hide file tree
Showing 6 changed files with 232 additions and 63 deletions.
3 changes: 2 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,8 @@ lazy val rules = projectMatrix
)
else
List(
"org.scala-lang" % "scala3-presentation-compiler_3" % scalaVersion.value
"org.scalameta" % "mtags-interfaces" % "1.3.4",
coursierInterfaces
)
}
)
Expand Down
2 changes: 1 addition & 1 deletion project/ScalafixBuild.scala
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ object ScalafixBuild extends AutoPlugin with GhpagesKeys {
val xsource3 = TargetAxis(sv, xsource3 = true)

(prevVersions :+ xsource3).map((sv, _))
}// :+ (scala213, TargetAxis(scala3))
} // :+ (scala213, TargetAxis(scala3))

lazy val publishLocalTransitive =
taskKey[Unit]("Run publishLocal on this project and its dependencies")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package scalafix.internal.rule

import java.net.URLClassLoader
import java.nio.file.Path
import java.util.ServiceLoader

import scala.jdk.CollectionConverters.*

import scala.meta.pc.PresentationCompiler

import coursierapi.Dependency
import coursierapi.Fetch
import coursierapi.MavenRepository

object Embedded {

def presentationCompiler(
scalaVersion: String
): PresentationCompiler = {
val deps =
scala3PresentationCompilerDependencies(scalaVersion)
val jars = Fetch
.create()
.addDependencies(deps*)
.addRepositories(
MavenRepository.of(
"https://oss.sonatype.org/content/repositories/snapshots"
)
)
.fetch()
.asScala
.map(_.toPath())
.toSeq
val classloader = newPresentationCompilerClassLoader(jars)

val presentationCompilerClassname =
if (supportPresentationCompilerInDotty(scalaVersion)) {
"dotty.tools.pc.ScalaPresentationCompiler"
} else {
"scala.meta.pc.ScalaPresentationCompiler"
}

serviceLoader(
classOf[PresentationCompiler],
presentationCompilerClassname,
classloader
)
}

private def supportPresentationCompilerInDotty(scalaVersion: String) = {
scalaVersion.split("\\.").take(3).map(_.toInt) match {
case Array(3, minor, patch) => minor > 3 || minor == 3 && patch >= 4
case _ => false
}
}

private def scala3PresentationCompilerDependencies(version: String) =
if (supportPresentationCompilerInDotty(version))
List(
Dependency
.of("org.scala-lang", "scala3-presentation-compiler_3", version)
)
else
List(
// TODO should use build info etc. instead of using 1.3.4
Dependency.of("org.scalameta", s"mtags_${version}", "1.3.4")
)

private def serviceLoader[T](
cls: Class[T],
className: String,
classloader: URLClassLoader
): T = {
val services = ServiceLoader.load(cls, classloader).iterator()
if (services.hasNext) services.next()
else {
val cls = classloader.loadClass(className)
val ctor = cls.getDeclaredConstructor()
ctor.setAccessible(true)
ctor.newInstance().asInstanceOf[T]
}
}

private def newPresentationCompilerClassLoader(
jars: Seq[Path]
): URLClassLoader = {
val allJars = jars.iterator
val allURLs = allJars.map(_.toUri.toURL).toArray
// Share classloader for a subset of types.
val parent =
new PresentationCompilerClassLoader(this.getClass.getClassLoader)
new URLClassLoader(allURLs, parent)
}
}
Original file line number Diff line number Diff line change
@@ -1,31 +1,31 @@
package scalafix.internal.rule

import scala.util.control.NonFatal
import java.net.URI
import java.nio.file.Paths
import java.util.concurrent.CompletableFuture
import java.util.concurrent.CompletionStage

import scala.jdk.CollectionConverters.*
import scala.util.Random
import scala.util.Try

import scala.meta._
import scala.meta.contrib._
import scala.meta.*
import scala.meta.contrib.*
import scala.meta.inputs.Input.File
import scala.meta.inputs.Input.VirtualFile
import scala.meta.pc.CancelToken
import scala.meta.pc.OffsetParams
import scala.meta.pc.PresentationCompiler
import scala.meta.trees.Origin.DialectOnly
import scala.meta.trees.Origin.Parsed

import buildinfo.RulesBuildInfo
import metaconfig.Configured
import scalafix.internal.v1.LazyValue
import scalafix.patch.Patch
import scalafix.util.TokenOps
import scalafix.v1._
import dotty.tools.pc.ScalaPresentationCompiler
import scala.meta.internal.metals.CompilerOffsetParams
import scala.meta.trees.Origin.DialectOnly
import scala.meta.trees.Origin.Parsed
import scala.meta.inputs.Input.File
import scala.meta.inputs.Input.VirtualFile
import scala.jdk.CollectionConverters._
import java.nio.file.Paths
import scala.util.Random
import scala.meta.internal.metals.EmptyCancelToken
import scalafix.patch.Patch.empty
import scalafix.internal.v1.LazyValue
import scala.util.Success
import scala.meta.pc.PresentationCompilerConfig
import scala.meta.internal.pc.PresentationCompilerConfigImpl
import scala.meta.pc.PresentationCompiler
import scalafix.util.TokenOps
import scalafix.v1.*

final class ExplicitResultTypes(
config: ExplicitResultTypesConfig,
Expand Down Expand Up @@ -70,23 +70,23 @@ final class ExplicitResultTypes(
s"To fix this problem, either remove ExplicitResultTypes from .scalafix.conf or make sure Scalafix is loaded with $inputBinaryScalaVersion."
)
} else {

val newPc: LazyValue[Option[PresentationCompiler]] =
if (config.scalacClasspath.isEmpty) {
LazyValue.now(None)
} else {
LazyValue.from { () =>
Success(
new ScalaPresentationCompiler(
classpath = config.scalacClasspath.map(_.toNIO).toSeq,
options = Nil
).withConfiguration(
LazyValue.from { () =>
Try(
Embedded
.presentationCompiler(config.scalaVersion)
.withConfiguration(
new PresentationCompilerConfigImpl(
_symbolPrefixes = symbolReplacements
symbolPrefixes = symbolReplacements.asJava
)
)
)
}
.newInstance(
this.name.toString,
config.scalacClasspath.map(_.toNIO).asJava,
// getting assertion errors if included
config.scalacOptions.filter(!_.contains("-release")).asJava
)
)
}
config.conf // Support deprecated explicitReturnTypes config
.getOrElse("explicitReturnTypes", "ExplicitResultTypes")(
Expand Down Expand Up @@ -120,11 +120,12 @@ final class ExplicitResultTypes(
}

// Don't explicitly annotate vals when the right-hand body is a single call
// to `implicitly`. Prevents ambiguous implicit. Not annotating in such cases,
// to `implicitly` or `summon`. Prevents ambiguous implicit. Not annotating in such cases,
// this a common trick employed implicit-heavy code to workaround SI-2712.
// Context: https://gitter.im/typelevel/cats?at=584573151eb3d648695b4a50
private def isImplicitly(term: Term): Boolean = term match {
case Term.ApplyType(Term.Name("implicitly"), _) => true
case Term.ApplyType(Term.Name("summon"), _) => true
case _ => false
}

Expand Down Expand Up @@ -177,7 +178,7 @@ final class ExplicitResultTypes(
config.skipSimpleDefinitions.isSimpleDefinition(body)

def isImplicit: Boolean = false
// defn && !isImplicitly(body)
defn.hasMod(Mod.Implicit()) && !isImplicitly(body)

def hasParentWihTemplate: Boolean =
defn.parent.exists(_.is[Template])
Expand All @@ -190,8 +191,8 @@ final class ExplicitResultTypes(

def qualifyingNonImplicit: Boolean = {
!onlyImplicits &&
hasParentWihTemplate // &&
// !defn.hasMod("implicit")
hasParentWihTemplate &&
!defn.hasMod(Mod.Implicit())
}

matchesConfig && {
Expand Down Expand Up @@ -224,8 +225,7 @@ final class ExplicitResultTypes(
val params = new CompilerOffsetParams(
uri,
text,
replace.pos.end,
EmptyCancelToken
replace.pos.end
)
val result = pc.insertInferredType(params).get()
result.asScala.toList
Expand All @@ -250,28 +250,29 @@ final class ExplicitResultTypes(
ctx: SemanticDocument
): Patch = {
val lst = ctx.tokenList
val option = SymbolMatcher.exact("scala/Option.")
val list = SymbolMatcher.exact(
"scala/package.List.",
"scala/collection/immutable/List."
)
val seq = SymbolMatcher.exact(
"scala/package.Seq.",
"scala/collection/Seq.",
"scala/collection/immutable/Seq."
)
def patchEmptyValue(term: Term): Patch = {
term match {
// case q"${option(_)}.empty[$_]" =>
// Patch.replaceTree(term, "None")
// case q"${list(_)}.empty[$_]" =>
// Patch.replaceTree(term, "Nil")
// case q"${seq(_)}.empty[$_]" =>
// Patch.replaceTree(term, "Nil")
case _ =>
Patch.empty
}
}
// val option = SymbolMatcher.exact("scala/Option.")
// val list = SymbolMatcher.exact(
// "scala/package.List.",
// "scala/collection/immutable/List."
// )
// val seq = SymbolMatcher.exact(
// "scala/package.Seq.",
// "scala/collection/Seq.",
// "scala/collection/immutable/Seq."
// )
// def patchEmptyValue(term: Term): Patch = {
// term match {
// // case q"${option(_)}.empty[$_]" =>
// // Patch.replaceTree(term, "None")
// // case q"${list(_)}.empty[$_]" =>
// // Patch.replaceTree(term, "Nil")
// // case q"${seq(_)}.empty[$_]" =>
// // Patch.replaceTree(term, "Nil")
// case _ =>
// Patch.empty
// }
// }
def patchEmptyValue(term: Term): Patch = Patch.empty
import lst._
for {
start <- defn.tokens.headOption
Expand All @@ -291,4 +292,20 @@ final class ExplicitResultTypes(
} yield typePatch + valuePatchOpt
}.asPatch.atomic

case class CompilerOffsetParams(
uri: URI,
text: String,
offset: Int
) extends OffsetParams {

override def token(): CancelToken = new CancelToken {

override def checkCanceled(): Unit = ()

override def onCancel(): CompletionStage[java.lang.Boolean] =
CompletableFuture.completedFuture(java.lang.Boolean.FALSE)

}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package scalafix.internal.rule

/**
* ClassLoader that is used to reflectively invoke presentation compiler APIs.
*
* The presentation compiler APIs are compiled against exact Scala versions of
* the compiler while Scalafix rule only runs in a single Scala version. In
* order to communicate between Scalafix and the reflectively loaded compiler,
* this classloader shares a subset of Java classes that appear in method
* signatures of the `PresentationCompiler` class.
*/
class PresentationCompilerClassLoader(parent: ClassLoader)
extends ClassLoader(null) {
override def findClass(name: String): Class[?] = {
val isShared =
name.startsWith("org.eclipse.lsp4j") ||
name.startsWith("com.google.gson") ||
name.startsWith("scala.meta.pc") ||
name.startsWith("javax")
if (isShared) {
parent.loadClass(name)
} else {
throw new ClassNotFoundException(name)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package scalafix.internal.rule

import java.util.Optional
import java.util.concurrent.TimeUnit

import scala.meta.pc.PresentationCompilerConfig
import scala.meta.pc.PresentationCompilerConfig.OverrideDefFormat

case class PresentationCompilerConfigImpl(
debug: Boolean = false,
parameterHintsCommand: Optional[String] = Optional.empty(),
completionCommand: Optional[String] = Optional.empty(),
symbolPrefixes: java.util.Map[String, String] =
PresentationCompilerConfig.defaultSymbolPrefixes(),
overrideDefFormat: OverrideDefFormat = OverrideDefFormat.Ascii,
isCompletionItemDetailEnabled: Boolean = true,
isCompletionItemDocumentationEnabled: Boolean = true,
isHoverDocumentationEnabled: Boolean = true,
snippetAutoIndent: Boolean = true,
isSignatureHelpDocumentationEnabled: Boolean = true,
isCompletionSnippetsEnabled: Boolean = true,
isCompletionItemResolve: Boolean = true,
isStripMarginOnTypeFormattingEnabled: Boolean = true,
timeoutDelay: Long = 20,
timeoutUnit: TimeUnit = TimeUnit.SECONDS,
semanticdbCompilerOptions: java.util.List[String] =
PresentationCompilerConfig.defaultSemanticdbCompilerOptions(),
) extends PresentationCompilerConfig {

override def isDefaultSymbolPrefixes(): Boolean = false
}

0 comments on commit 25384fa

Please sign in to comment.