Skip to content

Commit

Permalink
feature: Inferred type for Scala 3
Browse files Browse the repository at this point in the history
  • Loading branch information
tgodzik authored and bjaglin committed Sep 27, 2024
1 parent 7f471d0 commit 61dc47b
Show file tree
Hide file tree
Showing 61 changed files with 812 additions and 168 deletions.
13 changes: 12 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,14 @@ lazy val rules = projectMatrix
description := "Built-in Scalafix rules",
isFullCrossVersion,
buildInfoSettingsForRules,
libraryDependencies ++= List(
("org.scalameta" % "mtags-interfaces" % "1.3.4")
.exclude("org.eclipse.lsp4j", "org.eclipse.lsp4j")
.exclude("org.eclipse.lsp4j", "org.eclipse.lsp4j.jsonrpc"),
// latest version release for JDK 8, this will be dropped from interfaces at some point
"org.eclipse.lsp4j" % "org.eclipse.lsp4j" % "0.20.1",
coursierInterfaces
),
libraryDependencies ++= {
if (!isScala3.value)
Seq(
Expand All @@ -119,7 +127,10 @@ lazy val rules = projectMatrix
semanticdbScalacCore,
collectionCompat
)
else Nil
else
List(
"org.scala-lang" %% "scala3-presentation-compiler" % scalaVersion.value
)
},
// companion of `.dependsOn(core)`
// issue reported in https://github.com/sbt/sbt/issues/7405
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
scalafix.internal.rule.DisableSyntax
scalafix.internal.rule.ExplicitResultTypes
scalafix.internal.rule.NoAutoTupling
scalafix.internal.rule.NoValInForComprehension
scalafix.internal.rule.OrganizeImports
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,28 @@ package scalafix.internal.rule
import scala.util.control.NonFatal

import scala.meta._
import scala.meta.contrib._
import scala.meta.internal.pc.ScalafixGlobal

import buildinfo.RulesBuildInfo
import metaconfig.Configured
import scalafix.internal.compat.CompilerCompat._
import scalafix.internal.pc.PcExplicitResultTypes
import scalafix.internal.v1.LazyValue
import scalafix.patch.Patch
import scalafix.util.TokenOps
import scalafix.v1._

final class ExplicitResultTypes(
config: ExplicitResultTypesConfig,
global: LazyValue[Option[ScalafixGlobal]]
) extends SemanticRule("ExplicitResultTypes") {

def this() = this(ExplicitResultTypesConfig.default, LazyValue.now(None))
val config: ExplicitResultTypesConfig,
global: LazyValue[Option[ScalafixGlobal]],
fallback: LazyValue[Option[PcExplicitResultTypes]]
) extends SemanticRule("ExplicitResultTypes")
with ExplicitResultTypesBase[Scala2Printer] {

def this() = this(
ExplicitResultTypesConfig.default,
LazyValue.now(None),
LazyValue.now(None)
)

val compilerScalaVersion: String = RulesBuildInfo.scalaVersion

Expand Down Expand Up @@ -69,184 +74,63 @@ final class ExplicitResultTypes(
if (
config.scalacClasspath.nonEmpty && inputBinaryScalaVersion != runtimeBinaryScalaVersion
) {
Configured.error(
s"The ExplicitResultTypes rule needs to run with the same Scala binary version as the one used to compile target sources ($inputBinaryScalaVersion). " +
s"To fix this problem, either remove ExplicitResultTypes from .scalafix.conf or make sure Scalafix is loaded with $inputBinaryScalaVersion."
)
config.conf // Support deprecated explicitReturnTypes config
.getOrElse("explicitReturnTypes", "ExplicitResultTypes")(
ExplicitResultTypesConfig.default
)
.map(c =>
new ExplicitResultTypes(
c,
LazyValue.now(None),
LazyValue.now(Option(PcExplicitResultTypes.dynamic(config)))
)
)
} else {
config.conf // Support deprecated explicitReturnTypes config
.getOrElse("explicitReturnTypes", "ExplicitResultTypes")(
ExplicitResultTypesConfig.default
)
.map(c => new ExplicitResultTypes(c, newGlobal))
.map(c => new ExplicitResultTypes(c, newGlobal, LazyValue.now(None)))
}
}

override def fix(implicit ctx: SemanticDocument): Patch =
try unsafeFix()
catch {
try {
val typesLazy = global.value.map(new CompilerTypePrinter(_, config))
implicit val printer = new Scala2Printer(typesLazy, fallback)
unsafeFix()
} catch {
case _: CompilerException if !config.fatalWarnings =>
Patch.empty
}
}

def unsafeFix()(implicit ctx: SemanticDocument): Patch = {
global.value match {
case Some(value) =>
val types = new CompilerTypePrinter(value, config)
ctx.tree.collect {
case t @ Defn.Val(mods, Pat.Var(name) :: Nil, None, body)
if isRuleCandidate(t, name, mods, body) =>
fixDefinition(t, body, types)

case t @ Defn.Var(mods, Pat.Var(name) :: Nil, None, Some(body))
if isRuleCandidate(t, name, mods, body) =>
fixDefinition(t, body, types)

case t @ Defn.Def(mods, name, _, _, None, body)
if isRuleCandidate(t, name, mods, body) =>
fixDefinition(t, body, types)
}.asPatch
case None => Patch.empty
}
}

// Don't explicitly annotate vals when the right-hand body is a single call
// to `implicitly`. 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 _ => false
}

def defnName(defn: Defn): Option[Name] = Option(defn).collect {
case Defn.Val(_, Pat.Var(name) :: Nil, _, _) => name
case Defn.Var(_, Pat.Var(name) :: Nil, _, _) => name
case Defn.Def(_, name, _, _, _, _) => name
}

def defnBody(defn: Defn): Option[Term] = Option(defn).collect {
case Defn.Val(_, _, _, term) => term
case Defn.Var(_, _, _, Some(term)) => term
case Defn.Def(_, _, _, _, _, term) => term
}

def visibility(mods: Iterable[Mod]): MemberVisibility =
mods
.collectFirst {
case _: Mod.Private => MemberVisibility.Private
case _: Mod.Protected => MemberVisibility.Protected
}
.getOrElse(MemberVisibility.Public)

def kind(defn: Defn): Option[MemberKind] = Option(defn).collect {
case _: Defn.Val => MemberKind.Val
case _: Defn.Def => MemberKind.Def
case _: Defn.Var => MemberKind.Var
}

def isRuleCandidate[D <: Defn](
defn: D,
nm: Name,
mods: Iterable[Mod],
body: Term
)(implicit ev: Extract[D, Mod], ctx: SemanticDocument): Boolean = {
import config._

def matchesMemberVisibility(): Boolean =
memberVisibility.contains(visibility(mods))

def matchesMemberKind(): Boolean =
kind(defn).exists(memberKind.contains)

def isFinalLiteralVal: Boolean =
defn.is[Defn.Val] &&
mods.exists(_.is[Mod.Final]) &&
body.is[Lit]

def matchesSimpleDefinition(): Boolean =
config.skipSimpleDefinitions.isSimpleDefinition(body)

def isImplicit: Boolean =
defn.hasMod(mod"implicit") && !isImplicitly(body)

def hasParentWihTemplate: Boolean =
defn.parent.exists(_.is[Template.Body])

def qualifyingImplicit: Boolean =
isImplicit && !isFinalLiteralVal

def matchesConfig: Boolean =
matchesMemberKind && matchesMemberVisibility && !matchesSimpleDefinition

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

matchesConfig && {
qualifyingImplicit || qualifyingNonImplicit
}
}

class Scala2Printer(
globalPrinter: Option[CompilerTypePrinter],
fallback: LazyValue[Option[PcExplicitResultTypes]]
) extends Printer {
def defnType(
defn: Defn,
replace: Token,
space: String,
types: CompilerTypePrinter
space: String
)(implicit
ctx: SemanticDocument
): Option[Patch] =
for {
name <- defnName(defn)
defnSymbol <- name.symbol.asNonEmpty
patch <- types.toPatch(name.pos, defnSymbol, replace, defn, space)
} yield patch

def fixDefinition(defn: Defn, body: Term, types: CompilerTypePrinter)(implicit
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
}
): Option[Patch] = {

globalPrinter match {
case Some(types) =>
for {
name <- ExplicitResultTypesBase.defnName(defn)
defnSymbol <- name.symbol.asNonEmpty
patch <- types.toPatch(name.pos, defnSymbol, replace, defn, space)
} yield {
patch
}
case None =>
fallback.value.flatMap { fallbackExplicit =>
fallbackExplicit.defnType(replace)
}
}
import lst._
for {
start <- defn.tokens.headOption
end <- body.tokens.headOption
// Left-hand side tokens in definition.
// Example: `val x = ` from `val x = rhs.banana`
lhsTokens = slice(start, end)
replace <- lhsTokens.reverseIterator.find(x =>
!x.is[Token.Equals] && !x.is[Trivia]
)
space = {
if (TokenOps.needsLeadingSpaceBeforeColon(replace)) " "
else ""
}
typePatch <- defnType(defn, replace, space, types)
valuePatchOpt = defnBody(defn).map(patchEmptyValue)
} yield typePatch + valuePatchOpt
}.asPatch.atomic

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

import scala.meta.*

import buildinfo.RulesBuildInfo
import dotty.tools.pc.ScalaPresentationCompiler
import metaconfig.Configured
import scalafix.internal.pc.PcExplicitResultTypes
import scalafix.patch.Patch
import scalafix.v1.*

final class ExplicitResultTypes(
val config: ExplicitResultTypesConfig,
fallback: Option[PcExplicitResultTypes]
) extends SemanticRule("ExplicitResultTypes")
with ExplicitResultTypesBase[Scala3Printer] {

def this() = this(ExplicitResultTypesConfig.default, None)

val compilerScalaVersion: String = RulesBuildInfo.scalaVersion

private def toBinaryVersion(v: String) = v.split('.').take(2).mkString(".")

override def description: String =
"Inserts type annotations for inferred public members."

override def isRewrite: Boolean = true

override def afterComplete(): Unit = {
shutdownCompiler()
}

private def shutdownCompiler(): Unit = {
fallback.foreach(_.shutdownCompiler())
}

override def withConfiguration(config: Configuration): Configured[Rule] = {
config.conf // Support deprecated explicitReturnTypes config
.getOrElse("explicitReturnTypes", "ExplicitResultTypes")(
ExplicitResultTypesConfig.default
)
.map(c =>
new ExplicitResultTypes(
c,
Option {
if (
toBinaryVersion(config.scalaVersion) == toBinaryVersion(
compilerScalaVersion
)
)
PcExplicitResultTypes
.static(config, new ScalaPresentationCompiler())
else
PcExplicitResultTypes.dynamic(config)
}
)
)
}

override def fix(implicit ctx: SemanticDocument): Patch =
try {
implicit val printer = new Scala3Printer(fallback)
unsafeFix()
} catch {
case _: Throwable if !config.fatalWarnings =>
Patch.empty
}

}

class Scala3Printer(
fallback: Option[PcExplicitResultTypes]
) extends Printer {

def defnType(
defn: Defn,
replace: Token,
space: String
)(implicit
ctx: SemanticDocument
): Option[Patch] = {
fallback.flatMap(_.defnType(replace))
}
}
Loading

0 comments on commit 61dc47b

Please sign in to comment.