GitNexus/gitnexus/test/integration/closure-binding-labels.test.ts
Gergő Magyar 89bbdcf566
Some checks failed
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
Gitleaks / gitleaks (push) Waiting to run
Publish / Classify release event (push) Waiting to run
Publish / RC guard (marker + release-PR skip) (push) Blocked by required conditions
Publish / ci (push) Blocked by required conditions
Publish / Publish to npm (push) Blocked by required conditions
Publish / Build & Push RC Docker images (push) Blocked by required conditions
Scorecard / Scorecard analysis (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
Devcontainer Smoke / Config-transform unit tests (push) Has been cancelled
Devcontainer Smoke / Build devcontainer image (push) Has been cancelled
Skill copy sync / shipped skills drift guard (push) Has been cancelled
fix(ingestion): stop double-indexing const X = () => {} as Function + edgeless Const twin (#2687) (#2691)
2026-07-25 16:56:17 +01:00

189 lines
7.3 KiB
TypeScript

/**
* #2687 follow-up — a closure bound to a name emits ONE `Function` node in
* every language, not `Variable` in some and `Property` in others.
*
* `const f = () => {}` already produced a `Function` in TS/JS (that is what the
* #2687 twin fix preserved), but the same construct produced a `Variable` in
* Go/Python/Dart/C++ and a `Property` in Kotlin/Swift. The graph schema states
* "Function: Functions and arrow functions", and every syntactic tagger the
* convention was checked against (tree-sitter tags, universal-ctags) labels the
* binding a function — so the callable label is the consistent one.
*
* Each language's value capture still matches the same declaration node, so
* these rely on the #2687 pre-scan collapsing the pair; a regression there
* would surface here as a twin rather than a wrong label.
*
* The label alone does not make `f()` resolve — free-call resolution runs off
* the per-language scope-resolution queries. Go, Python and C++ now also carry a
* `@declaration.function` capture anchored on the inner closure literal, so
* calls resolve there too (asserted in the second describe). Kotlin, Swift and
* Dart still lack a `@scope.function` whose range matches the closure literal —
* Kotlin deliberately scopes `lambda_literal` as a BLOCK (#1757) — and an
* unaligned declaration anchor mis-attributes callers, so those three keep the
* label fix only.
*/
import { describe, expect, it } from 'vitest';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { runPipelineFromRepo } from '../../src/core/ingestion/pipeline.js';
import { DIST_WORKER_URL, distWorkerExists } from '../helpers/worker-parse.js';
import { parseFilesWithWorkers } from '../helpers/worker-parse.js';
const labelsFor = async (path: string, content: string, name: string): Promise<string[]> => {
const { graph } = await parseFilesWithWorkers([{ path, content }]);
return graph.nodes
.filter((node) => node.properties.name === name)
.map((node) => node.label)
.sort();
};
describe('closure bindings emit a single Function node in every language', () => {
it('Go: var f = func(){}', async () => {
expect(
await labelsFor(
'src/handler.go',
'package main\n\nvar Handler = func(x int) int { return x }\n',
'Handler',
),
).toEqual(['Function']);
});
it('Python: f = lambda x: x', async () => {
expect(await labelsFor('src/handler.py', 'handler = lambda x: x\n', 'handler')).toEqual([
'Function',
]);
});
it('Kotlin: val f = { x -> x }', async () => {
expect(await labelsFor('src/Handler.kt', 'val handler = { x: Int -> x }\n', 'handler')).toEqual(
['Function'],
);
});
it('Swift: let f = { ... }', async () => {
expect(
await labelsFor(
'src/Handler.swift',
'let handler = { (x: Int) -> Int in return x }\n',
'handler',
),
).toEqual(['Function']);
});
it('C++: auto f = [](int x){ ... }', async () => {
expect(
await labelsFor('src/handler.cpp', 'auto handler = [](int x) { return x; };\n', 'handler'),
).toEqual(['Function']);
});
it('Dart: var f = (int x) => x', async () => {
expect(await labelsFor('src/handler.dart', 'var handler = (int x) => x;\n', 'handler')).toEqual(
['Function'],
);
});
// The suppression must key on an actual closure value, never on the
// declaration keyword — otherwise ordinary constants would vanish. One `it`
// per language: each spins its own worker pool and four in a single test
// exceeds the default timeout.
it('Go: leaves a genuine const alone', async () => {
expect(
await labelsFor('src/consts.go', 'package main\n\nconst MaxSize = 10\n', 'MaxSize'),
).toEqual(['Const']);
});
it('Python: leaves a genuine assignment alone', async () => {
expect(await labelsFor('src/consts.py', 'MAX_SIZE = 10\n', 'MAX_SIZE')).toEqual(['Variable']);
});
it('Kotlin: leaves a genuine property alone', async () => {
expect(await labelsFor('src/Consts.kt', 'val maxSize = 10\n', 'maxSize')).toEqual(['Property']);
});
it('C++: leaves a genuine variable alone', async () => {
expect(await labelsFor('src/consts.cpp', 'auto maxSize = 10;\n', 'maxSize')).toEqual([
'Variable',
]);
});
it('Python: an annotated attribute stays a Property, not a Variable', async () => {
// Regression guard. Python matches BOTH `@definition.property` (annotated)
// and `@definition.variable` (bare assignment) on the same statement at the
// same byte offset. Ranking `Property` level with the value labels made the
// winner depend on match order, which silently turned every typed attribute
// — including dataclass fields — into a file-level `Variable`.
expect(await labelsFor('src/model.py', 'class C:\n name: str = "x"\n', 'name')).toEqual([
'Property',
]);
});
it('Python: an annotated attribute keeps its owning HAS_PROPERTY edge', async () => {
// The label regression above also detached the attribute from its class:
// the node became `Variable:<file>:name` reached by `File -DEFINES->`
// instead of `Property:<file>:C.name` reached by `Class -HAS_PROPERTY->`.
const { graph } = await parseFilesWithWorkers([
{ path: 'src/owned.py', content: 'class C:\n name: str = "x"\n' },
]);
expect(
graph.relationships
.filter((rel) => rel.type === 'HAS_PROPERTY')
.map((rel) => `${rel.sourceId} -> ${rel.targetId}`),
).toEqual(['Class:src/owned.py:C -> Property:src/owned.py:C.name']);
});
});
const describeIfWorkerBuilt = distWorkerExists() ? describe : describe.skip;
/** Call targets resolved in a one-file repo, for the closure-call assertions. */
const callTargetsFor = async (filename: string, source: string): Promise<string[]> => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gn-closure-calls-'));
try {
fs.writeFileSync(path.join(dir, filename), source, 'utf-8');
const result = await runPipelineFromRepo(dir, () => {}, {
workerPoolSize: 1,
workerUrlForTest: DIST_WORKER_URL,
});
return result.graph.relationships
.filter((rel) => rel.type === 'CALLS')
.map((rel) => rel.targetId)
.sort();
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
};
describeIfWorkerBuilt('calls to a closure binding resolve to its Function node', () => {
// The label change alone is not enough: each language also needs a
// `@declaration.function` anchored on the inner closure literal, so the def is
// owned by the closure's own scope and free-call resolution can find it.
it('Go: Handler(1) resolves', async () => {
const targets = await callTargetsFor(
'main.go',
'package main\n\nvar Handler = func(x int) int { return x }\n\nfunc Caller() int { return Handler(1) }\n',
);
expect(targets).toContain('Function:main.go:Handler');
});
it('Python: handler(1) resolves', async () => {
const targets = await callTargetsFor(
'app.py',
'handler = lambda x: x\n\ndef caller():\n return handler(1)\n',
);
expect(targets).toContain('Function:app.py:handler');
});
it('C++: handler(1) resolves', async () => {
const targets = await callTargetsFor(
'main.cpp',
'auto handler = [](int x) { return x; };\n\nint caller() { return handler(1); }\n',
);
expect(targets).toContain('Function:main.cpp:handler');
});
});