From bee3e82ab23f396dcbb4e870918faa9835584d81 Mon Sep 17 00:00:00 2001 From: azizur100389 Date: Thu, 30 Jul 2026 05:34:21 +0100 Subject: [PATCH] test(scope-resolution): guard closure identity invariants (#2748) --- .../closure-binding-labels.test.ts | 15 +++ .../function-local-identity.test.ts | 103 ++++++++++++++++++ .../test/unit/incremental-parse-cache.test.ts | 4 + 3 files changed, 122 insertions(+) diff --git a/gitnexus/test/integration/closure-binding-labels.test.ts b/gitnexus/test/integration/closure-binding-labels.test.ts index 66235040c..629a9b8c9 100644 --- a/gitnexus/test/integration/closure-binding-labels.test.ts +++ b/gitnexus/test/integration/closure-binding-labels.test.ts @@ -596,6 +596,21 @@ describeIfWorkerBuilt('a closure binding as a call SOURCE (#2699 part B)', () => expect(targets).toEqual(['rel:CALLS:Function:A.kt:handler->Function:A.kt:target']); }); + it('Kotlin: a call at block level is not attributed to a nested named function (#2736)', async () => { + // A Block-kind scope may own a nested function without being that + // function's body. The start-position alignment check is what keeps the + // block-level call attributed to `outer`, rather than swallowing it into + // the uncalled `nested`. + const targets = await callEdgeIdsFor( + 'Block.kt', + 'fun target(): Int = 1\n\nfun outer(): Int {\n' + + ' if (true) {\n fun nested(): Int = 0\n return target()\n }\n' + + ' return 0\n}\n', + ); + + expect(targets).toEqual(['rel:CALLS:Function:Block.kt:outer->Function:Block.kt:target']); + }); + it('PHP: a call inside the closure IS attributed to the binding (#2699 S1)', async () => { // FLIPPED by #2699 S1. php/query.ts now carries the closure-binding // declaration rule with javascript/query.ts's anchor discipline diff --git a/gitnexus/test/integration/function-local-identity.test.ts b/gitnexus/test/integration/function-local-identity.test.ts index e8050718e..0012c3014 100644 --- a/gitnexus/test/integration/function-local-identity.test.ts +++ b/gitnexus/test/integration/function-local-identity.test.ts @@ -25,11 +25,23 @@ * pass. Every assertion here is therefore on the EDGE, whose source and target * are produced by the two different phases: it can only pass if both agree. */ +import { + buildDefIndex, + buildScopeTree, + type Reference, + type Scope, + type ScopeId, + type SymbolDefinition, +} from 'gitnexus-shared'; import { describe, expect, it, vi } from 'vitest'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; +import { createKnowledgeGraph } from '../../src/core/graph/graph.js'; +import type { ScopeResolutionIndexes } from '../../src/core/ingestion/model/scope-resolution-indexes.js'; import { runPipelineFromRepo } from '../../src/core/ingestion/pipeline.js'; +import { buildGraphNodeLookup } from '../../src/core/ingestion/scope-resolution/graph-bridge/node-lookup.js'; +import { emitReferencesViaLookup } from '../../src/core/ingestion/scope-resolution/graph-bridge/references-to-edges.js'; import { DIST_WORKER_URL, distWorkerExists } from '../helpers/worker-parse.js'; vi.setConfig({ testTimeout: 90_000 }); @@ -352,3 +364,94 @@ describeIfWorkerBuilt('function-local VALUES carry their own identity (#2699 A1) expect(ids).toEqual(['Const:v.ts:handler', 'Const:v.ts:run.handler@3:2']); }); }); + +describe('function-local value identity resolves through to emitted edges (#2736)', () => { + it('targets the position-qualified local rather than its file-level twin', () => { + const filePath = 'v.ts'; + const caller: SymbolDefinition = { + nodeId: 'def:v.ts#3:0:Function:run', + filePath, + type: 'Function', + qualifiedName: 'run', + }; + const topLevel: SymbolDefinition = { + nodeId: 'def:v.ts#1:0:Const:handler', + filePath, + type: 'Const', + qualifiedName: 'handler', + }; + const local: SymbolDefinition = { + nodeId: 'def:v.ts#4:2:Const:run.handler', + filePath, + type: 'Const', + qualifiedName: 'run.handler', + }; + const moduleScope: Scope = { + id: 'scope:v.ts#1:0-100:0:Module', + parent: null, + kind: 'Module', + range: { startLine: 1, startCol: 0, endLine: 100, endCol: 0 }, + filePath, + bindings: new Map(), + ownedDefs: [caller, topLevel], + imports: [], + typeBindings: new Map(), + }; + const functionScope: Scope = { + id: 'scope:v.ts#3:0-6:1:Function', + parent: moduleScope.id, + kind: 'Function', + range: { startLine: 3, startCol: 0, endLine: 6, endCol: 1 }, + filePath, + bindings: new Map(), + ownedDefs: [local], + imports: [], + typeBindings: new Map(), + }; + const indexes = { + scopeTree: buildScopeTree([moduleScope, functionScope]), + defs: buildDefIndex([caller, topLevel, local]), + } as unknown as ScopeResolutionIndexes; + + const graph = createKnowledgeGraph(); + graph.addNode({ + id: 'Function:v.ts:run', + label: 'Function', + properties: { name: 'run', qualifiedName: 'run', filePath, startLine: 2 }, + }); + graph.addNode({ + id: 'Const:v.ts:handler', + label: 'Const', + properties: { name: 'handler', qualifiedName: 'handler', filePath, startLine: 0 }, + }); + graph.addNode({ + id: 'Const:v.ts:run.handler@3:2', + label: 'Const', + properties: { name: 'handler', qualifiedName: 'run.handler', filePath, startLine: 3 }, + }); + + const reference: Reference = { + fromScope: functionScope.id, + toDef: local.nodeId, + atRange: { startLine: 5, startCol: 9, endLine: 5, endCol: 16 }, + kind: 'read', + confidence: 1, + evidence: [], + }; + const emitted = emitReferencesViaLookup( + graph, + indexes, + { bySourceScope: new Map([[functionScope.id, [reference]]]) }, + buildGraphNodeLookup(graph), + ); + + expect(emitted).toEqual({ emitted: 1, skipped: 0 }); + expect(graph.relationships).toEqual([ + expect.objectContaining({ + sourceId: 'Function:v.ts:run', + targetId: 'Const:v.ts:run.handler@3:2', + type: 'ACCESSES', + }), + ]); + }); +}); diff --git a/gitnexus/test/unit/incremental-parse-cache.test.ts b/gitnexus/test/unit/incremental-parse-cache.test.ts index 8574a2c0e..f0373b4b5 100644 --- a/gitnexus/test/unit/incremental-parse-cache.test.ts +++ b/gitnexus/test/unit/incremental-parse-cache.test.ts @@ -101,6 +101,10 @@ describe('fileContentHash', () => { }); describe('PARSE_CACHE_VERSION', () => { + it('pins SCHEMA_BUMP to 31 so concurrent bumps cannot silently collide (#2736)', () => { + expect(Number(PARSE_CACHE_VERSION.split('+', 1)[0])).toBe(31); + }); + it('embeds the gitnexus package version (so upgrades invalidate the cache)', () => { // Looks like "1+1.6.4" — schema bump prefix + actual gitnexus version expect(PARSE_CACHE_VERSION).toMatch(/^\d+\+\d+\.\d+\.\d+/);