mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-08-28 05:25:25 +00:00
* fix(java): materialize record component accessors * fix(java): ignore receiver params in record accessor arity * fix(java): address record-accessor review findings (#2917) Five findings from the tri-review of #2936. P1 — a synthesized callable evicted a source-written one from the method map. `getMethodInfo` keyed its per-class map by `name:line`, but a callable that is SYNTHESIZED at a position that is not its own declaration shares its owner's line: a record's implicit accessor is minted at the component, and a C# 12 primary constructor at the owner's `parameter_list`. Both are appended last by their extractor, so on a single line the synthesized entry overwrote the explicit method's MethodInfo and both definitions collapsed onto one id — `record P(int x, int y) { int x(int s) {...} }` lost `P.x#1` and rebound the arity-1 call to the zero-argument accessor. Adds a required `MethodInfo.column` and keys the map by `name:line:column` through a single `methodInfoKey` helper. Required, not optional: an absent column would key an entry no lookup could reach — a silent, whole-language loss of enrichment instead of a compile error. All three lookup sites move together; the file's own lockstep docblock warns that a half-applied change loses caller edges silently rather than dangling. This also fixes the same collision in C#, which never touched record code. Degenerate component names no longer mint a node. tree-sitter's zero-width MISSING recovery token satisfies `name: (identifier)`, so `record M(int x, y) {}` minted an empty-named Method whose returnType was the neighbouring `y`; and the grammar admits `underscore_pattern` in the same field, which the query rejected but the scope path accepted, so `record R(int _) {}` left a scope declaration with no node behind it. One `isRecordComponentName` predicate now gates all three emitters — query suppression, scope synthesis, and the method extractor — so they cannot drift apart again. Component annotations reach the implicit accessor (JLS 8.10.3 / 9.7.4) by reusing the shared `extractAnnotations` helper. Deliberately over-approximate and commented as such: `@Target` lives in another file and parsing is per-file. `explicitZeroArgAccessorNames` is memoised per record node. It was rebuilt on every component capture — O(components x body members) for one record, measured at ~4x per 2x input — while the scope path already hoisted the identical call. Docs: the `java-local-types` baseline now stores the `capture_groups_fp` its own note cites, the SCHEMA_BUMP ledger no longer claims a v65 that nothing holds, and `shouldSkipDefinitionCapture` documents that `defaultLabel` may be ignored. Scope-capture fingerprints are unchanged (`measure.mjs --check` PASS, 15 languages): the bench corpus contains no degenerate components, so the new predicate is inert on it. SCHEMA_BUMP stays 67 — this branch's existing claim already covers the changed worker output; re-check it against origin/main before merging. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Px638Zyqa9CJMUU7DsJoB * docs(ingestion): reunite the overload-suffix JSDoc with typeTagForId The block describing the `~type1,type2` same-arity discriminator was stranded above `buildCollisionGroups` when that function was inserted between it and the `typeTagForId` it documents (#658). Adding `methodInfoKey` in this branch parked it directly above yet another unrelated function, which gitnexus-check flagged. Moves the comment down to the function it describes. No behaviour change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Px638Zyqa9CJMUU7DsJoB --------- Co-authored-by: Gergő Magyar <gergomagyar@icloud.com> Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
259 lines
9.2 KiB
TypeScript
259 lines
9.2 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import {
|
|
typeTagForId,
|
|
arityForIdFromInfo,
|
|
constTagForId,
|
|
} from '../../src/core/ingestion/utils/method-props.js';
|
|
import type { MethodInfo } from '../../src/core/ingestion/method-types.js';
|
|
import { SupportedLanguages } from 'gitnexus-shared';
|
|
|
|
function makeMethodInfo(
|
|
name: string,
|
|
params: Array<{ name: string; type: string | null; rawType?: string | null }>,
|
|
overrides: Partial<MethodInfo> = {},
|
|
): MethodInfo {
|
|
return {
|
|
name,
|
|
receiverType: null,
|
|
returnType: null,
|
|
parameters: params.map((p) => ({
|
|
name: p.name,
|
|
type: p.type,
|
|
...(p.rawType !== undefined ? { rawType: p.rawType } : {}),
|
|
isOptional: false,
|
|
isVariadic: false,
|
|
})),
|
|
visibility: 'public',
|
|
isStatic: false,
|
|
isAbstract: false,
|
|
isFinal: false,
|
|
annotations: [],
|
|
sourceFile: 'test.java',
|
|
line: 1,
|
|
column: 0,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function buildMethodMap(methods: MethodInfo[]): Map<string, MethodInfo> {
|
|
const map = new Map<string, MethodInfo>();
|
|
for (const m of methods) {
|
|
map.set(`${m.name}:${m.line}`, m);
|
|
}
|
|
return map;
|
|
}
|
|
|
|
describe('typeTagForId', () => {
|
|
it('returns type tag for same-arity collision: find(int) vs find(String)', () => {
|
|
const findInt = makeMethodInfo('find', [{ name: 'id', type: 'int' }], { line: 10 });
|
|
const findString = makeMethodInfo('find', [{ name: 'name', type: 'String' }], { line: 15 });
|
|
const map = buildMethodMap([findInt, findString]);
|
|
|
|
expect(typeTagForId(map, 'find', 1, findInt)).toBe('~int');
|
|
expect(typeTagForId(map, 'find', 1, findString)).toBe('~String');
|
|
});
|
|
|
|
it('returns type tag for three-way collision with multi-param methods', () => {
|
|
const a = makeMethodInfo(
|
|
'process',
|
|
[
|
|
{ name: 'x', type: 'int' },
|
|
{ name: 'y', type: 'int' },
|
|
],
|
|
{ line: 10 },
|
|
);
|
|
const b = makeMethodInfo(
|
|
'process',
|
|
[
|
|
{ name: 'x', type: 'int' },
|
|
{ name: 'y', type: 'String' },
|
|
],
|
|
{ line: 15 },
|
|
);
|
|
const c = makeMethodInfo(
|
|
'process',
|
|
[
|
|
{ name: 'x', type: 'String' },
|
|
{ name: 'y', type: 'String' },
|
|
],
|
|
{ line: 20 },
|
|
);
|
|
const map = buildMethodMap([a, b, c]);
|
|
|
|
expect(typeTagForId(map, 'process', 2, a)).toBe('~int,int');
|
|
expect(typeTagForId(map, 'process', 2, b)).toBe('~int,String');
|
|
expect(typeTagForId(map, 'process', 2, c)).toBe('~String,String');
|
|
});
|
|
|
|
it('returns empty string for single method (no collision)', () => {
|
|
const save = makeMethodInfo('save', [{ name: 'user', type: 'User' }], { line: 10 });
|
|
const map = buildMethodMap([save]);
|
|
|
|
expect(typeTagForId(map, 'save', 1, save)).toBe('');
|
|
});
|
|
|
|
it('returns empty string when same name but different arity (no collision in group)', () => {
|
|
const find1 = makeMethodInfo('find', [{ name: 'id', type: 'int' }], { line: 10 });
|
|
const find2 = makeMethodInfo(
|
|
'find',
|
|
[
|
|
{ name: 'id', type: 'int' },
|
|
{ name: 'name', type: 'String' },
|
|
],
|
|
{ line: 15 },
|
|
);
|
|
const map = buildMethodMap([find1, find2]);
|
|
|
|
expect(typeTagForId(map, 'find', 1, find1)).toBe('');
|
|
expect(typeTagForId(map, 'find', 2, find2)).toBe('');
|
|
});
|
|
|
|
it('returns empty string when collision exists but a method has null type', () => {
|
|
const findInt = makeMethodInfo('find', [{ name: 'id', type: 'int' }], { line: 10 });
|
|
const findUntyped = makeMethodInfo('find', [{ name: 'name', type: null }], { line: 15 });
|
|
const map = buildMethodMap([findInt, findUntyped]);
|
|
|
|
expect(typeTagForId(map, 'find', 1, findInt)).toBe('');
|
|
expect(typeTagForId(map, 'find', 1, findUntyped)).toBe('');
|
|
});
|
|
|
|
it('returns empty string for variadic method (arity undefined)', () => {
|
|
const m = makeMethodInfo('log', [{ name: 'args', type: 'String' }]);
|
|
m.parameters[0].isVariadic = true;
|
|
const map = buildMethodMap([m]);
|
|
|
|
expect(typeTagForId(map, 'log', undefined, m)).toBe('');
|
|
});
|
|
|
|
it('handles constructor overloads the same as methods', () => {
|
|
const ctorInt = makeMethodInfo('constructor', [{ name: 'id', type: 'int' }], { line: 5 });
|
|
const ctorString = makeMethodInfo('constructor', [{ name: 'name', type: 'String' }], {
|
|
line: 10,
|
|
});
|
|
const map = buildMethodMap([ctorInt, ctorString]);
|
|
|
|
expect(typeTagForId(map, 'constructor', 1, ctorInt)).toBe('~int');
|
|
expect(typeTagForId(map, 'constructor', 1, ctorString)).toBe('~String');
|
|
});
|
|
|
|
it('returns empty string for single zero-arity method', () => {
|
|
const m = makeMethodInfo('getName', [], { line: 10 });
|
|
const map = buildMethodMap([m]);
|
|
|
|
expect(typeTagForId(map, 'getName', 0, m)).toBe('');
|
|
});
|
|
|
|
it('returns empty string for zero-arity collision (no types to disambiguate)', () => {
|
|
const m1 = makeMethodInfo('begin', [], { line: 10 });
|
|
const m2 = makeMethodInfo('begin', [], { line: 15 });
|
|
const map = buildMethodMap([m1, m2]);
|
|
|
|
expect(typeTagForId(map, 'begin', 0, m1)).toBe('');
|
|
expect(typeTagForId(map, 'begin', 0, m2)).toBe('');
|
|
});
|
|
|
|
it('returns empty string for TypeScript (overload signatures should collapse)', () => {
|
|
const findNum = makeMethodInfo('find', [{ name: 'id', type: 'number' }], { line: 10 });
|
|
const findStr = makeMethodInfo('find', [{ name: 'name', type: 'string' }], { line: 15 });
|
|
const map = buildMethodMap([findNum, findStr]);
|
|
|
|
// Without language, same-arity collision → type tag
|
|
expect(typeTagForId(map, 'find', 1, findNum)).toBe('~number');
|
|
// With TypeScript language, type tag is skipped
|
|
expect(typeTagForId(map, 'find', 1, findNum, SupportedLanguages.TypeScript)).toBe('');
|
|
expect(typeTagForId(map, 'find', 1, findStr, SupportedLanguages.JavaScript)).toBe('');
|
|
});
|
|
|
|
it('rawType preserves generics: vector<int> vs vector<string> produce distinct tags', () => {
|
|
// With rawType, template/generic args are preserved for the type tag.
|
|
const vecInt = makeMethodInfo(
|
|
'process',
|
|
[{ name: 'items', type: 'vector', rawType: 'vector<int>' }],
|
|
{ line: 10 },
|
|
);
|
|
const vecStr = makeMethodInfo(
|
|
'process',
|
|
[{ name: 'items', type: 'vector', rawType: 'vector<string>' }],
|
|
{ line: 15 },
|
|
);
|
|
const map = buildMethodMap([vecInt, vecStr]);
|
|
|
|
expect(typeTagForId(map, 'process', 1, vecInt)).toBe('~vector<int>');
|
|
expect(typeTagForId(map, 'process', 1, vecStr)).toBe('~vector<string>');
|
|
});
|
|
|
|
it('falls back to type when rawType is not set', () => {
|
|
// Backward compat: old ParameterInfo without rawType still works
|
|
const findInt = makeMethodInfo('find', [{ name: 'id', type: 'int' }], { line: 10 });
|
|
const findStr = makeMethodInfo('find', [{ name: 'name', type: 'String' }], { line: 15 });
|
|
const map = buildMethodMap([findInt, findStr]);
|
|
|
|
expect(typeTagForId(map, 'find', 1, findInt)).toBe('~int');
|
|
expect(typeTagForId(map, 'find', 1, findStr)).toBe('~String');
|
|
});
|
|
});
|
|
|
|
describe('arityForIdFromInfo', () => {
|
|
it('returns parameter count for non-variadic methods', () => {
|
|
const m = makeMethodInfo('find', [
|
|
{ name: 'id', type: 'int' },
|
|
{ name: 'name', type: 'String' },
|
|
]);
|
|
expect(arityForIdFromInfo(m)).toBe(2);
|
|
});
|
|
|
|
it('returns undefined for variadic methods', () => {
|
|
const m = makeMethodInfo('log', [{ name: 'args', type: 'String' }]);
|
|
m.parameters[0].isVariadic = true;
|
|
expect(arityForIdFromInfo(m)).toBeUndefined();
|
|
});
|
|
|
|
it('returns 0 for parameterless methods', () => {
|
|
const m = makeMethodInfo('getName', []);
|
|
expect(arityForIdFromInfo(m)).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('constTagForId', () => {
|
|
it('returns $const for const method when non-const collision exists', () => {
|
|
const beginConst = makeMethodInfo('begin', [], { line: 10, isConst: true });
|
|
const beginNonConst = makeMethodInfo('begin', [], { line: 15 });
|
|
const map = buildMethodMap([beginConst, beginNonConst]);
|
|
|
|
expect(constTagForId(map, 'begin', 0, beginConst)).toBe('$const');
|
|
});
|
|
|
|
it('returns empty string for non-const method even when const collision exists', () => {
|
|
const beginConst = makeMethodInfo('begin', [], { line: 10, isConst: true });
|
|
const beginNonConst = makeMethodInfo('begin', [], { line: 15 });
|
|
const map = buildMethodMap([beginConst, beginNonConst]);
|
|
|
|
expect(constTagForId(map, 'begin', 0, beginNonConst)).toBe('');
|
|
});
|
|
|
|
it('returns empty string for single const method (no collision)', () => {
|
|
const sizeConst = makeMethodInfo('size', [], { line: 10, isConst: true });
|
|
const map = buildMethodMap([sizeConst]);
|
|
|
|
expect(constTagForId(map, 'size', 0, sizeConst)).toBe('');
|
|
});
|
|
|
|
it('works with typed parameters: find(int) vs find(int) const', () => {
|
|
const findConst = makeMethodInfo('find', [{ name: 'id', type: 'int' }], {
|
|
line: 10,
|
|
isConst: true,
|
|
});
|
|
const findNonConst = makeMethodInfo('find', [{ name: 'id', type: 'int' }], { line: 15 });
|
|
const map = buildMethodMap([findConst, findNonConst]);
|
|
|
|
expect(constTagForId(map, 'find', 1, findConst)).toBe('$const');
|
|
expect(constTagForId(map, 'find', 1, findNonConst)).toBe('');
|
|
});
|
|
|
|
it('returns empty string for non-const method without isConst field', () => {
|
|
const m = makeMethodInfo('clear', [], { line: 10 });
|
|
const map = buildMethodMap([m]);
|
|
|
|
expect(constTagForId(map, 'clear', 0, m)).toBe('');
|
|
});
|
|
});
|