Skip to content

Commit

Permalink
Replace java reflection with a macro based solution
Browse files Browse the repository at this point in the history
Necessary for cross compilation with scala native, since it does not
offer any reflection functionalities.
Instead of the previous method, we create a mapping between
strings (pointed out by the dialectOverride in scalafmt.conf) and
methods that allow us to replace dialect values.
  • Loading branch information
jchyb committed Sep 19, 2024
1 parent a8baa8b commit 9bf1d65
Show file tree
Hide file tree
Showing 4 changed files with 129 additions and 10 deletions.
21 changes: 13 additions & 8 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -115,13 +115,7 @@ lazy val core = crossProject(JVMPlatform).in(file("scalafmt-core")).settings(
moduleName := "scalafmt-core",
buildInfoSettings,
scalacOptions ++= scalacJvmOptions.value,
libraryDependencies ++= Seq(
scalameta.value,
"org.scalameta" %% "mdoc-parser" % mdocV,
// scala-reflect is an undeclared dependency of fansi, see #1252.
// Scalafmt itself does not require scala-reflect.
"org.scala-lang" % "scala-reflect" % scalaVersion.value,
),
libraryDependencies ++= Seq("org.scalameta" %% "mdoc-parser" % mdocV),
libraryDependencies ++= {
CrossVersion.partialVersion(scalaVersion.value) match {
case Some((2, 13)) => Seq()
Expand All @@ -136,11 +130,22 @@ lazy val core = crossProject(JVMPlatform).in(file("scalafmt-core")).settings(
// scalatest.value % Test // must be here for coreJS/test to run anything
// )
// )
.jvmSettings(Test / run / fork := true).dependsOn(sysops, config)
.jvmSettings(Test / run / fork := true).dependsOn(sysops, config, macros)
.enablePlugins(BuildInfoPlugin)
lazy val coreJVM = core.jvm
// lazy val coreJS = core.js

lazy val macros = crossProject(JVMPlatform).in(file("scalafmt-macros"))
.settings(
moduleName := "scalafmt-macros",
buildInfoSettings,
scalacOptions ++= scalacJvmOptions.value,
libraryDependencies ++= Seq(
scalameta.value,
"org.scala-lang" % "scala-reflect" % scalaVersion.value,
),
)

import sbtassembly.AssemblyPlugin.defaultUniversalScript

val scalacJvmOptions = Def.setting {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,11 +93,10 @@ object ScalafmtRunner {
implicit val encoder: ConfEncoder[ScalafmtRunner] = generic.deriveEncoder

private def overrideDialect[T: ClassTag](d: Dialect, k: String, v: T) = {
import org.scalafmt.config.ReflectOps._
val methodName =
if (k.isEmpty || k.startsWith("with")) k
else "with" + Character.toUpperCase(k.head) + k.tail
d.invokeAs[Dialect](methodName, v.asParam)
DialectMacro.dialectMap(methodName)(d, v)
}

implicit val decoder: ConfDecoderEx[ScalafmtRunner] = generic
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package org.scalafmt.config

import scala.meta.Dialect

import scala.language.experimental.macros
import scala.reflect.macros.blackbox

// Builds a map between string (the scalafmt method name)
// and dialect method application
private[scalafmt] object DialectMacro {
def dialectMap: Map[String, ((Dialect, Any) => Dialect)] =
macro dialectMap_impl

def dialectMap_impl(
c: blackbox.Context,
): c.Expr[Map[String, ((Dialect, Any) => Dialect)]] = {
import c.universe._
val methods = typeOf[Dialect].members.flatMap {
case v: MethodSymbol => v.paramLists match {
case (param :: Nil) :: Nil => // single parameter
val methodName = v.name
val methodNameStr = methodName.toString
if (methodNameStr.startsWith("with")) {
val tpe = param.typeSignature
Some(q"$methodNameStr -> ((dialect: scala.meta.Dialect, v: Any) => dialect.$methodName(v.asInstanceOf[$tpe]))")
} else None
case _ => None
}
case _ => None
}
c.Expr[Map[String, ((Dialect, Any) => Dialect)]](
q"""scala.collection.immutable.Map(..$methods)""",
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package org.scalafmt.config

import scala.meta.Dialect

import munit.FunSuite

class ConfigDialectOverrideTest extends FunSuite {

// toplevelSeparator is never actually used,
// but as the only non-boolean Dialect value it makes for a good test
test("dialect override - non boolean setting") {
ScalafmtConfig.fromHoconString(
"""|
|runner.dialectOverride.toplevelSeparator = ">"
|runner.dialect = scala213
|""".stripMargin,
).get
}

test("throws on an incorrect type of setting") {
intercept[java.util.NoSuchElementException] {
ScalafmtConfig.fromHoconString(
"""|
|runner.dialectOverride.toplevelSeparator = true
|runner.dialect = scala213
|""".stripMargin,
).get
}
}

def testBooleanFlag(methodName: String, getter: Dialect => Boolean): Unit = {
def makeBooleanConfig(setting: String, value: Boolean) = ScalafmtConfig
.fromHoconString(
s"""|
|runner.dialectOverride.$setting = $value
|runner.dialect = scala213
|""".stripMargin,
).get
Seq(true, false).foreach { flag =>
test(s"boolean flag: $methodName($flag)") {
assertEquals(
getter(makeBooleanConfig(methodName, flag).runner.getDialect),
flag,
)
}
}
}

testBooleanFlag("allowFewerBraces", _.allowFewerBraces)
testBooleanFlag("withAllowFewerBraces", _.allowFewerBraces)
testBooleanFlag("useInfixTypePrecedence", _.useInfixTypePrecedence)
testBooleanFlag("withUseInfixTypePrecedence", _.useInfixTypePrecedence)
testBooleanFlag(
"allowImplicitByNameParameters",
_.allowImplicitByNameParameters,
)
testBooleanFlag(
"withAllowImplicitByNameParameters",
_.allowImplicitByNameParameters,
)
testBooleanFlag("allowSignificantIndentation", _.allowSignificantIndentation)
testBooleanFlag(
"withAllowSignificantIndentation",
_.allowSignificantIndentation,
)

test("applying generated boolean map elements does not result in errors") {
val omittedMethods = Set(
"withToplevelSeparator", // non-boolean
"withAllowMultilinePrograms", // unimplemented in scalameta (???)
"withAllowTermUnquotes", // unimplemented in scalameta (???)
"withAllowPatUnquotes", // unimplemented in scalameta (???)
)
val generatedMap = DialectMacro.dialectMap
val baseDialect = scala.meta.dialects.Scala213
generatedMap.keys.filter(!omittedMethods.contains(_)).foreach { key =>
generatedMap(key)(baseDialect, true)
}
}
}

0 comments on commit 9bf1d65

Please sign in to comment.