Skip to content

Commit

Permalink
Introduce explicit error for non-implicit non-case parameters
Browse files Browse the repository at this point in the history
Fix compilation error when case class has a method named like a case field
Tuple1 should also be converted into Iso rather than Lens
  • Loading branch information
NTPape committed Mar 10, 2022
1 parent 4594037 commit c3eff55
Show file tree
Hide file tree
Showing 6 changed files with 138 additions and 104 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ private[focus] trait ErrorHandling {
s"Cannot generate Lens for field '$fieldName', because '$fromClass' is not a case class"
case FocusError.NotACaseField(caseClass, fieldName) =>
s"Can only create lenses for case fields, but '$fieldName' is not a case field of '$caseClass'"
case FocusError.NonImplicitNonCaseParameter(caseClass: String, parameters: List[String]) =>
s"Case class '$caseClass' has non-implicit non-case parameters, which is not supported: ${parameters.map("'" + _ + "'").mkString(", ")}"
case FocusError.NotAConcreteClass(fromClass) =>
s"Expecting a concrete case class in the 'From' position; cannot reify type $fromClass"
case FocusError.NotASimpleLambdaFunction =>
Expand All @@ -20,7 +22,7 @@ private[focus] trait ErrorHandling {
case FocusError.CouldntFindFieldType(fromType, fieldName) => s"Couldn't find type for $fromType.$fieldName"
case FocusError.InvalidDowncast(fromType, toType) => s"Type '$fromType' could not be cast to '$toType'"
case FocusError.ImplicitNotFound(implicitType) =>
s"Could not find implicit for '$implicitType'. Note: multiple non-implicit parameter sets or implicits with default values are not supported."
s"Could not find implicit for '$implicitType'. Note: implicits with default values are not supported."
case FocusError.ExpansionFailed(reason) =>
s"Case class with multiple parameter sets could not be expanded because of: $reason"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,23 @@ private[focus] trait FocusBase {

given Quotes = macroContext

type Symbol = macroContext.reflect.Symbol
type Term = macroContext.reflect.Term
type TypeRepr = macroContext.reflect.TypeRepr

case class LambdaConfig(argName: String, lambdaBody: Term)

enum FocusAction {
case SelectField(fieldName: String, fromType: TypeRepr, fromTypeArgs: List[TypeRepr], toType: TypeRepr)
case SelectFieldWithImplicits(fieldName: String, fromType: TypeRepr, toType: TypeRepr, setter: Term)
case SelectField(caseFieldSymbol: Symbol, fromType: TypeRepr, fromTypeArgs: List[TypeRepr], toType: TypeRepr)
case SelectFieldWithImplicits(caseFieldSymbol: Symbol, fromType: TypeRepr, toType: TypeRepr, setter: Term)
case SelectOnlyField(
fieldName: String,
caseFieldSymbol: Symbol,
fromType: TypeRepr,
fromTypeArgs: List[TypeRepr],
fromCompanion: Term,
toType: TypeRepr
)
case SelectOnlyFieldWithImplicits(fieldName: String, fromType: TypeRepr, toType: TypeRepr, reverseGet: Term)
case SelectOnlyFieldWithImplicits(caseFieldSymbol: Symbol, fromType: TypeRepr, toType: TypeRepr, reverseGet: Term)
case KeywordSome(toType: TypeRepr)
case KeywordAs(fromType: TypeRepr, toType: TypeRepr)
case KeywordEach(fromType: TypeRepr, toType: TypeRepr, eachInstance: Term)
Expand All @@ -31,14 +32,14 @@ private[focus] trait FocusBase {
case KeywordWithDefault(toType: TypeRepr, defaultValue: Term)

override def toString(): String = this match {
case SelectField(fieldName, fromType, fromTypeArgs, toType) =>
s"SelectField($fieldName, ${fromType.show}, ${fromTypeArgs.map(_.show)}, ${toType.show})"
case SelectFieldWithImplicits(fieldName, fromType, toType, setter) =>
s"SelectFieldWithImplicits($fieldName, ${fromType.show}, ${toType.show}, ...)"
case SelectOnlyField(fieldName, fromType, fromTypeArgs, _, toType) =>
s"SelectOnlyField($fieldName, ${fromType.show}, ${fromTypeArgs.map(_.show)}, ..., ${toType.show})"
case SelectOnlyFieldWithImplicits(fieldName, fromType, toType, reverseGet) =>
s"SelectOnlyFieldWithImplicits($fieldName, ${fromType.show}, ${toType.show}, ...)"
case SelectField(caseFieldSymbol, fromType, fromTypeArgs, toType) =>
s"SelectField(${caseFieldSymbol.name}, ${fromType.show}, ${fromTypeArgs.map(_.show)}, ${toType.show})"
case SelectFieldWithImplicits(caseFieldSymbol, fromType, toType, setter) =>
s"SelectFieldWithImplicits(${caseFieldSymbol.name}, ${fromType.show}, ${toType.show}, ...)"
case SelectOnlyField(caseFieldSymbol, fromType, fromTypeArgs, _, toType) =>
s"SelectOnlyField(${caseFieldSymbol.name}, ${fromType.show}, ${fromTypeArgs.map(_.show)}, ..., ${toType.show})"
case SelectOnlyFieldWithImplicits(caseFieldSymbol, fromType, toType, reverseGet) =>
s"SelectOnlyFieldWithImplicits(${caseFieldSymbol.name}, ${fromType.show}, ${toType.show}, ...)"
case KeywordSome(toType) => s"KeywordSome(${toType.show})"
case KeywordAs(fromType, toType) => s"KeywordAs(${fromType.show}, ${toType.show})"
case KeywordEach(fromType, toType, _) => s"KeywordEach(${fromType.show}, ${toType.show}, ...)"
Expand All @@ -51,6 +52,7 @@ private[focus] trait FocusBase {
enum FocusError {
case NotACaseClass(className: String, fieldName: String)
case NotACaseField(className: String, fieldName: String)
case NonImplicitNonCaseParameter(className: String, parameters: List[String])
case NotAConcreteClass(className: String)
case DidNotDirectlyAccessArgument(argName: String)
case NotASimpleLambdaFunction
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,48 @@ private[focus] trait SelectParserBase extends ParserBase {

import this.macroContext.reflect._

// Match on a term that is an instance of a case class
object CaseClass {
def unapply(term: Term): Option[(Term, Symbol)] =
case class CaseClass(typeRepr: TypeRepr, classSymbol: Symbol) {
val typeArgs: List[TypeRepr] = getSuppliedTypeArgs(typeRepr)
val companionObject: Term = Ref(classSymbol.companionModule)
// classSymbol.caseFields.length == 1 would miss Tuple1
val hasOnlyOneCaseField: Boolean = classSymbol.primaryConstructor.paramSymss match {
case (head :: _) :: (_ :: Nil) :: _ if head.isTypeParam => true
case (head :: Nil) :: _ if !head.isTypeParam => true
case _ => false
}
val hasOnlyOneParameterList: Boolean = classSymbol.primaryConstructor.paramSymss match {
case _ :: Nil => true
case (head :: _) :: _ :: Nil if head.isTypeParam => true
case _ => false
}
private val nonCaseNonImplicitParameters: List[Symbol] = {
def nonImplicits(list: List[Symbol]): List[Symbol] =
list.filterNot(symbol => symbol.flags.is(Flags.Implicit) || symbol.flags.is(Flags.Given))

classSymbol.primaryConstructor.paramSymss match {
case (head :: _) :: _ :: tail if head.isTypeParam => nonImplicits(tail.flatten)
case _ :: tail => nonImplicits(tail.flatten)
case _ => Nil
}
}
val allOtherParametersAreImplicitResult: FocusResult[Unit] = nonCaseNonImplicitParameters match {
case Nil => Right(())
case list => FocusError.NonImplicitNonCaseParameter(typeRepr.show, list.map(_.name)).asResult
}

def getCaseFieldSymbol(fieldName: String): FocusResult[Symbol] =
classSymbol.caseFields.find(_.name == fieldName) match {
case Some(symbol) => Right(symbol)
case None => FocusError.NotACaseField(typeRepr.show, fieldName).asResult
}
def getCaseFieldType(caseFieldSymbol: Symbol): FocusResult[TypeRepr] =
getFieldType(typeRepr, caseFieldSymbol)
}

object CaseClassExtractor {
def unapply(term: Term): Option[CaseClass] =
term.tpe.classSymbol.flatMap { sym =>
Option.when(sym.flags.is(Flags.Case))((term, sym))
Option.when(sym.flags.is(Flags.Case))(CaseClass(getType(term), sym))
}
}

Expand All @@ -25,32 +62,21 @@ private[focus] trait SelectParserBase extends ParserBase {
case _ => Nil
}

def getClassSymbol(tpe: TypeRepr): FocusResult[Symbol] = tpe.classSymbol match {
case Some(sym) => Right(sym)
case None => FocusError.NotAConcreteClass(tpe.show).asResult
}

def getFieldType(fromType: TypeRepr, fieldName: String): FocusResult[TypeRepr] = {
// We need to do this to support tuples, because even though they conform as case classes in other respects,
// for some reason their field names (_1, _2, etc) have a space at the end, ie `_1 `.
def getTrimmedFieldSymbol(fromTypeSymbol: Symbol): Symbol =
fromTypeSymbol.fieldMembers.find(_.name.trim == fieldName).getOrElse(Symbol.noSymbol)

getClassSymbol(fromType).flatMap { fromTypeSymbol =>
getTrimmedFieldSymbol(fromTypeSymbol) match {
case FieldType(possiblyTypeArg) => Right(swapWithSuppliedType(fromType, possiblyTypeArg))
case _ => FocusError.CouldntFindFieldType(fromType.show, fieldName).asResult
}
def getFieldType(fromType: TypeRepr, caseFieldSymbol: Symbol): FocusResult[TypeRepr] =
caseFieldSymbol match {
case FieldType(possiblyTypeArg) => Right(swapWithSuppliedType(fromType, possiblyTypeArg))
case _ => FocusError.CouldntFindFieldType(fromType.show, caseFieldSymbol.name).asResult
}
}

private object FieldType {
def unapply(fieldSymbol: Symbol): Option[TypeRepr] = fieldSymbol match {
case sym if sym.isNoSymbol => None
case sym =>
sym.tree match {
case ValDef(_, typeTree, _) => Some(typeTree.tpe)
case _ => None
// Only needed for Tuples because `_1` is a DefDef while `_1 ` is a ValDef.
case DefDef(_, _, typeTree, _) => Some(typeTree.tpe)
case _ => None
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,61 +9,61 @@ private[focus] trait SelectFieldGenerator {

import macroContext.reflect._

private def generateGetter(from: Term, fieldName: String): Term =
Select.unique(from, fieldName) // o.field
private def generateGetter(from: Term, caseFieldSymbol: Symbol): Term =
Select(from, caseFieldSymbol) // o.field

def generateSelectField(action: FocusAction.SelectField): Term = {
import action.{fieldName, fromType, fromTypeArgs, toType}
import action.{caseFieldSymbol, fromType, fromTypeArgs, toType}

def generateSetter(from: Term, to: Term): Term =
Select.overloaded(from, "copy", fromTypeArgs, NamedArg(fieldName, to) :: Nil) // o.copy(field = value)
Select.overloaded(from, "copy", fromTypeArgs, NamedArg(caseFieldSymbol.name, to) :: Nil) // o.copy(field = value)

(fromType.asType, toType.asType) match {
case ('[f], '[t]) =>
'{
Lens.apply[f, t]((from: f) => ${ generateGetter('{ from }.asTerm, fieldName).asExprOf[t] })((to: t) =>
Lens.apply[f, t]((from: f) => ${ generateGetter('{ from }.asTerm, caseFieldSymbol).asExprOf[t] })((to: t) =>
(from: f) => ${ generateSetter('{ from }.asTerm, '{ to }.asTerm).asExprOf[f] }
)
}.asTerm
}
}

def generateSelectFieldWithImplicits(action: FocusAction.SelectFieldWithImplicits): Term = {
import action.{fieldName, fromType, toType, setter}
import action.{caseFieldSymbol, fromType, toType, setter}

(fromType.asType, toType.asType) match {
case ('[f], '[t]) =>
'{
Lens.apply[f, t]((from: f) => ${ generateGetter('{ from }.asTerm, fieldName).asExprOf[t] })(
Lens.apply[f, t]((from: f) => ${ generateGetter('{ from }.asTerm, caseFieldSymbol).asExprOf[t] })(
${ setter.asExprOf[t => f => f] }
)
}.asTerm
}
}

def generateSelectOnlyField(action: FocusAction.SelectOnlyField): Term = {
import action.{fieldName, fromType, fromTypeArgs, fromCompanion, toType}
import action.{caseFieldSymbol, fromType, fromTypeArgs, fromCompanion, toType}

def generateReverseGet(to: Term): Term =
Select.overloaded(fromCompanion, "apply", fromTypeArgs, List(to)) // Companion.apply(value)

(fromType.asType, toType.asType) match {
case ('[f], '[t]) =>
'{
Iso.apply[f, t]((from: f) => ${ generateGetter('{ from }.asTerm, fieldName).asExprOf[t] })((to: t) =>
Iso.apply[f, t]((from: f) => ${ generateGetter('{ from }.asTerm, caseFieldSymbol).asExprOf[t] })((to: t) =>
${ generateReverseGet('{ to }.asTerm).asExprOf[f] }
)
}.asTerm
}
}

def generateSelectOnlyFieldWithImplicits(action: FocusAction.SelectOnlyFieldWithImplicits): Term = {
import action.{fieldName, fromType, toType, reverseGet}
import action.{caseFieldSymbol, fromType, toType, reverseGet}

(fromType.asType, toType.asType) match {
case ('[f], '[t]) =>
'{
Iso.apply[f, t]((from: f) => ${ generateGetter('{ from }.asTerm, fieldName).asExprOf[t] })(
Iso.apply[f, t]((from: f) => ${ generateGetter('{ from }.asTerm, caseFieldSymbol).asExprOf[t] })(
${ reverseGet.asExprOf[t => f] }
)
}.asTerm
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,19 @@ private[focus] trait SelectFieldParser {

def unapply(term: Term): Option[FocusResult[(RemainingCode, FocusAction)]] = term match {

case Select(CaseClass(remainingCode, classSymbol), fieldName) =>
if (isCaseField(classSymbol, fieldName)) {
val fromType = getType(remainingCode)
val action = (hasOnlyOneParameterList(classSymbol), hasOnlyOneField(classSymbol)) match {
case (true, false) => getSelectFieldAction(fromType, fieldName)
case (false, false) => getSelectFieldActionWithImplicits(fromType, fieldName)
case (true, true) => getSelectOnlyFieldAction(fromType, classSymbol, fieldName)
case (false, true) => getSelectOnlyFieldActionWithImplicits(fromType, classSymbol, fieldName)
}
val remainingCodeWithAction = action.map(a => (RemainingCode(remainingCode), a))
Some(remainingCodeWithAction)
} else {
Some(FocusError.NotACaseField(remainingCode.tpe.show, fieldName).asResult)
}
case Select(remainingCode @ CaseClassExtractor(caseClass: CaseClass), fieldName) =>
Some(
for {
_ <- caseClass.allOtherParametersAreImplicitResult
caseFieldSymbol <- caseClass.getCaseFieldSymbol(fieldName)
action <- (caseClass.hasOnlyOneParameterList, caseClass.hasOnlyOneCaseField) match {
case (true, false) => getSelectFieldAction(caseClass, caseFieldSymbol)
case (false, false) => getSelectFieldActionWithImplicits(caseClass, caseFieldSymbol)
case (true, true) => getSelectOnlyFieldAction(caseClass, caseFieldSymbol)
case (false, true) => getSelectOnlyFieldActionWithImplicits(caseClass, caseFieldSymbol)
}
} yield (RemainingCode(remainingCode), action)
)

case Select(remainingCode, fieldName) =>
Some(FocusError.NotACaseClass(remainingCode.tpe.show, fieldName).asResult)
Expand All @@ -34,57 +33,60 @@ private[focus] trait SelectFieldParser {
}
}

private def isCaseField(classSymbol: Symbol, fieldName: String): Boolean =
classSymbol.caseFields.exists(_.name == fieldName)

private def hasOnlyOneField(classSymbol: Symbol): Boolean =
classSymbol.caseFields.length == 1

private def hasOnlyOneParameterList(classSymbol: Symbol): Boolean =
classSymbol.primaryConstructor.paramSymss match {
case _ :: Nil => true
case (head :: _) :: _ :: Nil if head.isTypeParam => true
case _ => false
}

private def getCompanionObject(classSymbol: Symbol): Term =
Ref(classSymbol.companionModule)

private def getSelectFieldAction(fromType: TypeRepr, fieldName: String): FocusResult[FocusAction] =
getFieldType(fromType, fieldName).flatMap { toType =>
Right(FocusAction.SelectField(fieldName, fromType, getSuppliedTypeArgs(fromType), toType))
}

private def getSelectFieldActionWithImplicits(fromType: TypeRepr, fieldName: String): FocusResult[FocusAction] =
getFieldType(fromType, fieldName).flatMap { toType =>
val typeArgs = getSuppliedTypeArgs(fromType)
constructSetter(fieldName, fromType, toType, typeArgs).map { setter =>
FocusAction.SelectFieldWithImplicits(fieldName, fromType, toType, setter)
}
}
private def getSelectFieldAction(
caseClass: CaseClass,
caseFieldSymbol: Symbol
): FocusResult[FocusAction] =
for {
toType <- caseClass.getCaseFieldType(caseFieldSymbol)
} yield FocusAction.SelectField(
caseFieldSymbol,
caseClass.typeRepr,
caseClass.typeArgs,
toType
)

private def getSelectFieldActionWithImplicits(
caseClass: CaseClass,
caseFieldSymbol: Symbol
): FocusResult[FocusAction] =
for {
toType <- caseClass.getCaseFieldType(caseFieldSymbol)
setter <- constructSetter(caseFieldSymbol.name, caseClass.typeRepr, toType, caseClass.typeArgs)
} yield FocusAction.SelectFieldWithImplicits(
caseFieldSymbol,
caseClass.typeRepr,
toType,
setter
)

private def getSelectOnlyFieldAction(
fromType: TypeRepr,
fromClassSymbol: Symbol,
fieldName: String
caseClass: CaseClass,
caseFieldSymbol: Symbol
): FocusResult[FocusAction] =
for {
toType <- getFieldType(fromType, fieldName)
companion = getCompanionObject(fromClassSymbol)
supplied = getSuppliedTypeArgs(fromType)
} yield FocusAction.SelectOnlyField(fieldName, fromType, supplied, companion, toType)
toType <- caseClass.getCaseFieldType(caseFieldSymbol)
} yield FocusAction.SelectOnlyField(
caseFieldSymbol,
caseClass.typeRepr,
caseClass.typeArgs,
caseClass.companionObject,
toType
)

private def getSelectOnlyFieldActionWithImplicits(
fromType: TypeRepr,
fromClassSymbol: Symbol,
fieldName: String
caseClass: CaseClass,
caseFieldSymbol: Symbol
): FocusResult[FocusAction] =
for {
toType <- getFieldType(fromType, fieldName)
companion = getCompanionObject(fromClassSymbol)
supplied = getSuppliedTypeArgs(fromType)
reverseGet <- constructReverseGet(companion, fromType, toType, supplied)
} yield FocusAction.SelectOnlyFieldWithImplicits(fieldName, fromType, toType, reverseGet)
toType <- caseClass.getCaseFieldType(caseFieldSymbol)
reverseGet <- constructReverseGet(caseClass.companionObject, caseClass.typeRepr, toType, caseClass.typeArgs)
} yield FocusAction.SelectOnlyFieldWithImplicits(
caseFieldSymbol,
caseClass.typeRepr,
toType,
reverseGet
)

private case class LiftException(error: FocusError) extends Exception

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ class ContextBoundCompilationIssueSpec extends DisciplineSuite {
val lens: Lens[S[T], Bar[T]] = GenLens[S[T]](_.bar)
}

private case class S[T: Foo](bar: Bar[T])
private case class S[T: Foo](bar: Bar[T]) {
def bar(t: T): T = t
}

private case object FooImpl extends Foo[Unit]
private case object BarImpl extends Bar[Unit]
Expand Down

0 comments on commit c3eff55

Please sign in to comment.