Skip to content

Commit 41d3a25

Browse files
authored
Only rename method type params shadowing enclosing class type params (#24684)
Previously, method type parameters were always renamed if they had the same name as any class type parameter, regardless of whether the class type parameter was actually used in the method signature. As a result, we rename methods's type params for unnecessary names like: #24671 (Note that renaming actually doesn't cause binary incompatibility, and this change is just for make generic signature a bit clear and silence false-positive MIMA reporting). For example, in an example below, the Scala compiler rename the generic signature of `bar` to something like `bar[T1](x: T1): T1` because `T` is used by `Foo[T]`. However, this is unnessary rename because none of T in method signature refer the `T` of `Foo[T]`. ```scala class Foo[T]: def bar[T](x: T): T = ??? ``` This commit makes the renaming conditional: Method type parameters are only renamed when - (1) A class type parameter is referenced in the method signature, and - (2) That class type parameter is shadowed by a method type parameter
2 parents 8e8c8b9 + 1e392ee commit 41d3a25

File tree

8 files changed

+157
-11
lines changed

8 files changed

+157
-11
lines changed

compiler/src/dotty/tools/dotc/transform/GenericSignatures.scala

Lines changed: 46 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -49,24 +49,21 @@ object GenericSignatures {
4949
val builder = new StringBuilder(64)
5050
val isTraitSignature = sym0.enclosingClass.is(Trait)
5151

52-
// Collect class-level type parameter names to avoid conflicts with method-level type parameters
53-
val usedNames = collection.mutable.Set.empty[String]
54-
if(sym0.is(Method)) {
55-
sym0.enclosingClass.typeParams.foreach { tp =>
56-
usedNames += sanitizeName(tp.name)
57-
}
58-
}
52+
// Track class type parameter names that are shadowed by method type parameters
53+
// Used to trigger renaming of method type parameters to avoid conflicts
54+
val shadowedClassTypeParamNames = collection.mutable.Set.empty[String]
5955
val methodTypeParamRenaming = collection.mutable.Map.empty[String, String]
56+
6057
def freshTypeParamName(sanitizedName: String): String = {
61-
if !usedNames.contains(sanitizedName) then sanitizedName
58+
if !shadowedClassTypeParamNames.contains(sanitizedName) then sanitizedName
6259
else {
6360
var i = 1
6461
var newName = sanitizedName + i
65-
while usedNames.contains(newName) do
62+
while shadowedClassTypeParamNames.contains(newName) do
6663
i += 1
6764
newName = sanitizedName + i
6865
methodTypeParamRenaming(sanitizedName) = newName
69-
usedNames += newName
66+
shadowedClassTypeParamNames += newName
7067
newName
7168
}
7269
}
@@ -347,7 +344,22 @@ object GenericSignatures {
347344

348345
case mtd: MethodOrPoly =>
349346
val (tparams, vparams, rte) = collectMethodParams(mtd)
350-
if (toplevel && !sym0.isConstructor) polyParamSig(tparams)
347+
if (toplevel && !sym0.isConstructor) {
348+
if (sym0.is(Method)) {
349+
val (usedMethodTypeParamNames, usedClassTypeParams) = collectUsedTypeParams(vparams :+ rte, sym0)
350+
val methodTypeParamNames = tparams.map(tp => sanitizeName(tp.paramName.lastPart)).toSet
351+
// Only add class type parameters to shadowedClassTypeParamNames if they are:
352+
// 1. Referenced in the method signature, AND
353+
// 2. Shadowed by a method type parameter with the same name
354+
// This will trigger renaming of the method type parameter
355+
usedClassTypeParams.foreach { classTypeParam =>
356+
val classTypeParamName = sanitizeName(classTypeParam.name)
357+
if methodTypeParamNames.contains(classTypeParamName) then
358+
shadowedClassTypeParamNames += classTypeParamName
359+
}
360+
}
361+
polyParamSig(tparams)
362+
}
351363
builder.append('(')
352364
for vparam <- vparams do jsig1(vparam)
353365
builder.append(')')
@@ -528,4 +540,27 @@ object GenericSignatures {
528540
val rte = recur(mtd)
529541
(tparams.toList, vparams.toList, rte)
530542
end collectMethodParams
543+
544+
/** Collect type parameters that are actually used in the given types. */
545+
private def collectUsedTypeParams(types: List[Type], initialSymbol: Symbol)(using Context): (Set[Name], Set[Symbol]) =
546+
assert(initialSymbol.is(Method))
547+
def isTypeParameterInMethSig(sym: Symbol, initialSymbol: Symbol)(using Context) =
548+
!sym.maybeOwner.isTypeParam && // check if it's not higher order type param
549+
sym.isTypeParam && sym.owner == initialSymbol
550+
551+
val usedMethodTypeParamNames = collection.mutable.Set.empty[Name]
552+
val usedClassTypeParams = collection.mutable.Set.empty[Symbol]
553+
554+
def collect(tp: Type): Unit = tp.foreachPart:
555+
case ref @ TypeParamRef(_: PolyType, _) =>
556+
usedMethodTypeParamNames += ref.paramName
557+
case tp: TypeRef =>
558+
val sym = tp.typeSymbol
559+
if sym.isTypeParam && sym.isContainedIn(initialSymbol.topLevelClass) then
560+
usedClassTypeParams += sym
561+
case _ =>
562+
563+
types.foreach(collect)
564+
(usedMethodTypeParamNames.toSet, usedClassTypeParams.toSet)
565+
end collectUsedTypeParams
531566
}

compiler/test/dotc/pos-test-pickling.excludelist

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ i11982a.scala
3434
i17255
3535
i17735.scala
3636
i24134
37+
i24134-nested
3738

3839
# Tree is huge and blows stack for printing Text
3940
i7034.scala
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
noShadow: public scala.Tuple3<T, U, V> Outer$Middle$Inner.noShadow(T,U,V)
2+
shadowOuter: public <T1> T Outer$Middle$Inner.shadowOuter()
3+
shadowMiddle: public <U1> U Outer$Middle$Inner.shadowMiddle()
4+
shadowInner: public <V1> V Outer$Middle$Inner.shadowInner()
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
class Outer[T]:
2+
def outerValue: T = ???
3+
4+
class Middle[U]:
5+
def middleValue: U = ???
6+
7+
class Inner[V]:
8+
def innerValue: V = ???
9+
def noShadow(x: T, y: U, z: V): (T, U, V) = (x, y, z)
10+
def shadowOuter[T] = outerValue
11+
def shadowMiddle[U] = middleValue
12+
def shadowInner[V] = innerValue
13+
14+
@main def Test(): Unit =
15+
val innerMethods = classOf[Outer[_]].getDeclaredClasses()
16+
.find(_.getName.contains("Middle")).get.getDeclaredClasses()
17+
.find(_.getName.contains("Inner")).get.getDeclaredMethods()
18+
19+
printMethodSig(innerMethods, "noShadow")
20+
printMethodSig(innerMethods, "shadowOuter")
21+
printMethodSig(innerMethods, "shadowMiddle")
22+
printMethodSig(innerMethods, "shadowInner")
23+
24+
def printMethodSig(methods: Array[java.lang.reflect.Method], name: String): Unit =
25+
methods.find(_.getName.endsWith(name)).foreach { m =>
26+
println(s"$name: ${m.toGenericString}")
27+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
bar: public <T> T Foo.bar(T)
2+
baz: public <T> T Foo.baz(T,Foo<T>)
3+
qux: public <U> U Foo.qux(U,Foo<T>)
4+
quux: public <T,U> scala.Tuple2<T, U> Foo.quux(T,U,Foo<T>)
5+
copy: public <T> Bar<T> Bar.copy(T)
6+
copy$default$1: public <T1> T Bar.copy$default$1()
7+
m: public <T1> T C.m()
8+
compose: public <A1> scala.Function1<A1, B> JavaPartialFunction.compose(scala.Function1<A1, A>)
9+
compose: public <R> scala.PartialFunction<R, B> JavaPartialFunction.compose(scala.PartialFunction<R, A>)
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// All of type params shouldn't rename
2+
class Foo[T]:
3+
def bar[T](x: T): T = x
4+
def baz[T](x: T, y: Foo[T]): T = x
5+
def qux[U](x: U, y: Foo[T]): U = x
6+
def quux[T, U](x: T, y: U, z: Foo[T]): (T, U) = (x, y)
7+
8+
// https://github.com/scala/scala3/issues/24671
9+
final case class Bar[+T](t: T)
10+
11+
// `m: public <T1> T C.m()` rather than `m: public <T> T C.m()`
12+
// that was wrong and could be crashed with
13+
// `val c = C[String]; String x = c.<Object>m();`.
14+
abstract class C[T]:
15+
def x: T
16+
def m[T] = x
17+
18+
// https://github.com/scala/scala3/issues/24134
19+
// The mixin forwarders for compose in Function1 method has signature
20+
// `def compose[A](g: A => T1): A => R`
21+
// Where the JavaPartialFunction[A, B] has type parameter A (name clash),
22+
// The type parameter A in method should be renamed to avoid name duplication.
23+
abstract class JavaPartialFunction[A, B] extends PartialFunction[A, B]
24+
25+
@main def Test(): Unit =
26+
val fooMethods = classOf[Foo[_]].getDeclaredMethods()
27+
printMethodSig(fooMethods, "bar")
28+
printMethodSig(fooMethods, "baz")
29+
printMethodSig(fooMethods, "qux")
30+
printMethodSig(fooMethods, "quux")
31+
32+
val barMethods = classOf[Bar[_]].getDeclaredMethods()
33+
printMethodSig(barMethods, "copy")
34+
// copy$default$1 have `<T1> T Bar.copy$default$1` rather than `<T> T Bar.copy$default$1`
35+
// as reported in https://github.com/scala/scala3/issues/24671
36+
// The type parameter rename occurs because the return type T refers the enclosing class's type param T.
37+
printMethodSig(barMethods, "copy$default$1")
38+
39+
val cMethods = classOf[C[_]].getDeclaredMethods()
40+
printMethodSig(cMethods, "m")
41+
42+
val jpfMethods = classOf[JavaPartialFunction[_, _]].getDeclaredMethods()
43+
printMethodSigs(jpfMethods, "compose")
44+
45+
def printMethodSig(methods: Array[java.lang.reflect.Method], name: String): Unit =
46+
methods.find(_.getName.endsWith(name)).foreach { m =>
47+
println(s"$name: ${m.toGenericString}")
48+
}
49+
50+
def printMethodSigs(methods: Array[java.lang.reflect.Method], name: String): Unit =
51+
methods.filter(_.getName == name).sortBy(_.toGenericString).foreach { m =>
52+
println(s"$name: ${m.toGenericString}")
53+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
class Container[A]:
2+
abstract class JavaPartialFunction[B] extends PartialFunction[A, B]

tests/pos/i24134-nested/Main.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
public class Main {
2+
public static void main(String[] args) {
3+
Container<String> container = new Container<>();
4+
Container<String>.JavaPartialFunction<Integer> pf = container.new JavaPartialFunction<Integer>() {
5+
@Override
6+
public boolean isDefinedAt(String x) {
7+
return x != null && !x.isEmpty();
8+
}
9+
@Override
10+
public Integer apply(String x) {
11+
return x.length();
12+
}
13+
};
14+
}
15+
}

0 commit comments

Comments
 (0)