Skip to content

Commit

Permalink
Merge pull request #79 from geirolz/add_macros_for_scala_3
Browse files Browse the repository at this point in the history
Add scala 3 macros
  • Loading branch information
geirolz authored Jun 11, 2022
2 parents 908263d + 8c1f528 commit 98e192b
Show file tree
Hide file tree
Showing 4 changed files with 146 additions and 2 deletions.
19 changes: 17 additions & 2 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ lazy val generic: Project =
.settings(
libraryDependencies ++= ProjectDependencies.Generic.dedicated
)
.settings(
scalacOptions ++= macroSettings(scalaVersion.value)
)

lazy val scalatest: Project =
buildModule(
Expand Down Expand Up @@ -130,7 +133,6 @@ def scalacSettings(scalaVersion: String): Seq[String] =
"utf-8", // Specify character encoding used by source files.
"-feature", // Emit warning and location for usages of features that should be imported explicitly.
"-language:existentials", // Existential types (besides wildcard types) can be written and inferred
"-language:experimental.macros", // Allow macro definition (besides implementation and application)
"-language:higherKinds", // Allow higher-kinded types
"-language:implicitConversions" // Allow definition of implicit functions called views
) ++ {
Expand Down Expand Up @@ -174,13 +176,26 @@ def scalacSettings(scalaVersion: String): Seq[String] =
"-Ywarn-unused:explicits", // Warn if a explicit value parameter is unused.
"-Ywarn-unused:patvars", // Warn if a variable bound in a pattern is unused.
"-Ywarn-unused:privates", // Warn if a private member is unused.
"-Ywarn-macros:after", // Tells the compiler to make the unused checks after macro expansion
"-Xsource:3",
"-P:kind-projector:underscore-placeholders"
)
case _ => Nil
}
}

def macroSettings(scalaVersion: String): Seq[String] =
CrossVersion.partialVersion(scalaVersion) match {
case Some((3, _)) =>
Seq(
"-Xcheck-macros" // Fail the compilation if there are any warnings.
)
case Some((2, 13)) =>
Seq(
"-language:experimental.macros", // Allow macro definition (besides implementation and application)
"-Ywarn-macros:after" // Tells the compiler to make the unused checks after macro expansion
)
case _ => Nil
}

//=============================== ALIASES ===============================
addCommandAlias("check", ";clean;test")
89 changes: 89 additions & 0 deletions modules/generic/src/main/scala-3/erules/generic/RuleMacros.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package erules.generic

import erules.core.Rule

import scala.annotation.tailrec

private[generic] trait RuleMacros:

extension [F[_], T](rule: Rule[F, T])
/** Contramap `Rule` and add target info invoking `targetInfo` and passing the expression of the
* map function `f`
*
* For example
* {{{
* case class Foo(bar: Bar)
* case class Bar(test: Test)
* case class Test(value: Int)
*
* val rule: Rule[Int] = Rule("RULE").const(RuleVerdict.Ignore.withoutReasons)
* val fooRule: Rule[Foo] = rule.contramapTarget[Foo](_.bar.test.value)
*
* fooRule.targetInfo
* scala> val res0: Option[String] = Some(bar.test.value)
* }}}
*
* @see
* [[Rule.contramap()]] and [[Rule.targetInfo()]] for further information
*/
inline def contramapTarget[U](inline path: U => T): Rule[F, U] =
${ RuleImplMacros.contramapTargetImpl[F, U, T]('rule, 'path) }

private[generic] object RuleMacros extends RuleMacros

private object RuleImplMacros {

import scala.quoted.*

def contramapTargetImpl[F[_]: Type, U: Type, T: Type](
rule: Expr[Rule[F, T]],
path: Expr[U => T]
)(using Quotes): Expr[Rule[F, U]] =
'{
$rule.contramap($path).targetInfo(${ extractTargetInfoFromFunctionCall(path) })
}

def extractTargetInfoFromFunctionCall[T: Type, U: Type](
path: Expr[T => U]
)(using Quotes): Expr[String] = {

import quotes.reflect.*

val expectedShapeInfo = "Path must have shape: _.field1.field2.each.field3.(...)"

enum PathElement {
case TermPathElement(term: String, xargs: String*) extends PathElement
case FunctorPathElement(functor: String, method: String, xargs: String*) extends PathElement
}

def toPath(tree: Tree, acc: List[PathElement]): Seq[PathElement] = {
tree match {
/** Field access */
case Select(deep, ident) =>
toPath(deep, PathElement.TermPathElement(ident) :: acc)
/** The first segment from path (e.g. `_.age` -> `_`) */
case i: Ident =>
acc
case t =>
report.errorAndAbort(s"Unsupported path element $t")
}
}

val pathElements: Seq[PathElement] = path.asTerm match {
/** Single inlined path */
case Inlined(_, _, Block(List(DefDef(_, _, _, Some(p))), _)) =>
toPath(p, List.empty)
case _ =>
report.errorAndAbort(s"Unsupported path [$path]")
}

Expr(
pathElements
.map {
case PathElement.TermPathElement(c, _ @_*) => c
case PathElement.FunctorPathElement(_, method, _ @_*) => method
}
.mkString(".")
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package erules.generic

object implicits extends RuleMacros
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package erules.generic

import cats.Id
import erules.core.{PureRule, Rule, RuleVerdict}
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers

class RuleMacrosTest extends AnyFunSuite with Matchers {

import erules.generic.implicits.*

test("contramapTarget should contramap and add target info") {

case class Foo(bar: Bar)
case class Bar(test: Test)
case class Test(value: Int)

val rule: PureRule[Int] = Rule("RULE").const[Id, Int](RuleVerdict.Ignore.withoutReasons)
val fooRule: PureRule[Foo] = rule.contramapTarget[Foo](_.bar.test.value)

fooRule.targetInfo shouldBe Some("bar.test.value")
}

test("contramapTarget should not compile with monadic values") {
"""
import cats.Id
case class Foo(b: Option[Bar])
case class Bar(t: Option[Test])
case class Test(value: Int)
val rule: PureRule[Int] = Rule("RULE").const[Id, Int](RuleVerdict.Ignore.withoutReasons)
rule.contramapTarget[Foo](_.b.flatMap(_.t.map(_.value)).get)
""" shouldNot compile
}
}

0 comments on commit 98e192b

Please sign in to comment.