Skip to content

fix(java): a record is indexed as a class, with its components' fields and accessors - #2868

Merged
colbymchenry merged 1 commit into
mainfrom
claude/pr-1946-java-records
Oct 23, 2026
Merged

colbymchenry merged 1 commit into
mainfrom
claude/pr-1946-java-records

Conversation

@colbymchenry

@colbymchenry colbymchenry commented Oct 23, 2026 •

Copy link
Copy Markdown
Owner

Summary

Java records are now indexed as classes, in both the WebAssembly extractor and the native kernel. A record gets its own node, holds its methods, constructors (the compact form included), static fields and nested types, implements its interfaces, and gets a private field plus a public accessor name() for each component unless the body declares one. Calls on record-typed values, accessor calls and Record::accessor method references now resolve to the record. An implicit accessor is reached only through what types its receiver, so the name-only fallbacks don't spread guesses onto every record with a content or id component.

Root cause

Neither engine listed record_declaration as a class type. A record left no node: its methods and explicit constructor became package-level functions (p::norm, p::Point), a nested record's methods were filed under the class around it, and compact constructors, static fields and records nested in a record were dropped. Calls on record-typed values resolved to nothing, or by name to another type's member: on Minestom, a record's read/write filed under the interface around it became a dispatch root fanning out to 22 unrelated anonymous implementations, and new Key(...) in three files went to a fourth file's record constructor.

From the contributor, and what we changed

From @danusha2345 (#1946), unchanged: the record_declaration class type and compact constructor in languages/java.ts and codegraph-kernel/src/java.rs, the component field/accessor synthesis in both engines, the TortureRecord.java parity fixture (torture and CRLF runs) and __tests__/java-records.test.ts.

Our changes (33 source lines, a third of them comments):

  • Implicit accessors only through typed resolution. A record's implicit accessor (marked by the docstring both engines give it, now IMPLICIT_RECORD_ACCESSOR_DOC in src/types.ts) is reached through a typed receiver, a method reference, a qualified call, or a call from inside the record. The receiver-word strategies in matchMethodCall mark their picks byName (not persisted), and ReferenceResolver drops a byName, exact-name or fuzzy result that lands on an accessor from outside its record, on the final result, so nothing falls through to another guess. Typed chains (0.85 through a call's declared return type) are untouched.
  • Resolver fix the change exposed. Records usually sit in sealed hierarchies. The member-type reader took a head like sealed interface Blocks permits Block for a member named Block of type permits, typed the receiver of Block.fromKey(...) inside the record as a built-in and dropped the call. permits and throws are now among the words that are not a type there (name-matcher.ts, one line). Main already lost such calls for plain classes named first in a permits list.
  • Tests: two java-records.test.ts cases, on both engines: the sealed-permits shape, and an untyped point -> point.x() that gets no accessor edge while Point p; p.x(), Point::y and a bare x() inside the record keep theirs.
  • CHANGELOG: the bullet rewritten in the house format and moved next to the other Java bullets.
  • Rebased onto main a565f215 (which includes fix(java,ts): a parameterized supertype links its type, like implements Store<String> #2856's supertype names and fix(java,csharp): a call on a new object reaches the class the file imports, not a namesake #2864); no conflicts in the record code. The A/B ran on d02a88d0; main has since changed a test file, path validation and the viewer's trail store, none of it extraction or resolution.

Validation

Tests. java-records.test.ts: 5 cases × 2 engines (10 tests), all red on main, all green here; the sealed-permits and name-only-guess cases are each red with their resolver line reverted. kernel-tsjs-parity.test.ts green; its record fixture is red with main's kernel.

Kernel parity on real repos (scripts/kernel-parity.mjs --lang java): jextract 218/218, spring-modulith 682/682, playframework 801/801, kestra 2,997/2,997, spring-ai 2,491/2,492 and Minestom 1,859/1,894, 0 files with diffs and deferral counts identical to main's. Main itself has 4 Minestom files and 1 spring-ai file with kernel/wasm diffs that this change removes.

A/B against main d02a88d0, fresh indexes, edges compared by site (file, kind, position, ref name) and target identity (name, file, line):

repo Java files edge sites added removed retargeted (constructor → its record) renamed
openjdk/jextract a2bc4e4 218 207 1 7 7
spring-projects/spring-modulith e744475 682 375 1 33 (17) 10
playframework f3eb322 (+856 Scala) 801 80 2 183 (11) 13
Minestom 704be78 1,894 17,963 287 4,803 (4,052) 971
spring-ai b90fb6f 2,492 8,806 471 2,122 (1,677) 3,540
kestra bad4d5b (without ui/) 2,997 10,077 497 2,618 (2,545) 3,356
pallets/click, gin-gonic/gin (controls) identical

Triage, by sampling every bucket:

  • Typed calls and method references (0.9): 25/25 sampled correct on Minestom, all sampled correct on jextract and spring-modulith (indexList.decl(), spec.name(), LoginSuccessPacket::sessionId). Static calls on records (BuiltinSoundEvent.get(...), 5,489 on Minestom) all name their owner.
  • Retargets: mostly a call or new moving from the orphaned constructor function to the record. Cross-file retargets are corrections: on spring-ai 17 of 24 sampled moved wrong→right (new AssistantMessage.ToolCall(...) off DeepSeek's record), 7 wrong→wrong, 0 right→wrong.
  • Removals are mostly wrong edges going away: Minestom's 984 bogus dispatch edges (from 64 record methods), kestra's Mockito when and Hamcrest is landing on orphaned record methods, testDir.length() on a String component. The rest are name-only guesses main had made (0.65–0.7) whose site the record turned into an accessor guess, which the accessor rule now leaves unlinked.
  • What the accessor rule moved (record arm before → after the rule): spring-ai −1,063 sites, kestra −765, Minestom −408, playframework −33 (26 of them Pekko Flow.via(...) guessed at 0.3 onto ForwardingInfo::via), spring-modulith −6, jextract −4; all instance-method 0.65–0.8, exact-name or fuzzy results, nothing added, no 0.85/0.9 edge moved. Sampled: on spring-ai 22 of the 27 removed guesses were wrong (result.content() on MCP's CallToolResult, request.topP() from Mistral onto DeepSeek's record); on Minestom about 16 of 19 were right (result.collisionX(), section.blockLight()), the recall cost of not guessing.
  • Play: with main's first-in-file Play resolver, 17 Scala Scheme.parse/AuthorityPort.parse sites move from the Scala object to the Java record's parse, and 10 Java references written play.api.mvc.request.X now land on the Java record's constructor (the extractor keeps no qualifier). fix(play): look up Class.method among the class's own members, not the whole file #1950, with a same-language preference, removes the first.

Cost (process CPU, median of 3 interleaved fresh indexes; measured before the accessor rule, which runs only on a name-only result that lands on an accessor):

repo CPU main → this wall main → this
Minestom 47.2 s → 50.3 s (+6.6%, for 17% more nodes and 23% more edges) 19.6 s → 19.9 s
spring-ai 93.5 s → 94.8 s (+1.4%) 29.8 s → 28.0 s

Suite: CODEGRAPH_KERNEL_EXPECT=1 npx vitest run --maxWorkers=4 --minWorkers=1 on 7608a5b: 757 files passed, 6 skipped (763); 9,015 tests passed, 0 failed, 83 skipped.

Follow-ups (not in this PR)

  • A bare call inside a nested class resolves to another file's same-named nested class's method (verify(events) under import static org.mockito.Mockito.* → a test record $::verify); reproduces on main with plain classes.
  • A type reference inside a class with explicit constructors lands on the constructor, not the class (main too).
  • Record components emit no references to their types, as Lombok's synthesized members don't.

Lands #1946 by @danusha2345.

🤖 Generated with Claude Code

…s and accessors

Neither engine treated record_declaration as a class type, so a Java record
left no node of its own. Its methods and explicit constructor became
package-level functions (p::norm, p::Point), a nested record's methods were
filed under the class around it, compact constructors, static fields and
records nested in a record were dropped, and calls on a record-typed value
resolved to nothing or to a same-named member of another type.

The Java extractor and the kernel now treat record_declaration as a class
and a compact constructor as a method. Each component becomes a private
field ("<Type> <name>") and a public accessor name() with the component's
type as its return type, unless the body declares it; they are synthesized
before Lombok's members, the same way.

An implicit accessor is reached only through what types its receiver: a
typed receiver, a method reference, a qualified call, or a call from inside
the record. The name-only strategies (the receiver-word guesses, exact-name
and fuzzy matching) no longer settle on one, so result.content() on a
library type stops linking to whichever record has a content component.

Records usually sit in sealed hierarchies, and that exposed a resolver gap:
the member-type reader took the head "sealed interface Blocks permits Block"
for a member named Block of type "permits", typed the receiver of
Block.fromKey(...) inside the record as a built-in and dropped the call.
"permits" and "throws" are now among the words that are not a type there.

Validation:
- java-records.test.ts, 5 cases on both engines (10 tests), all red on
  main; the sealed-permits and name-only-guess cases are each red without
  their resolver line.
- Kernel parity: TortureRecord.java in the torture and CRLF runs; real-repo
  sweeps at full parity with deferrals unchanged (jextract 218/218,
  spring-modulith 682/682, playframework 801/801, kestra 2997/2997,
  spring-ai 2491/2492, Minestom 1859/1894).
- A/B against main d02a88d, every bucket triaged by sampling (edge sites
  added/removed): jextract +207/-1, spring-modulith +375/-1, playframework
  +80/-2, Minestom +17,963/-287, spring-ai +8,806/-471, kestra
  +10,077/-497; most retargets are a constructor function becoming its
  record. The accessor rule drops only name-only results (spring-ai 1,063,
  kestra 765, Minestom 408 sites at 0.65-0.8 confidence); no typed call,
  method reference or qualified call moves. click and gin byte-identical.
- Cost (process CPU, median of 3 interleaved indexes): Minestom
  47.2s -> 50.3s (+6.6%, for 17% more nodes and 23% more edges),
  spring-ai 93.5s -> 94.8s (+1.4%).

Original PR #1946 by @danusha2345.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@colbymchenry
colbymchenry force-pushed the claude/pr-1946-java-records branch from ac161e2 to 7608a5b Compare October 23, 2026 22:13
@colbymchenry
colbymchenry merged commit 3a93f57 into main Oct 23, 2026
@colbymchenry
colbymchenry deleted the claude/pr-1946-java-records branch October 23, 2026 22:14
danusha2345 pushed a commit to danusha2345/codegraph that referenced this pull request Oct 24, 2026
…mchenry#2876; 11 landed PRs yield to main; EXTRACTION_VERSION 55)

Main landed eleven of our PRs as our own commits with the maintainer's additions
(colbymchenry#2847, colbymchenry#2848, colbymchenry#2850, colbymchenry#2851, colbymchenry#2853, colbymchenry#2854, colbymchenry#2855, colbymchenry#2856, colbymchenry#2868, colbymchenry#2874, colbymchenry#2875), the
traversal half of colbymchenry#2839 (colbymchenry#2860) and the trail hardening of colbymchenry#1667 (colbymchenry#2869). Where the
integration carried our original version, main's wins: play.ts, routes.ts, the
tsjs kernel extractor, import-resolver's PHP alias lookup, isDecisiveSameFileMatch,
matchNewReceiverCall and the tests of those PRs. Kept beside main: colbymchenry#1952's
enclosing-scope member pick, colbymchenry#1936's closest-copy confidence, colbymchenry#2258's Kotlin
imported-type block, colbymchenry#1875's commitHdlProfile on indexFiles and hdlAccess, colbymchenry#1583's
impact budget (the type-hierarchy walk itself is main's), colbymchenry#1736's C #if tests.
22 changelog bullets of closed PRs removed where main has its own wording.

EXTRACTION_VERSION 55: Rust receiver chains (colbymchenry#2846), `new C().m` shapes (colbymchenry#2854),
Java records (colbymchenry#2868), Go method exportedness (colbymchenry#2857), generic supertypes (colbymchenry#2856).

Co-Authored-By: Claude Fable 5.1 <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