Repository navigation
fix(java): a call on a value a type holds in a static field reaches the method of the field's type - #3055
Merged
Conversation
…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>
colbymchenry
force-pushed
the
claude/java-static-field-receivers
branch
from
October 30, 2026 09:45
acc6318 to
e54bd7a
Compare
inth3shadows
added a commit
to inth3shadows/codegraph
that referenced
this pull request
Oct 30, 2026
Brings 9f379af..bc9291a (colbymchenry#3030, colbymchenry#3031, colbymchenry#3043, colbymchenry#3045, colbymchenry#3046, colbymchenry#3049, colbymchenry#3044, colbymchenry#3047, colbymchenry#3051, colbymchenry#3050, colbymchenry#3052, colbymchenry#3053, colbymchenry#3025, colbymchenry#3055). Clean auto-merge.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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'sColor.RED.hex()or aCREATOR'screateFromParcel(…), now links to the method of the field's type. An interface's constants are now indexed. A field of a library class, likeIntent.ACTION_VIEW.equals(…)orSystem.out.print(…), links nothing instead of a project method that shares the name.Root cause
matchMethodCallhad nothing for a receiver of the shapeType.FIELD: Java's receiver typing reads locals and the fields of the class around the call, soConsts.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.constant_declaration(an interface'sHelper HELPER = new Helper();):java.tslisted onlyfield_declarationas a field type, andjava.rsthe same, so an interface constant had no node and no type to read.What changed (our implementation)
java.ts,java.rs): aconstant_declarationis a field type; it is static and final (a constant), as Java makes every interface field.matchJavaStaticFieldCall, name-matcher, ahead of the receiver inference inmatchMethodCall): forType.FIELD.m()(alsoOuter.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 anew X(…)initializer builds; else the type the declaration writes (fieldSignatureType+resolveOnJavaType). A Kotlin object'sINSTANCEor a class'sCompanion, as Java writes them, reach the object's member. A library owner (javaTypeReffinds 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
newinitializer,Impl.INSTANCE.run(), aCREATORanonymous class) and the T5 fixtures (pr1949,pr1949b).Validation
Tests.
__tests__/java-static-field-receivers.test.ts: 9 tests (cases from mixxer's test, T5'spr1949/pr1949b, and aCREATORanonymous class, anInteger.TYPElibrary field andInterpolators.FAST_OUT_SLOW_INfrom the A/B); 8 fail on main6d3804cf(one already passes there: a field typed by its own declaration; the anonymous class's method passed on60b0d7a9by 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, on25144678), 0 files with diffs:Torture.java1/1 (interface constants added beside #1927's dotted anonymous class),TortureRecord.java1/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 against25144678and, before #1927 landed,60b0d7a9, but for two dispatch sites (Minestom'sNetworkBuffer.Type.read/write, kestra'sVNodeAssignmentListener) whose targets now also list #1927's anonymous overrides.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 (EntityTypeonArgumentType.EntityType(…),Particle,Integer) and 19NetworkBuffer.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 2new UUID(…)reach a static-imported constantNetworkBuffer.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 ownLowKeys.MODULE_IDENTIFIER.asString()).LowCardinalityKeyNames.AI_OPERATION_TYPE.asString()had been guessed onto another enum'sCHAT_CLIENT_TOOL_NAMES.asStringat 0.65), 66MistralAiApi.ChatModel.X.getValue()that went toDeepSeekApi.ChatModel's, and 823 same-target calls raised from 0.65 to 0.9. Minestom: 122 retargets (codec constants built bynew X()or an anonymous class,FluidImpl.REGISTRY.get→Registry.get) and 751 raised. jextract:Position.NO_POSITION.equals×6 → the anonymousPosition'sownequals(its 6 renames are that class now nested under the indexed constant). All right.HttpStatus.X.getCode()on Micronaut's enum, guessed onto a projectStatus.getCode;name()/equals()on project enums onto unrelated methods), spring-ai 21 (StringandEnummethods), 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: underCODEGRAPH_RESOLVE_PROFILE=2,matchJavaStaticFieldCallitself took 0.5 s summed over the workers for 39,380 calls (about 2 µs for a receiver that is noType.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, main7021de5c; kernel built on25144678, and no kernel file changed since),CODEGRAPH_KERNEL_EXPECT=1,CODEGRAPH_NO_DAEMONunset,--maxWorkers=4: 800 test files passed, 6 skipped; 9,552 tests passed, 0 failed, 86 skipped (732 s). The A/B ran on6d3804cf;7021de5cadds only Rust (#3025) and Ruby (#3053) resolution.Follow-ups (not in this PR)
EntityTypeto the static factoryArgumentType.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.NetworkBuffer.Type<X>, is resolved by its last name, as main does for class fields.Reimplements #1949 by @mixxer.
🤖 Generated with Claude Code