mcpbeat Sign in

Jvmname Disambiguate Erased Overloads Agent Skill

Resolve two extension functions that differ only in their generic receiver's type argument and so compile to a single JVM method, using @JvmName on one of them. Covers what the annotation changes, why it beats renaming the Kotlin function, and what non-Kotlin callers see afterwards. Use when the compiler reports a platform declaration clash between declarations you can plainly see are different, when adding a second converter over the same collection type breaks a file that compiled yesterday, or when a Java caller cannot find a function every Kotlin caller uses.

2k tokens
context cost
the whole folder, loaded on every use
1
files
instructions only
0
copies elsewhere
how many repositories repackaged it
100
stars on the repo
on the repository, not the skill itself

Install

one command, takes just this skill from the repository
npx skills add https://github.com/maxrave-dev/kotlin-footguns --skill jvmname-disambiguate-erased-overloads

The instruction itself

3 sections, as written by the author

Two extensions, one JVM method

Kotlin sees two functions here. The JVM sees one:

fun ArrayList<SongsResult>.toListTrack(): ArrayList<Track> { … }
fun ArrayList<VideosResult>.toListTrack(): ArrayList<Track> { … }

An extension function compiles to a static method whose first parameter is the receiver, and a

generic type's argument is erased on the way down. Both become toListTrack(ArrayList), and the

compiler stops with a platform declaration clash, naming both declarations and the one signature

they share.

One annotation on one of the two ends it:

import kotlin.jvm.JvmName

@JvmName("songsResultToListTrack")
fun ArrayList<SongsResult>.toListTrack(): ArrayList<Track> { … }

fun ArrayList<VideosResult>.toListTrack(): ArrayList<Track> { … }

Kotlin call sites are untouched — results.toListTrack() still resolves on the receiver's static

type, exactly as it did before the second overload existed.

Traps

Rename the JVM name, not the Kotlin one. The Kotlin name is the API, and overload resolution

already knows the element type; spelling it again as toListTrackFromSongs() /

toListTrackFromVideos() moves that knowledge into every call site. In the file inspected here,

functions named toTrack() sit on seven unrelated classes with no annotation at all — distinct

classes have distinct erasures and never collided. The annotation is only paying for the one place

erasure removed information.

Only an identical erasure clashes, so widening a receiver "fixes" it by changing the API.

List<A> and ArrayList<B> erase to different descriptors and coexist untouched. Declaring one of

the pair as List<…> therefore compiles — and quietly makes that function accept every list

implementation, which is a different promise than the one it was reviewed under. Change the JVM

name; leave the receiver alone.

The clash is scoped to the class the declarations land in, not to the module. Top-level

functions land in a file class named after their file, so two different files can each hold a

List<X>.toListTrack() with the same erased signature and never clash — and moving one of a

clashing pair into another file is a genuine fix. The corollary matters more: **a repository-wide

duplicate-name search is mostly false positives.** Group by file before believing it.

The JVM name is invisible to Kotlin, so nothing keeps it honest. In the pair inspected here one

annotation reads "VideoResulttoTrack" while the function it sits on is toListTrack. Kotlin never

mentions the string, so the mismatch has no symptom at all — until a Java call site, a reflection

lookup or a stack frame shows a name nobody chose. Write the name you would want to read there.

Non-Kotlin callers must spell the JVM name, and only that. From Java the annotated function is

ModelToEntityKt.songsResultToListTrack(list); the Kotlin name no longer exists on that class. If

the module is consumed from Java, annotation-shaped renames are a source-compatible change for

Kotlin and a breaking one for everyone else — so pick the JVM names once, when the clash first

appears, rather than tidying them later.

@JvmName is refused on open, override and abstract members. The annotation is only applicable

where the compiler knows no subclass can be affected by the renaming, so a clash between two members

of an open class, or between interface implementations, has no annotation-shaped exit — rename, or

change a parameter type, instead. On a property, the annotation goes on the accessor:

@get:JvmName(…) / @set:JvmName(…).

In a multiplatform module this is a JVM-only constraint written into shared code. The annotation

is imported from kotlin.jvm and compiles in the common source set, which is what makes the pattern

usable at all. But the constraint that forces it exists on one target, so the annotation reads as

unmotivated to anyone looking at the others. Leave a one-line reason next to it, or the next person

removes it and rediscovers the clash.

Verifying it

Find the real candidates — same function name, same erased receiver, same file:

grep -rlE "fun [A-Za-z_][A-Za-z0-9_]*<[^>]*>\??\.[a-zA-Z_]" --include="*.kt" . | while read -r f; do
  dup=$(grep -hoE "fun [A-Za-z_][A-Za-z0-9_]*<[^>]*>\??\.[a-zA-Z_][A-Za-z0-9_]*\(" "$f" \
    | sed -E 's/^fun ([A-Za-z_][A-Za-z0-9_]*)<[^>]*>\??\.([a-zA-Z_][A-Za-z0-9_]*)\($/\1.\2/' \
    | sort | uniq -d)
  [ -n "$dup" ] && printf '%s: %s\n' "$f" "$dup"
done || :

Each line is a file plus a Receiver.functionName pair declared in it more than once. Drop the

per-file loop and the same pipeline over the whole tree reports pairs that live in different files

and cannot clash — which is why the loop is there. The pattern needs a *generic* receiver, so it is

blind to the other clash class — two extensions on a plain receiver differing only in nullability;

those surface only in the annotation sweep below. When that pair *is* generic the detector does list

it, because the normalization drops the ?.

Then check that every annotation is carrying its weight, and that its string is the name you want

Java to see:

grep -rn -A1 "@JvmName" --include="*.kt" .

An annotation on a declaration the detector above does not list is defensive, left over from a clash

that has since moved, or guarding the one other clash class — two same-named extensions whose

receivers differ only in nullability, since T and T? erase to the same descriptor. All three are

worth a comment or a deletion. One of the unlisted annotations here names the opposite direction of

the function it sits on, and a second names a function that does exist in the same file but is not

the one it is on. Neither has a symptom until something outside Kotlin reads it.

How to use it

Copy the folder

Take maxrave-dev/jvmname-disambiguate-erased-overloads from the repository into ~/.claude/skills for personal use, or into .claude/skills inside a project.

Check the name does not clash

The agent identifies a skill by the name field in its header. Two skills with the same name cannot sit side by side — one of them will be ignored.