Skip to content

Commit

Permalink
Mix Scala 2 macros in MUnit Scala 3 module
Browse files Browse the repository at this point in the history
With this change, Scala 2.13 users will be able to depend on munit_3.0
artifacts instead of munit_2.13.

I wasn't able to manually test this change because MUnit doesn't
cross-build for 3.0.0-M1, which is the latest version supported by the
`-Ytasty-reader` flag in 2.13.4. Once 2.13.5 is out, it will be easier
ot make use of this feature.
  • Loading branch information
olafurpg committed Jan 5, 2021
1 parent d893869 commit adc8857
Show file tree
Hide file tree
Showing 4 changed files with 368 additions and 14 deletions.
21 changes: 7 additions & 14 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,7 @@ inThisBuild(
testFrameworks := List(
new TestFramework("munit.Framework")
),
useSuperShell := false,
scalacOptions ++= List(
"-target:jvm-1.8",
"-language:implicitConversions"
)
useSuperShell := false
)
)

Expand Down Expand Up @@ -187,15 +183,12 @@ lazy val munit = crossProject(JSPlatform, JVMPlatform, NativePlatform)
}
result.toList
},
libraryDependencies ++= {
CrossVersion.partialVersion(scalaVersion.value) match {
case Some((major, _)) if major != 2 => Nil
case _ =>
List(
"org.scala-lang" % "scala-reflect" % scalaVersion.value
)
}
}
libraryDependencies ++= List(
"org.scala-lang" % "scala-reflect" % {
if (isDotty.value) scala213
else scalaVersion.value
} % Provided
)
)
.nativeConfigure(sharedNativeConfigure)
.nativeSettings(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,23 @@ package munit.internal
import munit.Clue
import munit.Location
import scala.quoted._
import scala.language.experimental.macros
import scala.reflect.macros.blackbox.Context
import scala.reflect.macros.TypecheckException
import scala.reflect.macros.ParseException

object MacroCompat {

trait LocationMacro {
inline implicit def generate: Location = ${ locationImpl() }
implicit def generate: Location = macro locationImplScala2
}

def locationImplScala2(c: Context): c.Tree = {
import c.universe._
val line = Literal(Constant(c.enclosingPosition.line))
val path = Literal(Constant(c.enclosingPosition.source.path))
New(c.mirror.staticClass(classOf[Location].getName()), path, line)
}

def locationImpl()(using Quotes): Expr[Location] = {
Expand All @@ -20,6 +32,7 @@ object MacroCompat {

trait ClueMacro {
inline implicit def generate[T](value: T): Clue[T] = ${ clueImpl('value) }
implicit def generate[T](value: T): Clue[T] = macro clueImplScala2
}

def clueImpl[T: Type](value: Expr[T])(using Quotes): Expr[Clue[T]] = {
Expand All @@ -29,6 +42,44 @@ object MacroCompat {
'{ new Clue(${Expr(source)}, $value, ${Expr(valueType)}) }
}

def clueImplScala2(c: Context)(value: c.Tree): c.Tree = {
import c.universe._
import compat._
val text: String =
if (value.pos != null && value.pos.isRange) {
val chars = value.pos.source.content
val start = value.pos.start
val end = value.pos.end
if (
end > start &&
start >= 0 && start < chars.length &&
end >= 0 && end < chars.length
) {
new String(chars, start, end - start)
} else {
""
}
} else {
""
}
def simplifyType(tpe: Type): Type = tpe match {
case TypeRef(ThisType(pre), sym, args) if pre == sym.owner =>
simplifyType(TypeRef(NoPrefix, sym, args))
case t =>
// uncomment to debug:
// Printers.log(t)(Location.empty)
t.widen
}
val source = Literal(Constant(text))
val valueType = Literal(Constant(simplifyType(value.tpe).toString()))
New(
TypeRef(NoPrefix, c.mirror.staticClass(classOf[Clue[_]].getName), List(value.tpe.widen)),
source,
value,
valueType
)
}

trait CompileErrorMacro {
inline def compileErrors(inline code: String): String = {
val errors = scala.compiletime.testing.typeCheckErrors(code)
Expand All @@ -42,6 +93,44 @@ object MacroCompat {
s"error:${separator}${trimMessage}\n${error.lineContent}\n${indent}^"
}.mkString("\n")
}
def compileErrors(code: String): String = macro compileErrorsImplScala2
}

def compileErrorsImplScala2(c: Context)(code: c.Tree): c.Tree = {
import c.universe._
val toParse: String = code match {
case Literal(Constant(literal: String)) => literal
case _ =>
c.abort(
code.pos,
"cannot compile dynamic expressions, only constant literals.\n" +
"To fix this problem, pass in a string literal in double quotes \"...\""
)
}

def formatError(message: String, pos: scala.reflect.api.Position): String =
new StringBuilder()
.append("error:")
.append(if (message.contains('\n')) "\n" else " ")
.append(message)
.append("\n")
.append(pos.lineContent)
.append("\n")
.append(" " * (pos.column - 1))
.append("^")
.toString()

val message: String =
try {
c.typecheck(c.parse(s"{\n$toParse\n}"))
""
} catch {
case e: ParseException =>
formatError(e.getMessage(), e.pos)
case e: TypecheckException =>
formatError(e.getMessage(), e.pos)
}
Literal(Constant(message))
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,16 @@ package munit.internal
import munit.Clue
import munit.Location
import scala.quoted._
import scala.language.experimental.macros
import scala.reflect.macros.blackbox.Context
import scala.reflect.macros.TypecheckException
import scala.reflect.macros.ParseException

object MacroCompat {

trait LocationMacro {
inline implicit def generate: Location = ${ locationImpl() }
implicit def generate: Location = macro locationImplScala2
}

def locationImpl()(using Quotes): Expr[Location] = {
Expand All @@ -18,8 +23,16 @@ object MacroCompat {
'{ new Location(${Expr(path)}, ${Expr(startLine)}) }
}

def locationImplScala2(c: Context): c.Tree = {
import c.universe._
val line = Literal(Constant(c.enclosingPosition.line))
val path = Literal(Constant(c.enclosingPosition.source.path))
New(c.mirror.staticClass(classOf[Location].getName()), path, line)
}

trait ClueMacro {
inline implicit def generate[T](value: T): Clue[T] = ${ clueImpl('value) }
implicit def generate[T](value: T): Clue[T] = macro clueImplScala2
}

def clueImpl[T: Type](value: Expr[T])(using Quotes): Expr[Clue[T]] = {
Expand All @@ -29,6 +42,44 @@ object MacroCompat {
'{ new Clue(${Expr(source)}, $value, ${Expr(valueType)}) }
}

def clueImplScala2(c: Context)(value: c.Tree): c.Tree = {
import c.universe._
import compat._
val text: String =
if (value.pos != null && value.pos.isRange) {
val chars = value.pos.source.content
val start = value.pos.start
val end = value.pos.end
if (
end > start &&
start >= 0 && start < chars.length &&
end >= 0 && end < chars.length
) {
new String(chars, start, end - start)
} else {
""
}
} else {
""
}
def simplifyType(tpe: Type): Type = tpe match {
case TypeRef(ThisType(pre), sym, args) if pre == sym.owner =>
simplifyType(TypeRef(NoPrefix, sym, args))
case t =>
// uncomment to debug:
// Printers.log(t)(Location.empty)
t.widen
}
val source = Literal(Constant(text))
val valueType = Literal(Constant(simplifyType(value.tpe).toString()))
New(
TypeRef(NoPrefix, c.mirror.staticClass(classOf[Clue[_]].getName), List(value.tpe.widen)),
source,
value,
valueType
)
}

trait CompileErrorMacro {
inline def compileErrors(inline code: String): String = {
val errors = scala.compiletime.testing.typeCheckErrors(code)
Expand All @@ -42,6 +93,44 @@ object MacroCompat {
s"error:${separator}${trimMessage}\n${error.lineContent}\n${indent}^"
}.mkString("\n")
}
def compileErrors(code: String): String = macro compileErrorsImplScala2
}

def compileErrorsImplScala2(c: Context)(code: c.Tree): c.Tree = {
import c.universe._
val toParse: String = code match {
case Literal(Constant(literal: String)) => literal
case _ =>
c.abort(
code.pos,
"cannot compile dynamic expressions, only constant literals.\n" +
"To fix this problem, pass in a string literal in double quotes \"...\""
)
}

def formatError(message: String, pos: scala.reflect.api.Position): String =
new StringBuilder()
.append("error:")
.append(if (message.contains('\n')) "\n" else " ")
.append(message)
.append("\n")
.append(pos.lineContent)
.append("\n")
.append(" " * (pos.column - 1))
.append("^")
.toString()

val message: String =
try {
c.typecheck(c.parse(s"{\n$toParse\n}"))
""
} catch {
case e: ParseException =>
formatError(e.getMessage(), e.pos)
case e: TypecheckException =>
formatError(e.getMessage(), e.pos)
}
Literal(Constant(message))
}

}
Loading

0 comments on commit adc8857

Please sign in to comment.