mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-09 03:17:54 +00:00
feat(group): expand http extractor coverage across Go, NestJS, Python and Java
This commit is contained in:
parent
fa80ce6a25
commit
f614578926
2 changed files with 313 additions and 3 deletions
|
|
@ -226,7 +226,7 @@ export class HttpRouteExtractor implements ContractExtractor {
|
|||
}
|
||||
|
||||
private async extractProvidersSourceScan(repoPath: string): Promise<ExtractedContract[]> {
|
||||
const files = await glob('**/*.{ts,tsx,js,jsx,java,vue,svelte,php,py}', {
|
||||
const files = await glob('**/*.{ts,tsx,js,jsx,java,vue,svelte,php,py,go}', {
|
||||
cwd: repoPath,
|
||||
ignore: ['**/node_modules/**', '**/.git/**', '**/dist/**', '**/build/**'],
|
||||
nodir: true,
|
||||
|
|
@ -236,7 +236,9 @@ export class HttpRouteExtractor implements ContractExtractor {
|
|||
const content = readSafe(repoPath, rel);
|
||||
if (!content) continue;
|
||||
out.push(...this.scanSpringProviders(content, rel));
|
||||
out.push(...this.scanNestProviders(content, rel));
|
||||
out.push(...this.scanExpressProviders(content, rel));
|
||||
out.push(...this.scanGoProviders(content, rel));
|
||||
out.push(...this.scanLaravelProviders(content, rel));
|
||||
out.push(...this.scanFastApiProviders(content, rel));
|
||||
}
|
||||
|
|
@ -312,6 +314,48 @@ export class HttpRouteExtractor implements ContractExtractor {
|
|||
return out;
|
||||
}
|
||||
|
||||
private scanNestProviders(content: string, filePath: string): ExtractedContract[] {
|
||||
const out: ExtractedContract[] = [];
|
||||
const controllerMatch = content.match(/@Controller\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/);
|
||||
const controllerPrefix = controllerMatch ? controllerMatch[1].replace(/\/+$/, '') : '';
|
||||
const re = /@(Get|Post|Put|Delete|Patch)\s*\(\s*['"`]?([^'"`)]*)['"`]?\s*\)/gi;
|
||||
let m: RegExpExecArray | null;
|
||||
while ((m = re.exec(content)) !== null) {
|
||||
const method = m[1].toUpperCase();
|
||||
const routePath = String(m[2] || '');
|
||||
const fullPath = controllerPrefix
|
||||
? `${controllerPrefix}/${routePath.replace(/^\/+/, '')}`
|
||||
: routePath;
|
||||
const pathNorm = normalizeHttpPath(fullPath.startsWith('/') ? fullPath : `/${fullPath}`);
|
||||
const sub = content.slice(m.index);
|
||||
const nameMatch = sub.match(
|
||||
/(?:public|protected|private)?\s*(?:async\s+)?(\w+)\s*\([^)]*\)\s*\{/,
|
||||
);
|
||||
const name = nameMatch ? nameMatch[1] : m[0];
|
||||
out.push(this.makeProvider(filePath, method, pathNorm, name, 0.8));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private scanGoProviders(content: string, filePath: string): ExtractedContract[] {
|
||||
const out: ExtractedContract[] = [];
|
||||
const frameworkRe =
|
||||
/(?:^|\W)\w+\.(GET|POST|PUT|DELETE|PATCH)\s*\(\s*['"]([^'"]+)['"]\s*,\s*(\w+)/gim;
|
||||
let m: RegExpExecArray | null;
|
||||
while ((m = frameworkRe.exec(content)) !== null) {
|
||||
out.push(this.makeProvider(filePath, m[1].toUpperCase(), normalizeHttpPath(m[2]), m[3], 0.8));
|
||||
}
|
||||
|
||||
const handleFuncRe =
|
||||
/(?:http|\w+)\.HandleFunc\s*\(\s*['"]([^'"]+)['"]\s*,\s*(\w+)\s*\)(?:\s*\.\s*Methods\s*\(\s*['"](\w+)['"]\s*\))?/gim;
|
||||
while ((m = handleFuncRe.exec(content)) !== null) {
|
||||
const method = (m[3] || 'GET').toUpperCase();
|
||||
out.push(this.makeProvider(filePath, method, normalizeHttpPath(m[1]), m[2], 0.8));
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
private makeProvider(
|
||||
filePath: string,
|
||||
method: string,
|
||||
|
|
@ -407,7 +451,7 @@ export class HttpRouteExtractor implements ContractExtractor {
|
|||
}
|
||||
|
||||
private async extractConsumersSourceScan(repoPath: string): Promise<ExtractedContract[]> {
|
||||
const files = await glob('**/*.{ts,tsx,js,jsx,vue,svelte}', {
|
||||
const files = await glob('**/*.{ts,tsx,js,jsx,vue,svelte,py,java,go}', {
|
||||
cwd: repoPath,
|
||||
ignore: ['**/node_modules/**', '**/.git/**'],
|
||||
nodir: true,
|
||||
|
|
@ -418,6 +462,9 @@ export class HttpRouteExtractor implements ContractExtractor {
|
|||
if (!content) continue;
|
||||
out.push(...this.scanFetchConsumers(content, rel));
|
||||
out.push(...this.scanAxiosConsumers(content, rel));
|
||||
out.push(...this.scanPythonRequestsConsumers(content, rel));
|
||||
out.push(...this.scanJavaConsumers(content, rel));
|
||||
out.push(...this.scanGoConsumers(content, rel));
|
||||
}
|
||||
return this.dedupeContracts(out);
|
||||
}
|
||||
|
|
@ -439,18 +486,111 @@ export class HttpRouteExtractor implements ContractExtractor {
|
|||
return url.replace(/\$\{[^}]+\}/g, '{param}');
|
||||
}
|
||||
|
||||
private normalizeConsumerPath(url: string): string {
|
||||
const templated = this.templateToPattern(url.trim());
|
||||
let pathOnly = templated;
|
||||
if (/^https?:\/\//i.test(templated)) {
|
||||
try {
|
||||
pathOnly = new URL(templated).pathname;
|
||||
} catch {
|
||||
pathOnly = templated.replace(/^https?:\/\/[^/]+/i, '');
|
||||
}
|
||||
}
|
||||
|
||||
const normalized = normalizeHttpPath(pathOnly || '/');
|
||||
const segments = normalized.split('/').filter(Boolean).map((segment) => {
|
||||
if (/^\d+$/.test(segment)) return '{param}';
|
||||
return segment;
|
||||
});
|
||||
return `/${segments.join('/')}`.replace(/\/+$/, '') || '/';
|
||||
}
|
||||
|
||||
private scanAxiosConsumers(content: string, filePath: string): ExtractedContract[] {
|
||||
const out: ExtractedContract[] = [];
|
||||
const re = /axios\.(get|post|put|delete|patch)\s*\(\s*[`'"]([^`'"]+)[`'"]/gi;
|
||||
let m: RegExpExecArray | null;
|
||||
while ((m = re.exec(content)) !== null) {
|
||||
const method = m[1].toUpperCase();
|
||||
const pathNorm = normalizeHttpPath(this.templateToPattern(m[2]));
|
||||
const pathNorm = this.normalizeConsumerPath(m[2]);
|
||||
out.push(this.makeConsumer(filePath, method, pathNorm, 0.7));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private scanPythonRequestsConsumers(content: string, filePath: string): ExtractedContract[] {
|
||||
const out: ExtractedContract[] = [];
|
||||
const methodRe = /requests\.(get|post|put|delete|patch)\s*\(\s*['"]([^'"]+)['"]/gi;
|
||||
let m: RegExpExecArray | null;
|
||||
while ((m = methodRe.exec(content)) !== null) {
|
||||
out.push(this.makeConsumer(filePath, m[1].toUpperCase(), this.normalizeConsumerPath(m[2]), 0.7));
|
||||
}
|
||||
|
||||
const genericRe =
|
||||
/requests\.request\s*\(\s*['"](\w+)['"]\s*,\s*['"]([^'"]+)['"]/gi;
|
||||
while ((m = genericRe.exec(content)) !== null) {
|
||||
out.push(this.makeConsumer(filePath, m[1].toUpperCase(), this.normalizeConsumerPath(m[2]), 0.7));
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
private scanJavaConsumers(content: string, filePath: string): ExtractedContract[] {
|
||||
const out: ExtractedContract[] = [];
|
||||
const restTemplateMethods: Array<[RegExp, string]> = [
|
||||
[/restTemplate\.getFor(?:Object|Entity)\s*\(\s*['"]([^'"]+)['"]/gi, 'GET'],
|
||||
[/restTemplate\.postFor(?:Object|Entity)\s*\(\s*['"]([^'"]+)['"]/gi, 'POST'],
|
||||
[/restTemplate\.put\s*\(\s*['"]([^'"]+)['"]/gi, 'PUT'],
|
||||
[/restTemplate\.delete\s*\(\s*['"]([^'"]+)['"]/gi, 'DELETE'],
|
||||
[/restTemplate\.patchForObject\s*\(\s*['"]([^'"]+)['"]/gi, 'PATCH'],
|
||||
];
|
||||
for (const [re, method] of restTemplateMethods) {
|
||||
let m: RegExpExecArray | null;
|
||||
while ((m = re.exec(content)) !== null) {
|
||||
out.push(this.makeConsumer(filePath, method, this.normalizeConsumerPath(m[1]), 0.7));
|
||||
}
|
||||
}
|
||||
|
||||
const webClientMethodRe =
|
||||
/webClient\.method\s*\(\s*HttpMethod\.(GET|POST|PUT|DELETE|PATCH)\s*,\s*['"]([^'"]+)['"]/gi;
|
||||
let m: RegExpExecArray | null;
|
||||
while ((m = webClientMethodRe.exec(content)) !== null) {
|
||||
out.push(this.makeConsumer(filePath, m[1].toUpperCase(), this.normalizeConsumerPath(m[2]), 0.7));
|
||||
}
|
||||
|
||||
const okHttpRe =
|
||||
/new\s+Request\.Builder\s*\(\)\s*\.url\s*\(\s*['"]([^'"]+)['"]\s*\)(?:\s*\.\s*method\s*\(\s*['"](\w+)['"])?/gim;
|
||||
while ((m = okHttpRe.exec(content)) !== null) {
|
||||
out.push(
|
||||
this.makeConsumer(filePath, (m[2] || 'GET').toUpperCase(), this.normalizeConsumerPath(m[1]), 0.7),
|
||||
);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
private scanGoConsumers(content: string, filePath: string): ExtractedContract[] {
|
||||
const out: ExtractedContract[] = [];
|
||||
const httpMethodRe = /\bhttp\.(Get|Post|Head)\s*\(\s*['"]([^'"]+)['"]/gi;
|
||||
let m: RegExpExecArray | null;
|
||||
while ((m = httpMethodRe.exec(content)) !== null) {
|
||||
const method = m[1].toUpperCase() === 'HEAD' ? 'GET' : m[1].toUpperCase();
|
||||
out.push(this.makeConsumer(filePath, method, this.normalizeConsumerPath(m[2]), 0.7));
|
||||
}
|
||||
|
||||
const newRequestRe =
|
||||
/\bhttp\.NewRequest\s*\(\s*['"](\w+)['"]\s*,\s*['"]([^'"]+)['"]/gi;
|
||||
while ((m = newRequestRe.exec(content)) !== null) {
|
||||
out.push(this.makeConsumer(filePath, m[1].toUpperCase(), this.normalizeConsumerPath(m[2]), 0.7));
|
||||
}
|
||||
|
||||
const restyRe = /\b\w+\.R\(\)\.(Get|Post|Put|Delete|Patch)\s*\(\s*['"]([^'"]+)['"]/gi;
|
||||
while ((m = restyRe.exec(content)) !== null) {
|
||||
out.push(this.makeConsumer(filePath, m[1].toUpperCase(), this.normalizeConsumerPath(m[2]), 0.7));
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
private makeConsumer(
|
||||
filePath: string,
|
||||
method: string,
|
||||
|
|
|
|||
|
|
@ -157,6 +157,89 @@ export default router;
|
|||
providers.find((c) => c.contractId === 'http::DELETE::/api/users/{param}'),
|
||||
).toBeDefined();
|
||||
});
|
||||
|
||||
it('extracts Go Gin and Echo route registrations', async () => {
|
||||
const dir = path.join(tmpDir, 'go-frameworks');
|
||||
fs.mkdirSync(path.join(dir, 'cmd'), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(dir, 'cmd', 'server.go'),
|
||||
`
|
||||
package main
|
||||
|
||||
func createOrder(c *gin.Context) {}
|
||||
func listOrders(c echo.Context) error { return nil }
|
||||
|
||||
func main() {
|
||||
r := gin.Default()
|
||||
r.POST("/api/orders/:id", createOrder)
|
||||
|
||||
e := echo.New()
|
||||
e.GET("/api/orders", listOrders)
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
const contracts = await extractor.extract(null, dir, makeRepo(dir));
|
||||
const providers = contracts.filter((c) => c.role === 'provider');
|
||||
|
||||
const ginRoute = providers.find((c) => c.contractId === 'http::POST::/api/orders/{param}');
|
||||
expect(ginRoute).toBeDefined();
|
||||
expect(ginRoute?.symbolName).toBe('createOrder');
|
||||
|
||||
const echoRoute = providers.find((c) => c.contractId === 'http::GET::/api/orders');
|
||||
expect(echoRoute).toBeDefined();
|
||||
expect(echoRoute?.symbolName).toBe('listOrders');
|
||||
});
|
||||
|
||||
it('extracts stdlib HandleFunc providers', async () => {
|
||||
const dir = path.join(tmpDir, 'go-stdlib-provider');
|
||||
fs.mkdirSync(path.join(dir, 'cmd'), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(dir, 'cmd', 'server.go'),
|
||||
`
|
||||
package main
|
||||
|
||||
func healthHandler(w http.ResponseWriter, r *http.Request) {}
|
||||
|
||||
func main() {
|
||||
http.HandleFunc("/api/health", healthHandler)
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
const contracts = await extractor.extract(null, dir, makeRepo(dir));
|
||||
const providers = contracts.filter((c) => c.role === 'provider');
|
||||
|
||||
const healthRoute = providers.find((c) => c.contractId === 'http::GET::/api/health');
|
||||
expect(healthRoute).toBeDefined();
|
||||
expect(healthRoute?.symbolName).toBe('healthHandler');
|
||||
});
|
||||
|
||||
it('extracts NestJS controller decorators', async () => {
|
||||
const dir = path.join(tmpDir, 'nestjs');
|
||||
fs.mkdirSync(path.join(dir, 'src'), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(dir, 'src', 'orders.controller.ts'),
|
||||
`
|
||||
import { Controller, Patch } from '@nestjs/common';
|
||||
|
||||
@Controller('orders')
|
||||
export class OrdersController {
|
||||
@Patch(':id')
|
||||
updateOrder() {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
const contracts = await extractor.extract(null, dir, makeRepo(dir));
|
||||
const providers = contracts.filter((c) => c.role === 'provider');
|
||||
|
||||
const patchRoute = providers.find((c) => c.contractId === 'http::PATCH::/orders/{param}');
|
||||
expect(patchRoute).toBeDefined();
|
||||
expect(patchRoute?.symbolName).toBe('updateOrder');
|
||||
});
|
||||
});
|
||||
|
||||
describe('consumer extraction — fetch patterns', () => {
|
||||
|
|
@ -206,6 +289,93 @@ export const deleteUser = (id: string) => axios.delete(\`/api/users/\${id}\`);
|
|||
consumers.find((c) => c.contractId === 'http::DELETE::/api/users/{param}'),
|
||||
).toBeDefined();
|
||||
});
|
||||
|
||||
it('extracts Python requests calls', async () => {
|
||||
const dir = path.join(tmpDir, 'python-consumer');
|
||||
fs.mkdirSync(path.join(dir, 'src'), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(dir, 'src', 'client.py'),
|
||||
`
|
||||
import requests
|
||||
|
||||
def create_order():
|
||||
return requests.post("https://svc.local/api/orders/42", json={"id": 42})
|
||||
`,
|
||||
);
|
||||
|
||||
const contracts = await extractor.extract(null, dir, makeRepo(dir));
|
||||
const consumers = contracts.filter((c) => c.role === 'consumer');
|
||||
|
||||
expect(
|
||||
consumers.find((c) => c.contractId === 'http::POST::/api/orders/{param}'),
|
||||
).toBeDefined();
|
||||
});
|
||||
|
||||
it('extracts Java RestTemplate, WebClient and OkHttp calls', async () => {
|
||||
const dir = path.join(tmpDir, 'java-consumer');
|
||||
fs.mkdirSync(path.join(dir, 'src'), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(dir, 'src', 'ApiClient.java'),
|
||||
`
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
import okhttp3.Request;
|
||||
|
||||
class ApiClient {
|
||||
void run(RestTemplate restTemplate, WebClient webClient) {
|
||||
restTemplate.getForObject("/api/users/{id}", String.class, 42);
|
||||
webClient.method(HttpMethod.PATCH, "/api/users/42");
|
||||
new Request.Builder().url("/api/orders/42").build();
|
||||
}
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
const contracts = await extractor.extract(null, dir, makeRepo(dir));
|
||||
const consumers = contracts.filter((c) => c.role === 'consumer');
|
||||
|
||||
expect(
|
||||
consumers.find((c) => c.contractId === 'http::GET::/api/users/{param}'),
|
||||
).toBeDefined();
|
||||
expect(
|
||||
consumers.find((c) => c.contractId === 'http::PATCH::/api/users/{param}'),
|
||||
).toBeDefined();
|
||||
expect(
|
||||
consumers.find((c) => c.contractId === 'http::GET::/api/orders/{param}'),
|
||||
).toBeDefined();
|
||||
});
|
||||
|
||||
it('extracts Go stdlib and resty calls', async () => {
|
||||
const dir = path.join(tmpDir, 'go-consumer');
|
||||
fs.mkdirSync(path.join(dir, 'cmd'), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(dir, 'cmd', 'client.go'),
|
||||
`
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-resty/resty/v2"
|
||||
)
|
||||
|
||||
func main() {
|
||||
http.Get("/api/health")
|
||||
client := resty.New()
|
||||
client.R().Delete("/api/orders/42")
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
const contracts = await extractor.extract(null, dir, makeRepo(dir));
|
||||
const consumers = contracts.filter((c) => c.role === 'consumer');
|
||||
|
||||
expect(consumers.find((c) => c.contractId === 'http::GET::/api/health')).toBeDefined();
|
||||
expect(
|
||||
consumers.find((c) => c.contractId === 'http::DELETE::/api/orders/{param}'),
|
||||
).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('provider extraction — Laravel', () => {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue