mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-07 08:26:11 +00:00
* fix(ingestion): join multi-line closure bindings on initializer startLine Graph-node captures sit on the outer binding wrapper while scope-resolution anchors on the inner callable; the line-only position join missed when those split across lines and fail-closed dropped the real CALLS edge (#2735). * fix(ingestion): unwrap Ruby call+block for multi-line lambda joins Cover Kotlin/Ruby/Dart multi-line closure CALLS in integration tests, and dig Ruby's call/block field so do-end bindings join on the block start line. * style(ingestion): format closure join changes * fix(ingestion): make closure position join language agnostic
179 lines
6.5 KiB
TypeScript
179 lines
6.5 KiB
TypeScript
/**
|
|
* #2699 / #2714 — the nested-callable id rule has ONE definition.
|
|
*
|
|
* Three phases in `parse-worker.ts` build the id of a callable nested inside
|
|
* another callable, independently: the definition phase
|
|
* (`callableOwnQualifiedName`), the caller-attribution phase
|
|
* (`findEnclosingFunctionId`), and the worker-path node-id derivation in
|
|
* `processFileGroup`. They must agree byte-for-byte, and when they do not the
|
|
* failure is SILENT — the caller id names a node that does not exist, so the
|
|
* edge is dropped rather than reported. "Zero dangling edges" is what that
|
|
* looks like from the outside, which is why it went unnoticed.
|
|
*
|
|
* Caller attribution really did omit the position suffix until #2714. The fix
|
|
* routed all three through `nestedCallableQualifiedName`; this file pins both
|
|
* halves of that — the rule's contract, and the fact that no call site has
|
|
* re-inlined it.
|
|
*
|
|
* The rule reads only `startPosition` off the node, so a positional stub is a
|
|
* complete input here; parsing real source would add a tree-sitter dependency
|
|
* without testing anything more of this function.
|
|
*
|
|
* The rules live in `callable-id.ts` rather than `parse-worker.ts` precisely so
|
|
* this file can exist: parse-worker posts a `ready` message to `parentPort` at
|
|
* import, so value-importing it from a unit test throws before any test runs.
|
|
*/
|
|
import { readFileSync } from 'node:fs';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { describe, expect, it } from 'vitest';
|
|
import {
|
|
boundCallableStartRow,
|
|
nestedCallableQualifiedName,
|
|
} from '../../src/core/ingestion/workers/callable-id.js';
|
|
import type { NodeLabel, SymbolDefinition } from 'gitnexus-shared';
|
|
import type { SyntaxNode } from '../../src/core/ingestion/utils/ast-helpers.js';
|
|
|
|
const nodeAt = (row: number, column: number): SyntaxNode =>
|
|
({ startPosition: { row, column } }) as unknown as SyntaxNode;
|
|
|
|
function stubNode(opts: {
|
|
type: string;
|
|
id: number;
|
|
row?: number;
|
|
column?: number;
|
|
endRow?: number;
|
|
endColumn?: number;
|
|
}): SyntaxNode {
|
|
return {
|
|
type: opts.type,
|
|
id: opts.id,
|
|
startPosition: { row: opts.row ?? 0, column: opts.column ?? 0 },
|
|
endPosition: {
|
|
row: opts.endRow ?? opts.row ?? 0,
|
|
column: opts.endColumn ?? opts.column ?? 0,
|
|
},
|
|
} as unknown as SyntaxNode;
|
|
}
|
|
|
|
const semanticDef = (
|
|
filePath: string,
|
|
type: NodeLabel,
|
|
name: string,
|
|
line: number,
|
|
column: number,
|
|
): SymbolDefinition => ({
|
|
nodeId: `def:${filePath}#${line}:${column}:${type}:${name}`,
|
|
filePath,
|
|
type,
|
|
qualifiedName: name,
|
|
});
|
|
|
|
describe('boundCallableStartRow - #2735 semantic position join', () => {
|
|
it('keeps the wrapper row when no semantic definition is available', () => {
|
|
const wrapper = stubNode({
|
|
type: 'binding_wrapper',
|
|
id: 1,
|
|
row: 2,
|
|
column: 0,
|
|
endRow: 4,
|
|
endColumn: 1,
|
|
});
|
|
|
|
expect(boundCallableStartRow(wrapper, 'handler', 'Function', undefined)).toBe(2);
|
|
});
|
|
|
|
it('uses the matching semantic callable position inside the wrapper', () => {
|
|
const wrapper = stubNode({
|
|
type: 'binding_wrapper',
|
|
id: 1,
|
|
row: 1,
|
|
column: 0,
|
|
endRow: 5,
|
|
endColumn: 10,
|
|
});
|
|
const name = stubNode({ type: 'binding_name', id: 2, row: 1, column: 4 });
|
|
const defs = [semanticDef('src/file.ext', 'Function', 'handler', 4, 8)];
|
|
|
|
expect(boundCallableStartRow(wrapper, 'handler', 'Function', defs, name)).toBe(3);
|
|
});
|
|
|
|
it('selects by canonical name and label rather than grammar shape', () => {
|
|
const wrapper = stubNode({
|
|
type: 'binding_wrapper',
|
|
id: 1,
|
|
row: 1,
|
|
column: 0,
|
|
endRow: 8,
|
|
endColumn: 10,
|
|
});
|
|
const defs = [
|
|
semanticDef('src/file.ext', 'Function', 'sibling', 3, 4),
|
|
semanticDef('src/file.ext', 'Method', 'handler', 4, 4),
|
|
semanticDef('src/file.ext', 'Function', 'handler', 6, 4),
|
|
];
|
|
|
|
expect(boundCallableStartRow(wrapper, 'handler', 'Function', defs)).toBe(5);
|
|
});
|
|
|
|
it('ignores a same-named semantic definition outside the wrapper', () => {
|
|
const wrapper = stubNode({
|
|
type: 'binding_wrapper',
|
|
id: 1,
|
|
row: 4,
|
|
column: 0,
|
|
endRow: 6,
|
|
endColumn: 10,
|
|
});
|
|
const defs = [semanticDef('src/file.ext', 'Function', 'handler', 2, 0)];
|
|
|
|
expect(boundCallableStartRow(wrapper, 'handler', 'Function', defs)).toBe(4);
|
|
});
|
|
});
|
|
describe('nestedCallableQualifiedName — the shared nested-callable id rule', () => {
|
|
it('qualifies by the enclosing callable AND the declaration position', () => {
|
|
expect(nestedCallableQualifiedName('run', nodeAt(3, 2), 'save')).toBe('run.save@3:2');
|
|
});
|
|
|
|
it('takes the position from the node, never from the name', () => {
|
|
// Guards against a "fix" that formats the suffix from anything but the
|
|
// declaration site — the position is what makes the id unique.
|
|
expect(nestedCallableQualifiedName('outer', nodeAt(12, 9), 'fn')).toBe('outer.fn@12:9');
|
|
});
|
|
|
|
it('separates same-named siblings in different blocks', () => {
|
|
// The case names alone cannot express (#2699): two `pick` bindings in the
|
|
// if/else arms of one function are genuinely different bindings, and both
|
|
// are `outer.pick` by name.
|
|
const first = nestedCallableQualifiedName('outer', nodeAt(2, 4), 'pick');
|
|
const second = nestedCallableQualifiedName('outer', nodeAt(5, 4), 'pick');
|
|
|
|
expect(first).not.toBe(second);
|
|
});
|
|
|
|
it('carries a multi-level chain verbatim in the prefix', () => {
|
|
expect(nestedCallableQualifiedName('A.outer.mid', nodeAt(7, 0), 'inner')).toBe(
|
|
'A.outer.mid.inner@7:0',
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('no call site re-inlines the rule', () => {
|
|
it('parse-worker.ts contains no inlined `<prefix>.${localIdentity(...)}` template', () => {
|
|
// The structural half. The unit assertions above would still pass if a
|
|
// fourth phase appeared and spelled the rule out by hand — which is
|
|
// exactly how the divergence #2714 fixed came to exist.
|
|
//
|
|
// Scope, stated honestly: this matches ONE template spelling — the
|
|
// `${prefix}.${localIdentity(...)}` form the divergence actually took. A
|
|
// hand-rolled id built by string concatenation, or with the interpolation
|
|
// spelled differently, still slips past. It is a tripwire for the known
|
|
// shape, not a proof that no site reconstructs the id.
|
|
const source = readFileSync(
|
|
fileURLToPath(new URL('../../src/core/ingestion/workers/parse-worker.ts', import.meta.url)),
|
|
'utf8',
|
|
);
|
|
const inlined = source.match(/\}\.\$\{localIdentity\(/g) ?? [];
|
|
|
|
expect(inlined).toEqual([]);
|
|
});
|
|
});
|