From 476af9b152b32c7c652e58f8c832820421087b09 Mon Sep 17 00:00:00 2001 From: liyipeng06 Date: Wed, 29 Apr 2026 14:43:54 +0800 Subject: [PATCH] fix(group): assign thrift synthetic symbol uids --- .../core/group/extractors/thrift-extractor.ts | 13 +- gitnexus/test/unit/group/sync.test.ts | 134 ++++++++++++++++++ .../test/unit/group/thrift-extractor.test.ts | 35 +++++ 3 files changed, 181 insertions(+), 1 deletion(-) diff --git a/gitnexus/src/core/group/extractors/thrift-extractor.ts b/gitnexus/src/core/group/extractors/thrift-extractor.ts index 5ff9dd60c..06e75379a 100644 --- a/gitnexus/src/core/group/extractors/thrift-extractor.ts +++ b/gitnexus/src/core/group/extractors/thrift-extractor.ts @@ -178,6 +178,17 @@ function extractMethods(sanitizedServiceBody: string): string[] { return methods; } +function thriftSourceScanSymbolUid( + contractId: string, + role: 'provider' | 'consumer', + filePath: string, + symbolName: string, +): string { + return ['source-scan::thrift', role, contractId, normalizeThriftPath(filePath), symbolName].join( + '::', + ); +} + function makeContract( cid: string, role: 'provider' | 'consumer', @@ -190,7 +201,7 @@ function makeContract( contractId: cid, type: 'thrift', role, - symbolUid: '', + symbolUid: thriftSourceScanSymbolUid(cid, role, filePath, symbolName), symbolRef: { filePath: normalizeThriftPath(filePath), name: symbolName }, symbolName, confidence, diff --git a/gitnexus/test/unit/group/sync.test.ts b/gitnexus/test/unit/group/sync.test.ts index dc24b0b7f..933ed74af 100644 --- a/gitnexus/test/unit/group/sync.test.ts +++ b/gitnexus/test/unit/group/sync.test.ts @@ -303,6 +303,65 @@ describe('syncGroup', () => { expect(result.unmatched).toEqual([provider]); }); + it('keeps wildcard thrift links to multiple extracted IDL provider methods', async () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gitnexus-sync-thrift-wildcard-')); + fs.mkdirSync(path.join(tmpDir, 'idl'), { recursive: true }); + fs.writeFileSync( + path.join(tmpDir, 'idl', 'order.thrift'), + `namespace java billing.v1 + +service OrderService { + PlaceOrderResponse PlaceOrder(1: PlaceOrderRequest request) + OrderResponse GetOrder(1: string orderId) +}`, + ); + + try { + const { ThriftExtractor } = await import( + '../../../src/core/group/extractors/thrift-extractor.js' + ); + const extractedProviders = (await new ThriftExtractor().extract(null, tmpDir, { + id: 'provider-repo', + path: 'app/provider', + repoPath: tmpDir, + storagePath: path.join(tmpDir, '.gitnexus'), + })) + .filter((c) => c.role === 'provider') + .map( + (c): StoredContract => ({ + ...c, + repo: 'app/provider', + }), + ); + + const consumer: StoredContract = { + contractId: 'thrift::OrderService/*', + type: 'thrift', + role: 'consumer', + symbolUid: 'manifest::app/consumer::thrift::OrderService/*', + symbolRef: { filePath: 'group.yaml', name: 'OrderService' }, + symbolName: 'OrderService', + confidence: 1, + meta: {}, + repo: 'app/consumer', + }; + + const result = await syncGroup(makeConfig({}), { + extractorOverride: async () => [...extractedProviders, consumer], + skipWrite: true, + }); + + expect(result.crossLinks).toHaveLength(2); + expect(result.crossLinks.map((cl) => cl.to.symbolRef.name).sort()).toEqual([ + 'OrderService.GetOrder', + 'OrderService.PlaceOrder', + ]); + expect(new Set(result.crossLinks.map((cl) => cl.to.symbolUid)).size).toBe(2); + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } + }); + it('matches weak thrift method consumers to namespace-qualified providers during sync', async () => { const config = makeConfig({ 'app/provider': 'provider-repo', 'app/consumer': 'consumer-repo' }); const provider: StoredContract = { @@ -341,6 +400,81 @@ describe('syncGroup', () => { expect(result.unmatched).toHaveLength(0); }); + it('keeps exact thrift links to extracted IDL and Java providers for same method', async () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gitnexus-sync-thrift-exact-')); + fs.mkdirSync(path.join(tmpDir, 'idl'), { recursive: true }); + fs.mkdirSync(path.join(tmpDir, 'src', 'main', 'java', 'example'), { recursive: true }); + fs.writeFileSync( + path.join(tmpDir, 'idl', 'order.thrift'), + `namespace java billing.v1 + +service OrderService { + PlaceOrderResponse PlaceOrder(1: PlaceOrderRequest request) +}`, + ); + fs.writeFileSync( + path.join(tmpDir, 'src', 'main', 'java', 'example', 'IfaceOrderHandler.java'), + `package example; + +class IfaceOrderHandler implements OrderService.Iface { + public PlaceOrderResponse PlaceOrder(PlaceOrderRequest request) { + return new PlaceOrderResponse(); + } +}`, + ); + + try { + const { ThriftExtractor } = await import( + '../../../src/core/group/extractors/thrift-extractor.js' + ); + const extractedProviders = (await new ThriftExtractor().extract(null, tmpDir, { + id: 'provider-repo', + path: 'app/provider', + repoPath: tmpDir, + storagePath: path.join(tmpDir, '.gitnexus'), + })) + .filter((c) => c.role === 'provider') + .map( + (c): StoredContract => ({ + ...c, + repo: 'app/provider', + }), + ); + + const consumer: StoredContract = { + contractId: 'thrift::OrderService/PlaceOrder', + type: 'thrift', + role: 'consumer', + symbolUid: [ + 'source-scan::thrift', + 'consumer', + 'thrift::OrderService/PlaceOrder', + 'src/BillingWorkflow.java', + 'orderService.PlaceOrder', + ].join('::'), + symbolRef: { filePath: 'src/BillingWorkflow.java', name: 'orderService.PlaceOrder' }, + symbolName: 'orderService.PlaceOrder', + confidence: 0.45, + meta: {}, + repo: 'app/consumer', + }; + + const result = await syncGroup(makeConfig({}), { + extractorOverride: async () => [...extractedProviders, consumer], + skipWrite: true, + }); + + expect(result.crossLinks).toHaveLength(2); + expect(result.crossLinks.map((cl) => cl.to.symbolRef.filePath).sort()).toEqual([ + 'idl/order.thrift', + 'src/main/java/example/IfaceOrderHandler.java', + ]); + expect(new Set(result.crossLinks.map((cl) => cl.to.symbolUid)).size).toBe(2); + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } + }); + it('extracts thrift contracts during real sync when thrift detection is enabled', async () => { const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gitnexus-sync-thrift-')); const storageDir = path.join(tmpDir, '.gitnexus'); diff --git a/gitnexus/test/unit/group/thrift-extractor.test.ts b/gitnexus/test/unit/group/thrift-extractor.test.ts index 224db8b6f..f18cc905a 100644 --- a/gitnexus/test/unit/group/thrift-extractor.test.ts +++ b/gitnexus/test/unit/group/thrift-extractor.test.ts @@ -455,6 +455,41 @@ class GeneratedOrderHandler implements OrderService { } }); + it('test_extract_thrift_source_scan_contracts_have_stable_distinct_symbol_uids', async () => { + writeFile( + 'idl/order.thrift', + `namespace java billing.v1 + +service OrderService { + PlaceOrderResponse PlaceOrder(1: PlaceOrderRequest request) +}`, + ); + writeFile( + 'src/main/java/example/IfaceOrderHandler.java', + `package example; + +class IfaceOrderHandler implements OrderService.Iface { + public PlaceOrderResponse PlaceOrder(PlaceOrderRequest request) { + return new PlaceOrderResponse(); + } +}`, + ); + + const first = await extractor.extract(null, tmpDir, makeRepo(tmpDir)); + const second = await extractor.extract(null, tmpDir, makeRepo(tmpDir)); + const providers = first + .filter((c) => c.role === 'provider') + .sort((a, b) => a.symbolRef.filePath.localeCompare(b.symbolRef.filePath)); + const repeatedProviders = second + .filter((c) => c.role === 'provider') + .sort((a, b) => a.symbolRef.filePath.localeCompare(b.symbolRef.filePath)); + + expect(providers).toHaveLength(2); + expect(providers.map((c) => c.symbolUid)).toEqual(repeatedProviders.map((c) => c.symbolUid)); + expect(providers.every((c) => c.symbolUid.length > 0)).toBe(true); + expect(new Set(providers.map((c) => c.symbolUid)).size).toBe(2); + }); + it('test_extract_java_thrift_providers_from_fully_qualified_generated_iface', async () => { writeFile( 'idl/order.thrift',