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

Conversation

joroKr21
Copy link
Collaborator

@joroKr21 joroKr21 commented Jan 5, 2018

Unless:

  • the outer type is final and not parametrized
  • the inner type is a case class

All other type projections are potentially unsound because the
inner type can capture type members of the outer type.

Closes #579
Closes #848

@milessabin
Copy link
Owner

Could you add a test that None is yielded appropriately?

@milessabin milessabin added this to the shapeless-2.4.0 milestone Jan 5, 2018
@joroKr21
Copy link
Collaborator Author

joroKr21 commented Jan 5, 2018

You mean for an unrelated type?

@milessabin
Copy link
Owner

milessabin commented Jan 5, 2018

I'm looking at the comment above the code you replaced,

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.

I'd like to see a test which verifies that unsound cases are still rejected.

@milessabin
Copy link
Owner

milessabin commented Jan 5, 2018

Something like this maybe?

trait Outer {
  type T
  class Inner(val t: T)
  def boom(a: Any): Option[Inner] = a.cast[Inner]
}
val i = new Outer { type T = Int }
val ii = new i.Inner(23)
val s = new Outer { type T = String }
s.boom(ii) // If Outer compiles then this should be None

@joroKr21
Copy link
Collaborator Author

joroKr21 commented Jan 5, 2018

There are existing tests for type projections in testNested. The example you described should be better handled in #798 but to test this I need to combine it with this PR or wait for one or the other to be merged.

Or maybe something like Typeable[Outer#Inner] - that doesn't fix T though.

@joroKr21
Copy link
Collaborator Author

joroKr21 commented Jan 5, 2018

I found an unsound example:

  @Test
  def testTypeProjection(): Unit = {
    trait Outer {
      type T
      class Inner(val t: T)
    }

    trait OuterInt extends Outer {
      type T = Int
    }

    trait OuterStr extends Outer {
      type T = String
    }

    val typ = Typeable[OuterInt#Inner]
    val outStr = new OuterStr { }
    assertEquals(None, typ.cast(new outStr.Inner("boom")))
  }

However, note that the check in master is not enough to catch all such cases. It doesn't deal with: inheritance, methods, even more nested classes, etc. That's a very deep rabbit hole.

One option would be to just reject all type projections.

@milessabin
Copy link
Owner

When you say "type projection" do you mean "abstract type member"?

@milessabin
Copy link
Owner

Ahh, you mean type projections as the target of the cast?

@joroKr21
Copy link
Collaborator Author

joroKr21 commented Jan 5, 2018

Yes, I mean instances of Typeable[Type#Projection] where Projection is not a case class.

@joroKr21
Copy link
Collaborator Author

joroKr21 commented Jan 5, 2018

Apropo, regular pattern matching has the same problem. It will warn for erasure on case _: Outer[Int]#Inner, but not on case _: OuterInt#Inner

@milessabin
Copy link
Owner

milessabin commented Jan 6, 2018

I just checked my earlier example,

@Test
def testRefined: Unit = {
  trait Outer {
    type T
    class Inner(val t: T)
    def boom(a: Any): Option[Inner] = a.cast[Inner]
  }
  val i = new Outer { type T = Int }
  val ii = new i.Inner(23)
  val s = new Outer { type T = String }
  //assert(!s.boom(ii).isDefined) // If Outer compiles then this should be None}
  s.boom(ii).map(_.t.length) // CCE
}

This blows up with a ClassCastException.

Conclusion: there should be no Typeable instances for types with abstract type members.

Conclusion: there should be no Typeable instances for types which contain references to abstract types.

@milessabin
Copy link
Owner

Interestingly, in your example, I think there should be an instance for Typeable[OuterInt#Inner] because as seen from OuterInt T is not abstract.

@joroKr21
Copy link
Collaborator Author

joroKr21 commented Jan 6, 2018

@milessabin in your example we don't have a type projection, but a path dependent type. As such, the outer reference should be compared for equality (see #798).

Yes, I guess my example should have a Typeable instance according to the comments on master (although it is still unsound), but it doesn't. Looking more closely at the code rtpe.asSeenFrom(tpe, tpe.typeSymbol.owner) is nonsensical. We can't replace references to Outer.this.type with OuterInt#Inner because Inner is not a subclass of Outer. In effect removing this call to asSeenFrom would change nothing.

But what if there is a third class nested inside Inner that captures type variables? Should we check all nested decls? What about inheriting captured variables from a superclass? Check all nested members? Even if all cases are covered, the error won't make much sense to the user any more.

@joroKr21 joroKr21 changed the title Check explicitly for parametrized type projections in Typeable Avoid creating Typeable for general type projections Jan 16, 2018
@codecov-io
Copy link

codecov-io commented Jan 16, 2018

Codecov Report

❗ No coverage uploaded for pull request base (master@0a94fcf). Click here to learn what that means.
The diff coverage is n/a.

Impacted file tree graph

@@            Coverage Diff            @@
##             master     #803   +/-   ##
=========================================
  Coverage          ?   87.99%           
=========================================
  Files             ?       64           
  Lines             ?     1499           
  Branches          ?        4           
=========================================
  Hits              ?     1319           
  Misses            ?      180           
  Partials          ?        0
Impacted Files Coverage Δ
core/src/main/scala/shapeless/typeable.scala 0% <ø> (ø)

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 0a94fcf...3ed669c. Read the comment docs.

Unless:
* the outer type is final and not parametrized
* the inner type is a case class

All other type projections are potentially unsound because the
inner type can capture type members of the outer type.
@joroKr21 joroKr21 self-assigned this Mar 20, 2020
@joroKr21 joroKr21 requested a review from milessabin March 20, 2020 17:17
@joroKr21 joroKr21 added the Bug label Mar 20, 2020
@milessabin
Copy link
Owner

I'm going to defer to your judgement on this one.

@joroKr21
Copy link
Collaborator Author

If you approve I will merge it 😄 - it's the last of a series of fixes in Typeable and doesn't make much sense to leave it for later.

Copy link
Owner

@milessabin milessabin left a comment

Choose a reason for hiding this comment

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

LGTM!

@joroKr21 joroKr21 merged commit 138aa33 into milessabin:master Mar 22, 2020
@joroKr21 joroKr21 deleted the typeable-proj branch April 3, 2020 14:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
3 participants