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 authored and kitbellew committed Sep 19, 2024
1 parent a8baa8b commit 52c8954
Show file tree
Hide file tree
Showing 4 changed files with 164 additions and 11 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 @@ -92,12 +92,15 @@ 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._
private[config] def overrideDialect[T: ClassTag](
d: Dialect,
k: String,
v: T,
) = {
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,110 @@
package org.scalafmt.config

import scala.meta.Dialect
import scala.meta.dialects.Scala213

import munit.FunSuite

class ConfigDialectOverrideTest extends FunSuite {
private val generatedMap = DialectMacro.dialectMap

// 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,
testDirectly: 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)") {
if (testDirectly)
assertEquals(getter(generatedMap(methodName)(Scala213, flag)), flag)
assertEquals(
getter(ScalafmtRunner.overrideDialect(Scala213, methodName, flag)),
flag,
)
assertEquals(
getter(makeBooleanConfig(methodName, flag).runner.getDialect),
flag,
)
}
}
}

testBooleanFlag("allowFewerBraces", _.allowFewerBraces, testDirectly = false)
testBooleanFlag(
"withAllowFewerBraces",
_.allowFewerBraces,
testDirectly = true,
)
testBooleanFlag(
"useInfixTypePrecedence",
_.useInfixTypePrecedence,
testDirectly = false,
)
testBooleanFlag(
"withUseInfixTypePrecedence",
_.useInfixTypePrecedence,
testDirectly = true,
)
testBooleanFlag(
"allowImplicitByNameParameters",
_.allowImplicitByNameParameters,
testDirectly = false,
)
testBooleanFlag(
"withAllowImplicitByNameParameters",
_.allowImplicitByNameParameters,
testDirectly = true,
)
testBooleanFlag(
"allowSignificantIndentation",
_.allowSignificantIndentation,
testDirectly = false,
)
testBooleanFlag(
"withAllowSignificantIndentation",
_.allowSignificantIndentation,
testDirectly = true,
)

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 baseDialect = Scala213
generatedMap.keys.filter(!omittedMethods.contains(_)).foreach { key =>
generatedMap(key)(baseDialect, true)
}
}
}

0 comments on commit 52c8954

Please sign in to comment.