All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.softwaremill.macwire.internals.ConstructorCrimper.scala Maven / Gradle / Ivy

package com.softwaremill.macwire.internals

import scala.quoted.*
import scala.annotation.Annotation

private[macwire] class ConstructorCrimper[Q <: Quotes, T: Type](using val q: Q)(
    dependencyResolver: DependencyResolver[q.type, T],
    log: Logger
) {
  import q.reflect.*

  lazy val targetType = TypeRepr.of[T]

  // We need to get the "real" type in case the type parameter is a type alias - then it cannot
  // be directly instantiated
  lazy val targetTypeD = targetType.dealias

  private def isAccessibleConstructor(s: Symbol) =
    s.isClassConstructor && !(s.flags is Flags.Private) && !(s.flags is Flags.Protected)

  lazy val publicConstructors: Iterable[Symbol] = {
    val ctors = targetType.typeSymbol.declarations
      .filter(isAccessibleConstructor)
      .filterNot(isPhantomConstructor)
    log.withBlock(s"There are ${ctors.size} eligible constructors") { ctors.foreach(c => log(showConstructor(c))) }
    ctors
  }

  lazy val primaryConstructor: Option[Symbol] = targetType.typeSymbol.primaryConstructor match {
    case c if isAccessibleConstructor(c) => Some(c)
    case c                               => None
  }

  lazy val injectConstructors: Iterable[Symbol] = {
    val isInjectAnnotation = (a: Term) => a.tpe.typeSymbol.fullName == "javax.inject.Inject"
    val ctors = publicConstructors.filter(_.annotations.exists(isInjectAnnotation))
    log.withBlock(s"There are ${ctors.size} constructors annotated with @javax.inject.Inject") {
      ctors.foreach(c => log(showConstructor(c)))
    }
    ctors
  }

  lazy val injectConstructor: Option[Symbol] =
    if (injectConstructors.size > 1)
      abort(s"Ambiguous constructors annotated with @javax.inject.Inject for type [${targetType.typeSymbol.name}]")
    else injectConstructors.headOption

  lazy val constructor: Option[Symbol] = log.withBlock(s"Looking for constructor for $targetType") {
    val ctor = injectConstructor orElse primaryConstructor
    ctor.foreach(ctor => log(s"Found ${showConstructor(ctor)}"))
    ctor
  }

  private def constructorParamTypes(ctorType: TypeRepr): List[List[TypeRepr]] = {
    ctorType match {
      case MethodType(_, paramTypes, retType) =>
        paramTypes.map(_.widen.simplified) :: constructorParamTypes(retType.simplified)
      case _ =>
        Nil
    }
  }

  lazy val constructorParamLists: Option[List[List[(Symbol, TypeRepr)]]] = {
    constructor.map { c =>
      // paramSymss contains both type arg symbols (generic types) and value arg symbols
      val symLists = c.paramSymss.filter(_.forall(!_.isTypeDef))
      val ctorType =
        if (targetType.typeArgs.isEmpty) targetType.memberType(c)
        else targetType.memberType(c).appliedTo(targetType.typeArgs)
      val typeLists = constructorParamTypes(ctorType)
      symLists.zip(typeLists).map { case (syms, tpes) =>
        syms.zip(tpes)
      }
    }
  }

  lazy val constructorArgs: Option[List[List[Term]]] = log.withBlock("Looking for targetConstructor arguments") {
    constructorParamLists.map(wireConstructorParamsWithImplicitLookups)
  }

  lazy val constructorTree: Option[Tree] = log.withBlock(s"Creating Constructor Tree for $targetType") {
    for {
      constructorValue <- constructor
      constructorArgsValue <- constructorArgs
    } yield {
      val constructionMethodTree: Term = {
        val ctor = Select(New(TypeIdent(targetType.typeSymbol)), constructorValue)
        if (targetType.typeArgs.isEmpty) ctor else ctor.appliedToTypes(targetType.typeArgs)
      }
      constructorArgsValue.foldLeft(constructionMethodTree)((acc: Term, args: List[Term]) => Apply(acc, args))
    }
  }

  def wireConstructorParams(paramLists: List[List[(Symbol, TypeRepr)]]): List[List[Term]] =
    paramLists.map(_.map(p => dependencyResolver.resolve(p._1, /*SI-4751*/ p._2)))

  def wireConstructorParamsWithImplicitLookups(paramLists: List[List[(Symbol, TypeRepr)]]): List[List[Term]] =
    paramLists.map {
      case params if params.forall(_._1.flags is Flags.Implicit) => params.map(resolveImplicitOrFail)
      case params => params.map(p => dependencyResolver.resolve(p._1, /*SI-4751*/ p._2))
    }

  private def resolveImplicitOrFail(param: Symbol, paramType: TypeRepr): Term =
    Implicits.search(paramType) match {
      case iss: ImplicitSearchSuccess => iss.tree
      case isf: ImplicitSearchFailure => report.throwError(s"Failed to resolve an implicit for [$param].")
    }

  /** In some cases there is one extra (phantom) constructor. This happens when extended trait has implicit param:
    *
    * {{{
    *   trait A { implicit val a = ??? };
    *   class X extends A
    *   import scala.reflect.runtime.universe._
    *   typeOf[X].members.filter(m => m.isMethod && m.asMethod.isConstructor && m.asMethod.isPrimaryConstructor).map(_.asMethod.fullName)
    *
    *   //res1: Iterable[String] = List(X., A.$init$)
    * }}}
    *
    * The {{{A.$init$}}} is the phantom constructor and we don't want it.
    *
    * In other words, if we don't filter such constructor using this function 'wireActor-12-noPublicConstructor.failure'
    * will compile and throw exception during runtime but we want to fail it during compilation time.
    */
  // def isPhantomConstructor(constructor: Symbol): Boolean = constructor.asMethod.fullName.endsWith("$init$")
  def isPhantomConstructor(constructor: Symbol): Boolean = constructor.fullName.endsWith("$init$")

  // def showConstructor(c: Symbol): String = c.asMethod.typeSignature.toString
  def showConstructor(c: Symbol): String = c.toString

  def abort(msg: String): Nothing = report.throwError(msg)
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy