Skip to content

fix(java): resolve record accessor calls, and never bind a call to data - #2295

Open
MopicMP wants to merge 2 commits into
DeusData:mainfrom
MopicMP:fix/java-record-accessor-calls
Open

MopicMP wants to merge 2 commits into
DeusData:mainfrom
MopicMP:fix/java-record-accessor-calls

Conversation

@MopicMP

@MopicMP MopicMP commented Sep 23, 2026

Copy link
Copy Markdown

Written by an AI agent (Claude Code) working for @MopicMP, who signed off the
commit and answers for it.

A Java record accessor call binds to an unrelated field

p.x() on a record ends up as a CALLS edge into some other class's private
field named x.

A record's accessors are implicit, so record Point(double x, double y, double z) declares no x() anywhere in the source, and extraction emits no node for
one. The call has nothing to resolve to, falls through to cbm_registry_resolve,
and the short-name registry hands back the first x in the project — typically
a private field of a class that has nothing to do with the receiver.

Two answers invert:

  • the accessor reports no callers, because it has no node at all. I nearly
    deleted a live method on that answer;
  • the unrelated field reports every record read in the project as its caller.

How much of the graph this is

Two indexed Java trees, v0.10.8 release binary, mode=fast, Windows 11 x86_64:

project CALLS edges target is a Variable or Field
631-file Minecraft mod 2 444 278 (11.4%)
3 000-file Minecraft mod 29 509 2 649 (9.0%)

Counted with MATCH (a)-[:CALLS]->(b) WITH b.label AS kind, count(*) AS n RETURN kind, n. moderate and full resolve the same way, and so does the
v0.11.0 release binary.

The change, in two parts

Extraction emits the accessors (internal/cbm/extract_defs.c). One Method
definition per record component, skipped when the record declares that accessor
itself (JLS §8.10.3 allows the override). The record's own API becomes visible:
the accessor gets a node, and p.x() resolves to it.

A Java call may not bind to data (src/pipeline/registry.c, both call
sites). A Java call expression never names data — p.x() is a method
invocation whatever x spells elsewhere in the tree — so a CALLS bind whose
target is a Variable or a Field is a spelling collision and is dropped.
This follows cbm_go_suppress_bare_field_ref: a pure, label-keyed,
language-gated predicate, called from pass_calls.c and pass_parallel.c so
the sequential and parallel resolvers stay identical.

Java-gated deliberately: the veto is sound only where no callable name can be
anything but a method. Kotlin properties hold function values (val f: () -> Unit; f()) and C has function pointers, so both legitimately call through data.

Both halves are needed. The first makes the right target exist; the second
stops the wrong one being written where the right one is not reachable.

Measured

On the 631-file tree, indexed with this build: 2 103 CALLS edges, every one
into a Method (1 841) or a Class (262). No Variable, no Field — down
from 278.

A separate gap this uncovered, not fixed here

Java cross-file type resolution appears to stop working when the project has
more than one source root. Same four files, two layouts:

  • one root (src/com/x/...): where.x() and where.z() bind to Point.x /
    Point.z across files and across packages, with an unrelated Spring.x
    field present;
  • two roots (src/main/java/... plus src/client/java/..., the Gradle/Fabric
    shape): the same calls do not bind at all. A hand-written public double x()
    in the record does not bind either, so it is not about the implicit accessor
    — everything that binds in that layout binds by short name.

I have a four-file reproduction and can open a separate issue for it; it looked
like a different defect from this one, so I kept it out of this PR.

Tests

  • tests/test_registry.c — java_call_never_binds_data_member: the predicate,
    including the keeps (Method/Function/Class, other languages, NULL).
  • tests/test_call_reference_contract.c —
    call_java_record_accessor_binds_the_record: end to end through the indexing
    path. The accessor call binds to the record; no CALLS into the unrelated
    field; a builder method x(int) declared beside its own x field keeps its
    edge; the genuine static call in the same method still lands.

Built and run with gcc 15.2.0 on Debian (WSL2): scripts/test.sh, plus
clang-format clean on every changed file.

A record's accessors are implicit, so `record Point(double x, double y,
double z)` declares no `x()` and extraction emitted no node for one. The call
`p.x()` had nothing to resolve to, fell through to the short-name registry,
and bound to the first `x` in the project — typically an unrelated class's
private field. Measured on two indexed Java trees, that was 278 of 2444 and
2649 of 29509 CALLS edges.

Extraction now emits one Method definition per record component, skipping any
accessor the record declares itself (JLS 8.10.3), so the accessor has a node
and the call resolves to it.

And a Java call may no longer bind to a Variable or a Field. A Java call
expression never names data, so such an edge is a spelling collision, not a
call. The guard follows cbm_go_suppress_bare_field_ref — a pure, label-keyed
predicate — and is called from both pass_calls.c and pass_parallel.c so the
sequential and parallel resolvers stay identical. Java-gated: Kotlin
properties hold function values and C has function pointers, so both
legitimately call through data.

Signed-off-by: MopicMP <obshiq123@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@MopicMP
MopicMP requested a review from DeusData as a code owner September 23, 2026 20:45
@github-actions

Copy link
Copy Markdown

Thanks for opening this — it has been seen, and it is queued.

This note is automated, but it is not a brush-off: it exists so you know where your PR stands instead of having to guess from silence.

Current review status: working through a backlog. 0.9.1-rc.1 is out, so the release freeze that held reviews is over — but it left a large queue of open pull requests behind it, and we are reading through them oldest-first. The background is in discussion #1144.

What that means for this PR, concretely:

  • It will not be closed for inactivity. No stale bot touches pull requests here.
  • It may still sit a while before a human reads it. That is on us, not on you.
  • Older PRs are read first, so a recent one is not being skipped — it is behind a queue.

Things that will genuinely speed it up whenever review does happen:

  • Keep it rebased on main — the tree is moving quickly right now, and a conflicting branch cannot be reviewed as the diff you intended.
  • Get CI green, or say which failures you believe are pre-existing.
  • Keep the change to one claim. Bundled features and refactors get split before they get merged, which costs you a round trip.
  • Every commit needs a sign-off (git commit -s) — CI enforces DCO.

If this fixes a bug, a reproduction we can run is worth more than a description of the symptom.

Thanks for contributing, and sorry in advance for the wait.

@MopicMP

MopicMP commented Sep 24, 2026

Copy link
Copy Markdown
Author

Written by an AI agent (Claude Code) working for @MopicMP.

The only red here is test / test-windows-guards, and it is the failure mode tracked in #2057: tests/windows/test_daemon_stability.py, section_cold_storm — "cold-storm client 0 failed (racing daemon spawn): CBM daemon is active or starting but could not accept this client within 30000 ms". ci-ok is red only because it aggregates that leg.

Of the last 61 PR-workflow runs that ran this guard, it was green 56 times and red 5. All five reds are that same section, and four of them are on branches that cannot reach it:

branch job
codex/fix-1593-trace-filter 107192083571
fix/test-daemon-bootstrap-sync 106553770612
fix/1696-bench-runtime-isolation 106294337609
fix/hook-augment-worktree-test 106468158849
fix/java-record-accessor-calls (this PR) 107379415128

Everything else is green: all nine test-unix shards, both Windows shards, TSan, MSan, LSan, lint, lint-mem, CodeQL, the security and license gates, and DCO. This diff touches definition extraction, one pure label predicate, and CALLS edge emission — it has no path to daemon startup or client rendezvous.

Could a maintainer re-run that job? I have no permission to re-run it from here. Happy to rebase instead if you would rather see a fresh run of the whole leg.

@DeusData

Copy link
Copy Markdown
Owner

Thanks for the careful flake analysis, @MopicMP, and sorry for the wait. You're right that test-windows-guards (section_cold_storm) is the known #2057 race and not your change. A plain re-run would re-test the old merge commit from 23 September, so we've updated the branch from our side instead, and CI will run fresh against current main. No rebase is needed from you. We've already built and tested the merge result locally: it merges cleanly, and extraction, pipeline, registry, the Java LSP suites and call_reference_contract all pass (1209 tests), including your java_call_never_binds_data_member and call_java_record_accessor_binds_the_record. It merges once CI is green. Thanks for the record-accessor fix and for keeping calls from binding to data members.

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.

3 participants