Skip to content

fix(java): a call on a value a type holds in a static field reaches the method of the field's type - #3055

Merged
colbymchenry merged 1 commit into
mainfrom
claude/java-static-field-receivers
Oct 30, 2026
Merged

colbymchenry merged 1 commit into
mainfrom
claude/java-static-field-receivers

Conversation

@colbymchenry

@colbymchenry colbymchenry commented Oct 30, 2026 •

Copy link
Copy Markdown
Owner

Summary

A Java call on a value a type holds in a static field, like Consts.HELPER.go(), Holder.SHARED.run(), Outer.Inner.NESTED.run(), an enum constant's Color.RED.hex() or a CREATOR's createFromParcel(…), now links to the method of the field's type. An interface's constants are now indexed. A field of a library class, like Intent.ACTION_VIEW.equals(…) or System.out.print(…), links nothing instead of a project method that shares the name.

Root cause

  • matchMethodCall had nothing for a receiver of the shape Type.FIELD: Java's receiver typing reads locals and the fields of the class around the call, so Consts.HELPER.go() fell through to the method-by-name strategies, which pick a same-named method by the receiver's words or the file's proximity.
  • Neither engine read Java's constant_declaration (an interface's Helper HELPER = new Helper();): java.ts listed only field_declaration as a field type, and java.rs the same, so an interface constant had no node and no type to read.

What changed (our implementation)

  • Extraction, both engines (java.ts, java.rs): a constant_declaration is a field type; it is static and final (a constant), as Java makes every interface field.
  • Resolution (matchJavaStaticFieldCall, name-matcher, ahead of the receiver inference in matchMethodCall): for Type.FIELD.m() (also Outer.Inner.FIELD.m() and a package written in front), the type is the one the calling file names (javaTypeRef, fix(java): resolve receivers on their declared type, leave library types unguessed #1942's helper); the field is the type's own or one it inherits through its declared supertypes (javaStaticField, five levels, an interface's constant included). The call then reaches the method of the value the field's declaration gives it, read in the field's own file: an enum constant's own override (PLUS { int apply(…) }), else the enum's method; the method of the anonymous class the field is initialized with; the class a new X(…) initializer builds; else the type the declaration writes (fieldSignatureType + resolveOnJavaType). A Kotlin object's INSTANCE or a class's Companion, as Java writes them, reach the object's member. A library owner (javaTypeRef finds no project type) or a field of a library type returns null: no edge. A receiver whose head is a local or a field of that name, or whose last segment is a nested type (Outer.Inner.m(), a static call), is left to the strategies after it, as before.

Reimplements #1949 by @mixxer, independently: the PR's diff and tests were read, not run or merged. We reused its cases (an interface constant's new initializer, Impl.INSTANCE.run(), a CREATOR anonymous class) and the T5 fixtures (pr1949, pr1949b).

Validation

Tests. __tests__/java-static-field-receivers.test.ts: 9 tests (cases from mixxer's test, T5's pr1949/pr1949b, and a CREATOR anonymous class, an Integer.TYPE library field and Interpolators.FAST_OUT_SLOW_IN from the A/B); 8 fail on main 6d3804cf (one already passes there: a field typed by its own declaration; the anonymous class's method passed on 60b0d7a9 by a guess that #1927's scope rule has since ruled out), all 9 pass here on the wasm path and on the rebuilt kernel.

Kernel parity (scripts/kernel-parity.mjs, the rebuilt kernel against the wasm path, on 25144678), 0 files with diffs: Torture.java 1/1 (interface constants added beside #1927's dotted anonymous class), TortureRecord.java 1/1, T5 fixtures 9/9, jextract 218/218, spring-modulith 682/682, SystemUI 1141/1141, okhttp 71/71, Minestom 1859/1894 (the deferred files are main's, a parse error in the file).

A/B against main 6d3804cf (with #1927's #3051 and #3052's family ranking), main on its kernel and this commit on its own, every changed site sorted by the shape of its reference and its target (tools/triage-1949.cjs). The change sets are the same site for site as against 25144678 and, before #1927 landed, 60b0d7a9, but for two dispatch sites (Minestom's NetworkBuffer.Type.read/write, kestra's VNodeAssignmentListener) whose targets now also list #1927's anonymous overrides.

Repo Sites added / removed / retargeted Confidence only New nodes
Minestom +26,003 / −8 / 122 (4 renamed) 751 12,584 interface constants
spring-ai +14 / −21 / 786 823 15
kestra (Java) +27 / −273 / 14 (2 renamed) 37 34
playframework +5 / −1 / 0 21 157
SystemUI 0 / −11 / 0 2 183
spring-modulith +5 / −3 / 4 26 7
jextract +1 / −9 / 6 (6 renamed) 1 1
okhttp, click, gin, serilog identical
  • Added. Minestom's 26,003 are almost all the 12,584 interface constants now indexed (its generated registries, BlockKeys, SoundEventKeys, …) with their 25,524 type references, which are right except 751 that land at 0.4 on a static factory of the same name (EntityType on ArgumentType.EntityType(…), Particle, Integer) and 19 NetworkBuffer.Type<…> read by their last name: main resolves those names the same way on class constants (see follow-ups). 409 Minestom calls through a field are now typed (Model.CODEC.optional() → Codec.optional, BlockVec.ONE.blockX()), and 2 new UUID(…) reach a static-imported constant NetworkBuffer.UUID (wrong). Elsewhere: spring-ai 14 (RetryUtils.DEFAULT_RESPONSE_ERROR_HANDLER.handleError → the anonymous handler's own, ThinkLevel.HIGH.level()), kestra 27 (ProblemType.X.title(), interface constants' types), playframework 5 (WebSocket.Text.acceptOrResult), spring-modulith 5 (an enum constant's own LowKeys.MODULE_IDENTIFIER.asString()).
  • Retargeted and confidence. spring-ai: 719 enum constants' own overrides (LowCardinalityKeyNames.AI_OPERATION_TYPE.asString() had been guessed onto another enum's CHAT_CLIENT_TOOL_NAMES.asString at 0.65), 66 MistralAiApi.ChatModel.X.getValue() that went to DeepSeekApi.ChatModel's, and 823 same-target calls raised from 0.65 to 0.9. Minestom: 122 retargets (codec constants built by new X() or an anonymous class, FluidImpl.REGISTRY.get → Registry.get) and 751 raised. jextract: Position.NO_POSITION.equals ×6 → the anonymous Position's own equals (its 6 renames are that class now nested under the indexed constant). All right.
  • Removed. Guesses on a library type's field or a library method, all wrong: kestra 273 (180 HttpStatus.X.getCode() on Micronaut's enum, guessed onto a project Status.getCode; name()/equals() on project enums onto unrelated methods), spring-ai 21 (String and Enum methods), SystemUI 11, jextract 9 (System.out.print → PrettyPrinter.print, generated test bindings), Minestom 8, spring-modulith 3; playframework's 1 is a doc stub with lowercase class names (views.html.Application.list.render()), the one right edge lost.

Cost (against main 60b0d7a9, median of 3 interleaved index runs, tools/cputime.ps1): SystemUI CPU 92.0 → 93.7 s, wall 59.4 → 58.6 s; Minestom CPU 46.7 → 56.4 s, wall 17.5 → 19.4 s. Minestom's graph grows by 29% in nodes and 30% in edges (the generated registries' 12,584 interface constants and their 25,524 type references), which is where its time goes: under CODEGRAPH_RESOLVE_PROFILE=2, matchJavaStaticFieldCall itself took 0.5 s summed over the workers for 39,380 calls (about 2 µs for a receiver that is no Type.FIELD, 260 µs for one it types), and the method-call stage overall went from 7.8 s to 6.7 s. SystemUI, with 183 new constants, is within noise.

Suite: the full suite on this commit (e54bd7a8, main 7021de5c; kernel built on 25144678, and no kernel file changed since), CODEGRAPH_KERNEL_EXPECT=1, CODEGRAPH_NO_DAEMON unset, --maxWorkers=4: 800 test files passed, 6 skipped; 9,552 tests passed, 0 failed, 86 skipped (732 s). The A/B ran on 6d3804cf; 7021de5c adds only Rust (#3025) and Ruby (#3053) resolution.

Follow-ups (not in this PR)

  • A bare Java type name can resolve to a method of the same name, like Minestom's EntityType to the static factory ArgumentType.EntityType(…), or a type written in a generic argument to its own constructor: main already does this on class fields and constants (1,575 such references in Minestom, 352 in SystemUI), and the interface constants this PR indexes inherit it. A type reference never names a method in Java; that rule deserves its own A/B.
  • A type written through its outer type in a declaration, NetworkBuffer.Type<X>, is resolved by its last name, as main does for class fields.

Reimplements #1949 by @mixxer.

🤖 Generated with Claude Code

…he method of the field's type

A Java call written through a type's static field, Consts.HELPER.go()
or Holder.SHARED.run(), was resolved by the method's name alone: the
receiver Type.FIELD said nothing of its type, so the call went to a
same-named method of whatever class the name matcher found, and a field
of a library class (Intent.ACTION_VIEW.equals(...), System.out.print())
could reach a project method of that name. An interface's constants were
not indexed at all (neither engine read a constant_declaration), so a
constant a type declares in an interface, or inherits from one, had no
declaration to read.

A Java Type.FIELD.m() (Outer.Inner.FIELD.m(), a package written in
front) now finds the type as the calling file names it (javaTypeRef),
then the field, the type's own or one it inherits (five levels up), and
calls the method on the value the field's declaration gives it, read
with the field's own file's imports: an enum constant's own override,
else the enum's method; the method of the anonymous class the field is
initialized with (a CREATOR's createFromParcel); the class a new X(...)
initializer builds; else the type the declaration writes. A Kotlin
object's INSTANCE or Companion reaches the object's member. A library
owner, or a field of a library type, links nothing. A receiver whose
head is a local or a field links as before. Both engines index an
interface's constant_declaration as a static final constant.

Validation:
- java-static-field-receivers.test.ts: 9 tests, 7 fail on main 60b0d7a
  (two already pass there: a field typed by its own declaration and an
  anonymous class's method), all pass here.
- Kernel parity (java), 0 files with diffs: Torture.java 1/1,
  TortureRecord.java 1/1, jextract 218/218, spring-modulith 682/682,
  SystemUI 1141/1141, okhttp 71/71, Minestom 1859/1894 (deferrals are
  main's).
- A/B against main 60b0d7a, sites added/removed/retargeted (conf-only):
  Minestom +26003/-8/122 (751; 12,584 interface constants now indexed,
  409 calls typed), spring-ai +14/-21/786 (823; 719 enum constants' own
  overrides), kestra +27/-273/14 (37), playframework +5/-1/0 (21),
  SystemUI 0/-11/0 (2), spring-modulith +5/-3/4 (26), jextract
  +1/-9/6 (1); okhttp, nowinandroid, mihon, click, gin, serilog
  identical. The removed sites are guesses on library fields and
  methods (kestra's HttpStatus.X.getCode() x180).
- Cost (median of 3 interleaved): SystemUI CPU 92.0 -> 93.7 s, wall
  59.4 -> 58.6 s; Minestom CPU 46.7 -> 56.4 s, wall 17.5 -> 19.4 s, as
  its graph grows by the generated registries' 12,584 interface
  constants (29% more nodes, 30% more edges).

Reimplements #1949 by @mixxer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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