test(java): cover Record heritage review gaps

Document deferred enum and implicit-accessor behavior, make assertions order-independent, and add Record heritage to the capture benchmark.
This commit is contained in:
azizur100389 2026-08-09 19:35:09 +01:00
parent 9615f098c8
commit 8a7fc6d7c9
5 changed files with 67 additions and 44 deletions

View file

@ -132,8 +132,9 @@
"_rebaselined": "#1919 review CF3 fix: extended kotlin-local-property-owner (init/accessor destructuring) + new dart-accessor-owner fixture (getter/setter ownership). Fingerprint-only corpus drift; scaling ~1.0."
},
"java": {
"fingerprint": "b29e263524f55151dcb7cfc4c929d3d1d7bb360355cee4e832158f927857f663",
"fingerprint": "36d689c58526c4482fbd701d1d9ca156623a3970734ead145717858712271ab5",
"scaling_budget": 1.5,
"_rebaselined_2900_record_heritage": "#2900 review follow-up: the Java scale unit now includes a record implementing Marker, so the record-declaration @reference.inherits path is fingerprinted and exercised at scale. Prior b29e263524f55151dcb7cfc4c929d3d1d7bb360355cee4e832158f927857f663 -> 36d689c58526c4482fbd701d1d9ca156623a3970734ead145717858712271ab5; scaling 1.042 < 1.5.",
"_rebaselined_callable_flow_2522_review": "PR #2522 review hardening: callable operands retain expression/qualified identity and formals retain signature metadata; same-name lexical regions use an O(ancestor-depth) ID-set lookup. Prior d5c59d7dc9e206637515d5aea1163f7c1cdd76410c38c5fe6143d13d19677d6a -> 004a3592998dca1193bd1429a8284513725de7764f2a3eceedaaa984cfd763b4; scaling 0.992 < 1.5.",
"_rebaselined_callable_flow_2522_followup": "PR #2522 follow-up: Java method-reference/SAM callable flow facts with invocation-result suppression. Prior 062d754764aaa8a6772fb90875c710502a63e3e7a300e633942381ed914faada -> d5c59d7dc9e206637515d5aea1163f7c1cdd76410c38c5fe6143d13d19677d6a; scaling 1.074 < 1.5.",
"_rebaselined": "#2357 (supersedes #2353): + java-cast-receiver, java-this-field-chain, java-this-dispatch fixtures (cast-wrapped receivers, this.field chains incl. initializer contexts, bare-this dispatch pinning). Drift is purely fixture-additive: with the three new dirs parked, the fingerprint reproduces the prior baseline byte-identically — no emit/capture change. #1956 synth-widening: + java-iface-extends fixture; synthesizeJavaInheritanceReferences now ALSO walks interface_declaration extends_interfaces (interface IA extends IB, IC<T>), matching the #1940 legacy leg. (Earlier U2+review: java-qualified-base fixture covers 2- AND 3-segment qualified bases guarding the legacy end-anchor; synth tail-resolves scoped bases.) Linear (~1.03). (Earliest: java added to bench, exposed+fixed the O(n^2) findNodeAtRange root-walk; 3.09 -> ~0.99.) | #942: scope-resolution-only cleanup reworded fixture comments; capture byte-positions shift, capture LOGIC unchanged.",
@ -146,9 +147,9 @@
"_rebaselined_2562_local_classes": "#2562: Java block-local classes, enums, records, and interfaces use source-type-relative JLS 13.1 Host$NLocal identities with javac-compatible per-(host, simple-name) numbering; anonymous numbering remains separate. Lexical aliases begin at each declaration and end with its immediate block. Expanded java-local-class-naming fixtures cover declaration order, disjoint blocks, initializers, lambdas, local type kinds, and recursive local/member/anonymous host chains. Prior d04298a91beec76d0fa7099b3d71265723be60c1df688969aa954f135dd49686 -> 6dd5913a58400a191ff54abf9b852b03d5add657d16c11e60a7c4608ba186197; scaling 1.204 < 1.5.",
"_rebaselined_receiver_chain_2747": "#2747 receiver-chain rollout: call matches whose receiver is itself an expression now carry `@reference.receiver-chain`, a compact encoding of the receiver's structure, so resolution types it by folding instead of re-parsing receiver source text. Capture GROUP counts are unchanged — the tag is added to existing call matches, never a new match — so this is digest drift only. Prior 6dd5913a58400a191ff54abf9b852b03d5add657d16c11e60a7c4608ba186197 -> 310adbc2e0827b5ac749acaa981cd12d256fc5b7cbc5592c5bee219e92abf9ee.",
"_rebaselined_2766_receiver_chain_wire_v2": "#2766: receiver-chain wire format v1 -> v2 (name-free `await` / `index` step kinds). The VERSION prefix is part of every emitted `@reference.receiver-chain` capture, so every chain-minting language's capture text changed. WIRE-FORMAT CHANGE, NOT A CAPTURE-SET CHANGE: the same chains are minted for the same sites, spelled `2|…` instead of `1|…`. Exactly the 12 chain-minting languages drifted; c, cobol and dart did not, which is the check that this is the prefix and not a capture regression. Accompanied by SCHEMA_BUMP 34 -> 37 and INCREMENTAL_SCHEMA_VERSION 28 -> 31 so a stale index is rejected rather than replaying chains a v2 decoder refuses. Prior 310adbc2e0827b5ac749acaa981cd12d256fc5b7cbc5592c5bee219e92abf9ee -> a9943355e945e03ddb87c800f4cc1f62b3d04feefb3ec64c258d8e0bb3b3fcd9.",
"capture_groups_small": 5005,
"capture_groups_large": 16005,
"capture_groups_fp": 3452,
"capture_groups_small": 5755,
"capture_groups_large": 18405,
"capture_groups_fp": 3512,
"fixture_count": 206
},
"java-local-types": {

View file

@ -267,14 +267,15 @@ const LANGS = [
fixturePrefix: 'java',
exts: ['.java'],
file: 'bench.java',
// Java was previously unbenched. Heritage-bearing: extends Base + implements
// Marker (both forms) so the @reference.inherits synth (#1951) is driven at scale.
// Java was previously unbenched. Class and record heritage both implement
// Marker so the @reference.inherits synth (#1951, #2900) is driven at scale.
header: 'package generated;\n\nclass Base {}\n\ninterface Marker {}\n\n',
unit: (n) =>
`class Entity${n} extends Base implements Marker {\n` +
` long id = 0L;\n String name = "";\n` +
` public long getId() { return this.id; }\n` +
` public void setName(String v) { this.name = v; }\n}\n\n`,
` public void setName(String v) { this.name = v; }\n}\n\n` +
`record RecordEntity${n}(long id) implements Marker {}\n\n`,
},
{
name: 'java-local-types',

View file

@ -631,55 +631,42 @@ function findEnclosingTypeDeclaration(node: SyntaxNode): SyntaxNode | null {
}
/**
* Synthesize `@reference.inherits` captures from Java class heritage so the
* registry-primary scope-resolution path emits EXTENDS / IMPLEMENTS edges
* (mirrors C++ `emitCppInheritanceCaptures`). Without this, Java inheritance
* edges came only from the legacy heritage-capture leg (removed in #942), which
* is dropped for registry-primary languages in the worker pipeline (issue #1951).
* Synthesize `@reference.inherits` captures from Java type heritage for the
* authoritative registry-primary EXTENDS / IMPLEMENTS pre-pass (mirrors C++
* `emitCppInheritanceCaptures`).
*
* Scope covers `class_declaration` (`superclass` extends + `interfaces`
* implements clauses), `record_declaration` (`interfaces` implements clauses),
* AND `interface_declaration` (`extends_interfaces` → interface-to-interface
* EXTENDS), matching the legacy Java heritage query
* (tree-sitter-queries.ts), which has a dedicated `interface_declaration
* (extends_interfaces (type_list …))` arm. Without the interface arm the
* registry-primary synth silently dropped every `interface IA extends IB`
* edge while the legacy leg emitted it — the exact =0/=N parity break #1951
* targets. Record heritage is emitted even though the legacy query had no
* matching arm: Record graph nodes are canonical link targets since #2801, so
* dropping their `implements` clause prevents interface dispatch (#2900). Enum
* heritage remains unemitted. Generic
* bases (`extends Box<T>`, `implements IFoo<T>`) ARE emitted here: the legacy
* heritage query was widened to capture the inner `type_identifier` of a
* `generic_type` (tree-sitter-queries.ts), so both paths now agree on SIMPLE
* (unqualified) generic bases — the more-correct behavior, consistent with
* C#/Rust (#1951). Qualified bases (`a.b.Base`, `a.b.Box<T>`, `a.b.IFoo<T>`) are
* ALSO now at parity (#1956 tri-review U2): the synth resolves them by their
* `scoped_type_identifier` tail, and the legacy heritage query was widened
* with matching `scoped_type_identifier` arms (plain + generic-wrapped). The
* and `interface_declaration` (`extends_interfaces` clauses). Interface
* inheritance was restored for registry-primary resolution in #1951. Record
* graph nodes became canonical link targets in #2801 / PR #2871, so their
* `implements` clauses must participate for interface dispatch (#2900). Java
* enum interface heritage remains a separately tracked gap (#2918).
*
* Generic bases (`extends Box<T>`, `implements IFoo<T>`) and qualified bases
* (`a.b.Base`, `a.b.Box<T>`, `a.b.IFoo<T>`) are normalized to their simple
* lookup-name tails, consistent with C#/Rust and the V1 binding contract. The
* EXTENDS-vs-IMPLEMENTS split is decided downstream from the resolved target's
* symbol kind (`preEmitInheritanceEdges`): a superclass resolves to a class
* (EXTENDS), an implemented interface resolves to an interface (IMPLEMENTS).
* An `interface IA extends IB` base resolves to an Interface too, so it is
* emitted as IMPLEMENTS — matching the legacy `interface_declaration` arm,
* which tagged the bases as implements (`kind: 'implements'`) and likewise
* resolves them as interfaces. The synth therefore does not need to know the
* declaration's own kind; it only emits inherits sites and lets the resolved
* target decide the edge type.
* Base names are normalized to their bare simple identifier (`Box<T>` → `Box`,
* `java.io.Serializable` → `Serializable`) to match the V1 simple-name
* `findClassBindingInScope` contract.
* emitted as IMPLEMENTS. The synth only emits inheritance sites and lets the
* resolved target decide the edge type.
*/
function synthesizeJavaInheritanceReferences(root: SyntaxNode): CaptureMatch[] {
const out: CaptureMatch[] = [];
const stack: SyntaxNode[] = [root];
while (stack.length > 0) {
const node = stack.pop()!;
if (node.type === 'class_declaration' || node.type === 'record_declaration') {
if (node.type === 'class_declaration') {
const superclass = node.childForFieldName('superclass');
if (superclass !== null) {
for (const base of superclass.namedChildren) emitJavaInheritanceBase(out, base);
}
}
if (node.type === 'class_declaration' || node.type === 'record_declaration') {
// Records cannot declare a superclass; they share only the class
// `interfaces` arm.
const interfaces = node.childForFieldName('interfaces');
if (interfaces !== null) {
for (const typeList of interfaces.namedChildren) {

View file

@ -1274,7 +1274,7 @@ describe('Java record method resolution (#2564)', () => {
}
}, 60000);
it('links a Record implementor to its canonical Interface node (#2900)', async () => {
it('links and dispatches an explicit Record interface method (#2900)', async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gitnexus-java-record-heritage-'));
try {
writeFixtureRepo(root, {
@ -1300,13 +1300,45 @@ describe('Java record method resolution (#2564)', () => {
expect(implementsEdge?.sourceLabel).toBe('Record');
expect(implementsEdge?.targetLabel).toBe('Interface');
expect(fanout.map((edge) => `${edge.targetLabel}:${edge.targetFilePath}`)).toEqual([
expect(fanout.map((edge) => `${edge.targetLabel}:${edge.targetFilePath}`).sort()).toEqual([
'Method:RecordHeritage.java',
]);
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
}, 60000);
it('documents missing dispatch to an implicit Record component accessor (#2917)', async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gitnexus-java-record-accessor-'));
try {
writeFixtureRepo(root, {
'RecordAccessor.java': `interface Named { String name(); }
record User(String name) implements Named {}
class Reader {
String read(Named value) { return value.name(); }
}`,
});
const linked = await runPipelineFromRepo(root, () => {});
const implementsEdge = getRelationships(linked, 'IMPLEMENTS').find(
(edge) => edge.source === 'User' && edge.target === 'Named',
);
const fanout = getRelationships(linked, 'CALLS').filter(
(edge) =>
edge.source === 'read' &&
edge.target === 'name' &&
edge.rel.reason === 'interface-dispatch',
);
expect(implementsEdge?.sourceLabel).toBe('Record');
expect(implementsEdge?.targetLabel).toBe('Interface');
// TODO(#2917): implicit component accessors are not Method nodes yet.
// Replace this characterization with the expected User.name target.
expect(fanout).toEqual([]);
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
}, 60000);
});
// ---------------------------------------------------------------------------

View file

@ -33,7 +33,8 @@ function ctorRefs(src: string) {
function inheritanceRefs(src: string): string[] {
return emitJavaScopeCaptures(src, 'C.java')
.filter((m) => m['@reference.inherits'] !== undefined)
.flatMap((m) => m['@reference.name']?.text ?? []);
.flatMap((m) => m['@reference.name']?.text ?? [])
.sort();
}
describe('emitJavaScopeCaptures — constructor reference names (F35 #1928)', () => {
@ -95,10 +96,11 @@ describe('emitJavaScopeCaptures — record interface heritage (#2900)', () => {
'record User(int id) implements Named, Comparable<User>, audit.Auditable {}',
);
expect(refs).toEqual(['Named', 'Comparable', 'Auditable']);
expect(refs).toEqual(['Auditable', 'Comparable', 'Named']);
});
it('does not widen enum heritage while adding record support', () => {
it('does not yet emit enum heritage (#2918)', () => {
// Delete this characterization when #2918 adds enum interface heritage.
expect(inheritanceRefs('enum Status implements Named { ACTIVE }')).toEqual([]);
});
});