From 834539972213fd328a28952d165490c24c22bec9 Mon Sep 17 00:00:00 2001 From: Alexander Ioffe Date: Tue, 17 Aug 2021 09:25:56 -0400 Subject: [PATCH] Implement static operator for splicing constants e.g. static(Constants.Foo) (#16) --- .../src/main/scala/io/getquill/Dsl.scala | 3 + .../src/main/scala/io/getquill/GetQuill.scala | 1 + .../main/scala/io/getquill/StaticSplice.scala | 72 +++++++++ .../context/ReflectiveChainLookup.scala | 131 ++++++++++++++++ .../getquill/context/StaticSpliceMacro.scala | 147 ++++++++++++++++++ .../io/getquill/metaprog/Extractors.scala | 18 +++ .../io/getquill/util/CommonExtensions.scala | 43 +++++ .../scala/io/getquill/util/LoadObject.scala | 75 +++++---- .../io/getquill/util/prep/Hierarchies.scala | 25 +++ .../io/getquill/util/prep/UseSelectPath.scala | 15 ++ .../io/getquill/CustomParserFactory.scala | 14 -- .../io/getquill/metaprog/SelectPathSpec.scala | 51 ++++++ .../getquill/metaprog/StaticSpliceSpec.scala | 36 +++++ 13 files changed, 589 insertions(+), 42 deletions(-) create mode 100644 quill-sql/src/main/scala/io/getquill/StaticSplice.scala create mode 100644 quill-sql/src/main/scala/io/getquill/context/ReflectiveChainLookup.scala create mode 100644 quill-sql/src/main/scala/io/getquill/context/StaticSpliceMacro.scala create mode 100644 quill-sql/src/main/scala/io/getquill/util/CommonExtensions.scala create mode 100644 quill-sql/src/main/scala/io/getquill/util/prep/Hierarchies.scala create mode 100644 quill-sql/src/main/scala/io/getquill/util/prep/UseSelectPath.scala delete mode 100644 quill-sql/src/test/scala/io/getquill/CustomParserFactory.scala create mode 100644 quill-sql/src/test/scala/io/getquill/metaprog/SelectPathSpec.scala create mode 100644 quill-sql/src/test/scala/io/getquill/metaprog/StaticSpliceSpec.scala diff --git a/quill-sql/src/main/scala/io/getquill/Dsl.scala b/quill-sql/src/main/scala/io/getquill/Dsl.scala index 099fcbc6e3..c3292e87c2 100644 --- a/quill-sql/src/main/scala/io/getquill/Dsl.scala +++ b/quill-sql/src/main/scala/io/getquill/Dsl.scala @@ -26,6 +26,7 @@ import io.getquill.context.UnquoteMacro import io.getquill.context.LiftMacro import io.getquill._ import io.getquill.dsl.InfixDsl +import io.getquill.context.StaticSpliceMacro // trait Quoter { // def quote[T](bodyExpr: Quoted[T]): Quoted[T] = ??? @@ -77,6 +78,8 @@ trait QueryDsl { trait QuoteDsl { import scala.language.implicitConversions + inline def static[T](inline value: T): T = ${ StaticSpliceMacro('value) } + inline def insertMeta[T](inline exclude: (T => Any)*): InsertMeta[T] = ${ InsertMetaMacro[T]('exclude) } inline def updateMeta[T](inline exclude: (T => Any)*): UpdateMeta[T] = ${ UpdateMetaMacro[T]('exclude) } diff --git a/quill-sql/src/main/scala/io/getquill/GetQuill.scala b/quill-sql/src/main/scala/io/getquill/GetQuill.scala index 7ecf08cb1e..e441d36fe4 100644 --- a/quill-sql/src/main/scala/io/getquill/GetQuill.scala +++ b/quill-sql/src/main/scala/io/getquill/GetQuill.scala @@ -1,3 +1,4 @@ package io.getquill export Dsl._ +export StaticSplice._ diff --git a/quill-sql/src/main/scala/io/getquill/StaticSplice.scala b/quill-sql/src/main/scala/io/getquill/StaticSplice.scala new file mode 100644 index 0000000000..6ebb4d5034 --- /dev/null +++ b/quill-sql/src/main/scala/io/getquill/StaticSplice.scala @@ -0,0 +1,72 @@ +package io.getquill + +import scala.quoted._ +import java.time.format.DateTimeFormatter +import io.getquill.util.Format +import io.getquill.util.LoadModule +import scala.util.Try +import scala.util.Failure +import scala.util.Success +import io.getquill.util.CommonExtensions.Either._ + +/** + * Trait that allows usage of 'static' block. Can declared one of these and use similar to encoders + * but it needs to be compiled in a previous compilation unit and a global static. + * TODO More explanation + */ +trait StaticSplice[T]: + def apply(value: T): String + +object StaticSplice: + import io.getquill.metaprog.Extractors._ + + object Summon: + def apply[T: Type](using Quotes): Either[String, StaticSplice[T]] = + import quotes.reflect.{ Try => TTry, _} + for { + summonValue <- Expr.summon[StaticSplice[T]].toEitherOr(s"a StaticSplice[${Format.TypeOf[T]}] cannot be summoned") + // Summoning StaticSplice[T] will given (SpliceString: StaticSplice[String]) + // (a.k.a. Typed(Ident(SpliceString), TypeTree(StaticSplice[String])) usually with an outer inline surrounding it all) + // so then we need to use Untype to just get SpliceString which is a module that we can load + staticSpliceType = Untype(summonValue.asTerm.underlyingArgument).tpe.widen + + untypedModule <- LoadModule.TypeRepr(staticSpliceType).toEither.mapLeft(_.getMessage) + module <- Try(untypedModule.asInstanceOf[StaticSplice[T]]).toEither.mapLeft(_.getMessage) + } yield (module) + + object SpliceString extends StaticSplice[String]: + def apply(value: String) = s"'${value}'" + inline given StaticSplice[String] = SpliceString + + object SpliceInt extends StaticSplice[Int]: + def apply(value: Int) = s"${value}" + inline given StaticSplice[Int] = SpliceInt + + object SpliceShort extends StaticSplice[Short]: + def apply(value: Short) = s"${value}" + inline given StaticSplice[Short] = SpliceShort + + object SpliceLong extends StaticSplice[Long]: + def apply(value: Long) = s"${value}" + inline given StaticSplice[Long] = SpliceLong + + object SpliceFloat extends StaticSplice[Float]: + def apply(value: Float) = s"${value}" + inline given StaticSplice[Float] = SpliceFloat + + object SpliceDouble extends StaticSplice[Double]: + def apply(value: Double) = s"${value}" + inline given StaticSplice[Double] = SpliceDouble + + object SpliceDate extends StaticSplice[java.sql.Date]: + def apply(value: java.sql.Date) = + value.toLocalDate.format(DateTimeFormatter.ofPattern("yyyyMMdd")) + inline given StaticSplice[java.sql.Date] = SpliceDate + + object SpliceLocalDate extends StaticSplice[java.time.LocalDate]: + def apply(value: java.time.LocalDate) = + value.format(DateTimeFormatter.ofPattern("yyyyMMdd")) + inline given StaticSplice[java.time.LocalDate] = SpliceLocalDate + + +end StaticSplice \ No newline at end of file diff --git a/quill-sql/src/main/scala/io/getquill/context/ReflectiveChainLookup.scala b/quill-sql/src/main/scala/io/getquill/context/ReflectiveChainLookup.scala new file mode 100644 index 0000000000..3693252a49 --- /dev/null +++ b/quill-sql/src/main/scala/io/getquill/context/ReflectiveChainLookup.scala @@ -0,0 +1,131 @@ +package io.getquill.context + +import scala.quoted._ +import io.getquill.StaticSplice +import io.getquill.util.LoadModule +import io.getquill.metaprog.Extractors +import scala.util.Success +import scala.util.Failure +import scala.util.Try +import scala.util.Either +import io.getquill.util.Format + +private[getquill] object ReflectivePathChainLookup: + sealed trait LookupElement { def cls: Class[_]; def current: Object } + object LookupElement: + // For a module class the lookup-object is actualy a class. For example + // for: object Foo { object Bar { ... } } you would do: + // val submod: Class[Bar] = Class[Foo].getDeclaredClasses.find(_.name endsWith "Bar$") + // submod.getField("MODULE$").get(submod /*Object is passed into here*/) + // (note that the Class[---] things are typed above but in reality when dealing with them in the reflection they won't be) + case class ModuleClass(cls: Class[_]) extends LookupElement { val current: Object = cls } + // For a regular value reference, we can simply lookup the class from the object + case class Value(current: Object) extends LookupElement { val cls = current.getClass } + end LookupElement + + case class LookupPath(element: LookupElement, path: String): + def cls = element.cls + def current = element.current + + extension (elem: Option[Object]) + def nullCheck(path: String, cls: Class[_], lookupType: String) = + elem match + case Some(null) => + println(s"The ${lookupType} ${path} can be looked up from ${cls} but the value is null") + None + case other => + other + + object Lookup: + + + + // If it's a method on a regular (i.e. dynamic) class. Try to that up + object Method: + def unapply(lookup: LookupPath): Option[LookupElement.Value] = + lookupFirstMethod(lookup.path)(lookup.cls, lookup.current)("method").map(LookupElement.Value(_)) + + // if it's a field on a regular (i.e. dynamic) class. Try to look that up + object Field: + def unapply(lookup: LookupPath): Option[LookupElement.Value] = + lookupFirstMethod(lookup.path)(lookup.cls, lookup.current)("field").map(LookupElement.Value(_)) + + // Scala object-in-object e.g. object Foo { object Bar { ... } }. Lookup the `Bar` + // it will be represented in java as: path.to.Foo$Bar. + object Submodule: + def unapply(lookup: LookupPath): Option[LookupElement.ModuleClass] = + val submod = lookup.cls.getDeclaredClasses.find(c => c.getName.endsWith(lookup.path + "$")) + submod.orElse { + // Odd pattern for top level object: object Foo { object Bar } + // there won't be a Bar in getDeclaredClasses but instead a Bar field on Foo whose value is null + lookup.cls.getFields.find(_.getName == lookup.path).map(_.getType) + }.map(LookupElement.ModuleClass(_)) + + object HelperObjectField: + def unapply(lookup: LookupPath): Option[LookupElement.Value] = + // Get Foo.MODULE$ + val submodOpt: Option[Object] = lookupModuleObject(lookup.current)(lookup.cls) + // Get Foo.MODULE$.fields. The `Field` unapply can be recycled for this purpose + submodOpt.map(submod => + // I.e. lookup MODULE$.field + lookupFirstMethod(lookup.path)(lookup.cls, submod)("$MODULE.field").map(LookupElement.Value(_)) + ).flatten + + object HelperObjectMethod: + def unapply(lookup: LookupPath): Option[LookupElement.Value] = + // Get Foo.MODULE$ + val submodOpt: Option[Object] = lookupModuleObject(lookup.current)(lookup.cls) + // Get Foo.MODULE$.methods. The `Method` unapply can be recycled for this purpose + submodOpt.map(submod => + // I.e. lookup MODULE$.method + lookupFirstMethod(lookup.path)(lookup.cls, submod)("$MODULE.method").map(LookupElement.Value(_)) + ).flatten + + // Lookup object Foo { ... } element MODULE$ which is the singleton instance in the Java representation + def lookupModuleObject(obj: Object)(cls: Class[_] = obj.getClass): Option[Object] = + cls.getFields.find(_.getName == "MODULE$").map(m => Try(m.get(obj)).toOption).flatten + + def lookupFirstMethod(path: String)(cls: Class[_], instance: Object)(label: String) = + val methodOpt = cls.getMethods.find(m => m.getName == path && m.getParameterCount == 0) + methodOpt.map(m => Try(m.invoke(instance)).toOption).flatten.nullCheck(path, cls, label) + + def lookupFirstField(path: String)(cls: Class[_], instance: Object)(label: String) = + val fieldOpt = cls.getFields.find(_.getName == path) + fieldOpt.map(f => Try(f.get(instance)).toOption).flatten.nullCheck(path, cls, label) + + end Lookup + + import java.lang.reflect.{ Method, Field } + def singleLookup(elem: LookupElement, path: String): Option[LookupElement] = + LookupPath(elem, path) match + case Lookup.Submodule(elem) => Some(elem) + case Lookup.Method(elem) => Some(elem) + case Lookup.Field(elem) => Some(elem) + case Lookup.HelperObjectMethod(elem) => Some(elem) + case Lookup.HelperObjectField(elem) => Some(elem) + case _ => None + + def chainLookup(element: LookupElement, paths: List[String])(pathsSeen: List[String] = List()): Either[String, LookupElement] = + import StringOps._ + paths match + case Nil => Right(element) + case head :: tail => + val nextElementOpt = singleLookup(element, head) + nextElementOpt match + case Some(nextElement) => + chainLookup(nextElement, tail)(pathsSeen :+ head) + case None => + Left( + s"Could not look up the path `${pathsSeen.dots}[$head]` from the `${element.cls.getName}` ${element.current}.\n" + + s"Remaining path: ${paths.mkString(".")}" + ) + + def apply(obj: Object, paths: List[String]) = + chainLookup(LookupElement.Value(obj), paths)() + + object StringOps: + extension (strList: List[String]) + def dots = + if (!strList.isEmpty) strList.mkString("", ".", ".") else "" + +end ReflectivePathChainLookup \ No newline at end of file diff --git a/quill-sql/src/main/scala/io/getquill/context/StaticSpliceMacro.scala b/quill-sql/src/main/scala/io/getquill/context/StaticSpliceMacro.scala new file mode 100644 index 0000000000..10b3ae1120 --- /dev/null +++ b/quill-sql/src/main/scala/io/getquill/context/StaticSpliceMacro.scala @@ -0,0 +1,147 @@ +package io.getquill.context + +import scala.quoted._ +import io.getquill.StaticSplice +import io.getquill.util.LoadModule +import io.getquill.metaprog.Extractors +import scala.util.Success +import scala.util.Failure +import scala.util.Try +import scala.util.Right +import scala.util.Left +import io.getquill.util.Format +import io.getquill.Quoted +import io.getquill.quat.QuatMaking +import io.getquill.parser.Lifter +import scala.util.Try +import io.getquill.StaticSplice +import io.getquill.util.CommonExtensions.Either._ +import io.getquill.util.CommonExtensions.Throwable._ +import io.getquill.util.CommonExtensions.For._ + +object StaticSpliceMacro { + import Extractors._ + + private[getquill] object SelectPath: + def recurseInto(using Quotes)(term: quotes.reflect.Term, accum: List[String] = List()): Option[(quotes.reflect.Term, List[String])] = + import quotes.reflect._ + term match + // Recurses through a series of selects do the core identifier e.g: + // Select(Select(Ident("core"), "foo"), "bar") => recurseInto( {Select(Ident("core"), "foo")}, "bar" +: List("baz") ) + case IgnoreApplyNoargs(Select(inner, pathNode)) => recurseInto(inner, pathNode +: accum) + case id: Ident => Some((id, accum)) + // If at the core of the nested selects is not a Ident, this does not match + case other => None + + def unapply(using Quotes)(term: quotes.reflect.Term): Option[(quotes.reflect.Term, List[String])] = + import quotes.reflect._ + term match + // recurse on Module.Something + case select: Select => recurseInto(select) + // recurse on Module.SomethingAply() from which the empty-args apply i.e. `()` needs to be ignored + case select @ IgnoreApplyNoargs(_:Select) => recurseInto(select) + case id: Ident => Some((id, List())) + case _ => None + end SelectPath + + extension [T](opt: Option[T]) + def nullAsNone = + opt match + case Some(null) => None + case _ => opt + + object DefTerm: + def unapply(using Quotes)(term: quotes.reflect.Term): Option[quotes.reflect.Term] = + import quotes.reflect._ + if (term.tpe.termSymbol.isValDef || term.tpe.termSymbol.isDefDef) Some(term) + else None + + def isModule(using Quotes)(sym: quotes.reflect.Symbol) = + import quotes.reflect._ + val f = sym.flags + f.is(Flags.Module) && !f.is(Flags.Package) && !f.is(Flags.Param) && !f.is(Flags.ParamAccessor) && !f.is(Flags.Method) + + object TermIsModule: + def unapply(using Quotes)(value: quotes.reflect.Term): Boolean = + import quotes.reflect.{Try => _, _} + val tpe = value.tpe.widen + if (isModule(tpe.typeSymbol)) + true + else + false + + /** The term is a static module but not a package */ + object TermOwnerIsModule: + def unapply(using Quotes)(value: quotes.reflect.Term): Option[quotes.reflect.TypeRepr] = + import quotes.reflect.{Try => _, _} + Try(value.tpe.termSymbol.owner).toOption.flatMap { owner => + val memberType = value.tpe.memberType(owner) + if (isModule(memberType.typeSymbol)) + Some(memberType) + else + None + } + + def apply[T: Type](valueRaw: Expr[T])(using Quotes): Expr[T] = + import quotes.reflect.{ Try => _, _ } + import ReflectivePathChainLookup.StringOps._ + import io.getquill.ast._ + + val value = valueRaw.asTerm.underlyingArgument + + // TODO summon a Expr[StaticSplicer] using the T type passed originally. + // Then use use LoadModule to get the value of that thing during runtime so we can use it + // (i.e. see io.getquill.metaprog.SummonParser on how to do that) + // for primitive types e.g. String, Int, Float etc... rather then making summonable splicers + // it is easier to just splice them directly, since otherwise those StaticSplicer modules themselves + // need to be compiled in a previous compilation unit, and we want to define them here. + // Technically that should be fine because they will only actually be used in the testing code though + // should think about this more later. For now just do toString to check that stuff from the main return works + + val (pathRoot, selectPath) = + Untype(value) match + case SelectPath(pathRoot, selectPath) => (pathRoot, selectPath) + case other => + // TODO Long explanatory message about how it has to some value inside object foo inside object bar... and it needs to be a thing compiled in a previous compilation unit + report.throwError(s"Could not load a static value `${Format.Term(value)}` from ${Printer.TreeStructure.show(other)}") + + + val (ownerTpe, path) = + pathRoot match + case term @ DefTerm(TermIsModule()) => + (pathRoot.tpe, selectPath) + // TODO Maybe only check module owner if Path is Nil? + case term @ DefTerm(TermOwnerIsModule(owner)) => + (owner, pathRoot.symbol.name +: selectPath) + case _ => + report.throwError(s"Cannot evaluate the static path ${Format.Term(value)}. Neither it's type ${Format.TypeRepr(pathRoot.tpe)} nor the owner of this type is a static module.") + + val module = LoadModule.TypeRepr(ownerTpe).toEither.discardLeft(e => + // TODO Long explanatory message about how it has to some value inside object foo inside object bar... and it needs to be a thing compiled in a previous compilation unit + report.throwError(s"Could not look up {${(ownerTpe)}}.${path.mkString(".")} from the object.\nStatic load failed due to: ${e.stackTraceToString}") + ) + + val splicedValue = + ReflectivePathChainLookup(module, path).discardLeft(msg => + report.throwError(s"Could not look up {${(ownerTpe)}}.${path.mkString(".")}. Failed because:\n${msg}") + ) + + val quat = Lifter.quat(QuatMaking.ofType[T]) + + def errorMsg(error: String) = + s"Could not statically splice ${Format.Term(value)} because ${error}" + + val spliceEither = + for { + castSplice <- Try(splicedValue.current.asInstanceOf[T]).toEither.mapLeft(e => errorMsg(e.getMessage)) + splicer <- StaticSplice.Summon[T].mapLeft(str => errorMsg(str)) + splice <- Try(splicer(castSplice)).toEither.mapLeft(e => errorMsg(e.getMessage)) + } yield splice + + val spliceStr = + spliceEither match + case Left(msg) => report.throwError(msg, valueRaw) + case Right(value) => value + + UnquoteMacro('{ Quoted[T](Infix(List(${Expr(spliceStr)}), List(), true, $quat),Nil, Nil) }) +} \ No newline at end of file diff --git a/quill-sql/src/main/scala/io/getquill/metaprog/Extractors.scala b/quill-sql/src/main/scala/io/getquill/metaprog/Extractors.scala index 2a3103f7a8..0221b6891e 100644 --- a/quill-sql/src/main/scala/io/getquill/metaprog/Extractors.scala +++ b/quill-sql/src/main/scala/io/getquill/metaprog/Extractors.scala @@ -116,6 +116,24 @@ object Extractors { def apply(using Quotes)(term: quotes.reflect.Term) = Untype.unapply(term).get } + /** + * Ignore case where there happens to be an apply e.g. java functions where "str".length in scala + * will translate into "str".lenth() since for java methods () is automatically added in. + * Hence it's `Apply( Select(Literal(IntConstant("str")), "length") )` + * Not just `Select(Literal(IntConstant("str")), "length")` + * + * Note maybe there's even a case where you want multiple empty-applies e.g. foo()() to be ignored + * hence this would be done recursively like `Untype` + */ + object IgnoreApplyNoargs { + def unapply(using Quotes)(term: quotes.reflect.Term): Option[quotes.reflect.Term] = + import quotes.reflect._ + term match { + case Apply(inner, Nil) => Some(inner) + case _ => Some(term) + } + } + object TypedMatroshkaTerm { def recurse(using Quotes)(innerTerm: quotes.reflect.Term): quotes.reflect.Term = import quotes.reflect._ diff --git a/quill-sql/src/main/scala/io/getquill/util/CommonExtensions.scala b/quill-sql/src/main/scala/io/getquill/util/CommonExtensions.scala new file mode 100644 index 0000000000..bee881a166 --- /dev/null +++ b/quill-sql/src/main/scala/io/getquill/util/CommonExtensions.scala @@ -0,0 +1,43 @@ +package io.getquill.util + +import java.io.ByteArrayOutputStream + +/** Extensions for common scala data types */ +object CommonExtensions: + + object Either: + extension [T](opt: Option[T]) + def toEitherOr[L](leftValue: L) = + opt match + case Some(value) => Right(value) + case None => Left(leftValue) + + extension [L, R](either: Either[L, R]) + def mapLeft[L1](f: L => L1): Either[L1, R] = + either.left.map(f) + def discardLeft(f: L => Nothing): R = + either match + case Left(l) => f(l) + case Right(value) => value + end Either + + object Throwable: + extension (t: Throwable) + def stackTraceToString = + val stream = new ByteArrayOutputStream() + val writer = new java.io.BufferedWriter(new java.io.OutputStreamWriter(stream)) + t.printStackTrace(new java.io.PrintWriter(writer)) + writer.flush + stream.toString + + object For: + // Allows using tuple deconstruction with either: (a, b) <- Right(("foo", "bar")) + extension [L, R](e: Either[L, R]) { + def withFilter(pred: R => Boolean): Either[L, R] = + e.flatMap { value => + // TODO: What we need here is a lazy Either.Left!!!! + if (pred(value)) Right(value) else Left(throw new IllegalArgumentException("Could not filter an either")) + } + } + +end CommonExtensions \ No newline at end of file diff --git a/quill-sql/src/main/scala/io/getquill/util/LoadObject.scala b/quill-sql/src/main/scala/io/getquill/util/LoadObject.scala index bac641a3e0..9e7f6853fd 100644 --- a/quill-sql/src/main/scala/io/getquill/util/LoadObject.scala +++ b/quill-sql/src/main/scala/io/getquill/util/LoadObject.scala @@ -19,37 +19,56 @@ object LoadModule { field.get(cls).asInstanceOf[T] } - def apply[T: Type](using Quotes): Try[T] = { - import quotes.reflect.{Try => _, _} - Try { + object TypeRepr { + def apply(using Quotes)(loadClassType: quotes.reflect.TypeRepr): Try[Object] = { + import quotes.reflect.{Try => _, TypeRepr => TTypeRepr, _} + Try { - // if (TypeRepr.of[T].classSymbol.isEmpty) { - // println(s"~~~~~~~~~~~~~~~~~ EMPTY SYMBOL FOR: ${TypeRepr.of[T]} *** ~~~~~~~~~~~~~~~~~") - // println(s"~~~~~~~~~~~~~~~~~ EMPTY SYMBOL FOR: ${TypeRepr.of[T].termSymbol} *** ~~~~~~~~~~~~~~~~~") - // println(s"~~~~~~~~~~~~~~~~~ EMPTY SYMBOL FOR: ${TypeRepr.of[T].termSymbol.moduleClass.fullName} *** ~~~~~~~~~~~~~~~~~") - // println(s"~~~~~~~~~~~~~~~~~ EMPTY SYMBOL FOR: ${TypeRepr.of[T].termSymbol.companionClass.fullName} *** ~~~~~~~~~~~~~~~~~") - // } - val loadClassType = TypeRepr.of[T] - val optClassSymbol = loadClassType.classSymbol - val className = - optClassSymbol match { - case Some(value) => value.fullName - case None => - //println(s"${'[$tpe].show} is not a class type. Attempting to load it as a module.") - if (!loadClassType.termSymbol.moduleClass.isNoSymbol) { - loadClassType.termSymbol.moduleClass.fullName - } else { - //println(s"The class ${'[$tpe].show} cannot be loaded because it is either a scala class or module") - report.throwError(s"The class ${Type.show[T]} cannot be loaded because it is either a scala class or module") - } - } - - val clsFull = `endWith$`(className) - val cls = Class.forName(clsFull) - val field = cls.getField("MODULE$") - field.get(cls).asInstanceOf[T] + // if (TypeRepr.of[T].classSymbol.isEmpty) { + // println(s"~~~~~~~~~~~~~~~~~ EMPTY SYMBOL FOR: ${TypeRepr.of[T]} *** ~~~~~~~~~~~~~~~~~") + // println(s"~~~~~~~~~~~~~~~~~ EMPTY SYMBOL FOR: ${TypeRepr.of[T].termSymbol} *** ~~~~~~~~~~~~~~~~~") + // println(s"~~~~~~~~~~~~~~~~~ EMPTY SYMBOL FOR: ${TypeRepr.of[T].termSymbol.moduleClass.fullName} *** ~~~~~~~~~~~~~~~~~") + // println(s"~~~~~~~~~~~~~~~~~ EMPTY SYMBOL FOR: ${TypeRepr.of[T].termSymbol.companionClass.fullName} *** ~~~~~~~~~~~~~~~~~") + // } + val optClassSymbol = loadClassType.classSymbol + val className = + optClassSymbol match { + case Some(value) => value.fullName + case None => + //println(s"${'[$tpe].show} is not a class type. Attempting to load it as a module.") + if (!loadClassType.termSymbol.moduleClass.isNoSymbol) { + loadClassType.termSymbol.moduleClass.fullName + } else { + //println(s"The class ${'[$tpe].show} cannot be loaded because it is either a scala class or module") + report.throwError(s"The class ${Format.TypeRepr(loadClassType.widen)} cannot be loaded because it not a static module. Either it is a class or some other dynamic value.") + } + } + + // println(s"================== Class Symbol: ${optClassSymbol} ================") + // println(s"================== Class Symbol FullName: ${optClassSymbol.map(_.fullName)} ================") + // println(s"================== Class Name: ${className} ================") + + val clsFullRaw = `endWith$`(className) + + // TODO This is a hack! Need to actually use scala compile-time tpe.memberType(tpe.owner) over and over + // again to get the actual static-lineage until we get to the package name and then compute the name from that + // Replace io.getquill.Foo$.Bar$ with io.getquill.Foo$Bar which is the java convention for nested modules + val clsFull = clsFullRaw.replace("$.", "$") + + // println(s"================== Loading Class: ${clsFull} ================") + val cls = Class.forName(clsFull) + val field = cls.getField("MODULE$") + field.get(cls) + } } } + + def apply[T: Type](using Quotes): Try[T] = { + import quotes.reflect.{ TypeRepr => TTypeRepr, _ } + val loadClassType = TTypeRepr.of[T] + val tryLoad = TypeRepr(loadClassType) + tryLoad.map(_.asInstanceOf[T]) + } } // TODO Move this to a test diff --git a/quill-sql/src/main/scala/io/getquill/util/prep/Hierarchies.scala b/quill-sql/src/main/scala/io/getquill/util/prep/Hierarchies.scala new file mode 100644 index 0000000000..fa7d0483ad --- /dev/null +++ b/quill-sql/src/main/scala/io/getquill/util/prep/Hierarchies.scala @@ -0,0 +1,25 @@ +package io.getquill.util.prep + +case class Inst(instVal: String) + +object Mod { + // val inst = Inst("instValValue") + // def inst + + def modAp() = "modApValue" + def modDef = "modDefValue" + val modVal = "modValValue" + val modIntVal = 123 + + object Foo { + def fooAp() = "fooApValue" + def fooDef = "fooDefValue" + val fooVal = "fooValValue" + + object Bar { + def barAp() = "barApValue" + def barDef = "barDefValue" + val barVal = "barValValue" + } + } +} diff --git a/quill-sql/src/main/scala/io/getquill/util/prep/UseSelectPath.scala b/quill-sql/src/main/scala/io/getquill/util/prep/UseSelectPath.scala new file mode 100644 index 0000000000..a0bda1197b --- /dev/null +++ b/quill-sql/src/main/scala/io/getquill/util/prep/UseSelectPath.scala @@ -0,0 +1,15 @@ +// package io.getquill.util.prep + +// import scala.quoted._ +// import io.getquill.context.StaticSpliceMacro.SelectPath + +// /** Macro that directly uses the SelectPath for testing purposes */ +// private[getquill] object UseSelectPath: +// inline def apply[T](value: T): Option[(String, List[String])] = ${ applyImpl('value) } + +// def applyImpl[T: Type](value: Expr[T])(using Quotes): Expr[Option[(String, List[String])]] = +// import quotes.reflect._ + +// SelectPath() + +// end UseSelectPath \ No newline at end of file diff --git a/quill-sql/src/test/scala/io/getquill/CustomParserFactory.scala b/quill-sql/src/test/scala/io/getquill/CustomParserFactory.scala deleted file mode 100644 index 1335ca6c3c..0000000000 --- a/quill-sql/src/test/scala/io/getquill/CustomParserFactory.scala +++ /dev/null @@ -1,14 +0,0 @@ -// package io.getquill - -// import io.getquill.parser._ -// import scala.quoted._ -// -// import io.getquill.ast._ - -// trait CustomParserFactory extends BaseParserFactory { -// override def userDefined(using qctxInput: Quotes) = Parser(new ParserComponent { -// val qctx = qctxInput -// def apply(root: Parser) = PartialFunction.empty[Expr[_], Ast] -// }) -// } -// object CustomParserFactory extends CustomParserFactory \ No newline at end of file diff --git a/quill-sql/src/test/scala/io/getquill/metaprog/SelectPathSpec.scala b/quill-sql/src/test/scala/io/getquill/metaprog/SelectPathSpec.scala new file mode 100644 index 0000000000..0130f3ba26 --- /dev/null +++ b/quill-sql/src/test/scala/io/getquill/metaprog/SelectPathSpec.scala @@ -0,0 +1,51 @@ +package io.getquill.metaprog + +import org.scalatest._ +import io.getquill.Spec +import io.getquill.context.ReflectivePathChainLookup +import io.getquill.context.ReflectivePathChainLookup.LookupElement +import io.getquill.util.prep.Mod + +class SelectPath extends Spec { + "Basic ReflectiveLookup should select correct path from" - { + "Mod.modVal" in { + ReflectivePathChainLookup.apply(Mod, List("modVal")) mustEqual + Right(LookupElement.Value("modValValue")) + } + "Mod.modDef" in { + ReflectivePathChainLookup.apply(Mod, List("modDef")) mustEqual + Right(LookupElement.Value("modDefValue")) + } + "Mod.modAp" in { + ReflectivePathChainLookup.apply(Mod, List("modAp")) mustEqual + Right(LookupElement.Value("modApValue")) + } + + "Mod.Foo.fooVal" in { + ReflectivePathChainLookup.apply(Mod, List("Foo", "fooVal")) mustEqual + Right(LookupElement.Value("fooValValue")) + } + "Mod.Foo.fooDef" in { + ReflectivePathChainLookup.apply(Mod, List("Foo", "fooDef")) mustEqual + Right(LookupElement.Value("fooDefValue")) + } + "Mod.Foo.fooAp" in { + ReflectivePathChainLookup.apply(Mod, List("Foo", "fooAp")) mustEqual + Right(LookupElement.Value("fooApValue")) + } + + "Mod.Foo.Bar.barVal" in { + ReflectivePathChainLookup.apply(Mod, List("Foo", "Bar", "barVal")) mustEqual + Right(LookupElement.Value("barValValue")) + } + "Mod.Foo.Bar.barDef" in { + ReflectivePathChainLookup.apply(Mod, List("Foo", "Bar", "barDef")) mustEqual + Right(LookupElement.Value("barDefValue")) + } + "Mod.Foo.Bar.barAp()" in { + ReflectivePathChainLookup.apply(Mod, List("Foo", "Bar", "barAp")) mustEqual + Right(LookupElement.Value("barApValue")) + } + } + +} \ No newline at end of file diff --git a/quill-sql/src/test/scala/io/getquill/metaprog/StaticSpliceSpec.scala b/quill-sql/src/test/scala/io/getquill/metaprog/StaticSpliceSpec.scala new file mode 100644 index 0000000000..5453216759 --- /dev/null +++ b/quill-sql/src/test/scala/io/getquill/metaprog/StaticSpliceSpec.scala @@ -0,0 +1,36 @@ +package io.getquill.metaprog + +import io.getquill._ +import io.getquill.util.prep.Mod + +class StaticSpliceSpec extends Spec { + val ctx = new MirrorContext(PostgresDialect, Literal) + import ctx._ + + case class Person(name: String, age: Int) + + "simple string splice should work" in { + ctx.run { query[Person].filter(p => p.name == static(Mod.modVal)) }.string mustEqual + "SELECT p.name, p.age FROM Person p WHERE p.name = 'modValValue'" + ctx.run { query[Person].filter(p => p.age == static(Mod.modIntVal)) }.string mustEqual + "SELECT p.name, p.age FROM Person p WHERE p.age = 123" + ctx.run { query[Person].filter(p => p.name == static(Mod.modDef)) }.string mustEqual + "SELECT p.name, p.age FROM Person p WHERE p.name = 'modDefValue'" + ctx.run { query[Person].filter(p => p.name == static(Mod.modAp())) }.string mustEqual + "SELECT p.name, p.age FROM Person p WHERE p.name = 'modApValue'" + + ctx.run { query[Person].filter(p => p.name == static(Mod.Foo.fooVal)) }.string mustEqual + "SELECT p.name, p.age FROM Person p WHERE p.name = 'fooValValue'" + ctx.run { query[Person].filter(p => p.name == static(Mod.Foo.fooDef)) }.string mustEqual + "SELECT p.name, p.age FROM Person p WHERE p.name = 'fooDefValue'" + ctx.run { query[Person].filter(p => p.name == static(Mod.Foo.fooAp())) }.string mustEqual + "SELECT p.name, p.age FROM Person p WHERE p.name = 'fooApValue'" + + ctx.run { query[Person].filter(p => p.name == static(Mod.Foo.Bar.barVal)) }.string mustEqual + "SELECT p.name, p.age FROM Person p WHERE p.name = 'barValValue'" + ctx.run { query[Person].filter(p => p.name == static(Mod.Foo.Bar.barDef)) }.string mustEqual + "SELECT p.name, p.age FROM Person p WHERE p.name = 'barDefValue'" + ctx.run { query[Person].filter(p => p.name == static(Mod.Foo.Bar.barAp())) }.string mustEqual + "SELECT p.name, p.age FROM Person p WHERE p.name = 'barApValue'" + } +} \ No newline at end of file