metals icon indicating copy to clipboard operation
metals copied to clipboard

Parameter hints of extension methods do not instantiate outer (extension[A]) type parameters

Open TomasMikula opened this issue 6 months ago • 6 comments

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:

Image

Expected behavior

The outer type parameters should reduce, otherwise, the behavior is not on par with Scala 2.x implicit class pattern:

Image

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:

Image

TomasMikula avatar Jun 08 '25 20:06 TomasMikula

Thanks for reporting! Looks like we do it in completion but not in the signature provider.

tgodzik avatar Jun 09 '25 14:06 tgodzik

The following might be a (simpler) manifestation of the same issue, where the hint shows A instead of String:

Parameter hint showing `A` instead of `String`

TomasMikula avatar Jun 11 '25 10:06 TomasMikula

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

bishabosha avatar Jun 26 '25 16:06 bishabosha

Yeah, I'm finding myself creating a lot of intermediate classes just to get reasonable IDE hints.

TomasMikula avatar Jun 26 '25 20:06 TomasMikula

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

rochala avatar Jun 27 '25 09:06 rochala

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)

rochala avatar Jul 01 '25 21:07 rochala