Skip to content

Commit d256af3

Browse files
committed
feat: Optimize reference resolution with indexed queries and built-in filtering
Replaces O(n) file scanning with O(log n) indexed database lookups by adding getAllNodeNames query and caching node lookups by name/qualified name. Pre-filters references against known symbol names to skip expensive resolution for non-existent symbols. Consolidates Go resolver helper functions into a unified resolveByNameAndKind function and moves built-in symbol sets to module-level constants for better performance.
1 parent cacc213 commit d256af3

3 files changed

Lines changed: 223 additions & 162 deletions

File tree

src/db/queries.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ export class QueryBuilder {
178178
getUnresolvedCount?: SqliteStatement;
179179
getUnresolvedBatch?: SqliteStatement;
180180
getAllFilePaths?: SqliteStatement;
181+
getAllNodeNames?: SqliteStatement;
181182
} = {};
182183

183184
constructor(db: SqliteDatabase) {
@@ -976,6 +977,17 @@ export class QueryBuilder {
976977
return rows.map((r) => r.path);
977978
}
978979

980+
/**
981+
* Get all distinct node names (lightweight — just name strings for pre-filtering)
982+
*/
983+
getAllNodeNames(): string[] {
984+
if (!this.stmts.getAllNodeNames) {
985+
this.stmts.getAllNodeNames = this.db.prepare('SELECT DISTINCT name FROM nodes');
986+
}
987+
const rows = this.stmts.getAllNodeNames.all() as Array<{ name: string }>;
988+
return rows.map((r) => r.name);
989+
}
990+
979991
/**
980992
* Get unresolved references scoped to specific file paths.
981993
* Uses the idx_unresolved_file_path index for efficient lookup.

src/resolution/frameworks/go.ts

Lines changed: 42 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export const goResolver: FrameworkResolver = {
2525
resolve(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null {
2626
// Pattern 1: Handler references
2727
if (ref.referenceName.endsWith('Handler') || ref.referenceName.startsWith('Handle')) {
28-
const result = resolveHandler(ref.referenceName, context);
28+
const result = resolveByNameAndKind(ref.referenceName, 'function', HANDLER_DIRS, context);
2929
if (result) {
3030
return {
3131
original: ref,
@@ -38,7 +38,7 @@ export const goResolver: FrameworkResolver = {
3838

3939
// Pattern 2: Service/Repository references
4040
if (ref.referenceName.endsWith('Service') || ref.referenceName.endsWith('Repository') || ref.referenceName.endsWith('Store')) {
41-
const result = resolveService(ref.referenceName, context);
41+
const result = resolveByNameAndKind(ref.referenceName, null, SERVICE_DIRS, context, SERVICE_KINDS);
4242
if (result) {
4343
return {
4444
original: ref,
@@ -51,7 +51,7 @@ export const goResolver: FrameworkResolver = {
5151

5252
// Pattern 3: Middleware references
5353
if (ref.referenceName.endsWith('Middleware') || ref.referenceName.startsWith('Auth') || ref.referenceName.startsWith('Log')) {
54-
const result = resolveMiddleware(ref.referenceName, context);
54+
const result = resolveByNameAndKind(ref.referenceName, 'function', MIDDLEWARE_DIRS, context);
5555
if (result) {
5656
return {
5757
original: ref,
@@ -64,7 +64,7 @@ export const goResolver: FrameworkResolver = {
6464

6565
// Pattern 4: Model/Entity references (typically PascalCase structs)
6666
if (/^[A-Z][a-zA-Z]+$/.test(ref.referenceName)) {
67-
const result = resolveModel(ref.referenceName, context);
67+
const result = resolveByNameAndKind(ref.referenceName, 'struct', MODEL_DIRS, context);
6868
if (result) {
6969
return {
7070
original: ref,
@@ -178,93 +178,43 @@ export const goResolver: FrameworkResolver = {
178178
},
179179
};
180180

181-
// Helper functions
181+
// Directory patterns for framework resolution
182+
const HANDLER_DIRS = ['handler', 'handlers', 'api', 'routes', 'controller', 'controllers'];
183+
const SERVICE_DIRS = ['service', 'services', 'repository', 'store', 'pkg'];
184+
const MIDDLEWARE_DIRS = ['middleware', 'middlewares'];
185+
const MODEL_DIRS = ['model', 'models', 'entity', 'entities', 'domain', 'pkg'];
186+
const SERVICE_KINDS = new Set(['struct', 'interface']);
182187

183-
function resolveHandler(name: string, context: ResolutionContext): string | null {
184-
const handlerDirs = ['handler', 'handlers', 'api', 'routes', 'controller', 'controllers'];
185-
186-
const allFiles = context.getAllFiles();
187-
for (const file of allFiles) {
188-
if (file.endsWith('.go') && handlerDirs.some((d) => file.includes(`/${d}/`))) {
189-
const nodes = context.getNodesInFile(file);
190-
const handlerNode = nodes.find(
191-
(n) => n.kind === 'function' && n.name === name
192-
);
193-
if (handlerNode) {
194-
return handlerNode.id;
195-
}
196-
}
197-
}
198-
199-
// Search all go files
200-
for (const file of allFiles) {
201-
if (file.endsWith('.go')) {
202-
const nodes = context.getNodesInFile(file);
203-
const handlerNode = nodes.find(
204-
(n) => n.kind === 'function' && n.name === name
205-
);
206-
if (handlerNode) {
207-
return handlerNode.id;
208-
}
209-
}
210-
}
211-
212-
return null;
213-
}
214-
215-
function resolveService(name: string, context: ResolutionContext): string | null {
216-
const serviceDirs = ['service', 'services', 'repository', 'store', 'pkg'];
217-
218-
const allFiles = context.getAllFiles();
219-
for (const file of allFiles) {
220-
if (file.endsWith('.go') && serviceDirs.some((d) => file.includes(`/${d}/`))) {
221-
const nodes = context.getNodesInFile(file);
222-
const serviceNode = nodes.find(
223-
(n) => (n.kind === 'struct' || n.kind === 'interface') && n.name === name
224-
);
225-
if (serviceNode) {
226-
return serviceNode.id;
227-
}
228-
}
229-
}
230-
231-
return null;
232-
}
233-
234-
function resolveMiddleware(name: string, context: ResolutionContext): string | null {
235-
const middlewareDirs = ['middleware', 'middlewares'];
236-
237-
const allFiles = context.getAllFiles();
238-
for (const file of allFiles) {
239-
if (file.endsWith('.go') && middlewareDirs.some((d) => file.includes(`/${d}/`))) {
240-
const nodes = context.getNodesInFile(file);
241-
const mwNode = nodes.find(
242-
(n) => n.kind === 'function' && n.name === name
243-
);
244-
if (mwNode) {
245-
return mwNode.id;
246-
}
247-
}
248-
}
249-
250-
return null;
251-
}
252-
253-
function resolveModel(name: string, context: ResolutionContext): string | null {
254-
const modelDirs = ['model', 'models', 'entity', 'entities', 'domain', 'pkg'];
255-
256-
const allFiles = context.getAllFiles();
257-
for (const file of allFiles) {
258-
if (file.endsWith('.go') && modelDirs.some((d) => file.includes(`/${d}/`))) {
259-
const nodes = context.getNodesInFile(file);
260-
const modelNode = nodes.find(
261-
(n) => n.kind === 'struct' && n.name === name
262-
);
263-
if (modelNode) {
264-
return modelNode.id;
265-
}
266-
}
267-
}
268-
269-
return null;
188+
/**
189+
* Resolve a symbol by name using indexed queries instead of scanning all files.
190+
* Uses getNodesByName (O(log n) indexed lookup) instead of iterating every file.
191+
*/
192+
function resolveByNameAndKind(
193+
name: string,
194+
kind: string | null,
195+
preferredDirs: string[],
196+
context: ResolutionContext,
197+
kinds?: Set<string>
198+
): string | null {
199+
const candidates = context.getNodesByName(name);
200+
if (candidates.length === 0) return null;
201+
202+
// Filter by kind
203+
const kindFiltered = candidates.filter((n) => {
204+
if (kinds) return kinds.has(n.kind);
205+
if (kind) return n.kind === kind;
206+
return true;
207+
});
208+
209+
if (kindFiltered.length === 0) return null;
210+
211+
// Prefer candidates in framework-conventional directories
212+
const preferred = kindFiltered.filter((n) =>
213+
preferredDirs.some((d) => n.filePath.includes(`/${d}/`))
214+
);
215+
216+
if (preferred.length > 0) return preferred[0]!.id;
217+
218+
// Fall back to any match
219+
return kindFiltered[0]!.id;
270220
}

0 commit comments

Comments
 (0)