Parameter hints of extension methods do not instantiate outer (extension[A]) type parameters
Describe the bug
class Foo[A]
extension [A](foo: Foo[A])
def bar(a: A): Unit = ()
def test =
Foo[String].bar("123")
Neither parameters hints not Suggest on the bar method instantiate the type parameter A to String:
Expected behavior
The outer type parameters should reduce, otherwise, the behavior is not on par with Scala 2.x implicit class pattern:
Operating system
None
Editor/Extension
VS Code
Version of Metals
v1.6.0
Extra context or search terms
Interestingly, the type parameter is correctly instantiated to String in Suggest before the the opening paren of bar is typed:
Thanks for reporting! Looks like we do it in completion but not in the signature provider.
The following might be a (simpler) manifestation of the same issue, where the hint shows A instead of String:
During the Scala Tooling Spree, we found that this type parameter substitution is only working for classes, all method type arguments are ignored if explicitly instantiated e.g. List[Int](1).map[String](a => @@) shows map[B](f: Int => B): List[B]. This will probably need a bigger fix
Yeah, I'm finding myself creating a lot of intermediate classes just to get reasonable IDE hints.
After some discussions we've found a possible approach to fix this, but as Jamie pointed, it will be a bigger fix.
def test[A, B, C](a: A)(b: A => B)(c: C): Unit
test(5)(@@) // Type of this after typechecker + tree repairing is (b: Int => Any)(c: Any): Unit
At that point we lose information about original type parameters. What it means is that we will need to stitch it all together and it will not be trivial. There are a lot of cases to handle e.g:
def test[A, B, C](a: A)(b: A => B)(c: C): Unit
test(@@) // (a: Any)(b: Any => Any)(c: Any): Unit
test[Int, String, Long](@@) // (a: Int)(b: Int => String)(c: Long): Unit
test(@@)(_.toString)(7) // This will also be (a: Any)(b: Any => Any)(c: Any): Unit
//but here
test(5)(@@) // This will also be (b: Any => Any)(c: Any): Unit
// and we will need to find parent node type to get the signature, but that also means that we will lose information about params instantiated later on.
Basically we would need to create a heuristic that allows us to recursively stitch signature from the type of the function instead of type of denotation which loses that sort of information. We already do something similar when we try to match type to parameter name. Probably fix would land somewhere here due to convenience (the method is called toParamss in Signatures.scala). With some clever pattern matching we could probably achieve it.
My proposition for this signature is:
def test[A, B, C](a: A)(b: A => B)(c: C): Unit
test("")(@@) // test[A (String), B, C](a: String)(b: String => B)(c: C): Unit
or maybe even further
test("")(@@) // test[A (String), B, C](a: A (String))(b: A (String) => B)(c: C): Unit
or even further
test("")(@@) // test[A, B, C](a: A)(b: A => B)(c: C): Unit
^^^^^^^^^ // b: String => B
Now that I think of it we can't help with this:
def aaa[A](a: A, b: B): Unit = ???
aaa("", @@) // Expected aaa(a: String, b: String): Unit but it is incorrect
// what if:
aaa("", 1) // It is valid code, so basically until every member is provided we can't help you with this signature
That said we should still show correct signature helps for extension methods with type params + we should also insert type if it is provided explicitly: aaa[String](@@) // aaa(a: String, b: String)