Skip to content

fix(java): resolve receivers on their declared type, leave library types unguessed - #1942

Open
danusha2345 wants to merge 1 commit into
colbymchenry:mainfrom
danusha2345:fix/java-receiver-type
Open

danusha2345 wants to merge 1 commit into
colbymchenry:mainfrom
danusha2345:fix/java-receiver-type

Conversation

@danusha2345

Copy link
Copy Markdown
Contributor

Problem

Java already types a call receiver from a local declaration or a plain field. Two gaps remain.

  • The declared type is a library class, but the call still gets a project edge. Here the call falls through to the name-only strategies and binds to whichever project method has the same name. Examples:
    • parcel.readInt() on an android.os.Parcel → a project VersionedParcel.readInt;
    • mHandler.post(...) or list.size() → any project post/size;
    • the static Log.e(...) → a project FalsingLog.e.
  • Several receiver shapes aren't typed at all, so they're guessed too:
    • this.f and Outer.this.f;
    • a field of an outer class read inside an inner or anonymous class;
    • a declaration with type arguments (Map<String, Foo> byName);
    • a for-each variable;
    • a field chain (this.mOwner.mRepo.save()).

On decompiled Android SystemUI (3,421 Java files), main has about 21.8k call edges whose receiver's declared type is a library class but that land on a project method. Another ~2.9k have a project-class receiver but go to an unrelated class. In playframework's Java sources (790 files) the numbers are ~1.7k and 125.

Fix

All changes are in src/resolution/name-matcher.ts. The extractor already emits the full receiver text, so the kernel and extraction version are unchanged.

  • A declared type is read for every receiver shape above. A chain is typed one hop at a time, and each hop's field type is read with its own file's imports.

  • A written qualifier is kept: Map.Entry and play.api.mvc.BodyParser<B> stay as written, and import p.Http.Cookie pins that exact nested class.

  • When the declared type is a library class, the call gets no edge. A type counts as a library class when:

    • the project declares no JVM type with that name;
    • or an import or written package names something the project doesn't declare (import java.util.Iterator next to a project UByteArray.Iterator);
    • or the only same-named project types are nested in another package and not imported.

    A short all-caps name (T, VH) is taken for a type parameter and left alone.

  • A static call on an imported or java.lang library class (Log.d, TextUtils.isEmpty) is left unresolved the same way.

  • When no import decides between same-named types, a Java call prefers the Java declaration over a Scala or Kotlin one (play.data.Form vs play.api.data.Form).

This is the Java counterpart of the Kotlin receiver typing in #1933. The two share an idea but not code yet: JAVA_EXTERNAL_TYPE here, KOTLIN_EXTERNAL_TYPE there. Whichever lands second will need a small textual merge in inferJavaFieldReceiverType, whose Java/Kotlin field block this PR narrows to Kotlin.

Measured

Each corpus was indexed with main (ba3c21e) and with this branch. Node counts are identical.

corpus edges main → PR lost gained
SystemUI (decompiled, 3,421 Java files) 352,202 → 330,758 22,793 1,349
playframework (Java part) 72,777 → 72,004 993 220
small Android app (55 Java files) 6,482 → 6,480 2 0

Every gained edge is a typed resolution at 0.9. I hand-checked samples against source; every sampled edge was correct:

corpus removed guesses retargeted to the declared type newly resolved
SystemUI 37 32 40
playframework 45 20 20

Indexing time on SystemUI isn't higher.

Known limits

  • Edges point at the declared type, not the runtime class. For example, PARSER.parseFrom → Parser rather than AbstractParser. This is the same choice as for Kotlin.
  • Java records aren't indexed as types, an existing extractor gap. A call on a record-typed receiver now gets no edge instead of a guess.
  • Wildcard imports aren't in the import mappings, so top-level types of other packages are assumed visible.

Tests

The new __tests__/java-receiver-type.test.ts has 12 cases: 8 fail on main, and 4 are regression guards. The Java-over-Scala guard also fails if the tie-break is removed. The full suite passes: 275 files, 4,730 tests.

🤖 Generated with Claude Code

@danusha2345

Copy link
Copy Markdown
Contributor Author

Rebased onto main after the resolution wave. 7 of this PR's 12 cases now pass on main (#2126, #2166, #2169, #2171, #2177), so their code and tests are dropped: the local-variable regex (main's version with type arguments and for-each is kept) and isImportedJavaLibraryClass / JAVA_LANG_CLASSES (Log.d(...) is covered). What remains — all failing on main:

  • an outer class's field read inside an anonymous class, and Outer.this.field;
  • a library type in a field chain (mOwner.ctx.getResources());
  • import java.util.Iterator beside a project q.Bytes.Iterator;
  • a Java type vs a same-named Scala type;
  • an imported nested class (import p.Http.Cookie).
    The Java field branch now sits after inferMemberReceiverType.

🤖 Generated with Claude Code

@danusha2345
danusha2345 force-pushed the fix/java-receiver-type branch 4 times, most recently from 86f3552 to 5a22e2a Compare October 7, 2026 11:53
…pes unguessed

Receivers the field lookup missed are typed: `Outer.this.f`, and a field
of an outer class read inside an inner or anonymous class (the lookup
only read the tightest enclosing class, so `mStore.save()` inside a
`new Runnable() {…}` went to a name-only guess). A chain of fields
(`owner.ctx.getResources()`) is read hop by hop, and a hop whose type is
a library class gets no edge instead of a same-named project method.

A type is read with the package or outer class written in source and
with the file's imports, so `import java.util.Iterator` is not taken for
a same-named type nested in another package of the project, and an
imported nested type (`import p.Http.Cookie`) is the one the import
names. Among same-named candidates no import pins, a Java call prefers
the Java declaration over a Scala or Kotlin one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@danusha2345
danusha2345 force-pushed the fix/java-receiver-type branch from 5a22e2a to 80b7d4e Compare October 7, 2026 16:12

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant