fix(group): assign thrift synthetic symbol uids

This commit is contained in:
liyipeng06 2026-04-29 14:43:54 +08:00
parent 904457afa3
commit 476af9b152
3 changed files with 181 additions and 1 deletions

View file

@ -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,

View file

@ -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');

View file

@ -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',