mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-03 02:21:44 +00:00
Merge 718b80336c into 6bb99767ff
This commit is contained in:
commit
41feb26a00
6 changed files with 184 additions and 2 deletions
|
|
@ -37,6 +37,7 @@ const DEFAULT_DETECT = {
|
|||
topics: true,
|
||||
includes: false,
|
||||
workspace_deps: false,
|
||||
sofa: false,
|
||||
};
|
||||
|
||||
const DEFAULT_MATCHING = {
|
||||
|
|
|
|||
136
gitnexus/src/core/group/extractors/sofa-extractor.ts
Normal file
136
gitnexus/src/core/group/extractors/sofa-extractor.ts
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
import { glob } from 'glob';
|
||||
import { createIgnoreFilter } from '../../../config/ignore-service.js';
|
||||
import type { ContractExtractor, CypherExecutor } from '../contract-extractor.js';
|
||||
import type { ExtractedContract, RepoHandle } from '../types.js';
|
||||
import { readSafe } from './fs-utils.js';
|
||||
|
||||
/**
|
||||
* SOFA framework extractor for RPC contract discovery from XML files.
|
||||
*
|
||||
* SOFA RPC (Alibaba SOFA framework) uses XML declarations:
|
||||
* - `<sofa:service interface="com.xxx.YyyService">` + `<sofa:binding.tr/>` → provider
|
||||
* - `<sofa:reference interface="com.xxx.YyyService">` + `<sofa:binding.tr/>` → consumer
|
||||
*
|
||||
* SOFAMQ topic extraction is handled by the Java tree-sitter topic extractor
|
||||
* (topic-patterns/java.ts), which detects OpenMessaging API calls:
|
||||
* - `consumer.subscribe("topic", "tag", listener)` → topic consumer
|
||||
* - `new Message("topic", "tag", body)` → topic provider
|
||||
*
|
||||
* XML `sofa:consumer` + `sofa:channel` + `sofa:binding.msg_broker` declarations
|
||||
* are NOT extracted because they represent distributed scheduled task dispatching
|
||||
* (e.g. `TP_F_SC` channel), not real message pub/sub relationships.
|
||||
*
|
||||
* Because SOFA XML uses a distinctive namespace prefix, regex extraction is
|
||||
* reliable and avoids the need for a full XML tree-sitter grammar.
|
||||
*/
|
||||
|
||||
const SOFA_XML_GLOB = '**/*.xml';
|
||||
|
||||
// ─── Regex patterns for SOFA XML elements ───────────────────────────
|
||||
|
||||
/**
|
||||
* Match <sofa:service interface="...">…</sofa:service> blocks.
|
||||
* Captures the interface attribute and the full block content.
|
||||
* Note: self-closing <sofa:service …/> is not matched; SOFA XML always
|
||||
* uses the closing tag because it contains binding children.
|
||||
*/
|
||||
const SOFA_SERVICE_RE = /<sofa:service\s+[^>]*interface="([^"]+)"[^>]*>([\s\S]*?)<\/sofa:service>/g;
|
||||
|
||||
/**
|
||||
* Match <sofa:reference interface="…" …> (may be self-closing or have body).
|
||||
* Captures the interface attribute.
|
||||
*/
|
||||
const SOFA_REFERENCE_RE = /<sofa:reference\s+[^>]*interface="([^"]+)"[^>]*\/?>/g;
|
||||
|
||||
/**
|
||||
* Detect TR (bolt) binding inside a sofa:service block.
|
||||
*/
|
||||
const BINDING_TR_RE = /sofa:binding\.tr/;
|
||||
|
||||
// ─── Helper: build an ExtractedContract ──────────────────────────────
|
||||
|
||||
function makeRpcContract(
|
||||
interfaceName: string,
|
||||
role: 'provider' | 'consumer',
|
||||
filePath: string,
|
||||
): ExtractedContract {
|
||||
return {
|
||||
contractId: `custom::${interfaceName}`,
|
||||
type: 'custom',
|
||||
role,
|
||||
symbolUid: '',
|
||||
symbolRef: { filePath: filePath.replace(/\\/g, '/'), name: interfaceName },
|
||||
symbolName: interfaceName,
|
||||
confidence: 0.85,
|
||||
meta: {
|
||||
framework: 'sofa-rpc',
|
||||
interface: interfaceName,
|
||||
extractionStrategy: 'regex_xml',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Extractor class ─────────────────────────────────────────────────
|
||||
|
||||
export class SofaExtractor implements ContractExtractor {
|
||||
type = 'custom' as const;
|
||||
|
||||
async canExtract(_repo: RepoHandle): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
async extract(
|
||||
_dbExecutor: CypherExecutor | null,
|
||||
repoPath: string,
|
||||
_repo: RepoHandle,
|
||||
): Promise<ExtractedContract[]> {
|
||||
const baseFilter = await createIgnoreFilter(repoPath);
|
||||
const files = await glob(SOFA_XML_GLOB, {
|
||||
cwd: repoPath,
|
||||
ignore: baseFilter,
|
||||
nodir: true,
|
||||
});
|
||||
|
||||
const out: ExtractedContract[] = [];
|
||||
|
||||
for (const rel of files) {
|
||||
const content = readSafe(repoPath, rel);
|
||||
if (!content) continue;
|
||||
|
||||
// Skip files that don't contain any SOFA namespace declarations
|
||||
if (!content.includes('sofa:')) continue;
|
||||
|
||||
// ── SOFA RPC: sofa:service (provider) ──
|
||||
let m: RegExpExecArray | null;
|
||||
SOFA_SERVICE_RE.lastIndex = 0;
|
||||
while ((m = SOFA_SERVICE_RE.exec(content)) !== null) {
|
||||
const interfaceName = m[1];
|
||||
const body = m[2];
|
||||
if (BINDING_TR_RE.test(body)) {
|
||||
out.push(makeRpcContract(interfaceName, 'provider', rel));
|
||||
}
|
||||
}
|
||||
|
||||
// ── SOFA RPC: sofa:reference (consumer) ──
|
||||
SOFA_REFERENCE_RE.lastIndex = 0;
|
||||
while ((m = SOFA_REFERENCE_RE.exec(content)) !== null) {
|
||||
const interfaceName = m[1];
|
||||
out.push(makeRpcContract(interfaceName, 'consumer', rel));
|
||||
}
|
||||
}
|
||||
|
||||
return this.dedupe(out);
|
||||
}
|
||||
|
||||
private dedupe(items: ExtractedContract[]): ExtractedContract[] {
|
||||
const seen = new Set<string>();
|
||||
const out: ExtractedContract[] = [];
|
||||
for (const c of items) {
|
||||
const k = `${c.contractId}|${c.role}|${c.symbolRef.filePath}`;
|
||||
if (seen.has(k)) continue;
|
||||
seen.add(k);
|
||||
out.push(c);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
|
@ -5,11 +5,14 @@ import type { TopicMeta } from './types.js';
|
|||
/**
|
||||
* Java topic extraction patterns.
|
||||
*
|
||||
* Detects Kafka and RabbitMQ (Spring conventions) producer/consumer APIs:
|
||||
* Detects Kafka, RabbitMQ (Spring conventions) and SOFAMQ (OpenMessaging API)
|
||||
* producer/consumer APIs:
|
||||
* - `@KafkaListener(topics = "xxx")`
|
||||
* - `@RabbitListener(queues = "xxx")`
|
||||
* - `kafkaTemplate.send("xxx", ...)`
|
||||
* - `rabbitTemplate.convertAndSend("xxx", ...)`
|
||||
* - `consumer.subscribe("topic", "tag", listener)` (SOFAMQ consumer)
|
||||
* - `new Message("topic", "tag", body)` (SOFAMQ producer)
|
||||
*
|
||||
* Every query MUST bind `@value` to the topic literal node.
|
||||
*/
|
||||
|
|
@ -77,6 +80,33 @@ const JAVA_TOPIC_SPEC: LanguagePatterns<TopicMeta> = {
|
|||
arguments: (argument_list . (string_literal) @value))
|
||||
`,
|
||||
},
|
||||
{
|
||||
meta: {
|
||||
role: 'consumer',
|
||||
broker: 'sofamq',
|
||||
confidence: 0.7,
|
||||
symbolName: 'openMessaging.Consumer.subscribe',
|
||||
},
|
||||
query: `
|
||||
(method_invocation
|
||||
object: (identifier) @obj (#eq? @obj "consumer")
|
||||
name: (identifier) @method (#eq? @method "subscribe")
|
||||
arguments: (argument_list . (string_literal) @value))
|
||||
`,
|
||||
},
|
||||
{
|
||||
meta: {
|
||||
role: 'provider',
|
||||
broker: 'sofamq',
|
||||
confidence: 0.7,
|
||||
symbolName: 'openMessaging.Message.constructor',
|
||||
},
|
||||
query: `
|
||||
(object_creation_expression
|
||||
type: (type_identifier) @cls (#eq? @cls "Message")
|
||||
arguments: (argument_list . (string_literal) @value))
|
||||
`,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
* query text directly — that's the whole point of the split.
|
||||
*/
|
||||
|
||||
export type Broker = 'kafka' | 'rabbitmq' | 'nats';
|
||||
export type Broker = 'kafka' | 'rabbitmq' | 'nats' | 'sofamq';
|
||||
|
||||
/**
|
||||
* Per-pattern payload every topic plugin attaches to its query. Whatever
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ import { GraphqlExtractor } from './extractors/graphql-extractor.js';
|
|||
import { GrpcExtractor } from './extractors/grpc-extractor.js';
|
||||
import { ThriftExtractor } from './extractors/thrift-extractor.js';
|
||||
import { TopicExtractor } from './extractors/topic-extractor.js';
|
||||
import { SofaExtractor } from './extractors/sofa-extractor.js';
|
||||
import { IncludeExtractor } from './extractors/include-extractor.js';
|
||||
import { ManifestExtractor } from './extractors/manifest-extractor.js';
|
||||
import { discoverWorkspaceLinks } from './extractors/workspace-extractor.js';
|
||||
|
|
@ -349,6 +350,7 @@ export async function syncGroup(config: GroupConfig, opts?: SyncOptions): Promis
|
|||
const grpcEx = new GrpcExtractor();
|
||||
const thriftEx = new ThriftExtractor();
|
||||
const topicEx = new TopicExtractor();
|
||||
const sofaEx = new SofaExtractor();
|
||||
const includeEx = new IncludeExtractor();
|
||||
|
||||
for (const [groupPath, regName] of Object.entries(config.repos)) {
|
||||
|
|
@ -452,6 +454,17 @@ export async function syncGroup(config: GroupConfig, opts?: SyncOptions): Promis
|
|||
}
|
||||
}
|
||||
|
||||
if (config.detect.sofa) {
|
||||
const extracted = await sofaEx.extract(executor, handle.repoPath, handle);
|
||||
for (const c of extracted) {
|
||||
autoContracts.push({
|
||||
...c,
|
||||
repo: groupPath,
|
||||
service: assignService(c.symbolRef.filePath, boundaries),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (config.detect.includes) {
|
||||
const extracted = await includeEx.extract(executor, handle.repoPath, handle);
|
||||
for (const c of extracted) {
|
||||
|
|
|
|||
|
|
@ -40,6 +40,8 @@ export interface DetectConfig {
|
|||
topics: boolean;
|
||||
includes: boolean;
|
||||
workspace_deps: boolean;
|
||||
/** SOFA framework (SOFA RPC + SOFAMQ) XML contract extraction. Opt-in. */
|
||||
sofa: boolean;
|
||||
}
|
||||
|
||||
export interface MatchingConfig {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue