Initial commit
This commit is contained in:
commit
7dc37783d8
11 changed files with 503 additions and 0 deletions
34
.gitignore
vendored
Normal file
34
.gitignore
vendored
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
# macOS
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# sbt specific
|
||||||
|
dist/*
|
||||||
|
target/
|
||||||
|
lib_managed/
|
||||||
|
src_managed/
|
||||||
|
project/boot/
|
||||||
|
project/plugins/project/
|
||||||
|
project/local-plugins.sbt
|
||||||
|
project/project/
|
||||||
|
project/target/
|
||||||
|
.history
|
||||||
|
.ensime
|
||||||
|
.ensime_cache/
|
||||||
|
.sbt-scripted/
|
||||||
|
local.sbt
|
||||||
|
|
||||||
|
# Bloop
|
||||||
|
.bsp
|
||||||
|
|
||||||
|
# VS Code
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
# Metals
|
||||||
|
.bloop/
|
||||||
|
.metals/
|
||||||
|
metals.sbt
|
||||||
|
|
||||||
|
# IDEA
|
||||||
|
.idea
|
||||||
|
.idea_modules
|
||||||
|
/.worksheet/
|
1
.java-version
Normal file
1
.java-version
Normal file
|
@ -0,0 +1 @@
|
||||||
|
21
|
2
.scalafmt.conf
Normal file
2
.scalafmt.conf
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
version = 3.7.14
|
||||||
|
runner.dialect = scala3
|
3
README.md
Normal file
3
README.md
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
## Lecture 12
|
||||||
|
|
||||||
|
Code for Lecture 12.
|
11
build.sbt
Normal file
11
build.sbt
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
val scala3Version = "3.3.4"
|
||||||
|
|
||||||
|
lazy val root = project
|
||||||
|
.in(file("."))
|
||||||
|
.settings(
|
||||||
|
organization := "ch.usi.si.msde.edsl",
|
||||||
|
name := "lecture-10",
|
||||||
|
version := "2024.01",
|
||||||
|
scalaVersion := scala3Version,
|
||||||
|
libraryDependencies += "org.typelevel" %% "squants" % "1.8.3",
|
||||||
|
)
|
1
project/build.properties
Normal file
1
project/build.properties
Normal file
|
@ -0,0 +1 @@
|
||||||
|
sbt.version=1.10.2
|
1
project/plugins.sbt
Normal file
1
project/plugins.sbt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.2")
|
|
@ -0,0 +1,247 @@
|
||||||
|
package ch.usi.si.msde.edsl.lecture10
|
||||||
|
|
||||||
|
import scala.quoted.*
|
||||||
|
import scala.annotation.meta.field
|
||||||
|
import scala.language.postfixOps
|
||||||
|
|
||||||
|
/** This object defines the assertions generative DSL.
|
||||||
|
*/
|
||||||
|
object GenerativeFluentAssertionsDSL:
|
||||||
|
|
||||||
|
/** Entrypoint. This macro method takes an expression composed of
|
||||||
|
* AssertionProperty expressions and rewrites them into actual assertions.
|
||||||
|
*/
|
||||||
|
inline def assertions(inline assertions: AssertionProperty): Unit = ${
|
||||||
|
assertionsGenerativeImpl('assertions)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Macro implementation. The method is recursive, each single expression is
|
||||||
|
* then separately rewritten into the generateAssertion method. The method
|
||||||
|
* returns a compiler error if the expression is not an AssertionProperty.
|
||||||
|
*/
|
||||||
|
private def assertionsGenerativeImpl(assertionsExpr: Expr[AssertionProperty])(
|
||||||
|
using Quotes
|
||||||
|
): Expr[Unit] =
|
||||||
|
import quotes.reflect.*
|
||||||
|
|
||||||
|
assertionsExpr match
|
||||||
|
// The expression is an AssertionProperty followed by some other expressions
|
||||||
|
case '{
|
||||||
|
${ assertion }: AssertionProperty
|
||||||
|
$rest
|
||||||
|
} =>
|
||||||
|
'{
|
||||||
|
// rewrite the assertion on top
|
||||||
|
${ generateAssertion(assertion) }
|
||||||
|
// invoke this recursively on the remaining expressions.
|
||||||
|
${ assertionsGenerativeImpl(rest) }
|
||||||
|
}
|
||||||
|
// The expression is a single AssertionProperty
|
||||||
|
case '{
|
||||||
|
${ assertion }: AssertionProperty
|
||||||
|
} =>
|
||||||
|
generateAssertion(assertion)
|
||||||
|
// The expression is anything else (invalid)
|
||||||
|
case anyOther =>
|
||||||
|
report.errorAndAbort(
|
||||||
|
"Invalid Expression, must be an assertion: " + anyOther.show,
|
||||||
|
anyOther
|
||||||
|
)
|
||||||
|
|
||||||
|
end assertionsGenerativeImpl
|
||||||
|
|
||||||
|
/** Checks a single assertion property if it's a should be or a should have
|
||||||
|
* assertion.
|
||||||
|
*/
|
||||||
|
private def generateAssertion(assertionExpr: Expr[AssertionProperty])(using
|
||||||
|
Quotes
|
||||||
|
): Expr[Unit] =
|
||||||
|
import quotes.reflect.*
|
||||||
|
|
||||||
|
val generatedAssertion = assertionExpr match
|
||||||
|
// be assertion. Check the explicit selectDynamic to extract the property name
|
||||||
|
case '{
|
||||||
|
(${ subjectExpr }: subjectType) should be.selectDynamic(
|
||||||
|
$propertyNameExpr
|
||||||
|
)
|
||||||
|
} =>
|
||||||
|
generateShouldBeAssertion[subjectType](subjectExpr, propertyNameExpr)
|
||||||
|
// The more complicated structure of property type provider construction in
|
||||||
|
// the should have assertion. Again, the applyDynamic is explicit.
|
||||||
|
// The quoted pattern also extracts: the type of the assertion subject,
|
||||||
|
// and the type of the property (actually... not exactly that. Why?)
|
||||||
|
case '{
|
||||||
|
(${ typeProvider }: HavePropertyTypeProvider[subjectType])
|
||||||
|
.applyDynamic($propertyNameExpr)($valueExpr: valueType)
|
||||||
|
.$asInstanceOf$[AssertionProperty]
|
||||||
|
} =>
|
||||||
|
val subjectExpr = '{ $typeProvider.subject }
|
||||||
|
generateShouldHaveAssertion[subjectType, valueType](
|
||||||
|
subjectExpr,
|
||||||
|
propertyNameExpr,
|
||||||
|
valueExpr
|
||||||
|
)
|
||||||
|
case _ =>
|
||||||
|
report.errorAndAbort(
|
||||||
|
"Invalid expression, must be a 'should be' or 'should have' assertion. ",
|
||||||
|
assertionExpr
|
||||||
|
)
|
||||||
|
report.info(clean(generatedAssertion.show), assertionExpr)
|
||||||
|
generatedAssertion
|
||||||
|
end generateAssertion
|
||||||
|
|
||||||
|
private def generateShouldBeAssertion[T](
|
||||||
|
subject: Expr[T],
|
||||||
|
propertyNameExpr: Expr[String]
|
||||||
|
)(using Type[T])(using Quotes): Expr[Unit] =
|
||||||
|
import quotes.reflect.*
|
||||||
|
|
||||||
|
val subjectTypeRepr = TypeRepr.of[T]
|
||||||
|
val propertyName = propertyNameExpr.valueOrAbort
|
||||||
|
val isPropertyName = s"is${propertyName.capitalize}"
|
||||||
|
|
||||||
|
// Looks for the first Boolean field or arity-zero method named exactly as specified or in the isXXXX form.
|
||||||
|
val optionalValidSymbol =
|
||||||
|
findCandidateSymbol[T, Boolean](propertyName, isPropertyName)
|
||||||
|
|
||||||
|
if optionalValidSymbol.isEmpty then
|
||||||
|
report.errorAndAbort(
|
||||||
|
s"Assertion Error: No field or arity-0 method with name ${propertyName} or ${isPropertyName} on type ${subjectTypeRepr.show}",
|
||||||
|
propertyNameExpr.asTerm.pos
|
||||||
|
)
|
||||||
|
else
|
||||||
|
val validSymbol = optionalValidSymbol.get
|
||||||
|
val validSymbolNameExpr = Expr(validSymbol.toString)
|
||||||
|
// Constructs a 'select' tree, that is, a selection to the field or method
|
||||||
|
// of the subject.
|
||||||
|
val call = Select(subject.asTerm, validSymbol).asExprOf[Boolean]
|
||||||
|
|
||||||
|
val assertion = '{
|
||||||
|
assert(
|
||||||
|
$call,
|
||||||
|
s"${$subject} was not ${$propertyNameExpr} (${$validSymbolNameExpr} is false)"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
assertion
|
||||||
|
end if
|
||||||
|
end generateShouldBeAssertion
|
||||||
|
|
||||||
|
private def generateShouldHaveAssertion[T, R](
|
||||||
|
subjectExpr: Expr[T],
|
||||||
|
propertyNameExpr: Expr[String],
|
||||||
|
propertyValueExpr: Expr[R]
|
||||||
|
)(using Type[T], Type[R])(using Quotes): Expr[Unit] =
|
||||||
|
import quotes.reflect.*
|
||||||
|
|
||||||
|
val subjectTypeRepr = TypeRepr.of[T]
|
||||||
|
val optionalCandidateSymbol =
|
||||||
|
findCandidateSymbol[T, R](propertyNameExpr.valueOrAbort)
|
||||||
|
|
||||||
|
if optionalCandidateSymbol.isEmpty then
|
||||||
|
report.errorAndAbort(
|
||||||
|
s"Assertion Error: No field or arity-0 method with name ${propertyNameExpr.valueOrAbort} on type ${subjectTypeRepr.show}",
|
||||||
|
propertyNameExpr
|
||||||
|
)
|
||||||
|
else
|
||||||
|
val candidateMethod = optionalCandidateSymbol.get
|
||||||
|
/*
|
||||||
|
* This version is slightly different than the one presented during
|
||||||
|
* the lecture. In this case we also generate a proper message for
|
||||||
|
* the assertion violation, so we need to declare and reuse a local
|
||||||
|
* variable, otherwise the same expression with the type provider needs
|
||||||
|
* to be duplicated and inserted multiple times. So we take the subjectExpr,
|
||||||
|
* which contains the type provider and selects the subject, and we
|
||||||
|
* save it to a local val 'subject'. Then we use the low-level AST
|
||||||
|
* API to retrieve the result:
|
||||||
|
* first we take '(subject), which is the expression corresponding
|
||||||
|
* to the subject variable, and we take the low-level term version;
|
||||||
|
* then, we construct the Select AST which corresponds to the method
|
||||||
|
* invocation;
|
||||||
|
* finally, we convert all to an expression back again, which can be
|
||||||
|
* spliced in the quote.
|
||||||
|
*/
|
||||||
|
val assertion = '{
|
||||||
|
val subject = $subjectExpr
|
||||||
|
val expectedValue = $propertyValueExpr
|
||||||
|
lazy val result = ${ Select(('subject).asTerm, candidateMethod).asExpr }
|
||||||
|
assert(
|
||||||
|
result == expectedValue,
|
||||||
|
s"${subject} did not have ${$propertyNameExpr} equal" +
|
||||||
|
s" to ${expectedValue}, but it was equal to ${result}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
report.info(assertion.show, subjectExpr.asTerm.pos)
|
||||||
|
assertion
|
||||||
|
end if
|
||||||
|
end generateShouldHaveAssertion
|
||||||
|
|
||||||
|
/** Looks for candidate symbols with a given set of possible names on a type.
|
||||||
|
*
|
||||||
|
* Improved version w.r.t. the one presented during the lecture.
|
||||||
|
*
|
||||||
|
* @param T
|
||||||
|
* the type of the assertion subject (i.e., the object before the should)
|
||||||
|
* @param R
|
||||||
|
* the type of the value after 'have'.
|
||||||
|
* @param names
|
||||||
|
* the possible names to look for.
|
||||||
|
* @return
|
||||||
|
* optionally a symbol of an arity-0 method or val with name included in
|
||||||
|
* names, say foo, for which subject.foo(r: R) typechecks.
|
||||||
|
*/
|
||||||
|
private def findCandidateSymbol[T, R](names: String*)(using Type[T], Type[R])(
|
||||||
|
using Quotes
|
||||||
|
): Option[quotes.reflect.Symbol] =
|
||||||
|
import quotes.reflect.*
|
||||||
|
|
||||||
|
val subjectTypeRepr = TypeRepr.of[T]
|
||||||
|
|
||||||
|
// Extracts (possibly) the field with a given name from T.
|
||||||
|
// If the field does not exist, it returns Symbol.noSymbol.
|
||||||
|
val possibleFieldSymbols = names.map: name =>
|
||||||
|
subjectTypeRepr.typeSymbol.fieldMember(name)
|
||||||
|
|
||||||
|
// Extracts the methods with a given name (why a list?)
|
||||||
|
// If no method exists, returns noSymbol.
|
||||||
|
val possibleMethodSymbols = names.map: name =>
|
||||||
|
subjectTypeRepr.typeSymbol
|
||||||
|
.methodMember(name)
|
||||||
|
.headOption
|
||||||
|
.getOrElse(Symbol.noSymbol)
|
||||||
|
|
||||||
|
// Filter all symbols that are not noSymbol, and that are of arity 0.
|
||||||
|
val possibleSymbols = (possibleFieldSymbols ++ possibleMethodSymbols)
|
||||||
|
.filter: symbol =>
|
||||||
|
symbol != Symbol.noSymbol && symbol.paramSymss.isEmpty
|
||||||
|
.filter: symbol =>
|
||||||
|
// This filters all methods for which the specified value is compatible
|
||||||
|
// with the return type of the method. For example, the method may take a
|
||||||
|
// trait A and the value could be of type R <: A.
|
||||||
|
val memberType = subjectTypeRepr.memberType(symbol).widen
|
||||||
|
val typeOfValue = TypeRepr.of[R].widen
|
||||||
|
typeOfValue <:< memberType
|
||||||
|
|
||||||
|
possibleSymbols.find(symbol =>
|
||||||
|
symbol != Symbol.noSymbol && symbol.paramSymss.isEmpty
|
||||||
|
)
|
||||||
|
|
||||||
|
end findCandidateSymbol
|
||||||
|
|
||||||
|
/** Removes some common package names and other synthetic code from some code
|
||||||
|
* representation.
|
||||||
|
*
|
||||||
|
* @param codeRepresentation
|
||||||
|
* some string representing code.
|
||||||
|
* @return
|
||||||
|
* the code without common scala package names and other synthetic
|
||||||
|
* elements.
|
||||||
|
*/
|
||||||
|
def clean(codeRepresentation: String) =
|
||||||
|
codeRepresentation
|
||||||
|
.replaceAll("(_root_|scala|Predef)\\.", "")
|
||||||
|
.replaceAll("List\\.apply", "List")
|
||||||
|
.replaceAll("collection\\.immutable\\.", "")
|
||||||
|
.replaceAll("ch\\.usi\\.si\\.msde\\.edsl\\.lecture10\\.", "")
|
||||||
|
.replaceAll(" @annotation\\.unchecked\\.uncheckedVariance", "")
|
||||||
|
|
||||||
|
end GenerativeFluentAssertionsDSL
|
|
@ -0,0 +1,70 @@
|
||||||
|
package ch.usi.si.msde.edsl.lecture10
|
||||||
|
|
||||||
|
import scala.language.dynamics
|
||||||
|
|
||||||
|
sealed trait AssertionProperty
|
||||||
|
|
||||||
|
/** Represents (but with no implementation) the result of a call to
|
||||||
|
* be.selectDynamic(String), used to specify the property. The semantics of
|
||||||
|
* this method is implemented through macros - in the assertions method.
|
||||||
|
*/
|
||||||
|
class DynamicShouldBeProperty extends AssertionProperty with Dynamic:
|
||||||
|
def applyDynamic(fieldName: String)(foo: Any*) = ???
|
||||||
|
end DynamicShouldBeProperty
|
||||||
|
|
||||||
|
/** The class that implements the syntactic aspect of the type provider. It
|
||||||
|
* extends Selectable and implements applyDynamic with arity 1, so that any
|
||||||
|
* object of this type can be invoked with an arbitrary method with 1 argument,
|
||||||
|
* and a specific return type.
|
||||||
|
*
|
||||||
|
* Selectable types can be refined with a list of specific methods that are
|
||||||
|
* available and thus checked by the compiler. The macro in the should method
|
||||||
|
* generates a refinement of this type according to type T, so that - for any
|
||||||
|
* arity zero method or field in T - say foo: X, the type provider has a
|
||||||
|
* corresponding method def foo(arg: X): AssertionProperty.
|
||||||
|
*/
|
||||||
|
class HavePropertyTypeProvider[T](val subject: T) extends Selectable:
|
||||||
|
def applyDynamic(fieldName: String)(arg: Any): AssertionProperty = ???
|
||||||
|
end HavePropertyTypeProvider
|
||||||
|
|
||||||
|
/** A trait for each type of assertion (be or have).
|
||||||
|
*/
|
||||||
|
sealed trait AssertionDSLVerb
|
||||||
|
case object be extends AssertionDSLVerb with Dynamic:
|
||||||
|
def selectDynamic(fieldName: String): AssertionProperty = ???
|
||||||
|
case object have extends AssertionDSLVerb
|
||||||
|
|
||||||
|
/** Implicit class to add 'should' assertions to a generic type T.
|
||||||
|
*
|
||||||
|
* @param subject
|
||||||
|
* the object to which perform the assertion.
|
||||||
|
*/
|
||||||
|
extension [T](subject: T)
|
||||||
|
/** Specifies a 'be' assertion. The method is just a placeholder to fix the
|
||||||
|
* syntax, its semantics is performed by the assertions macro.
|
||||||
|
*
|
||||||
|
* @param verb
|
||||||
|
* The be word.
|
||||||
|
* @return
|
||||||
|
* a DynamicProperty object which allows to specify a further property
|
||||||
|
* name.
|
||||||
|
*/
|
||||||
|
def should(property: AssertionProperty) = ???
|
||||||
|
|
||||||
|
/** Specifies the 'have' assertion.
|
||||||
|
*
|
||||||
|
* The method is a type provider: it generates, through a macro, a refinement
|
||||||
|
* of the HavePropertyTypeProvider[T] (which is Selectable) which contains,
|
||||||
|
* for every arity-0 method or field named foo: R in T, a method in the type
|
||||||
|
* provider foo(value: R): AssertionProperty.
|
||||||
|
*
|
||||||
|
* The method semantics is then provided by the assertions macro.
|
||||||
|
*
|
||||||
|
* @param verbWord
|
||||||
|
* The have word.
|
||||||
|
* @return
|
||||||
|
* a type provider for properties of type T.
|
||||||
|
*/
|
||||||
|
transparent inline def should(inline verbWord: have.type) = ${
|
||||||
|
ShouldHaveTypeProvider.generateShouldHaveTypeProvider[T]('subject)
|
||||||
|
}
|
46
src/main/scala/ch/usi/si/msde/edsl/lecture10/Main.scala
Normal file
46
src/main/scala/ch/usi/si/msde/edsl/lecture10/Main.scala
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
package ch.usi.si.msde.edsl.lecture10
|
||||||
|
|
||||||
|
// import scala.language.postfixOps
|
||||||
|
|
||||||
|
import squants.mass.MassConversions.MassConversions
|
||||||
|
import squants.market.MoneyConversions.MoneyConversions
|
||||||
|
|
||||||
|
import GenerativeFluentAssertionsDSL.*
|
||||||
|
import squants.mass.Mass
|
||||||
|
import squants.market.Money
|
||||||
|
|
||||||
|
extension (value: Double) def i = Complex(0, value)
|
||||||
|
|
||||||
|
case class Complex(re: Double, im: Double):
|
||||||
|
def +(other: Complex) = Complex(re + other.re, im + other.im)
|
||||||
|
def +(other: Double) = Complex(re + other, im)
|
||||||
|
|
||||||
|
case class Person(firstName: String, lastName: String, age: Int):
|
||||||
|
def adult: Boolean = age >= 18
|
||||||
|
|
||||||
|
case class Box(label: String, weight: Mass, price: Money)
|
||||||
|
|
||||||
|
@main def assertionsExample: Unit =
|
||||||
|
val person = Person("Andrea", "Mocci", 0x29)
|
||||||
|
val box = Box("aBox", 30.kg, 10.CHF)
|
||||||
|
|
||||||
|
assertions:
|
||||||
|
/* be.property assertions */
|
||||||
|
// assert(person.adult, ...)
|
||||||
|
person should be.adult
|
||||||
|
// assert(List().isEmpty, ...)
|
||||||
|
List().should(be.empty)
|
||||||
|
// assert(List(1,2,3).nonEmpty)
|
||||||
|
List(1, 2, 3) should be.nonEmpty
|
||||||
|
|
||||||
|
/* should have assertions */
|
||||||
|
// assert(List(1).head == 1, ...)
|
||||||
|
List(1) should have head 1
|
||||||
|
// assert(box.weight == 30.kg, ...)
|
||||||
|
box should have weight 30.kg
|
||||||
|
// assert(person.age == 0x29, ...)
|
||||||
|
person should have age 0x29
|
||||||
|
List(2, 3) should have tail List(3)
|
||||||
|
3.i + 1 should have re 1.0
|
||||||
|
|
||||||
|
end assertionsExample
|
|
@ -0,0 +1,87 @@
|
||||||
|
package ch.usi.si.msde.edsl.lecture10
|
||||||
|
|
||||||
|
import scala.quoted.*
|
||||||
|
|
||||||
|
object ShouldHaveTypeProvider:
|
||||||
|
|
||||||
|
/** The type provider for the should have assertion, which generates a refined
|
||||||
|
* structural type for type T.
|
||||||
|
*/
|
||||||
|
def generateShouldHaveTypeProvider[T](subject: Expr[T])(using Type[T])(using
|
||||||
|
Quotes
|
||||||
|
): Expr[Any] =
|
||||||
|
import quotes.reflect.*
|
||||||
|
|
||||||
|
val subjectTypeRepr = TypeRepr.of[T]
|
||||||
|
|
||||||
|
/** Given a refinable current type, and the symbol of a arity-0 method or a
|
||||||
|
* field of type foo: X, generates a refinement containing a method with
|
||||||
|
* signature def foo(arg: X): AssertionProperty.
|
||||||
|
*/
|
||||||
|
def refineTypeBySymbol(
|
||||||
|
currentTypeRepr: TypeRepr,
|
||||||
|
symbol: Symbol
|
||||||
|
): TypeRepr =
|
||||||
|
// The type of the field, or the return type of the method.
|
||||||
|
val fieldTypeRepr = subjectTypeRepr.memberType(symbol).widen
|
||||||
|
|
||||||
|
// Generates the "type" of the method to be generated for the refinement.
|
||||||
|
// The first parameter is the list of arguments, the second is a function returning
|
||||||
|
// the type of arguments, and the third one is a function returning the return type
|
||||||
|
// of the method.
|
||||||
|
// In this case: arg is the name of the parameter;
|
||||||
|
// _ => List(fieldTypeRepr) returns a list with the type of arg;
|
||||||
|
// _ => TypeRepr.of[AssertionProperty] returns the (reflection) type of the method.
|
||||||
|
val methodType = MethodType(List("arg"))(
|
||||||
|
_ => List(fieldTypeRepr),
|
||||||
|
_ => TypeRepr.of[AssertionProperty]
|
||||||
|
)
|
||||||
|
// returns the refinement of currentTypeRepr -
|
||||||
|
// symbol.name is the name of the method,
|
||||||
|
// methodType is its type.
|
||||||
|
val refinement = Refinement(currentTypeRepr, symbol.name, methodType)
|
||||||
|
refinement
|
||||||
|
|
||||||
|
/** Refines a type according to a list of fields or methods of arity 0.
|
||||||
|
*/
|
||||||
|
def refineTypeBySymbols(
|
||||||
|
currentTypeRepr: TypeRepr,
|
||||||
|
fields: List[Symbol]
|
||||||
|
): TypeRepr =
|
||||||
|
fields match
|
||||||
|
// this is pattern matching on list - like head :: rest
|
||||||
|
case symbol :: symbols =>
|
||||||
|
refineTypeBySymbols(
|
||||||
|
refineTypeBySymbol(currentTypeRepr, symbol),
|
||||||
|
symbols
|
||||||
|
)
|
||||||
|
// empty list case
|
||||||
|
case Nil =>
|
||||||
|
currentTypeRepr
|
||||||
|
|
||||||
|
val fields = subjectTypeRepr.typeSymbol.fieldMembers
|
||||||
|
val arityZeroMethods = subjectTypeRepr.typeSymbol.methodMembers
|
||||||
|
.filter: methodMember =>
|
||||||
|
methodMember.paramSymss.size == 0 && !methodMember.flags.is(
|
||||||
|
Flags.Synthetic
|
||||||
|
)
|
||||||
|
|
||||||
|
val refinedType = refineTypeBySymbols(
|
||||||
|
TypeRepr.of[HavePropertyTypeProvider[T]],
|
||||||
|
fields ++ arityZeroMethods
|
||||||
|
)
|
||||||
|
|
||||||
|
// This is the way to extract a (reflection) type to a
|
||||||
|
// type that can be used in some quoted code.
|
||||||
|
// it's the equivalent of .asExpr for expressions,
|
||||||
|
// but it's more complicated because in this case the
|
||||||
|
// exact type is unknown until compilation time.
|
||||||
|
refinedType.asType match
|
||||||
|
case '[tpe] => // tpe is the exact refined type
|
||||||
|
val res = '{
|
||||||
|
val p = HavePropertyTypeProvider[T]($subject)
|
||||||
|
p.asInstanceOf[tpe]
|
||||||
|
}
|
||||||
|
res
|
||||||
|
|
||||||
|
end ShouldHaveTypeProvider
|
Loading…
Reference in a new issue