From 69c827f48365169429294787c5fcc0bb1c1b4c7b Mon Sep 17 00:00:00 2001 From: Tomasz Godzik Date: Thu, 25 Jul 2024 18:19:21 +0200 Subject: [PATCH] improvement: Load exact version of the presentation compiler --- build.sbt | 3 +- project/ScalafixBuild.scala | 2 +- .../scalafix/internal/rule/Embedded.scala | 94 ++++++++++++ .../internal/rule/ExplicitResultTypes.scala | 139 ++++++++++-------- .../PresentationCompilerClassloader.scala | 26 ++++ .../rule/PresentationCompilerConfigImpl.scala | 31 ++++ .../rule/ExplicitResultTypesConfig.scala | 6 +- 7 files changed, 236 insertions(+), 65 deletions(-) create mode 100644 scalafix-rules/src/main/scala-3/scalafix/internal/rule/Embedded.scala create mode 100644 scalafix-rules/src/main/scala-3/scalafix/internal/rule/PresentationCompilerClassloader.scala create mode 100644 scalafix-rules/src/main/scala-3/scalafix/internal/rule/PresentationCompilerConfigImpl.scala diff --git a/build.sbt b/build.sbt index 845678564..474ca7fd2 100644 --- a/build.sbt +++ b/build.sbt @@ -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 ) } ) diff --git a/project/ScalafixBuild.scala b/project/ScalafixBuild.scala index a602e6b80..8a518f835 100644 --- a/project/ScalafixBuild.scala +++ b/project/ScalafixBuild.scala @@ -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") diff --git a/scalafix-rules/src/main/scala-3/scalafix/internal/rule/Embedded.scala b/scalafix-rules/src/main/scala-3/scalafix/internal/rule/Embedded.scala new file mode 100644 index 000000000..7f0ed2bb5 --- /dev/null +++ b/scalafix-rules/src/main/scala-3/scalafix/internal/rule/Embedded.scala @@ -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) + } +} diff --git a/scalafix-rules/src/main/scala-3/scalafix/internal/rule/ExplicitResultTypes.scala b/scalafix-rules/src/main/scala-3/scalafix/internal/rule/ExplicitResultTypes.scala index 593df37e0..02e251ce8 100644 --- a/scalafix-rules/src/main/scala-3/scalafix/internal/rule/ExplicitResultTypes.scala +++ b/scalafix-rules/src/main/scala-3/scalafix/internal/rule/ExplicitResultTypes.scala @@ -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, @@ -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")( @@ -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 } @@ -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]) @@ -190,8 +191,8 @@ final class ExplicitResultTypes( def qualifyingNonImplicit: Boolean = { !onlyImplicits && - hasParentWihTemplate // && - // !defn.hasMod("implicit") + hasParentWihTemplate && + !defn.hasMod(Mod.Implicit()) } matchesConfig && { @@ -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 @@ -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 @@ -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) + + } + + } } diff --git a/scalafix-rules/src/main/scala-3/scalafix/internal/rule/PresentationCompilerClassloader.scala b/scalafix-rules/src/main/scala-3/scalafix/internal/rule/PresentationCompilerClassloader.scala new file mode 100644 index 000000000..bd1744fad --- /dev/null +++ b/scalafix-rules/src/main/scala-3/scalafix/internal/rule/PresentationCompilerClassloader.scala @@ -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) + } + } +} diff --git a/scalafix-rules/src/main/scala-3/scalafix/internal/rule/PresentationCompilerConfigImpl.scala b/scalafix-rules/src/main/scala-3/scalafix/internal/rule/PresentationCompilerConfigImpl.scala new file mode 100644 index 000000000..a62fc6ad3 --- /dev/null +++ b/scalafix-rules/src/main/scala-3/scalafix/internal/rule/PresentationCompilerConfigImpl.scala @@ -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 +} diff --git a/scalafix-rules/src/main/scala/scalafix/internal/rule/ExplicitResultTypesConfig.scala b/scalafix-rules/src/main/scala/scalafix/internal/rule/ExplicitResultTypesConfig.scala index 66ce023b6..50c1bdb0a 100644 --- a/scalafix-rules/src/main/scala/scalafix/internal/rule/ExplicitResultTypesConfig.scala +++ b/scalafix-rules/src/main/scala/scalafix/internal/rule/ExplicitResultTypesConfig.scala @@ -9,7 +9,6 @@ import metaconfig._ import metaconfig.annotation._ import metaconfig.generic.Surface import scalafix.internal.config._ -import scala.meta.classifiers.XtensionClassifiable case class ExplicitResultTypesConfig( @Description("Enable/disable this rule for defs, vals or vars.") @@ -53,12 +52,15 @@ object ExplicitResultTypesConfig { } case class SimpleDefinitions(kinds: Set[String]) { + + import scala.meta.classifiers.XtensionClassifiable + private def isSimpleRef(tree: m.Tree): Boolean = tree match { case _: m.Name => true case t: m.Term.Select => isSimpleRef(t.qual) case _ => false } - + def isSimpleDefinition(body: m.Term): Boolean = { val kind = if (body.is[m.Lit]) Some("Lit")