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

Avoid creating Typeable for general type projections #803

Merged
merged 9 commits into from
Mar 22, 2020
26 changes: 6 additions & 20 deletions core/src/main/scala/shapeless/typeable.scala
Original file line number Diff line number Diff line change
Expand Up @@ -446,28 +446,14 @@ class TypeableMacros(val c: blackbox.Context) extends SingletonTypeUtils {
case ConstantType(c) =>
q"""_root_.shapeless.Typeable.valueSingletonTypeable[$tpe]($c.asInstanceOf[$tpe], ${nameOf(c.tpe)})"""

case other =>
/* There is potential unsoundness if we allow a simple cast between two
* unparameterized types, if they contain values of an abstract type variable
* from outside of their definition. Therefore, check to see if any values
* have types that look different from the inside and outside of the type. */
val closesOverType = other.decls.exists {
case sym: TermSymbol if sym.isVal || sym.isVar || sym.isParamAccessor =>
val rtpe = sym.typeSignature
rtpe.asSeenFrom(tpe, tpe.typeSymbol) != rtpe.asSeenFrom(tpe, tpe.typeSymbol.owner)
case _ => false
}
// Outer#Inner is unsound in general since Inner can capture type members of Outer.
case TypeRef(TypeRef(_, outer, args), inner, _) if !outer.isFinal || args.nonEmpty =>
if (inner.isClass && inner.asClass.isCaseClass) mkCaseClassTypeable(tpe)
else c.abort(c.enclosingPosition, s"No default Typeable for type projection $tpe")

case _ =>
val tsym = tpe.typeSymbol
if (closesOverType) {
if (tsym.isClass && tsym.asClass.isCaseClass) {
/* it appears to be sound to treat captured type variables as if they were
* simply case class parameters, as they'll be checked by their own Typeables later. */
mkCaseClassTypeable(tpe)
} else {
c.abort(c.enclosingPosition, s"No default Typeable for type $tpe capturing an outer type variable")
}
} else if (tsym.isStatic || tsym.isFinal || (tsym.isClass && tsym.asClass.isTrait)) {
if (tsym.isStatic || tsym.isFinal || (tsym.isClass && tsym.asClass.isTrait)) {
// scala/bug#4440 Final inner classes and traits have no outer accessor.
q"_root_.shapeless.Typeable.namedSimpleTypeable(_root_.scala.Predef.classOf[$tpe], ${nameOf(tsym)})"
} else {
Expand Down
37 changes: 37 additions & 0 deletions core/src/test/scala/shapeless/typeable.scala
Original file line number Diff line number Diff line change
Expand Up @@ -704,4 +704,41 @@ class TypeableTests {
assertEquals(None, outer1.inner.cast[outer2.Inner])
assertEquals(Some(outer1.inner), outer1.inner.cast[outer1.Inner])
}

def testThisType(): Unit = {
trait Node { val children: List[this.type] }
val nodeTyp = Typeable[Node]
object nil extends Node { val children = Nil }
assertEquals(Some(nil), nodeTyp.cast(nil))
assertEquals(None, nodeTyp.cast(Nil))
assertEquals("Node", nodeTyp.describe)
}

@Test
def testTypeProjections(): Unit = {
class Outer {
class Inner
case class CC(i: Int)
}

final class OuterFin {
class Inner
case class CC(i: Int)
}

illTyped("Typeable[Outer#Inner]")
val typCC = Typeable[Outer#CC]
val typFinIn = Typeable[OuterFin#Inner]
val typFinCC = Typeable[OuterFin#CC]

val outer = new Outer
val cc = outer.CC(1)
assertEquals(Some(cc), typCC.cast(cc))

val outerFin = new OuterFin
val innerFin = new outerFin.Inner
val ccFin = outerFin.CC(2)
assertEquals(Some(innerFin), typFinIn.cast(innerFin))
assertEquals(Some(ccFin), typFinCC.cast(ccFin))
}
}