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

Scaladoc implicits: multiple conversions may lead to duplicate members #9620

Closed
scabug opened this issue Jan 9, 2016 · 12 comments
Closed

Scaladoc implicits: multiple conversions may lead to duplicate members #9620

scabug opened this issue Jan 9, 2016 · 12 comments
Assignees
Milestone

Comments

@scabug
Copy link

scabug commented Jan 9, 2016

Having multiple implicit conversions can lead to duplicate members in a template. This affects scala.Array which has its members duplicated 10 times.

Here's a minimized example:

$ cat A.scala 
package a

trait Foo[S] {
  def foo(t: S): Int = 123
}

trait Boo[T]

object Boo {
  implicit class BooCharIsFoo(boo: Boo[Char]) extends Foo[Char]
  implicit class BooLongIsFoo(boo: Boo[Long]) extends Foo[Long]
}

If you look at the result of running:

$ scaladoc -implicits A.scala

You see that the member foo in trait Boo is duplicated:
!double-def.png!

The reason is that it's added through the different conversions (BooCharIsFoo and BooLongIsFoo).

Now, if the signatures are identical, we could fold the "Implicit Member" information:

This member can be added in several ways:
  * by the implicit conversion X if ...
  * by the implicit conversion Y if ...

Now, what if the signatures are different, as is the case here? I don't know what to do. Any suggestions?

@scabug
Copy link
Author

scabug commented Jan 9, 2016

Imported From: https://issues.scala-lang.org/browse/SI-9620?orig=1
Reporter: @VladUreche
See #7046
Attachments:

@scabug
Copy link
Author

scabug commented Jan 9, 2016

@VladUreche said (edited on Jan 9, 2016 6:21:44 PM UTC):
Ideally, we should extrapolate that there's a method foo(t: T): Int added to trait Boo only if S =:= Char or S =:= Long.
But we have no way to do so -- what if we had:

object Boo {
  implicit class BooCharIsFoo(boo: Boo[Char]) extends Foo[/* >>> */Nothing/* <<< */]
  implicit class BooLongIsFoo(boo: Boo[Long]) extends Foo[/* >>> */AnyRef /* <<< */]
}

As an alternative, we could show the signature of foo from trait Foo, but that wouldn't make sense, as it refers to type parameter S, which doesn't exist in trait Boo.

The only sane approach I can think of is to add a scaladoc annotation that instructs the inferencer to ignore the implicit conversion -- this way, we can hide very specific conversions. WDYT?

@scabug
Copy link
Author

scabug commented Feb 6, 2016

@felixmulder said (edited on Feb 6, 2016 10:10:23 AM UTC):
So I was thinking of having a look at this. The last option seems to be fairly easy to implement. How would this look?

object Boo {
  /**
   * @hideImplicitConversion
   */
  implicit class BooCharIsFoo(boo: Boo[Char]) extends Foo[Char]
}

What I mean by this is: that we would want to hide each implicit conversion explicitly - not all of them right?

@scabug
Copy link
Author

scabug commented Feb 6, 2016

@VladUreche said:
Felix, I think this would work very well, especially given the restricted scope of the problem (limited to Array).

@scabug
Copy link
Author

scabug commented Feb 6, 2016

@VladUreche said:
Please ping me if I can help with the implementation! 👍

@scabug
Copy link
Author

scabug commented Feb 6, 2016

@scabug
Copy link
Author

scabug commented Feb 6, 2016

@felixmulder said (edited on Feb 6, 2016 3:54:11 PM UTC):
So for this:

package a

import scala.annotation.hideImplicitConversion

trait Foo[S] {
  def foo(t: S): Int = 123
}

trait Boo[T]

trait ShouldNotAppear

object Boo {
  @hideImplicitConversion implicit class BooCharIsFoo(boo: Boo[ShouldNotAppear]) extends Foo[ShouldNotAppear]
  implicit class BooLongIsFoo(boo: Boo[Long]) extends Foo[Long]
}

I figured I could do something like this in ModelFactoryImplicitSupport.scala:545:

def implicitShouldDocument(aSym: Symbol): Boolean = {
  //...
  && !aSym.owner.annotations.map(_.symbol.nameString).contains("hideImplicitConversion")
}

But unfortunately, the entities that have their owner set to the annotated implicit class - not one of them has the method foo. So I started looking at it from the perspective "what owner does the method foo have"? Turns out it has Foo as owner (surprise!).

So then I figured: maybe I can get the implicit class from the method foo by checking if its owner has any subclasses that have the @hideImplicitConversion annotation. But when I look at the _.owner.knownDirectSubclasses list of entities - I get an empty set for Foo. I looked around and found: #7046. I'm not sure this has anything to do with this. I just don't know how to get the subclasses from the Foo trait this way. Any ideas?

(PS: #4949 on github is ready to be merged)

PS2: I hope this approach enables us to mask stuff out in the Predef btw!

@scabug
Copy link
Author

scabug commented Feb 6, 2016

@VladUreche said (edited on Feb 6, 2016 4:39:12 PM UTC):
Felix, I'm sorry, seems my last comment misled you.

There are two places where we can annotate:

  1. At the implicit conversion (which hides it all the time) - i.e. at FooCharIsBoo
  2. At the implicit conversion target (which hides the implicit conversion locally) - i.e. at Foo

The problem with approach (1) is that we need a true annotation (as you showed before), since the doc comments are not persisted in the class files. While this is a working approach, it's probably not going to scale as well, because in some cases you may want to see the implicit conversion while in others you may want to hide it.

The second approach places annotations at the implicit conversion targets. The good news is that, to get a page in Scaladoc for the entity, you need to have the source in the first place, so you also have the doc comment (since source entities have priority over classpath entities). So we can have an annotation in the doc comment. So now, instead of marking implicit def booIsFoo, you mark class Boo directly, saying you want to mask/hide implicit conversion booIsFoo.

IMO, the second approach is markedly better, for two reasons:

  1. Better granularity, as for an implicit conversion we can show it in one entity and mask it in another
  2. We can use doc comments, which does not entail any risk of breaking binary compatibility (a class with the annotation will require the annotation to be present in the classpath, thus won't compile under 2.{10,11}.x.

Now, going back to your question: the owner is the lexically enclosing entity, such as trait Foo for method foo. Getting the subclasses of a class Foo is not possible realistically unless class Foo is final or sealed. Otherwise, we're in an open world, so any new code can subclass Foo, which makes it impossible to list all the subclasses of Foo.

What I would imagine you want to do is:
(1) Identify the symbol that corresponds to the synthetic implicit def BooCharIsFoo corresponding to the implicit class BooCharIsFoo (it should be convSym: https://github.com/scala/scala/blob/2.12.x/src/scaladoc/scala/tools/nsc/doc/model/ModelFactoryImplicitSupport.scala#L328)
(2) and check for the "hideImplicitConversion" annotation directly on it

If you decide to keep going on this path, I can show you how you match the hideImplicitConversion as a symbol instead of by name.

@scabug
Copy link
Author

scabug commented Feb 7, 2016

@felixmulder said:
Thanks for the explanation! Good thinking about the binary compatibility thing, then let's go for annotating in the comments. I will add it to the model and get back to you once I feel like I know what I'm doing.

Just to take what you said and put it in an example:

package a
 
trait Foo[S] {
  def foo(t: S): Int = 123
}
 
/** Boo hides BooShouldNotAppearIsFoo
 *
 * @hideImplicitConversion BooShouldNotAppearIsFoo
 */
trait Boo[T]
trait ShouldNotAppear
 
object Boo {
  implicit class BooShouldNotAppearIsFoo(boo: Boo[ShouldNotAppear]) extends Foo[ShouldNotAppear]
  implicit class BooLongIsFoo(boo: Boo[Long]) extends Foo[Long]
}

@scabug
Copy link
Author

scabug commented Feb 7, 2016

@VladUreche said:
Thanks Felix, the example shows perfectly what needs to be done.
Ping me anytime if you need help!

@scabug
Copy link
Author

scabug commented Feb 9, 2016

@VladUreche said:
scala/scala#4952

@scabug
Copy link
Author

scabug commented Apr 29, 2016

@gourlaysama said:
Fixed in 2.12.0-M4 (#4952) by @felixmulder, closing.

@scabug scabug closed this as completed Apr 29, 2016
@scabug scabug added this to the 2.12.0-M4 milestone Apr 7, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants