mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-06 02:49:56 +00:00
refactor: generalize deferred route finalization
Move deferred route finalization behind language provider hooks so Spring-specific candidate handling no longer leaks into the shared pipeline contract. Also broaden Java controller heuristics and preserve FQCN-backed Spring constant resolution. Made-with: Cursor
This commit is contained in:
parent
59db54f284
commit
70f39eabca
9 changed files with 95 additions and 25 deletions
|
|
@ -20,7 +20,9 @@ import type { ImportResolverFn } from './import-resolvers/types.js';
|
|||
import type { NamedBindingExtractorFn } from './named-bindings/types.js';
|
||||
import type { SyntaxNode } from './utils/ast-helpers.js';
|
||||
import type { NodeLabel } from 'gitnexus-shared';
|
||||
import type { ExtractedDeferredRouteCandidate } from './route-extractors/spring-java-types.js';
|
||||
import type { DeferredRouteCandidate } from './route-extractors/deferred-route-types.js';
|
||||
import type { ResolutionContext } from './resolution-context.js';
|
||||
import type { ExtractedRoute } from './workers/parse-worker.js';
|
||||
|
||||
// ── Shared type aliases ────────────────────────────────────────────────────
|
||||
/** Tree-sitter query captures: capture name → AST node (or undefined if not captured). */
|
||||
|
|
@ -150,7 +152,13 @@ interface LanguageProviderConfig {
|
|||
readonly deferredRouteExtractor?: (
|
||||
tree: Parser.Tree,
|
||||
filePath: string,
|
||||
) => ExtractedDeferredRouteCandidate[];
|
||||
) => DeferredRouteCandidate[];
|
||||
/** Finalize deferred route candidates after imports/symbols are available.
|
||||
* Default: undefined (no deferred route finalization). */
|
||||
readonly deferredRouteFinalizer?: (
|
||||
candidates: DeferredRouteCandidate[],
|
||||
ctx: ResolutionContext,
|
||||
) => ExtractedRoute[];
|
||||
|
||||
// ── Noise filtering ────────────────────────────────────────────────
|
||||
/** Built-in/stdlib names that should be filtered from the call graph for this language.
|
||||
|
|
|
|||
|
|
@ -17,15 +17,26 @@ import { JAVA_QUERIES } from '../tree-sitter-queries.js';
|
|||
import { createFieldExtractor } from '../field-extractors/generic.js';
|
||||
import { javaConfig } from '../field-extractors/configs/jvm.js';
|
||||
import { createMethodExtractor } from '../method-extractors/generic.js';
|
||||
import { extractSpringJavaRouteCandidates } from '../route-extractors/spring-java.js';
|
||||
import {
|
||||
extractSpringJavaRouteCandidates,
|
||||
finalizeSpringJavaRoutes,
|
||||
} from '../route-extractors/spring-java.js';
|
||||
import { javaMethodConfig } from '../method-extractors/configs/jvm.js';
|
||||
|
||||
const SPRING_ROUTE_FILE_SUFFIXES = [
|
||||
'Controller.java',
|
||||
'Resource.java',
|
||||
'Endpoint.java',
|
||||
'Api.java',
|
||||
'Handler.java',
|
||||
];
|
||||
const SPRING_ROUTE_DIR_HINTS = ['/controller/', '/controllers/', '/rest/', '/api/', '/web/'];
|
||||
|
||||
function isSpringRouteFile(filePath: string): boolean {
|
||||
const normalized = filePath.replace(/\\/g, '/');
|
||||
return (
|
||||
normalized.endsWith('Controller.java') ||
|
||||
normalized.includes('/controller/') ||
|
||||
normalized.includes('/controllers/')
|
||||
SPRING_ROUTE_FILE_SUFFIXES.some((suffix) => normalized.endsWith(suffix)) ||
|
||||
SPRING_ROUTE_DIR_HINTS.some((segment) => normalized.includes(segment))
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -43,4 +54,5 @@ export const javaProvider = defineLanguage({
|
|||
methodExtractor: createMethodExtractor(javaMethodConfig),
|
||||
isRouteFile: isSpringRouteFile,
|
||||
deferredRouteExtractor: extractSpringJavaRouteCandidates,
|
||||
deferredRouteFinalizer: finalizeSpringJavaRoutes,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import {
|
|||
} from './utils/ast-helpers.js';
|
||||
import { detectFrameworkFromAST } from './framework-detection.js';
|
||||
import { buildTypeEnv } from './type-env.js';
|
||||
import type { ExtractedDeferredRouteCandidate } from './route-extractors/spring-java-types.js';
|
||||
import type { DeferredRouteCandidate } from './route-extractors/deferred-route-types.js';
|
||||
import type { FieldInfo, FieldExtractorContext } from './field-types.js';
|
||||
import type { LanguageProvider } from './language-provider.js';
|
||||
import { WorkerPool } from './workers/worker-pool.js';
|
||||
|
|
@ -49,7 +49,7 @@ export interface WorkerExtractedData {
|
|||
routes: ExtractedRoute[];
|
||||
fetchCalls: ExtractedFetchCall[];
|
||||
decoratorRoutes: ExtractedDecoratorRoute[];
|
||||
deferredRouteCandidates: ExtractedDeferredRouteCandidate[];
|
||||
deferredRouteCandidates: DeferredRouteCandidate[];
|
||||
toolDefs: ExtractedToolDef[];
|
||||
ormQueries: ExtractedORMQuery[];
|
||||
constructorBindings: FileConstructorBindings[];
|
||||
|
|
@ -112,7 +112,7 @@ const processParsingWithWorkers = async (
|
|||
const allRoutes: ExtractedRoute[] = [];
|
||||
const allFetchCalls: ExtractedFetchCall[] = [];
|
||||
const allDecoratorRoutes: ExtractedDecoratorRoute[] = [];
|
||||
const allDeferredRouteCandidates: ExtractedDeferredRouteCandidate[] = [];
|
||||
const allDeferredRouteCandidates: DeferredRouteCandidate[] = [];
|
||||
const allToolDefs: ExtractedToolDef[] = [];
|
||||
const allORMQueries: ExtractedORMQuery[] = [];
|
||||
const allConstructorBindings: FileConstructorBindings[] = [];
|
||||
|
|
|
|||
|
|
@ -25,8 +25,7 @@ import {
|
|||
mergeImplementorMaps,
|
||||
} from './call-processor.js';
|
||||
import { nextjsFileToRouteURL, normalizeFetchURL } from './route-extractors/nextjs.js';
|
||||
import { finalizeSpringJavaRoutes } from './route-extractors/spring-java.js';
|
||||
import type { ExtractedDeferredRouteCandidate } from './route-extractors/spring-java-types.js';
|
||||
import type { DeferredRouteCandidate } from './route-extractors/deferred-route-types.js';
|
||||
import { expoFileToRouteURL } from './route-extractors/expo.js';
|
||||
import { phpFileToRouteURL } from './route-extractors/php.js';
|
||||
import {
|
||||
|
|
@ -786,7 +785,7 @@ async function runChunkedParseAndResolve(
|
|||
const allExtractedRoutes: ExtractedRoute[] = [];
|
||||
// Accumulate decorator-based routes (@Get, @Post, @app.route, etc.)
|
||||
const allDecoratorRoutes: ExtractedDecoratorRoute[] = [];
|
||||
const allDeferredRouteCandidates: ExtractedDeferredRouteCandidate[] = [];
|
||||
const allDeferredRouteCandidates: DeferredRouteCandidate[] = [];
|
||||
// Accumulate MCP/RPC tool definitions (@mcp.tool(), @app.tool(), etc.)
|
||||
const allToolDefs: ExtractedToolDef[] = [];
|
||||
const allORMQueries: ExtractedORMQuery[] = [];
|
||||
|
|
@ -1032,10 +1031,17 @@ async function runChunkedParseAndResolve(
|
|||
astCache.clear();
|
||||
}
|
||||
|
||||
const finalizedSpringRoutes = finalizeSpringJavaRoutes(allDeferredRouteCandidates, ctx);
|
||||
if (finalizedSpringRoutes.length > 0) {
|
||||
await processRoutesFromExtracted(graph, finalizedSpringRoutes, ctx);
|
||||
allExtractedRoutes.push(...finalizedSpringRoutes);
|
||||
for (const provider of Object.values(providers)) {
|
||||
if (!provider.deferredRouteFinalizer) continue;
|
||||
|
||||
const finalizedRoutes = provider.deferredRouteFinalizer(
|
||||
allDeferredRouteCandidates.filter((candidate) => candidate.language === provider.id),
|
||||
ctx,
|
||||
);
|
||||
if (finalizedRoutes.length === 0) continue;
|
||||
|
||||
await processRoutesFromExtracted(graph, finalizedRoutes, ctx);
|
||||
allExtractedRoutes.push(...finalizedRoutes);
|
||||
}
|
||||
|
||||
// Log resolution cache stats
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
import type { SupportedLanguages } from 'gitnexus-shared';
|
||||
|
||||
export interface DeferredRouteCandidate {
|
||||
kind: string;
|
||||
language: SupportedLanguages;
|
||||
filePath: string;
|
||||
lineNumber: number;
|
||||
}
|
||||
|
|
@ -1,9 +1,14 @@
|
|||
import { SupportedLanguages } from 'gitnexus-shared';
|
||||
import type { DeferredRouteCandidate } from './deferred-route-types.js';
|
||||
|
||||
export type SpringRoutePathExpression =
|
||||
| { kind: 'literal'; value: string }
|
||||
| { kind: 'identifier'; name: string }
|
||||
| { kind: 'field-access'; ownerPath: string[]; fieldName: string };
|
||||
|
||||
export interface ExtractedSpringJavaRouteCandidate {
|
||||
export interface ExtractedSpringJavaRouteCandidate extends DeferredRouteCandidate {
|
||||
kind: 'spring-java';
|
||||
language: SupportedLanguages.Java;
|
||||
filePath: string;
|
||||
controllerName: string;
|
||||
methodName: string;
|
||||
|
|
@ -14,5 +19,3 @@ export interface ExtractedSpringJavaRouteCandidate {
|
|||
hasExplicitMethodPath: boolean;
|
||||
lineNumber: number;
|
||||
}
|
||||
|
||||
export type ExtractedDeferredRouteCandidate = ExtractedSpringJavaRouteCandidate;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { SupportedLanguages } from 'gitnexus-shared';
|
||||
import type Parser from 'tree-sitter';
|
||||
import type { ResolutionContext } from '../resolution-context.js';
|
||||
import type { SymbolDefinition } from '../symbol-table.js';
|
||||
|
|
@ -6,6 +7,7 @@ import type {
|
|||
ExtractedSpringJavaRouteCandidate,
|
||||
SpringRoutePathExpression,
|
||||
} from './spring-java-types.js';
|
||||
import type { DeferredRouteCandidate } from './deferred-route-types.js';
|
||||
import { extractJavaStringLiteral } from '../utils/java-strings.js';
|
||||
import { findChild, type SyntaxNode } from '../utils/ast-helpers.js';
|
||||
|
||||
|
|
@ -227,15 +229,36 @@ function resolveNamedImportConstant(
|
|||
return def?.constantValue ?? null;
|
||||
}
|
||||
|
||||
function normalizeFilePath(filePath: string): string {
|
||||
return filePath.replace(/\\/g, '/');
|
||||
}
|
||||
|
||||
function resolveQualifiedClassLike(
|
||||
ownerPath: string[],
|
||||
filePath: string,
|
||||
ctx: ResolutionContext,
|
||||
): SymbolDefinition | null {
|
||||
const ownerName = ownerPath[ownerPath.length - 1];
|
||||
if (!ownerName) return null;
|
||||
|
||||
const exact = resolveUniqueClassLike(ownerName, filePath, ctx);
|
||||
if (exact || ownerPath.length === 1) return exact;
|
||||
|
||||
const qualifiedSuffix = `/${ownerPath.join('/')}.java`;
|
||||
const classLikes = ctx.symbols
|
||||
.lookupFuzzy(ownerName)
|
||||
.filter((candidate) => CLASS_LIKE_TYPES.has(candidate.type))
|
||||
.filter((candidate) => normalizeFilePath(candidate.filePath).endsWith(qualifiedSuffix));
|
||||
return classLikes.length === 1 ? classLikes[0] : null;
|
||||
}
|
||||
|
||||
function resolveFieldAccessConstant(
|
||||
ownerPath: string[],
|
||||
fieldName: string,
|
||||
filePath: string,
|
||||
ctx: ResolutionContext,
|
||||
): string | null {
|
||||
const ownerName = ownerPath[ownerPath.length - 1];
|
||||
if (!ownerName) return null;
|
||||
const ownerDef = resolveUniqueClassLike(ownerName, filePath, ctx);
|
||||
const ownerDef = resolveQualifiedClassLike(ownerPath, filePath, ctx);
|
||||
if (!ownerDef) return null;
|
||||
return ctx.symbols.lookupFieldByOwner(ownerDef.nodeId, fieldName)?.constantValue ?? null;
|
||||
}
|
||||
|
|
@ -300,6 +323,8 @@ function buildSpringRouteCandidate(
|
|||
extractRequestMappingPath(mappingAnnotation);
|
||||
|
||||
return {
|
||||
kind: 'spring-java',
|
||||
language: SupportedLanguages.Java,
|
||||
filePath,
|
||||
controllerName: className,
|
||||
methodName,
|
||||
|
|
@ -348,13 +373,20 @@ export function extractSpringJavaRouteCandidates(
|
|||
return candidates;
|
||||
}
|
||||
|
||||
function isSpringJavaRouteCandidate(
|
||||
candidate: DeferredRouteCandidate,
|
||||
): candidate is ExtractedSpringJavaRouteCandidate {
|
||||
return candidate.kind === 'spring-java';
|
||||
}
|
||||
|
||||
export function finalizeSpringJavaRoutes(
|
||||
candidates: ExtractedSpringJavaRouteCandidate[],
|
||||
candidates: DeferredRouteCandidate[],
|
||||
ctx: ResolutionContext,
|
||||
): ExtractedRoute[] {
|
||||
const routes: ExtractedRoute[] = [];
|
||||
|
||||
for (const candidate of candidates) {
|
||||
if (!isSpringJavaRouteCandidate(candidate)) continue;
|
||||
const classPrefix = resolvePathExpression(
|
||||
candidate.classPathExpression,
|
||||
candidate.filePath,
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ try {
|
|||
Kotlin = _require('tree-sitter-kotlin');
|
||||
} catch {}
|
||||
import { getLanguageFromFilename } from 'gitnexus-shared';
|
||||
import type { ExtractedDeferredRouteCandidate } from '../route-extractors/spring-java-types.js';
|
||||
import type { DeferredRouteCandidate } from '../route-extractors/deferred-route-types.js';
|
||||
import {
|
||||
FUNCTION_NODE_TYPES,
|
||||
extractFunctionName,
|
||||
|
|
@ -247,7 +247,7 @@ export interface ParseWorkerResult {
|
|||
routes: ExtractedRoute[];
|
||||
fetchCalls: ExtractedFetchCall[];
|
||||
decoratorRoutes: ExtractedDecoratorRoute[];
|
||||
deferredRouteCandidates: ExtractedDeferredRouteCandidate[];
|
||||
deferredRouteCandidates: DeferredRouteCandidate[];
|
||||
toolDefs: ExtractedToolDef[];
|
||||
ormQueries: ExtractedORMQuery[];
|
||||
constructorBindings: FileConstructorBindings[];
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ describe('Spring route mapping', () => {
|
|||
expect(routes).toContain('/api/users/fqcn');
|
||||
expect(routes).toContain('/health');
|
||||
expect(routes).toContain('/status');
|
||||
expect(routes).toHaveLength(7);
|
||||
|
||||
expect(routes).not.toContain('/');
|
||||
expect(routes).not.toContain('/api');
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue