Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Autowire refactor #188

Merged
merged 32 commits into from
Dec 23, 2021
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
45c1e63
Add support for resource factory methods
mbore Nov 1, 2021
34cfb78
add pure autowire
mbore Nov 1, 2021
0f93867
Test allocation order
mbore Nov 3, 2021
190fa64
Add resource reusability in factory methods
mbore Nov 3, 2021
21639ff
Fallback to autowire in factory method
mbore Nov 3, 2021
75942fc
Extract providers
mbore Nov 9, 2021
91a74a9
Sort incomming factory methods dependencies
mbore Nov 15, 2021
cc584a6
Mixed factory method params test
mbore Nov 15, 2021
37ec5f7
Add empty constructor fallback test
mbore Nov 19, 2021
eaa4bc2
WIP graph based autowire
mbore Dec 3, 2021
886e938
Extract providers
mbore Dec 4, 2021
1ade7e3
Futher breakdown
mbore Dec 4, 2021
87f6d17
Green tests
mbore Dec 5, 2021
7e4357d
Add support for copanion object
mbore Dec 6, 2021
e357521
Fix scala 2.12 compilation
mbore Dec 7, 2021
3a8c55b
Fix imports in tests
mbore Dec 7, 2021
6b9cad5
clean up
mbore Dec 7, 2021
0cd49ca
remove pure aoutowire
mbore Dec 14, 2021
d3d7d6e
Order improvements
mbore Dec 15, 2021
0def472
Fix test name resolution
mbore Dec 15, 2021
608fc63
constructor -> creator
mbore Dec 15, 2021
8a4ea17
Fix error msg
mbore Dec 15, 2021
37c26d6
another factory method test
mbore Dec 15, 2021
df5b354
rename test
mbore Dec 15, 2021
dd64e49
Use FactoryMethod instead of creator
mbore Dec 17, 2021
bd3c407
Make Provider sealed
mbore Dec 17, 2021
b1d057f
Improve ambiguity check
mbore Dec 17, 2021
a78010f
Fix class name
mbore Dec 17, 2021
aec8757
Simplify rawProviders iterations
mbore Dec 17, 2021
97ef382
Update readme
mbore Dec 17, 2021
e55d614
Add comments
mbore Dec 22, 2021
2f3e465
Test for at least one wire step
mbore Dec 22, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,30 @@ object CompanionCrimper {
} yield pl.foldLeft(applyMethod)((acc: Tree, args: List[Tree]) => Apply(acc, args))
}

def applyFactory[C <: blackbox.Context](
c: C,
log: Logger
)(targetType: c.Type): Option[(List[List[c.Symbol]], List[List[c.Tree]] => c.Tree)] = {
import c.universe._

lazy val apply: Option[Symbol] = CompanionCrimper
.applies(c, log)(targetType)
.flatMap(_ match {
case applyMethod :: Nil => Some(applyMethod)
case _ => None
})

lazy val applySelect: Option[Select] = apply.map(a => Select(Ident(targetType.typeSymbol.companion), a))

lazy val applyParamLists: Option[List[List[Symbol]]] = apply.map(_.asMethod.paramLists)

def factory(applyMethod: Tree)(applyArgs: List[List[Tree]]) =
applyArgs.foldLeft(applyMethod)((acc: Tree, args: List[Tree]) => Apply(acc, args))

for {
params <- applyParamLists
applyMethod <- applySelect
} yield (params, factory(applyMethod)(_))
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,14 @@ private[macwire] class ConstructorCrimper[C <: blackbox.Context, T: C#WeakTypeTa
def wireConstructorParams(
dependencyResolver: DependencyResolverType
)(paramLists: List[List[Symbol]]): List[List[Tree]] = paramLists.map(
_.map(p => dependencyResolver.resolve(p, /*SI-4751*/ ConstructorCrimper.paramType(c)(targetTypeD, p)))
_.map(p => dependencyResolver.resolve(p, /*SI-4751*/ paramType(c)(targetTypeD, p)))
)

def wireConstructorParamsWithImplicitLookups(
dependencyResolver: DependencyResolverType
)(paramLists: List[List[Symbol]]): List[List[Tree]] = paramLists.map(_.map {
case i if i.isImplicit => q"implicitly[${ConstructorCrimper.paramType(c)(targetType, i)}]"
case p => dependencyResolver.resolve(p, /*SI-4751*/ ConstructorCrimper.paramType(c)(targetTypeD, p))
case i if i.isImplicit => q"implicitly[${paramType(c)(targetType, i)}]"
case p => dependencyResolver.resolve(p, /*SI-4751*/ paramType(c)(targetTypeD, p))
})

}
Expand Down Expand Up @@ -97,44 +97,44 @@ object ConstructorCrimper {
}
}

private def paramType[C <: blackbox.Context](c: C)(targetTypeD: c.Type, param: c.Symbol): c.Type = {
import c.universe._
def constructorTree[C <: blackbox.Context](
c: C,
log: Logger
)(targetType: c.Type, resolver: (c.Symbol, c.Type) => c.Tree): Option[c.Tree] =
constructorFactory(c, log)(targetType).map { case (paramLists, factory) =>
import c.universe._

val (sym: Symbol, tpeArgs: List[Type]) = targetTypeD match {
case TypeRef(_, sym, tpeArgs) => (sym, tpeArgs)
case t =>
c.abort(
c.enclosingPosition,
s"Target type not supported for wiring: $t. Please file a bug report with your use-case."
)
lazy val targetTypeD: Type = targetType.dealias

def wireConstructorParams(paramLists: List[List[Symbol]]): List[List[Tree]] =
paramLists.map(_.map(p => resolver(p, /*SI-4751*/ paramType(c)(targetTypeD, p))))

def constructorArgs: List[List[Tree]] = log.withBlock("Looking for targetConstructor arguments") {
wireConstructorParams(paramLists)
}

factory(constructorArgs)
}
val pTpe = param.typeSignature.substituteTypes(sym.asClass.typeParams, tpeArgs)
if (param.asTerm.isByNameParam) pTpe.typeArgs.head else pTpe
}

def constructorTree[C <: blackbox.Context](
def constructorFactory[C <: blackbox.Context](
c: C,
log: Logger
)(targetType: c.Type, resolver: (c.Symbol, c.Type) => c.Tree): Option[c.Tree] = {
)(targetType: c.Type): Option[(List[List[c.Symbol]], List[List[c.Tree]] => c.Tree)] = {
import c.universe._

lazy val targetTypeD: Type = targetType.dealias

lazy val constructor: Option[Symbol] = ConstructorCrimper.constructor(c, log)(targetType)
val constructor: Option[Symbol] = ConstructorCrimper.constructor(c, log)(targetType)

lazy val constructorParamLists: Option[List[List[Symbol]]] =
val constructorParamLists: Option[List[List[Symbol]]] =
constructor.map(_.asMethod.paramLists.filterNot(_.headOption.exists(_.isImplicit)))

def constructorArgs: Option[List[List[Tree]]] = log.withBlock("Looking for targetConstructor arguments") {
constructorParamLists.map(wireConstructorParams(_))
}

def wireConstructorParams(paramLists: List[List[Symbol]]): List[List[Tree]] =
paramLists.map(_.map(p => resolver(p, /*SI-4751*/ paramType(c)(targetTypeD, p))))

log.withBlock(s"Creating Constructor Tree for $targetType") {
def factory(constructorArgs: List[List[Tree]]): Tree = {
val constructionMethodTree: Tree = Select(New(Ident(targetTypeD.typeSymbol)), termNames.CONSTRUCTOR)
constructorArgs.map(_.foldLeft(constructionMethodTree)((acc: Tree, args: List[Tree]) => Apply(acc, args)))
constructorArgs.foldLeft(constructionMethodTree)((acc: Tree, args: List[Tree]) => Apply(acc, args))
}

constructorParamLists.map(cpl => (cpl, factory(_)))
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ private[macwire] final class Logger {
}
}

def withResult[T](block: => T)(msgFactory: T => String): T = {
val result = block
apply(msgFactory(result))
result
}

def beginBlock() {
ident += 1
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.softwaremill.macwire

import scala.reflect.macros.blackbox

package object internals {
//FIXME any built-in solution?
def sequence[A](l: List[Option[A]]) = (Option(List.empty[A]) /: l) {
case (Some(sofar), Some(value)) => Some(value :: sofar);
case (_, _) => None
}

def composeOpts[A, B](f: A => Option[B], fs: A => Option[B]*): A => Option[B] = fs.fold(f) { case (f1, f2) =>
(a: A) => f1(a).orElse(f2(a))
}
def composeWithFallback[A, B](f: A => Option[B], fs: A => Option[B]*)(value: A => B): A => B = (a: A) =>
composeOpts(f, fs: _*)(a).getOrElse(value(a))

def combine[A, B](fs: Seq[A => B])(op: B => B => B): A => B = fs.reduce { (f1, f2) => (a: A) => op(f1(a))(f2(a)) }

def isWireable[C <: blackbox.Context](c: C)(tpe: c.Type): Boolean = {
val name = tpe.typeSymbol.fullName

!name.startsWith("java.lang.") && !name.startsWith("scala.")
}

def paramType[C <: blackbox.Context](c: C)(targetTypeD: c.Type, param: c.Symbol): c.Type = {
import c.universe._

val (sym: Symbol, tpeArgs: List[Type]) = targetTypeD match {
case TypeRef(_, sym, tpeArgs) => (sym, tpeArgs)
case t =>
c.abort(
c.enclosingPosition,
s"Target type not supported for wiring: $t. Please file a bug report with your use-case."
)
}
val pTpe = param.typeSignature.substituteTypes(sym.asClass.typeParams, tpeArgs)
if (param.asTerm.isByNameParam) pTpe.typeArgs.head else pTpe
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.softwaremill.macwire.autocats
import scala.reflect.macros.blackbox
import cats.effect.{IO, Resource => CatsResource}
import com.softwaremill.macwire.internals._
import com.softwaremill.macwire.autocats.internals._

object MacwireCatsEffectMacros {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[minor] name doesn't match file name :)

private val log = new Logger()
Expand All @@ -12,147 +13,27 @@ object MacwireCatsEffectMacros {
)(dependencies: c.Expr[Any]*): c.Expr[CatsResource[IO, T]] = {
import c.universe._

type Resolver = (Symbol, Type) => Tree

val targetType = implicitly[c.WeakTypeTag[T]]
lazy val typeCheckUtil = new TypeCheckUtil[c.type](c, log)

trait Provider {
def `type`: Type
}

case class Resource(value: Tree) extends Provider {
val `type`: Type = typeCheckUtil.typeCheckIfNeeded(value).typeArgs(1)
val ident: Tree = Ident(TermName(c.freshName()))
lazy val tpe = typeCheckUtil.typeCheckIfNeeded(value)
}
val graphContext = new CatsProvidersGraphContext[c.type](c, log)
val graph = graphContext.buildGraph(dependencies.toList, targetType.tpe)
val sortedProviders = graph.topologicalSort()

case class Instance(value: Tree) extends Provider {
lazy val `type`: Type = typeCheckUtil.typeCheckIfNeeded(value)
lazy val ident: Tree = value
}
log(s"Sorted providers [${sortedProviders.mkString(", ")}]")

case class FactoryMethod(value: Tree) extends Provider {
val (params, fun) = value match {
// Function with two parameter lists (implicit parameters) (<2.13)
case Block(Nil, Function(p, Apply(Apply(f, _), _))) => (p, f)
case Block(Nil, Function(p, Apply(f, _))) => (p, f)
// Function with two parameter lists (implicit parameters) (>=2.13)
case Function(p, Apply(Apply(f, _), _)) => (p, f)
case Function(p, Apply(f, _)) => (p, f)
// Other types not supported
case _ => c.abort(c.enclosingPosition, s"Not supported factory type: [$value]")
val code = sortedProviders
.map {
case fm: graphContext.FactoryMethod => fm.result
case p => p
}

lazy val `type`: Type = fun.symbol.asMethod.returnType

def applyWith(resolver: Resolver): Tree = {
val values = params.map { case vd @ ValDef(_, name, tpt, rhs) =>
resolver(vd.symbol, typeCheckUtil.typeCheckIfNeeded(tpt))
}

q"$fun(..$values)"
.collect { case p @ (_: graphContext.Effect | _: graphContext.Resource | _: graphContext.Creator) => p }
.foldRight(
q"cats.effect.Resource.pure[cats.effect.IO, $targetType](${graph.root.ident})"
) { case (resource, acc) =>
q"${resource.value}.flatMap((${resource.ident}: ${resource.resultType}) => $acc)"
}

}

case class Effect(value: Tree) extends Provider {

override val `type`: Type = typeCheckUtil.typeCheckIfNeeded(value).typeArgs(0)

lazy val asResource = Resource(q"cats.effect.kernel.Resource.eval[cats.effect.IO, ${`type`}]($value)")
}

def isResource(expr: Expr[Any]): Boolean = {
val checkedType = typeCheckUtil.typeCheckIfNeeded(expr.tree)

checkedType.typeSymbol.fullName.startsWith("cats.effect.kernel.Resource") && checkedType.typeArgs.size == 2
}

def isFactoryMethod(expr: Expr[Any]): Boolean = expr.tree match {
// Function with two parameter lists (implicit parameters) (<2.13)
case Block(Nil, Function(p, Apply(Apply(f, _), _))) => true
case Block(Nil, Function(p, Apply(f, _))) => true
// Function with two parameter lists (implicit parameters) (>=2.13)
case Function(p, Apply(Apply(f, _), _)) => true
case Function(p, Apply(f, _)) => true
// Other types not supported
case _ => false
}

def isEffect(expr: Expr[Any]): Boolean = {
val checkedType = typeCheckUtil.typeCheckIfNeeded(expr.tree)

checkedType.typeSymbol.fullName.startsWith("cats.effect.IO") && checkedType.typeArgs.size == 1
}

val resourcesExprs = dependencies.filter(isResource)
val factoryMethodsExprs = dependencies.filter(isFactoryMethod)
val effectsExprs = dependencies.filter(isEffect)
val instancesExprs = dependencies.diff(resourcesExprs).diff(factoryMethodsExprs).diff(effectsExprs)

val resources =
(effectsExprs.map(expr => Effect(expr.tree).asResource) ++ resourcesExprs.map(expr => Resource(expr.tree)))
.map(r => (r.`type`, r))
.toMap

val instances = instancesExprs
.map(expr => Instance(expr.tree))
.map(i => (i.`type`, i))
.toMap

val factoryMethods = factoryMethodsExprs
.map(expr => FactoryMethod(expr.tree))
.map(i => (i.`type`, i))
.toMap

log(s"exprs: s[${dependencies.mkString(", ")}]")
log(s"resources: [${resources.mkString(", ")}]")
log(s"instances: [${instances.mkString(", ")}]")
log(s"factory methods: [${factoryMethods.mkString(", ")}]")

def doFind[T <: Provider](values: Map[Type, T])(tpe: Type): Option[T] =
values.find { case (t, _) => t <:< tpe }.map(_._2)

def findInstance(t: Type): Option[Instance] = doFind(instances)(t)

def findResource(t: Type): Option[Resource] = doFind(resources)(t)
def findFactoryMethod(t: Type): Option[FactoryMethod] = doFind(factoryMethods)(t)

def isWireable(tpe: Type): Boolean = {
val name = tpe.typeSymbol.fullName

!name.startsWith("java.lang.") && !name.startsWith("scala.")
}

def findProvider(tpe: Type): Option[Tree] = findInstance(tpe)
.map(_.ident)
.orElse(findResource(tpe).map(_.ident))
.orElse(findFactoryMethod(tpe).map(_.applyWith(resolutionWithFallback)))

lazy val resolutionWithFallback: (Symbol, Type) => Tree = (_, tpe) =>
if (isWireable(tpe)) findProvider(tpe).getOrElse(go(tpe))
else c.abort(c.enclosingPosition, s"Cannot find a value of type: [${tpe}]")

def go(t: Type): Tree = {

val r =
(ConstructorCrimper.constructorTree(c, log)(t, resolutionWithFallback) orElse CompanionCrimper
.applyTree(c, log)(t, resolutionWithFallback)) getOrElse
c.abort(c.enclosingPosition, s"Failed for [$t]")

log(s"Constructed [$r]")
r
}

val generatedInstance = go(targetType.tpe)

val code =
resources.values.foldRight(q"cats.effect.Resource.pure[cats.effect.IO, $targetType]($generatedInstance)") {
case (resource, acc) =>
q"${resource.value}.flatMap((${resource.ident}: ${resource.tpe}) => $acc)"
}
log(s"Code: [$code]")
log(s"CODE: [$code]")

c.Expr[CatsResource[IO, T]](code)
}
Expand Down
Loading