feat: Enhance symbol search with co-location boosting and receiver type support
Improves search accuracy by boosting results when multiple query symbols appear in the same file, addressing cases where common names like "run" return too many results. Adds Go method receiver type extraction to qualified names for better searchability (e.g., "scrapeLoop.run"). Optimizes database queries with two-pass approach to handle distinctive vs common symbol names efficiently.
This commit is contained in:
+27
-1
@@ -291,10 +291,36 @@ export class ContextBuilder {
|
|||||||
let exactMatches: SearchResult[] = [];
|
let exactMatches: SearchResult[] = [];
|
||||||
if (symbolsFromQuery.length > 0) {
|
if (symbolsFromQuery.length > 0) {
|
||||||
try {
|
try {
|
||||||
|
// Get more results so we can apply co-location boosting before trimming
|
||||||
exactMatches = this.queries.findNodesByExactName(symbolsFromQuery, {
|
exactMatches = this.queries.findNodesByExactName(symbolsFromQuery, {
|
||||||
limit: Math.ceil(opts.searchLimit * 2), // Get more since we'll merge
|
limit: Math.ceil(opts.searchLimit * 5),
|
||||||
kinds: opts.nodeKinds && opts.nodeKinds.length > 0 ? opts.nodeKinds : undefined,
|
kinds: opts.nodeKinds && opts.nodeKinds.length > 0 ? opts.nodeKinds : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Co-location boost: when multiple extracted symbols appear in the same file,
|
||||||
|
// those results are much more likely to be what the user is looking for.
|
||||||
|
// E.g., "scrapeLoop" + "run" both in scrape/scrape.go → boost both.
|
||||||
|
if (exactMatches.length > 1) {
|
||||||
|
// Build a map of files → how many distinct symbol names matched in that file
|
||||||
|
const fileSymbolCounts = new Map<string, Set<string>>();
|
||||||
|
for (const r of exactMatches) {
|
||||||
|
const names = fileSymbolCounts.get(r.node.filePath) || new Set();
|
||||||
|
names.add(r.node.name.toLowerCase());
|
||||||
|
fileSymbolCounts.set(r.node.filePath, names);
|
||||||
|
}
|
||||||
|
// Boost results in files where multiple query symbols co-occur
|
||||||
|
exactMatches = exactMatches.map(r => {
|
||||||
|
const symbolCount = fileSymbolCounts.get(r.node.filePath)?.size || 1;
|
||||||
|
return {
|
||||||
|
...r,
|
||||||
|
score: symbolCount > 1 ? r.score + (symbolCount - 1) * 20 : r.score,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
exactMatches.sort((a, b) => b.score - a.score);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trim back to reasonable size
|
||||||
|
exactMatches = exactMatches.slice(0, Math.ceil(opts.searchLimit * 2));
|
||||||
logDebug('Exact symbol matches', { count: exactMatches.length });
|
logDebug('Exact symbol matches', { count: exactMatches.length });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logDebug('Exact symbol lookup failed', { error: String(error) });
|
logDebug('Exact symbol lookup failed', { error: String(error) });
|
||||||
|
|||||||
+59
-18
@@ -630,19 +630,44 @@ export class QueryBuilder {
|
|||||||
|
|
||||||
const { kinds, languages, limit = 50 } = options;
|
const { kinds, languages, limit = 50 } = options;
|
||||||
|
|
||||||
// Build query with exact matches (case-insensitive)
|
// Two-pass approach to handle common names (e.g., "run" has 40+ matches):
|
||||||
let sql = `
|
// Pass 1: Find which files contain distinctive (rare) symbols from the query.
|
||||||
SELECT nodes.*,
|
// Pass 2: Query each name, boosting results that co-locate with distinctive symbols.
|
||||||
CASE
|
|
||||||
WHEN name COLLATE NOCASE IN (${names.map(() => '?').join(',')}) THEN 1.0
|
|
||||||
ELSE 0.9
|
|
||||||
END as score
|
|
||||||
FROM nodes
|
|
||||||
WHERE name COLLATE NOCASE IN (${names.map(() => '?').join(',')})
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Duplicate names for both SELECT and WHERE clauses
|
// Pass 1: Find files containing each queried name, identify distinctive names
|
||||||
const params: (string | number)[] = [...names, ...names];
|
const nameToFiles = new Map<string, Set<string>>();
|
||||||
|
for (const name of names) {
|
||||||
|
let sql = 'SELECT DISTINCT file_path FROM nodes WHERE name COLLATE NOCASE = ?';
|
||||||
|
const params: (string | number)[] = [name];
|
||||||
|
if (kinds && kinds.length > 0) {
|
||||||
|
sql += ` AND kind IN (${kinds.map(() => '?').join(',')})`;
|
||||||
|
params.push(...kinds);
|
||||||
|
}
|
||||||
|
sql += ' LIMIT 100';
|
||||||
|
const rows = this.db.prepare(sql).all(...params) as { file_path: string }[];
|
||||||
|
nameToFiles.set(name.toLowerCase(), new Set(rows.map(r => r.file_path)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Distinctive names are those with fewer than 10 file matches (e.g., "scrapeLoop" = 1 file)
|
||||||
|
const distinctiveFiles = new Set<string>();
|
||||||
|
for (const [, files] of nameToFiles) {
|
||||||
|
if (files.size > 0 && files.size < 10) {
|
||||||
|
for (const f of files) distinctiveFiles.add(f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass 2: Query each name with per-name limit, scoring by co-location
|
||||||
|
const perNameLimit = Math.max(8, Math.ceil(limit / names.length));
|
||||||
|
const allResults: SearchResult[] = [];
|
||||||
|
const seenIds = new Set<string>();
|
||||||
|
|
||||||
|
for (const name of names) {
|
||||||
|
let sql = `
|
||||||
|
SELECT nodes.*, 1.0 as score
|
||||||
|
FROM nodes
|
||||||
|
WHERE name COLLATE NOCASE = ?
|
||||||
|
`;
|
||||||
|
const params: (string | number)[] = [name];
|
||||||
|
|
||||||
if (kinds && kinds.length > 0) {
|
if (kinds && kinds.length > 0) {
|
||||||
sql += ` AND kind IN (${kinds.map(() => '?').join(',')})`;
|
sql += ` AND kind IN (${kinds.map(() => '?').join(',')})`;
|
||||||
@@ -654,15 +679,31 @@ export class QueryBuilder {
|
|||||||
params.push(...languages);
|
params.push(...languages);
|
||||||
}
|
}
|
||||||
|
|
||||||
sql += ' ORDER BY score DESC, length(name) ASC LIMIT ?';
|
// Fetch enough to find co-located results among common names
|
||||||
params.push(limit);
|
sql += ' LIMIT ?';
|
||||||
|
params.push(Math.max(perNameLimit * 3, 50));
|
||||||
|
|
||||||
const rows = this.db.prepare(sql).all(...params) as (NodeRow & { score: number })[];
|
const rows = this.db.prepare(sql).all(...params) as (NodeRow & { score: number })[];
|
||||||
|
const nameResults: SearchResult[] = [];
|
||||||
|
for (const row of rows) {
|
||||||
|
const node = rowToNode(row);
|
||||||
|
if (seenIds.has(node.id)) continue;
|
||||||
|
// Boost results in files that also contain distinctive symbols
|
||||||
|
const coLocationBoost = distinctiveFiles.has(node.filePath) ? 20 : 0;
|
||||||
|
nameResults.push({ node, score: row.score + coLocationBoost });
|
||||||
|
}
|
||||||
|
|
||||||
return rows.map((row) => ({
|
// Sort by score (co-located first), take per-name limit
|
||||||
node: rowToNode(row),
|
nameResults.sort((a, b) => b.score - a.score);
|
||||||
score: row.score,
|
for (const r of nameResults.slice(0, perNameLimit)) {
|
||||||
}));
|
seenIds.add(r.node.id);
|
||||||
|
allResults.push(r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort all results by score so co-located results bubble up
|
||||||
|
allResults.sort((a, b) => b.score - a.score);
|
||||||
|
return allResults.slice(0, limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
|
|||||||
@@ -27,4 +27,16 @@ export const goExtractor: LanguageExtractor = {
|
|||||||
}
|
}
|
||||||
return sig;
|
return sig;
|
||||||
},
|
},
|
||||||
|
getReceiverType: (node, source) => {
|
||||||
|
// Go method_declaration has a "receiver" field: func (sl *scrapeLoop) run(...)
|
||||||
|
// The receiver is a parameter_list containing a parameter_declaration
|
||||||
|
// with a type that may be a pointer_type (*scrapeLoop) or plain type (scrapeLoop)
|
||||||
|
const receiver = getChildByField(node, 'receiver');
|
||||||
|
if (!receiver) return undefined;
|
||||||
|
// Find the type identifier inside the receiver
|
||||||
|
const text = getNodeText(receiver, source);
|
||||||
|
// Extract type name from patterns like "(sl *Type)", "(sl Type)", "(*Type)", "(Type)"
|
||||||
|
const match = text.match(/\*?\s*([A-Za-z_][A-Za-z0-9_]*)\s*\)/);
|
||||||
|
return match?.[1];
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -163,4 +163,11 @@ export interface LanguageExtractor {
|
|||||||
* Returns info about each declared variable, allowing the core to create nodes.
|
* Returns info about each declared variable, allowing the core to create nodes.
|
||||||
*/
|
*/
|
||||||
extractVariables?: (node: SyntaxNode, source: string) => VariableInfo[];
|
extractVariables?: (node: SyntaxNode, source: string) => VariableInfo[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract receiver/owner type name from a method declaration.
|
||||||
|
* Used by Go to get the struct receiver (e.g., "scrapeLoop" from "func (sl *scrapeLoop) run()").
|
||||||
|
* When present, the receiver type is included in the qualified name for better searchability.
|
||||||
|
*/
|
||||||
|
getReceiverType?: (node: SyntaxNode, source: string) => string | undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -505,13 +505,21 @@ export class TreeSitterExtractor {
|
|||||||
const isAsync = this.extractor.isAsync?.(node);
|
const isAsync = this.extractor.isAsync?.(node);
|
||||||
const isStatic = this.extractor.isStatic?.(node);
|
const isStatic = this.extractor.isStatic?.(node);
|
||||||
|
|
||||||
const methodNode = this.createNode('method', name, node, {
|
// For languages with receiver types (Go), include receiver in qualified name
|
||||||
|
// so FTS can match "scrapeLoop.run" → qualified_name "...::scrapeLoop::run"
|
||||||
|
const receiverType = this.extractor.getReceiverType?.(node, this.source);
|
||||||
|
const extraProps: Partial<Node> = {
|
||||||
docstring,
|
docstring,
|
||||||
signature,
|
signature,
|
||||||
visibility,
|
visibility,
|
||||||
isAsync,
|
isAsync,
|
||||||
isStatic,
|
isStatic,
|
||||||
});
|
};
|
||||||
|
if (receiverType) {
|
||||||
|
extraProps.qualifiedName = `${this.filePath}::${receiverType}::${name}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const methodNode = this.createNode('method', name, node, extraProps);
|
||||||
if (!methodNode) return;
|
if (!methodNode) return;
|
||||||
|
|
||||||
// Extract type annotations (parameter types and return type)
|
// Extract type annotations (parameter types and return type)
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export const aspnetResolver: FrameworkResolver = {
|
|||||||
resolve(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null {
|
resolve(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null {
|
||||||
// Pattern 1: Controller references
|
// Pattern 1: Controller references
|
||||||
if (ref.referenceName.endsWith('Controller')) {
|
if (ref.referenceName.endsWith('Controller')) {
|
||||||
const result = resolveController(ref.referenceName, context);
|
const result = resolveByNameAndKind(ref.referenceName, CLASS_KINDS, CONTROLLER_DIRS, context);
|
||||||
if (result) {
|
if (result) {
|
||||||
return {
|
return {
|
||||||
original: ref,
|
original: ref,
|
||||||
@@ -61,7 +61,7 @@ export const aspnetResolver: FrameworkResolver = {
|
|||||||
|
|
||||||
// Pattern 2: Service references (dependency injection)
|
// Pattern 2: Service references (dependency injection)
|
||||||
if (ref.referenceName.endsWith('Service') || ref.referenceName.startsWith('I') && ref.referenceName.length > 1) {
|
if (ref.referenceName.endsWith('Service') || ref.referenceName.startsWith('I') && ref.referenceName.length > 1) {
|
||||||
const result = resolveService(ref.referenceName, context);
|
const result = resolveByNameAndKind(ref.referenceName, SERVICE_KINDS, SERVICE_DIRS, context);
|
||||||
if (result) {
|
if (result) {
|
||||||
return {
|
return {
|
||||||
original: ref,
|
original: ref,
|
||||||
@@ -74,7 +74,7 @@ export const aspnetResolver: FrameworkResolver = {
|
|||||||
|
|
||||||
// Pattern 3: Repository references
|
// Pattern 3: Repository references
|
||||||
if (ref.referenceName.endsWith('Repository')) {
|
if (ref.referenceName.endsWith('Repository')) {
|
||||||
const result = resolveRepository(ref.referenceName, context);
|
const result = resolveByNameAndKind(ref.referenceName, SERVICE_KINDS, REPO_DIRS, context);
|
||||||
if (result) {
|
if (result) {
|
||||||
return {
|
return {
|
||||||
original: ref,
|
original: ref,
|
||||||
@@ -87,7 +87,7 @@ export const aspnetResolver: FrameworkResolver = {
|
|||||||
|
|
||||||
// Pattern 4: Model/Entity references
|
// Pattern 4: Model/Entity references
|
||||||
if (/^[A-Z][a-zA-Z]+$/.test(ref.referenceName)) {
|
if (/^[A-Z][a-zA-Z]+$/.test(ref.referenceName)) {
|
||||||
const result = resolveModel(ref.referenceName, context);
|
const result = resolveByNameAndKind(ref.referenceName, CLASS_KINDS, MODEL_DIRS, context);
|
||||||
if (result) {
|
if (result) {
|
||||||
return {
|
return {
|
||||||
original: ref,
|
original: ref,
|
||||||
@@ -100,7 +100,7 @@ export const aspnetResolver: FrameworkResolver = {
|
|||||||
|
|
||||||
// Pattern 5: ViewModel references
|
// Pattern 5: ViewModel references
|
||||||
if (ref.referenceName.endsWith('ViewModel') || ref.referenceName.endsWith('Dto')) {
|
if (ref.referenceName.endsWith('ViewModel') || ref.referenceName.endsWith('Dto')) {
|
||||||
const result = resolveViewModel(ref.referenceName, context);
|
const result = resolveByNameAndKind(ref.referenceName, CLASS_KINDS, VIEWMODEL_DIRS, context);
|
||||||
if (result) {
|
if (result) {
|
||||||
return {
|
return {
|
||||||
original: ref,
|
original: ref,
|
||||||
@@ -213,111 +213,38 @@ export const aspnetResolver: FrameworkResolver = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper functions
|
// Directory patterns
|
||||||
|
const CONTROLLER_DIRS = ['/Controllers/'];
|
||||||
|
const SERVICE_DIRS = ['/Services/', '/Service/', '/Application/'];
|
||||||
|
const REPO_DIRS = ['/Repositories/', '/Repository/', '/Data/', '/Infrastructure/'];
|
||||||
|
const MODEL_DIRS = ['/Models/', '/Model/', '/Entities/', '/Entity/', '/Domain/'];
|
||||||
|
const VIEWMODEL_DIRS = ['/ViewModels/', '/ViewModel/', '/DTOs/', '/Dto/'];
|
||||||
|
|
||||||
function resolveController(name: string, context: ResolutionContext): string | null {
|
const CLASS_KINDS = new Set(['class']);
|
||||||
const allFiles = context.getAllFiles();
|
const SERVICE_KINDS = new Set(['class', 'interface']);
|
||||||
|
|
||||||
for (const file of allFiles) {
|
/**
|
||||||
if (file.endsWith('.cs') && file.includes('/Controllers/')) {
|
* Resolve a symbol by name using indexed queries instead of scanning all files.
|
||||||
const nodes = context.getNodesInFile(file);
|
*/
|
||||||
const controllerNode = nodes.find(
|
function resolveByNameAndKind(
|
||||||
(n) => n.kind === 'class' && n.name === name
|
name: string,
|
||||||
|
kinds: Set<string>,
|
||||||
|
preferredDirPatterns: string[],
|
||||||
|
context: ResolutionContext,
|
||||||
|
): string | null {
|
||||||
|
const candidates = context.getNodesByName(name);
|
||||||
|
if (candidates.length === 0) return null;
|
||||||
|
|
||||||
|
const kindFiltered = candidates.filter((n) => kinds.has(n.kind));
|
||||||
|
if (kindFiltered.length === 0) return null;
|
||||||
|
|
||||||
|
// Prefer candidates in framework-conventional directories
|
||||||
|
const preferred = kindFiltered.filter((n) =>
|
||||||
|
preferredDirPatterns.some((d) => n.filePath.includes(d))
|
||||||
);
|
);
|
||||||
if (controllerNode) {
|
|
||||||
return controllerNode.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
if (preferred.length > 0) return preferred[0]!.id;
|
||||||
}
|
|
||||||
|
// Fall back to any match
|
||||||
function resolveService(name: string, context: ResolutionContext): string | null {
|
return kindFiltered[0]!.id;
|
||||||
const serviceDirs = ['Services', 'Service', 'Application'];
|
|
||||||
|
|
||||||
const allFiles = context.getAllFiles();
|
|
||||||
for (const file of allFiles) {
|
|
||||||
if (file.endsWith('.cs') && serviceDirs.some((d) => file.includes(`/${d}/`))) {
|
|
||||||
const nodes = context.getNodesInFile(file);
|
|
||||||
const serviceNode = nodes.find(
|
|
||||||
(n) => (n.kind === 'class' || n.kind === 'interface') && n.name === name
|
|
||||||
);
|
|
||||||
if (serviceNode) {
|
|
||||||
return serviceNode.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Search all C# files for interfaces (often services are injected via interface)
|
|
||||||
for (const file of allFiles) {
|
|
||||||
if (file.endsWith('.cs')) {
|
|
||||||
const nodes = context.getNodesInFile(file);
|
|
||||||
const serviceNode = nodes.find(
|
|
||||||
(n) => (n.kind === 'class' || n.kind === 'interface') && n.name === name
|
|
||||||
);
|
|
||||||
if (serviceNode) {
|
|
||||||
return serviceNode.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveRepository(name: string, context: ResolutionContext): string | null {
|
|
||||||
const repoDirs = ['Repositories', 'Repository', 'Data', 'Infrastructure'];
|
|
||||||
|
|
||||||
const allFiles = context.getAllFiles();
|
|
||||||
for (const file of allFiles) {
|
|
||||||
if (file.endsWith('.cs') && repoDirs.some((d) => file.includes(`/${d}/`))) {
|
|
||||||
const nodes = context.getNodesInFile(file);
|
|
||||||
const repoNode = nodes.find(
|
|
||||||
(n) => (n.kind === 'class' || n.kind === 'interface') && n.name === name
|
|
||||||
);
|
|
||||||
if (repoNode) {
|
|
||||||
return repoNode.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveModel(name: string, context: ResolutionContext): string | null {
|
|
||||||
const modelDirs = ['Models', 'Model', 'Entities', 'Entity', 'Domain'];
|
|
||||||
|
|
||||||
const allFiles = context.getAllFiles();
|
|
||||||
for (const file of allFiles) {
|
|
||||||
if (file.endsWith('.cs') && modelDirs.some((d) => file.includes(`/${d}/`))) {
|
|
||||||
const nodes = context.getNodesInFile(file);
|
|
||||||
const modelNode = nodes.find(
|
|
||||||
(n) => n.kind === 'class' && n.name === name
|
|
||||||
);
|
|
||||||
if (modelNode) {
|
|
||||||
return modelNode.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveViewModel(name: string, context: ResolutionContext): string | null {
|
|
||||||
const viewModelDirs = ['ViewModels', 'ViewModel', 'DTOs', 'Dto'];
|
|
||||||
|
|
||||||
const allFiles = context.getAllFiles();
|
|
||||||
for (const file of allFiles) {
|
|
||||||
if (file.endsWith('.cs') && viewModelDirs.some((d) => file.includes(`/${d}/`))) {
|
|
||||||
const nodes = context.getNodesInFile(file);
|
|
||||||
const vmNode = nodes.find(
|
|
||||||
(n) => n.kind === 'class' && n.name === name
|
|
||||||
);
|
|
||||||
if (vmNode) {
|
|
||||||
return vmNode.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export const expressResolver: FrameworkResolver = {
|
|||||||
const controllerMatch = ref.referenceName.match(/^(\w+)Controller\.(\w+)$/);
|
const controllerMatch = ref.referenceName.match(/^(\w+)Controller\.(\w+)$/);
|
||||||
if (controllerMatch) {
|
if (controllerMatch) {
|
||||||
const [, controller, method] = controllerMatch;
|
const [, controller, method] = controllerMatch;
|
||||||
const result = resolveController(controller!, method!, context);
|
const result = resolveControllerMethod(controller!, method!, context);
|
||||||
if (result) {
|
if (result) {
|
||||||
return {
|
return {
|
||||||
original: ref,
|
original: ref,
|
||||||
@@ -76,7 +76,7 @@ export const expressResolver: FrameworkResolver = {
|
|||||||
const serviceMatch = ref.referenceName.match(/^(\w+)(Service|Helper|Utils?)\.(\w+)$/);
|
const serviceMatch = ref.referenceName.match(/^(\w+)(Service|Helper|Utils?)\.(\w+)$/);
|
||||||
if (serviceMatch) {
|
if (serviceMatch) {
|
||||||
const [, name, suffix, method] = serviceMatch;
|
const [, name, suffix, method] = serviceMatch;
|
||||||
const result = resolveService(name! + suffix!, method!, context);
|
const result = resolveServiceMethod(name! + suffix!, method!, context);
|
||||||
if (result) {
|
if (result) {
|
||||||
return {
|
return {
|
||||||
original: ref,
|
original: ref,
|
||||||
@@ -154,93 +154,83 @@ function isMiddlewareName(name: string): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve middleware reference
|
* Resolve middleware reference using name-based lookup
|
||||||
*/
|
*/
|
||||||
function resolveMiddleware(
|
function resolveMiddleware(
|
||||||
name: string,
|
name: string,
|
||||||
context: ResolutionContext
|
context: ResolutionContext
|
||||||
): string | null {
|
): string | null {
|
||||||
// Look in middleware directories
|
// Try exact name first
|
||||||
const middlewareDirs = ['middleware', 'middlewares', 'src/middleware', 'src/middlewares'];
|
const candidates = context.getNodesByName(name);
|
||||||
|
const match = candidates.find((n) =>
|
||||||
for (const dir of middlewareDirs) {
|
|
||||||
const allFiles = context.getAllFiles();
|
|
||||||
for (const file of allFiles) {
|
|
||||||
if (file.startsWith(dir) || file.includes('/middleware/')) {
|
|
||||||
const nodes = context.getNodesInFile(file);
|
|
||||||
const match = nodes.find(
|
|
||||||
(n) =>
|
|
||||||
n.name.toLowerCase() === name.toLowerCase() ||
|
n.name.toLowerCase() === name.toLowerCase() ||
|
||||||
n.name.toLowerCase() === name.replace(/Middleware$/i, '').toLowerCase()
|
n.name.toLowerCase() === name.replace(/Middleware$/i, '').toLowerCase()
|
||||||
);
|
);
|
||||||
if (match) {
|
if (match) return match.id;
|
||||||
return match.id;
|
|
||||||
}
|
// Try without Middleware suffix
|
||||||
}
|
const baseName = name.replace(/Middleware$/i, '');
|
||||||
}
|
if (baseName !== name) {
|
||||||
|
const baseCandidates = context.getNodesByName(baseName);
|
||||||
|
const MIDDLEWARE_DIRS = ['/middleware/', '/middlewares/'];
|
||||||
|
const preferred = baseCandidates.filter((n) =>
|
||||||
|
MIDDLEWARE_DIRS.some((d) => n.filePath.includes(d))
|
||||||
|
);
|
||||||
|
if (preferred.length > 0) return preferred[0]!.id;
|
||||||
|
if (baseCandidates.length > 0) return baseCandidates[0]!.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve controller method
|
* Resolve controller method using name-based lookup
|
||||||
*/
|
*/
|
||||||
function resolveController(
|
function resolveControllerMethod(
|
||||||
controller: string,
|
controller: string,
|
||||||
method: string,
|
method: string,
|
||||||
context: ResolutionContext
|
context: ResolutionContext
|
||||||
): string | null {
|
): string | null {
|
||||||
const controllerDirs = ['controllers', 'src/controllers', 'app/controllers'];
|
// Look for the method name directly
|
||||||
|
const methodCandidates = context.getNodesByName(method);
|
||||||
|
const methodNodes = methodCandidates.filter(
|
||||||
|
(n) => (n.kind === 'method' || n.kind === 'function') &&
|
||||||
|
n.filePath.toLowerCase().includes(controller.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
for (const dir of controllerDirs) {
|
if (methodNodes.length > 0) return methodNodes[0]!.id;
|
||||||
const allFiles = context.getAllFiles();
|
|
||||||
for (const file of allFiles) {
|
// Fall back: look for controller class, then find the method in its file
|
||||||
if (
|
const controllerName = controller + 'Controller';
|
||||||
(file.startsWith(dir) || file.includes('/controllers/')) &&
|
const controllerCandidates = context.getNodesByName(controllerName);
|
||||||
file.toLowerCase().includes(controller.toLowerCase())
|
for (const ctrl of controllerCandidates) {
|
||||||
) {
|
const nodesInFile = context.getNodesInFile(ctrl.filePath);
|
||||||
const nodes = context.getNodesInFile(file);
|
const methodNode = nodesInFile.find(
|
||||||
const methodNode = nodes.find(
|
|
||||||
(n) => (n.kind === 'method' || n.kind === 'function') && n.name === method
|
(n) => (n.kind === 'method' || n.kind === 'function') && n.name === method
|
||||||
);
|
);
|
||||||
if (methodNode) {
|
if (methodNode) return methodNode.id;
|
||||||
return methodNode.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve service/helper
|
* Resolve service/helper method using name-based lookup
|
||||||
*/
|
*/
|
||||||
function resolveService(
|
function resolveServiceMethod(
|
||||||
serviceName: string,
|
serviceName: string,
|
||||||
method: string,
|
method: string,
|
||||||
context: ResolutionContext
|
context: ResolutionContext
|
||||||
): string | null {
|
): string | null {
|
||||||
const serviceDirs = ['services', 'src/services', 'helpers', 'src/helpers', 'utils', 'src/utils'];
|
// Look for the method in files matching the service name
|
||||||
|
const methodCandidates = context.getNodesByName(method);
|
||||||
for (const dir of serviceDirs) {
|
const stripped = serviceName.replace(/(Service|Helper|Utils?)$/i, '').toLowerCase();
|
||||||
const allFiles = context.getAllFiles();
|
const methodNodes = methodCandidates.filter(
|
||||||
for (const file of allFiles) {
|
(n) => (n.kind === 'method' || n.kind === 'function') &&
|
||||||
if (
|
n.filePath.toLowerCase().includes(stripped)
|
||||||
(file.startsWith(dir) || file.includes('/services/') || file.includes('/helpers/') || file.includes('/utils/')) &&
|
|
||||||
file.toLowerCase().includes(serviceName.toLowerCase().replace(/(service|helper|utils?)$/i, ''))
|
|
||||||
) {
|
|
||||||
const nodes = context.getNodesInFile(file);
|
|
||||||
const methodNode = nodes.find(
|
|
||||||
(n) => (n.kind === 'method' || n.kind === 'function') && n.name === method
|
|
||||||
);
|
);
|
||||||
if (methodNode) {
|
|
||||||
return methodNode.id;
|
if (methodNodes.length > 0) return methodNodes[0]!.id;
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export const springResolver: FrameworkResolver = {
|
|||||||
resolve(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null {
|
resolve(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null {
|
||||||
// Pattern 1: Service references (dependency injection)
|
// Pattern 1: Service references (dependency injection)
|
||||||
if (ref.referenceName.endsWith('Service')) {
|
if (ref.referenceName.endsWith('Service')) {
|
||||||
const result = resolveService(ref.referenceName, context);
|
const result = resolveByNameAndKind(ref.referenceName, SERVICE_KINDS, SERVICE_DIRS, context);
|
||||||
if (result) {
|
if (result) {
|
||||||
return {
|
return {
|
||||||
original: ref,
|
original: ref,
|
||||||
@@ -63,7 +63,7 @@ export const springResolver: FrameworkResolver = {
|
|||||||
|
|
||||||
// Pattern 2: Repository references
|
// Pattern 2: Repository references
|
||||||
if (ref.referenceName.endsWith('Repository')) {
|
if (ref.referenceName.endsWith('Repository')) {
|
||||||
const result = resolveRepository(ref.referenceName, context);
|
const result = resolveByNameAndKind(ref.referenceName, SERVICE_KINDS, REPO_DIRS, context);
|
||||||
if (result) {
|
if (result) {
|
||||||
return {
|
return {
|
||||||
original: ref,
|
original: ref,
|
||||||
@@ -76,7 +76,7 @@ export const springResolver: FrameworkResolver = {
|
|||||||
|
|
||||||
// Pattern 3: Controller references
|
// Pattern 3: Controller references
|
||||||
if (ref.referenceName.endsWith('Controller')) {
|
if (ref.referenceName.endsWith('Controller')) {
|
||||||
const result = resolveController(ref.referenceName, context);
|
const result = resolveByNameAndKind(ref.referenceName, CLASS_KINDS, CONTROLLER_DIRS, context);
|
||||||
if (result) {
|
if (result) {
|
||||||
return {
|
return {
|
||||||
original: ref,
|
original: ref,
|
||||||
@@ -89,7 +89,7 @@ export const springResolver: FrameworkResolver = {
|
|||||||
|
|
||||||
// Pattern 4: Entity/Model references
|
// Pattern 4: Entity/Model references
|
||||||
if (/^[A-Z][a-zA-Z]+$/.test(ref.referenceName)) {
|
if (/^[A-Z][a-zA-Z]+$/.test(ref.referenceName)) {
|
||||||
const result = resolveEntity(ref.referenceName, context);
|
const result = resolveByNameAndKind(ref.referenceName, CLASS_KINDS, ENTITY_DIRS, context);
|
||||||
if (result) {
|
if (result) {
|
||||||
return {
|
return {
|
||||||
original: ref,
|
original: ref,
|
||||||
@@ -102,7 +102,7 @@ export const springResolver: FrameworkResolver = {
|
|||||||
|
|
||||||
// Pattern 5: Component references
|
// Pattern 5: Component references
|
||||||
if (ref.referenceName.endsWith('Component') || ref.referenceName.endsWith('Config')) {
|
if (ref.referenceName.endsWith('Component') || ref.referenceName.endsWith('Config')) {
|
||||||
const result = resolveComponent(ref.referenceName, context);
|
const result = resolveByNameAndKind(ref.referenceName, CLASS_KINDS, COMPONENT_DIRS, context);
|
||||||
if (result) {
|
if (result) {
|
||||||
return {
|
return {
|
||||||
original: ref,
|
original: ref,
|
||||||
@@ -176,118 +176,38 @@ export const springResolver: FrameworkResolver = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper functions
|
// Directory patterns
|
||||||
|
const SERVICE_DIRS = ['/service/', '/services/'];
|
||||||
|
const REPO_DIRS = ['/repository/', '/repositories/'];
|
||||||
|
const CONTROLLER_DIRS = ['/controller/', '/controllers/'];
|
||||||
|
const ENTITY_DIRS = ['/entity/', '/entities/', '/model/', '/models/', '/domain/'];
|
||||||
|
const COMPONENT_DIRS = ['/component/', '/components/', '/config/'];
|
||||||
|
|
||||||
function resolveService(name: string, context: ResolutionContext): string | null {
|
const CLASS_KINDS = new Set(['class']);
|
||||||
const allFiles = context.getAllFiles();
|
const SERVICE_KINDS = new Set(['class', 'interface']);
|
||||||
|
|
||||||
for (const file of allFiles) {
|
/**
|
||||||
if (file.endsWith('.java') && (file.includes('/service/') || file.includes('/services/'))) {
|
* Resolve a symbol by name using indexed queries instead of scanning all files.
|
||||||
const nodes = context.getNodesInFile(file);
|
*/
|
||||||
const serviceNode = nodes.find(
|
function resolveByNameAndKind(
|
||||||
(n) => n.kind === 'class' && n.name === name
|
name: string,
|
||||||
|
kinds: Set<string>,
|
||||||
|
preferredDirPatterns: string[],
|
||||||
|
context: ResolutionContext,
|
||||||
|
): string | null {
|
||||||
|
const candidates = context.getNodesByName(name);
|
||||||
|
if (candidates.length === 0) return null;
|
||||||
|
|
||||||
|
const kindFiltered = candidates.filter((n) => kinds.has(n.kind));
|
||||||
|
if (kindFiltered.length === 0) return null;
|
||||||
|
|
||||||
|
// Prefer candidates in framework-conventional directories
|
||||||
|
const preferred = kindFiltered.filter((n) =>
|
||||||
|
preferredDirPatterns.some((d) => n.filePath.includes(d))
|
||||||
);
|
);
|
||||||
if (serviceNode) {
|
|
||||||
return serviceNode.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also check interface definitions
|
if (preferred.length > 0) return preferred[0]!.id;
|
||||||
for (const file of allFiles) {
|
|
||||||
if (file.endsWith('.java')) {
|
|
||||||
const nodes = context.getNodesInFile(file);
|
|
||||||
const serviceNode = nodes.find(
|
|
||||||
(n) => (n.kind === 'class' || n.kind === 'interface') && n.name === name
|
|
||||||
);
|
|
||||||
if (serviceNode) {
|
|
||||||
return serviceNode.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
// Fall back to any match
|
||||||
}
|
return kindFiltered[0]!.id;
|
||||||
|
|
||||||
function resolveRepository(name: string, context: ResolutionContext): string | null {
|
|
||||||
const allFiles = context.getAllFiles();
|
|
||||||
|
|
||||||
for (const file of allFiles) {
|
|
||||||
if (file.endsWith('.java') && (file.includes('/repository/') || file.includes('/repositories/'))) {
|
|
||||||
const nodes = context.getNodesInFile(file);
|
|
||||||
const repoNode = nodes.find(
|
|
||||||
(n) => (n.kind === 'class' || n.kind === 'interface') && n.name === name
|
|
||||||
);
|
|
||||||
if (repoNode) {
|
|
||||||
return repoNode.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveController(name: string, context: ResolutionContext): string | null {
|
|
||||||
const allFiles = context.getAllFiles();
|
|
||||||
|
|
||||||
for (const file of allFiles) {
|
|
||||||
if (file.endsWith('.java') && (file.includes('/controller/') || file.includes('/controllers/'))) {
|
|
||||||
const nodes = context.getNodesInFile(file);
|
|
||||||
const controllerNode = nodes.find(
|
|
||||||
(n) => n.kind === 'class' && n.name === name
|
|
||||||
);
|
|
||||||
if (controllerNode) {
|
|
||||||
return controllerNode.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveEntity(name: string, context: ResolutionContext): string | null {
|
|
||||||
const allFiles = context.getAllFiles();
|
|
||||||
|
|
||||||
// Check entity/model directories first
|
|
||||||
for (const file of allFiles) {
|
|
||||||
if (file.endsWith('.java') && (
|
|
||||||
file.includes('/entity/') ||
|
|
||||||
file.includes('/entities/') ||
|
|
||||||
file.includes('/model/') ||
|
|
||||||
file.includes('/models/') ||
|
|
||||||
file.includes('/domain/')
|
|
||||||
)) {
|
|
||||||
const nodes = context.getNodesInFile(file);
|
|
||||||
const entityNode = nodes.find(
|
|
||||||
(n) => n.kind === 'class' && n.name === name
|
|
||||||
);
|
|
||||||
if (entityNode) {
|
|
||||||
return entityNode.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveComponent(name: string, context: ResolutionContext): string | null {
|
|
||||||
const allFiles = context.getAllFiles();
|
|
||||||
|
|
||||||
for (const file of allFiles) {
|
|
||||||
if (file.endsWith('.java') && (
|
|
||||||
file.includes('/component/') ||
|
|
||||||
file.includes('/components/') ||
|
|
||||||
file.includes('/config/')
|
|
||||||
)) {
|
|
||||||
const nodes = context.getNodesInFile(file);
|
|
||||||
const componentNode = nodes.find(
|
|
||||||
(n) => n.kind === 'class' && n.name === name
|
|
||||||
);
|
|
||||||
if (componentNode) {
|
|
||||||
return componentNode.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -216,12 +216,12 @@ function resolveControllerMethod(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try subdirectories (namespaced controllers)
|
// Try name-based lookup for namespaced controllers
|
||||||
const allFiles = context.getAllFiles();
|
const controllerCandidates = context.getNodesByName(controller);
|
||||||
for (const file of allFiles) {
|
for (const ctrl of controllerCandidates) {
|
||||||
if (file.endsWith(`${controller}.php`) && file.includes('Controllers')) {
|
if (ctrl.kind === 'class' && ctrl.filePath.includes('Controllers')) {
|
||||||
const nodes = context.getNodesInFile(file);
|
const nodesInFile = context.getNodesInFile(ctrl.filePath);
|
||||||
const methodNode = nodes.find(
|
const methodNode = nodesInFile.find(
|
||||||
(n) => n.kind === 'method' && n.name === method
|
(n) => n.kind === 'method' && n.name === method
|
||||||
);
|
);
|
||||||
if (methodNode) {
|
if (methodNode) {
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export const djangoResolver: FrameworkResolver = {
|
|||||||
resolve(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null {
|
resolve(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null {
|
||||||
// Pattern 1: Model references
|
// Pattern 1: Model references
|
||||||
if (ref.referenceName.endsWith('Model') || /^[A-Z][a-z]+$/.test(ref.referenceName)) {
|
if (ref.referenceName.endsWith('Model') || /^[A-Z][a-z]+$/.test(ref.referenceName)) {
|
||||||
const result = resolveModel(ref.referenceName, context);
|
const result = resolveByNameAndKind(ref.referenceName, CLASS_KINDS, MODEL_DIRS, context);
|
||||||
if (result) {
|
if (result) {
|
||||||
return {
|
return {
|
||||||
original: ref,
|
original: ref,
|
||||||
@@ -47,7 +47,7 @@ export const djangoResolver: FrameworkResolver = {
|
|||||||
|
|
||||||
// Pattern 2: View references
|
// Pattern 2: View references
|
||||||
if (ref.referenceName.endsWith('View') || ref.referenceName.endsWith('ViewSet')) {
|
if (ref.referenceName.endsWith('View') || ref.referenceName.endsWith('ViewSet')) {
|
||||||
const result = resolveView(ref.referenceName, context);
|
const result = resolveByNameAndKind(ref.referenceName, VIEW_KINDS, VIEW_DIRS, context);
|
||||||
if (result) {
|
if (result) {
|
||||||
return {
|
return {
|
||||||
original: ref,
|
original: ref,
|
||||||
@@ -60,7 +60,7 @@ export const djangoResolver: FrameworkResolver = {
|
|||||||
|
|
||||||
// Pattern 3: Form references
|
// Pattern 3: Form references
|
||||||
if (ref.referenceName.endsWith('Form')) {
|
if (ref.referenceName.endsWith('Form')) {
|
||||||
const result = resolveForm(ref.referenceName, context);
|
const result = resolveByNameAndKind(ref.referenceName, CLASS_KINDS, FORM_DIRS, context);
|
||||||
if (result) {
|
if (result) {
|
||||||
return {
|
return {
|
||||||
original: ref,
|
original: ref,
|
||||||
@@ -140,7 +140,7 @@ export const flaskResolver: FrameworkResolver = {
|
|||||||
resolve(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null {
|
resolve(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null {
|
||||||
// Pattern 1: Blueprint references
|
// Pattern 1: Blueprint references
|
||||||
if (ref.referenceName.endsWith('_bp') || ref.referenceName.endsWith('_blueprint')) {
|
if (ref.referenceName.endsWith('_bp') || ref.referenceName.endsWith('_blueprint')) {
|
||||||
const result = resolveBlueprint(ref.referenceName, context);
|
const result = resolveByNameAndKind(ref.referenceName, VARIABLE_KINDS, [], context);
|
||||||
if (result) {
|
if (result) {
|
||||||
return {
|
return {
|
||||||
original: ref,
|
original: ref,
|
||||||
@@ -215,7 +215,7 @@ export const fastapiResolver: FrameworkResolver = {
|
|||||||
resolve(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null {
|
resolve(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null {
|
||||||
// Pattern 1: Router references
|
// Pattern 1: Router references
|
||||||
if (ref.referenceName.endsWith('_router') || ref.referenceName === 'router') {
|
if (ref.referenceName.endsWith('_router') || ref.referenceName === 'router') {
|
||||||
const result = resolveRouter(ref.referenceName, context);
|
const result = resolveByNameAndKind(ref.referenceName, VARIABLE_KINDS, ROUTER_DIRS, context);
|
||||||
if (result) {
|
if (result) {
|
||||||
return {
|
return {
|
||||||
original: ref,
|
original: ref,
|
||||||
@@ -228,7 +228,7 @@ export const fastapiResolver: FrameworkResolver = {
|
|||||||
|
|
||||||
// Pattern 2: Dependency references
|
// Pattern 2: Dependency references
|
||||||
if (ref.referenceName.startsWith('get_') || ref.referenceName.startsWith('Depends')) {
|
if (ref.referenceName.startsWith('get_') || ref.referenceName.startsWith('Depends')) {
|
||||||
const result = resolveDependency(ref.referenceName, context);
|
const result = resolveByNameAndKind(ref.referenceName, FUNCTION_KINDS, DEP_DIRS, context);
|
||||||
if (result) {
|
if (result) {
|
||||||
return {
|
return {
|
||||||
original: ref,
|
original: ref,
|
||||||
@@ -274,126 +274,41 @@ export const fastapiResolver: FrameworkResolver = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper functions
|
// Directory patterns
|
||||||
|
const MODEL_DIRS = ['models', 'app/models', 'src/models'];
|
||||||
|
const VIEW_DIRS = ['views', 'app/views', 'src/views', 'api/views'];
|
||||||
|
const FORM_DIRS = ['forms', 'app/forms', 'src/forms'];
|
||||||
|
const ROUTER_DIRS = ['/routers/', '/api/', '/routes/', '/endpoints/'];
|
||||||
|
const DEP_DIRS = ['/dependencies/', '/deps/', '/core/'];
|
||||||
|
|
||||||
function resolveModel(name: string, context: ResolutionContext): string | null {
|
const CLASS_KINDS = new Set(['class']);
|
||||||
const modelDirs = ['models', 'app/models', 'src/models'];
|
const VIEW_KINDS = new Set(['class', 'function']);
|
||||||
|
const VARIABLE_KINDS = new Set(['variable']);
|
||||||
|
const FUNCTION_KINDS = new Set(['function']);
|
||||||
|
|
||||||
for (const dir of modelDirs) {
|
/**
|
||||||
const allFiles = context.getAllFiles();
|
* Resolve a symbol by name using indexed queries instead of scanning all files.
|
||||||
for (const file of allFiles) {
|
*/
|
||||||
if (file.startsWith(dir) && file.endsWith('.py')) {
|
function resolveByNameAndKind(
|
||||||
const nodes = context.getNodesInFile(file);
|
name: string,
|
||||||
const modelNode = nodes.find(
|
kinds: Set<string>,
|
||||||
(n) => n.kind === 'class' && n.name === name
|
preferredDirPatterns: string[],
|
||||||
|
context: ResolutionContext,
|
||||||
|
): string | null {
|
||||||
|
const candidates = context.getNodesByName(name);
|
||||||
|
if (candidates.length === 0) return null;
|
||||||
|
|
||||||
|
const kindFiltered = candidates.filter((n) => kinds.has(n.kind));
|
||||||
|
if (kindFiltered.length === 0) return null;
|
||||||
|
|
||||||
|
// Prefer candidates in framework-conventional directories
|
||||||
|
if (preferredDirPatterns.length > 0) {
|
||||||
|
const preferred = kindFiltered.filter((n) =>
|
||||||
|
preferredDirPatterns.some((d) => n.filePath.includes(d))
|
||||||
);
|
);
|
||||||
if (modelNode) {
|
if (preferred.length > 0) return preferred[0]!.id;
|
||||||
return modelNode.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
// Fall back to any match
|
||||||
}
|
return kindFiltered[0]!.id;
|
||||||
|
|
||||||
function resolveView(name: string, context: ResolutionContext): string | null {
|
|
||||||
const viewDirs = ['views', 'app/views', 'src/views', 'api/views'];
|
|
||||||
|
|
||||||
for (const dir of viewDirs) {
|
|
||||||
const allFiles = context.getAllFiles();
|
|
||||||
for (const file of allFiles) {
|
|
||||||
if (file.startsWith(dir) && file.endsWith('.py')) {
|
|
||||||
const nodes = context.getNodesInFile(file);
|
|
||||||
const viewNode = nodes.find(
|
|
||||||
(n) => (n.kind === 'class' || n.kind === 'function') && n.name === name
|
|
||||||
);
|
|
||||||
if (viewNode) {
|
|
||||||
return viewNode.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveForm(name: string, context: ResolutionContext): string | null {
|
|
||||||
const formDirs = ['forms', 'app/forms', 'src/forms'];
|
|
||||||
|
|
||||||
for (const dir of formDirs) {
|
|
||||||
const allFiles = context.getAllFiles();
|
|
||||||
for (const file of allFiles) {
|
|
||||||
if (file.startsWith(dir) && file.endsWith('.py')) {
|
|
||||||
const nodes = context.getNodesInFile(file);
|
|
||||||
const formNode = nodes.find(
|
|
||||||
(n) => n.kind === 'class' && n.name === name
|
|
||||||
);
|
|
||||||
if (formNode) {
|
|
||||||
return formNode.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveBlueprint(name: string, context: ResolutionContext): string | null {
|
|
||||||
const allFiles = context.getAllFiles();
|
|
||||||
for (const file of allFiles) {
|
|
||||||
if (file.endsWith('.py')) {
|
|
||||||
const nodes = context.getNodesInFile(file);
|
|
||||||
const bpNode = nodes.find(
|
|
||||||
(n) => n.kind === 'variable' && n.name === name
|
|
||||||
);
|
|
||||||
if (bpNode) {
|
|
||||||
return bpNode.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveRouter(name: string, context: ResolutionContext): string | null {
|
|
||||||
const routerDirs = ['routers', 'api', 'routes', 'endpoints'];
|
|
||||||
|
|
||||||
for (const dir of routerDirs) {
|
|
||||||
const allFiles = context.getAllFiles();
|
|
||||||
for (const file of allFiles) {
|
|
||||||
if ((file.startsWith(dir) || file.includes('/routers/')) && file.endsWith('.py')) {
|
|
||||||
const nodes = context.getNodesInFile(file);
|
|
||||||
const routerNode = nodes.find(
|
|
||||||
(n) => n.kind === 'variable' && n.name === name
|
|
||||||
);
|
|
||||||
if (routerNode) {
|
|
||||||
return routerNode.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveDependency(name: string, context: ResolutionContext): string | null {
|
|
||||||
const depDirs = ['dependencies', 'deps', 'core'];
|
|
||||||
|
|
||||||
for (const dir of depDirs) {
|
|
||||||
const allFiles = context.getAllFiles();
|
|
||||||
for (const file of allFiles) {
|
|
||||||
if ((file.startsWith(dir) || file.includes('/dependencies/')) && file.endsWith('.py')) {
|
|
||||||
const nodes = context.getNodesInFile(file);
|
|
||||||
const depNode = nodes.find(
|
|
||||||
(n) => n.kind === 'function' && n.name === name
|
|
||||||
);
|
|
||||||
if (depNode) {
|
|
||||||
return depNode.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -183,116 +183,89 @@ function isPascalCase(str: string): boolean {
|
|||||||
* Check if name is a built-in type
|
* Check if name is a built-in type
|
||||||
*/
|
*/
|
||||||
function isBuiltInType(name: string): boolean {
|
function isBuiltInType(name: string): boolean {
|
||||||
const builtIns = [
|
return BUILT_IN_TYPES.has(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
const BUILT_IN_TYPES = new Set([
|
||||||
'Array', 'Boolean', 'Date', 'Error', 'Function', 'JSON', 'Math', 'Number',
|
'Array', 'Boolean', 'Date', 'Error', 'Function', 'JSON', 'Math', 'Number',
|
||||||
'Object', 'Promise', 'RegExp', 'String', 'Symbol', 'Map', 'Set', 'WeakMap', 'WeakSet',
|
'Object', 'Promise', 'RegExp', 'String', 'Symbol', 'Map', 'Set', 'WeakMap', 'WeakSet',
|
||||||
'React', 'Component', 'Fragment', 'Suspense', 'StrictMode',
|
'React', 'Component', 'Fragment', 'Suspense', 'StrictMode',
|
||||||
];
|
]);
|
||||||
return builtIns.includes(name);
|
|
||||||
}
|
const COMPONENT_KINDS = new Set(['component', 'function', 'class']);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve a component reference
|
* Resolve a component reference using name-based lookup
|
||||||
*/
|
*/
|
||||||
function resolveComponent(
|
function resolveComponent(
|
||||||
name: string,
|
name: string,
|
||||||
fromFile: string,
|
fromFile: string,
|
||||||
context: ResolutionContext
|
context: ResolutionContext
|
||||||
): string | null {
|
): string | null {
|
||||||
// Look for component in common locations
|
const candidates = context.getNodesByName(name);
|
||||||
const componentDirs = [
|
if (candidates.length === 0) return null;
|
||||||
'components',
|
|
||||||
'src/components',
|
|
||||||
'app/components',
|
|
||||||
'pages',
|
|
||||||
'src/pages',
|
|
||||||
'views',
|
|
||||||
'src/views',
|
|
||||||
];
|
|
||||||
|
|
||||||
// First, check same directory
|
const components = candidates.filter((n) => COMPONENT_KINDS.has(n.kind));
|
||||||
|
if (components.length === 0) return null;
|
||||||
|
|
||||||
|
// Prefer same directory
|
||||||
const fromDir = fromFile.substring(0, fromFile.lastIndexOf('/'));
|
const fromDir = fromFile.substring(0, fromFile.lastIndexOf('/'));
|
||||||
const sameDir = context.getAllFiles().filter((f) => f.startsWith(fromDir));
|
const sameDir = components.filter((n) => n.filePath.startsWith(fromDir));
|
||||||
for (const file of sameDir) {
|
if (sameDir.length > 0) return sameDir[0]!.id;
|
||||||
if (file.toLowerCase().includes(name.toLowerCase())) {
|
|
||||||
const nodes = context.getNodesInFile(file);
|
|
||||||
const component = nodes.find(
|
|
||||||
(n) => (n.kind === 'component' || n.kind === 'function' || n.kind === 'class') && n.name === name
|
|
||||||
);
|
|
||||||
if (component) {
|
|
||||||
return component.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Then check component directories
|
// Prefer component directories
|
||||||
for (const dir of componentDirs) {
|
const COMPONENT_DIRS = ['/components/', '/src/components/', '/app/components/', '/pages/', '/src/pages/', '/views/', '/src/views/'];
|
||||||
const allFiles = context.getAllFiles();
|
const preferred = components.filter((n) =>
|
||||||
for (const file of allFiles) {
|
COMPONENT_DIRS.some((d) => n.filePath.includes(d))
|
||||||
if (file.startsWith(dir) && file.toLowerCase().includes(name.toLowerCase())) {
|
|
||||||
const nodes = context.getNodesInFile(file);
|
|
||||||
const component = nodes.find(
|
|
||||||
(n) => (n.kind === 'component' || n.kind === 'function' || n.kind === 'class') && n.name === name
|
|
||||||
);
|
);
|
||||||
if (component) {
|
if (preferred.length > 0) return preferred[0]!.id;
|
||||||
return component.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
return components[0]!.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve a custom hook reference
|
* Resolve a custom hook reference using name-based lookup
|
||||||
*/
|
*/
|
||||||
function resolveHook(name: string, context: ResolutionContext): string | null {
|
function resolveHook(name: string, context: ResolutionContext): string | null {
|
||||||
const hookDirs = ['hooks', 'src/hooks', 'lib/hooks', 'utils/hooks'];
|
const candidates = context.getNodesByName(name);
|
||||||
|
if (candidates.length === 0) return null;
|
||||||
|
|
||||||
for (const dir of hookDirs) {
|
const hooks = candidates.filter((n) => n.kind === 'function' && n.name.startsWith('use'));
|
||||||
const allFiles = context.getAllFiles();
|
if (hooks.length === 0) return null;
|
||||||
for (const file of allFiles) {
|
|
||||||
if (file.startsWith(dir) || file.includes('/hooks/')) {
|
|
||||||
const nodes = context.getNodesInFile(file);
|
|
||||||
const hook = nodes.find((n) => n.kind === 'function' && n.name === name);
|
|
||||||
if (hook) {
|
|
||||||
return hook.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also check all files for the hook
|
// Prefer hooks directories
|
||||||
const allNodes = context.getNodesByName(name);
|
const HOOK_DIRS = ['/hooks/', '/src/hooks/', '/lib/hooks/', '/utils/hooks/'];
|
||||||
const hookNode = allNodes.find((n) => n.kind === 'function' && n.name.startsWith('use'));
|
const preferred = hooks.filter((n) =>
|
||||||
if (hookNode) {
|
HOOK_DIRS.some((d) => n.filePath.includes(d))
|
||||||
return hookNode.id;
|
);
|
||||||
}
|
if (preferred.length > 0) return preferred[0]!.id;
|
||||||
|
|
||||||
return null;
|
return hooks[0]!.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve a context reference
|
* Resolve a context reference using name-based lookup
|
||||||
*/
|
*/
|
||||||
function resolveContext(name: string, context: ResolutionContext): string | null {
|
function resolveContext(name: string, context: ResolutionContext): string | null {
|
||||||
const contextDirs = ['context', 'contexts', 'src/context', 'src/contexts', 'providers', 'src/providers'];
|
const candidates = context.getNodesByName(name);
|
||||||
|
if (candidates.length === 0) {
|
||||||
for (const dir of contextDirs) {
|
// Try without Context/Provider suffix
|
||||||
const allFiles = context.getAllFiles();
|
const baseName = name.replace(/Context$|Provider$/, '');
|
||||||
for (const file of allFiles) {
|
if (baseName !== name) {
|
||||||
if (file.startsWith(dir) || file.includes('/context/') || file.includes('/contexts/')) {
|
const baseCandidates = context.getNodesByName(baseName);
|
||||||
const nodes = context.getNodesInFile(file);
|
if (baseCandidates.length > 0) return baseCandidates[0]!.id;
|
||||||
const contextNode = nodes.find((n) => n.name === name || n.name === name.replace(/Context$|Provider$/, ''));
|
|
||||||
if (contextNode) {
|
|
||||||
return contextNode.id;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefer context directories
|
||||||
|
const CONTEXT_DIRS = ['/context/', '/contexts/', '/src/context/', '/src/contexts/', '/providers/', '/src/providers/'];
|
||||||
|
const preferred = candidates.filter((n) =>
|
||||||
|
CONTEXT_DIRS.some((d) => n.filePath.includes(d))
|
||||||
|
);
|
||||||
|
if (preferred.length > 0) return preferred[0]!.id;
|
||||||
|
|
||||||
|
return candidates[0]!.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -189,7 +189,7 @@ export const railsResolver: FrameworkResolver = {
|
|||||||
// Helper functions
|
// Helper functions
|
||||||
|
|
||||||
function resolveModel(name: string, context: ResolutionContext): string | null {
|
function resolveModel(name: string, context: ResolutionContext): string | null {
|
||||||
// Convert CamelCase to snake_case for file lookup
|
// Try direct file path lookup first (Rails convention: CamelCase -> snake_case.rb)
|
||||||
const snakeName = name.replace(/([A-Z])/g, '_$1').toLowerCase().slice(1);
|
const snakeName = name.replace(/([A-Z])/g, '_$1').toLowerCase().slice(1);
|
||||||
const possiblePaths = [
|
const possiblePaths = [
|
||||||
`app/models/${snakeName}.rb`,
|
`app/models/${snakeName}.rb`,
|
||||||
@@ -208,25 +208,18 @@ function resolveModel(name: string, context: ResolutionContext): string | null {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Search all model files
|
// Fall back to name-based lookup
|
||||||
const allFiles = context.getAllFiles();
|
const candidates = context.getNodesByName(name);
|
||||||
for (const file of allFiles) {
|
const modelNode = candidates.find(
|
||||||
if (file.includes('app/models/') && file.endsWith('.rb')) {
|
(n) => n.kind === 'class' && n.filePath.includes('app/models/')
|
||||||
const nodes = context.getNodesInFile(file);
|
|
||||||
const modelNode = nodes.find(
|
|
||||||
(n) => n.kind === 'class' && n.name === name
|
|
||||||
);
|
);
|
||||||
if (modelNode) {
|
if (modelNode) return modelNode.id;
|
||||||
return modelNode.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveController(name: string, context: ResolutionContext): string | null {
|
function resolveController(name: string, context: ResolutionContext): string | null {
|
||||||
// Convert CamelCase to snake_case
|
// Try direct file path lookup first
|
||||||
const snakeName = name.replace(/([A-Z])/g, '_$1').toLowerCase().slice(1);
|
const snakeName = name.replace(/([A-Z])/g, '_$1').toLowerCase().slice(1);
|
||||||
const possiblePaths = [
|
const possiblePaths = [
|
||||||
`app/controllers/${snakeName}.rb`,
|
`app/controllers/${snakeName}.rb`,
|
||||||
@@ -246,19 +239,12 @@ function resolveController(name: string, context: ResolutionContext): string | n
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Search all controller files
|
// Fall back to name-based lookup
|
||||||
const allFiles = context.getAllFiles();
|
const candidates = context.getNodesByName(name);
|
||||||
for (const file of allFiles) {
|
const controllerNode = candidates.find(
|
||||||
if (file.includes('controllers/') && file.endsWith('.rb')) {
|
(n) => n.kind === 'class' && n.filePath.includes('controllers/')
|
||||||
const nodes = context.getNodesInFile(file);
|
|
||||||
const controllerNode = nodes.find(
|
|
||||||
(n) => n.kind === 'class' && n.name === name
|
|
||||||
);
|
);
|
||||||
if (controllerNode) {
|
if (controllerNode) return controllerNode.id;
|
||||||
return controllerNode.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export const rustResolver: FrameworkResolver = {
|
|||||||
resolve(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null {
|
resolve(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null {
|
||||||
// Pattern 1: Handler references
|
// Pattern 1: Handler references
|
||||||
if (ref.referenceName.endsWith('_handler') || ref.referenceName.startsWith('handle_')) {
|
if (ref.referenceName.endsWith('_handler') || ref.referenceName.startsWith('handle_')) {
|
||||||
const result = resolveHandler(ref.referenceName, context);
|
const result = resolveByNameAndKind(ref.referenceName, FUNCTION_KINDS, HANDLER_DIRS, context);
|
||||||
if (result) {
|
if (result) {
|
||||||
return {
|
return {
|
||||||
original: ref,
|
original: ref,
|
||||||
@@ -31,7 +31,7 @@ export const rustResolver: FrameworkResolver = {
|
|||||||
|
|
||||||
// Pattern 2: Service/Repository trait implementations
|
// Pattern 2: Service/Repository trait implementations
|
||||||
if (ref.referenceName.endsWith('Service') || ref.referenceName.endsWith('Repository')) {
|
if (ref.referenceName.endsWith('Service') || ref.referenceName.endsWith('Repository')) {
|
||||||
const result = resolveService(ref.referenceName, context);
|
const result = resolveByNameAndKind(ref.referenceName, SERVICE_KINDS, SERVICE_DIRS, context);
|
||||||
if (result) {
|
if (result) {
|
||||||
return {
|
return {
|
||||||
original: ref,
|
original: ref,
|
||||||
@@ -44,7 +44,7 @@ export const rustResolver: FrameworkResolver = {
|
|||||||
|
|
||||||
// Pattern 3: Struct references (PascalCase)
|
// Pattern 3: Struct references (PascalCase)
|
||||||
if (/^[A-Z][a-zA-Z]+$/.test(ref.referenceName)) {
|
if (/^[A-Z][a-zA-Z]+$/.test(ref.referenceName)) {
|
||||||
const result = resolveStruct(ref.referenceName, context);
|
const result = resolveByNameAndKind(ref.referenceName, STRUCT_KINDS, MODEL_DIRS, context);
|
||||||
if (result) {
|
if (result) {
|
||||||
return {
|
return {
|
||||||
original: ref,
|
original: ref,
|
||||||
@@ -153,91 +153,39 @@ export const rustResolver: FrameworkResolver = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper functions
|
// Directory patterns
|
||||||
|
const HANDLER_DIRS = ['/handlers/', '/handler/', '/api/', '/routes/', '/controllers/'];
|
||||||
|
const SERVICE_DIRS = ['/services/', '/service/', '/repository/', '/domain/'];
|
||||||
|
const MODEL_DIRS = ['/models/', '/model/', '/entities/', '/entity/', '/domain/', '/types/'];
|
||||||
|
|
||||||
function resolveHandler(name: string, context: ResolutionContext): string | null {
|
const FUNCTION_KINDS = new Set(['function']);
|
||||||
const handlerDirs = ['handlers', 'handler', 'api', 'routes', 'controllers'];
|
const SERVICE_KINDS = new Set(['struct', 'trait']);
|
||||||
|
const STRUCT_KINDS = new Set(['struct']);
|
||||||
|
|
||||||
const allFiles = context.getAllFiles();
|
/**
|
||||||
for (const file of allFiles) {
|
* Resolve a symbol by name using indexed queries instead of scanning all files.
|
||||||
if (file.endsWith('.rs') && handlerDirs.some((d) => file.includes(`/${d}/`) || file.includes(`/${d}.rs`))) {
|
*/
|
||||||
const nodes = context.getNodesInFile(file);
|
function resolveByNameAndKind(
|
||||||
const handlerNode = nodes.find(
|
name: string,
|
||||||
(n) => n.kind === 'function' && n.name === name
|
kinds: Set<string>,
|
||||||
|
preferredDirPatterns: string[],
|
||||||
|
context: ResolutionContext,
|
||||||
|
): string | null {
|
||||||
|
const candidates = context.getNodesByName(name);
|
||||||
|
if (candidates.length === 0) return null;
|
||||||
|
|
||||||
|
const kindFiltered = candidates.filter((n) => kinds.has(n.kind));
|
||||||
|
if (kindFiltered.length === 0) return null;
|
||||||
|
|
||||||
|
// Prefer candidates in framework-conventional directories
|
||||||
|
const preferred = kindFiltered.filter((n) =>
|
||||||
|
preferredDirPatterns.some((d) => n.filePath.includes(d))
|
||||||
);
|
);
|
||||||
if (handlerNode) {
|
|
||||||
return handlerNode.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Search all Rust files
|
if (preferred.length > 0) return preferred[0]!.id;
|
||||||
for (const file of allFiles) {
|
|
||||||
if (file.endsWith('.rs')) {
|
|
||||||
const nodes = context.getNodesInFile(file);
|
|
||||||
const handlerNode = nodes.find(
|
|
||||||
(n) => n.kind === 'function' && n.name === name
|
|
||||||
);
|
|
||||||
if (handlerNode) {
|
|
||||||
return handlerNode.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
// Fall back to any match
|
||||||
}
|
return kindFiltered[0]!.id;
|
||||||
|
|
||||||
function resolveService(name: string, context: ResolutionContext): string | null {
|
|
||||||
const serviceDirs = ['services', 'service', 'repository', 'domain'];
|
|
||||||
|
|
||||||
const allFiles = context.getAllFiles();
|
|
||||||
for (const file of allFiles) {
|
|
||||||
if (file.endsWith('.rs') && serviceDirs.some((d) => file.includes(`/${d}/`) || file.includes(`/${d}.rs`))) {
|
|
||||||
const nodes = context.getNodesInFile(file);
|
|
||||||
const serviceNode = nodes.find(
|
|
||||||
(n) => (n.kind === 'struct' || n.kind === 'trait') && n.name === name
|
|
||||||
);
|
|
||||||
if (serviceNode) {
|
|
||||||
return serviceNode.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveStruct(name: string, context: ResolutionContext): string | null {
|
|
||||||
const modelDirs = ['models', 'model', 'entities', 'entity', 'domain', 'types'];
|
|
||||||
|
|
||||||
const allFiles = context.getAllFiles();
|
|
||||||
|
|
||||||
// Check model directories first
|
|
||||||
for (const file of allFiles) {
|
|
||||||
if (file.endsWith('.rs') && modelDirs.some((d) => file.includes(`/${d}/`) || file.includes(`/${d}.rs`))) {
|
|
||||||
const nodes = context.getNodesInFile(file);
|
|
||||||
const structNode = nodes.find(
|
|
||||||
(n) => n.kind === 'struct' && n.name === name
|
|
||||||
);
|
|
||||||
if (structNode) {
|
|
||||||
return structNode.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Search all Rust files
|
|
||||||
for (const file of allFiles) {
|
|
||||||
if (file.endsWith('.rs')) {
|
|
||||||
const nodes = context.getNodesInFile(file);
|
|
||||||
const structNode = nodes.find(
|
|
||||||
(n) => n.kind === 'struct' && n.name === name
|
|
||||||
);
|
|
||||||
if (structNode) {
|
|
||||||
return structNode.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveModule(name: string, context: ResolutionContext): string | null {
|
function resolveModule(name: string, context: ResolutionContext): string | null {
|
||||||
|
|||||||
@@ -201,47 +201,25 @@ function isPascalCase(str: string): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve a Svelte component reference to its .svelte file
|
* Resolve a Svelte component reference using name-based lookup
|
||||||
*/
|
*/
|
||||||
function resolveComponent(
|
function resolveComponent(
|
||||||
name: string,
|
name: string,
|
||||||
fromFile: string,
|
fromFile: string,
|
||||||
context: ResolutionContext
|
context: ResolutionContext
|
||||||
): string | null {
|
): string | null {
|
||||||
// Look for matching .svelte files
|
// Look for component nodes by name
|
||||||
const allFiles = context.getAllFiles();
|
const candidates = context.getNodesByName(name);
|
||||||
const svelteFiles = allFiles.filter((f) => f.endsWith('.svelte'));
|
const components = candidates.filter((n) => n.kind === 'component');
|
||||||
|
|
||||||
// Check for exact name match (Button -> Button.svelte)
|
if (components.length === 0) return null;
|
||||||
for (const file of svelteFiles) {
|
|
||||||
const fileName = file.split(/[/\\]/).pop() || '';
|
|
||||||
const componentName = fileName.replace(/\.svelte$/, '');
|
|
||||||
if (componentName === name) {
|
|
||||||
const nodes = context.getNodesInFile(file);
|
|
||||||
const component = nodes.find((n) => n.kind === 'component' && n.name === name);
|
|
||||||
if (component) {
|
|
||||||
return component.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check same directory first for better specificity
|
// Prefer same directory
|
||||||
const fromDir = fromFile.substring(0, fromFile.lastIndexOf('/'));
|
const fromDir = fromFile.substring(0, fromFile.lastIndexOf('/'));
|
||||||
for (const file of svelteFiles) {
|
const sameDir = components.filter((n) => n.filePath.startsWith(fromDir));
|
||||||
if (file.startsWith(fromDir)) {
|
if (sameDir.length > 0) return sameDir[0]!.id;
|
||||||
const fileName = file.split(/[/\\]/).pop() || '';
|
|
||||||
const componentName = fileName.replace(/\.svelte$/, '');
|
|
||||||
if (componentName === name) {
|
|
||||||
const nodes = context.getNodesInFile(file);
|
|
||||||
const component = nodes.find((n) => n.kind === 'component');
|
|
||||||
if (component) {
|
|
||||||
return component.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
return components[0]!.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export const swiftUIResolver: FrameworkResolver = {
|
|||||||
resolve(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null {
|
resolve(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null {
|
||||||
// Pattern 1: View references (SwiftUI views are PascalCase ending in View)
|
// Pattern 1: View references (SwiftUI views are PascalCase ending in View)
|
||||||
if (ref.referenceName.endsWith('View') && /^[A-Z]/.test(ref.referenceName)) {
|
if (ref.referenceName.endsWith('View') && /^[A-Z]/.test(ref.referenceName)) {
|
||||||
const result = resolveView(ref.referenceName, context);
|
const result = resolveByNameAndKind(ref.referenceName, VIEW_KINDS, VIEW_DIRS, context);
|
||||||
if (result) {
|
if (result) {
|
||||||
return {
|
return {
|
||||||
original: ref,
|
original: ref,
|
||||||
@@ -48,7 +48,7 @@ export const swiftUIResolver: FrameworkResolver = {
|
|||||||
|
|
||||||
// Pattern 2: ViewModel/ObservableObject references
|
// Pattern 2: ViewModel/ObservableObject references
|
||||||
if (ref.referenceName.endsWith('ViewModel') || ref.referenceName.endsWith('Store') || ref.referenceName.endsWith('Manager')) {
|
if (ref.referenceName.endsWith('ViewModel') || ref.referenceName.endsWith('Store') || ref.referenceName.endsWith('Manager')) {
|
||||||
const result = resolveViewModel(ref.referenceName, context);
|
const result = resolveByNameAndKind(ref.referenceName, CLASS_KINDS, VIEWMODEL_DIRS, context);
|
||||||
if (result) {
|
if (result) {
|
||||||
return {
|
return {
|
||||||
original: ref,
|
original: ref,
|
||||||
@@ -61,7 +61,7 @@ export const swiftUIResolver: FrameworkResolver = {
|
|||||||
|
|
||||||
// Pattern 3: Model references
|
// Pattern 3: Model references
|
||||||
if (/^[A-Z][a-zA-Z]+$/.test(ref.referenceName)) {
|
if (/^[A-Z][a-zA-Z]+$/.test(ref.referenceName)) {
|
||||||
const result = resolveModel(ref.referenceName, context);
|
const result = resolveByNameAndKind(ref.referenceName, MODEL_KINDS, MODEL_DIRS, context);
|
||||||
if (result) {
|
if (result) {
|
||||||
return {
|
return {
|
||||||
original: ref,
|
original: ref,
|
||||||
@@ -153,7 +153,7 @@ export const uikitResolver: FrameworkResolver = {
|
|||||||
resolve(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null {
|
resolve(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null {
|
||||||
// Pattern 1: ViewController references
|
// Pattern 1: ViewController references
|
||||||
if (ref.referenceName.endsWith('ViewController')) {
|
if (ref.referenceName.endsWith('ViewController')) {
|
||||||
const result = resolveViewController(ref.referenceName, context);
|
const result = resolveByNameAndKind(ref.referenceName, CLASS_KINDS, VC_DIRS, context);
|
||||||
if (result) {
|
if (result) {
|
||||||
return {
|
return {
|
||||||
original: ref,
|
original: ref,
|
||||||
@@ -166,7 +166,7 @@ export const uikitResolver: FrameworkResolver = {
|
|||||||
|
|
||||||
// Pattern 2: UIView subclass references
|
// Pattern 2: UIView subclass references
|
||||||
if (ref.referenceName.endsWith('View') && !ref.referenceName.endsWith('ViewController')) {
|
if (ref.referenceName.endsWith('View') && !ref.referenceName.endsWith('ViewController')) {
|
||||||
const result = resolveUIView(ref.referenceName, context);
|
const result = resolveByNameAndKind(ref.referenceName, CLASS_KINDS, UIVIEW_DIRS, context);
|
||||||
if (result) {
|
if (result) {
|
||||||
return {
|
return {
|
||||||
original: ref,
|
original: ref,
|
||||||
@@ -179,7 +179,7 @@ export const uikitResolver: FrameworkResolver = {
|
|||||||
|
|
||||||
// Pattern 3: Cell references
|
// Pattern 3: Cell references
|
||||||
if (ref.referenceName.endsWith('Cell')) {
|
if (ref.referenceName.endsWith('Cell')) {
|
||||||
const result = resolveCell(ref.referenceName, context);
|
const result = resolveByNameAndKind(ref.referenceName, CLASS_KINDS, CELL_DIRS, context);
|
||||||
if (result) {
|
if (result) {
|
||||||
return {
|
return {
|
||||||
original: ref,
|
original: ref,
|
||||||
@@ -192,7 +192,7 @@ export const uikitResolver: FrameworkResolver = {
|
|||||||
|
|
||||||
// Pattern 4: Delegate/DataSource references
|
// Pattern 4: Delegate/DataSource references
|
||||||
if (ref.referenceName.endsWith('Delegate') || ref.referenceName.endsWith('DataSource')) {
|
if (ref.referenceName.endsWith('Delegate') || ref.referenceName.endsWith('DataSource')) {
|
||||||
const result = resolveProtocol(ref.referenceName, context);
|
const result = resolveByNameAndKind(ref.referenceName, PROTOCOL_KINDS, [], context);
|
||||||
if (result) {
|
if (result) {
|
||||||
return {
|
return {
|
||||||
original: ref,
|
original: ref,
|
||||||
@@ -286,7 +286,7 @@ export const vaporResolver: FrameworkResolver = {
|
|||||||
resolve(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null {
|
resolve(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null {
|
||||||
// Pattern 1: Controller references
|
// Pattern 1: Controller references
|
||||||
if (ref.referenceName.endsWith('Controller')) {
|
if (ref.referenceName.endsWith('Controller')) {
|
||||||
const result = resolveVaporController(ref.referenceName, context);
|
const result = resolveByNameAndKind(ref.referenceName, VAPOR_CONTROLLER_KINDS, VAPOR_CONTROLLER_DIRS, context);
|
||||||
if (result) {
|
if (result) {
|
||||||
return {
|
return {
|
||||||
original: ref,
|
original: ref,
|
||||||
@@ -299,7 +299,7 @@ export const vaporResolver: FrameworkResolver = {
|
|||||||
|
|
||||||
// Pattern 2: Model references (Fluent)
|
// Pattern 2: Model references (Fluent)
|
||||||
if (/^[A-Z][a-zA-Z]+$/.test(ref.referenceName)) {
|
if (/^[A-Z][a-zA-Z]+$/.test(ref.referenceName)) {
|
||||||
const result = resolveFluentModel(ref.referenceName, context);
|
const result = resolveByNameAndKind(ref.referenceName, CLASS_KINDS, FLUENT_MODEL_DIRS, context);
|
||||||
if (result) {
|
if (result) {
|
||||||
return {
|
return {
|
||||||
original: ref,
|
original: ref,
|
||||||
@@ -312,7 +312,7 @@ export const vaporResolver: FrameworkResolver = {
|
|||||||
|
|
||||||
// Pattern 3: Middleware references
|
// Pattern 3: Middleware references
|
||||||
if (ref.referenceName.endsWith('Middleware')) {
|
if (ref.referenceName.endsWith('Middleware')) {
|
||||||
const result = resolveVaporMiddleware(ref.referenceName, context);
|
const result = resolveByNameAndKind(ref.referenceName, VAPOR_CONTROLLER_KINDS, VAPOR_MIDDLEWARE_DIRS, context);
|
||||||
if (result) {
|
if (result) {
|
||||||
return {
|
return {
|
||||||
original: ref,
|
original: ref,
|
||||||
@@ -382,210 +382,46 @@ export const vaporResolver: FrameworkResolver = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper functions for SwiftUI
|
// Directory patterns
|
||||||
|
const VIEW_DIRS = ['/Views/', '/View/', '/Screens/', '/Components/', '/UI/'];
|
||||||
|
const VIEWMODEL_DIRS = ['/ViewModels/', '/ViewModel/', '/Stores/', '/Managers/', '/Services/'];
|
||||||
|
const MODEL_DIRS = ['/Models/', '/Model/', '/Entities/', '/Domain/'];
|
||||||
|
const VC_DIRS = ['/ViewControllers/', '/ViewController/', '/Controllers/', '/Screens/'];
|
||||||
|
const UIVIEW_DIRS = ['/Views/', '/View/', '/UI/', '/Components/'];
|
||||||
|
const CELL_DIRS = ['/Cells/', '/Cell/', '/Views/', '/TableViewCells/', '/CollectionViewCells/'];
|
||||||
|
const VAPOR_CONTROLLER_DIRS = ['/Controllers/', '/Controller/', '/Routes/'];
|
||||||
|
const FLUENT_MODEL_DIRS = ['/Models/', '/Model/', '/Entities/', '/Database/'];
|
||||||
|
const VAPOR_MIDDLEWARE_DIRS = ['/Middleware/', '/Middlewares/'];
|
||||||
|
|
||||||
function resolveView(name: string, context: ResolutionContext): string | null {
|
const VIEW_KINDS = new Set(['struct', 'component']);
|
||||||
const viewDirs = ['Views', 'View', 'Screens', 'Components', 'UI'];
|
const CLASS_KINDS = new Set(['class']);
|
||||||
|
const MODEL_KINDS = new Set(['struct', 'class']);
|
||||||
|
const PROTOCOL_KINDS = new Set(['protocol']);
|
||||||
|
const VAPOR_CONTROLLER_KINDS = new Set(['class', 'struct']);
|
||||||
|
|
||||||
const allFiles = context.getAllFiles();
|
/**
|
||||||
for (const file of allFiles) {
|
* Resolve a symbol by name using indexed queries instead of scanning all files.
|
||||||
if (file.endsWith('.swift') && viewDirs.some((d) => file.includes(`/${d}/`))) {
|
*/
|
||||||
const nodes = context.getNodesInFile(file);
|
function resolveByNameAndKind(
|
||||||
const viewNode = nodes.find(
|
name: string,
|
||||||
(n) => (n.kind === 'struct' || n.kind === 'component') && n.name === name
|
kinds: Set<string>,
|
||||||
|
preferredDirPatterns: string[],
|
||||||
|
context: ResolutionContext,
|
||||||
|
): string | null {
|
||||||
|
const candidates = context.getNodesByName(name);
|
||||||
|
if (candidates.length === 0) return null;
|
||||||
|
|
||||||
|
const kindFiltered = candidates.filter((n) => kinds.has(n.kind));
|
||||||
|
if (kindFiltered.length === 0) return null;
|
||||||
|
|
||||||
|
// Prefer candidates in framework-conventional directories
|
||||||
|
if (preferredDirPatterns.length > 0) {
|
||||||
|
const preferred = kindFiltered.filter((n) =>
|
||||||
|
preferredDirPatterns.some((d) => n.filePath.includes(d))
|
||||||
);
|
);
|
||||||
if (viewNode) {
|
if (preferred.length > 0) return preferred[0]!.id;
|
||||||
return viewNode.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Search all Swift files
|
// Fall back to any match
|
||||||
for (const file of allFiles) {
|
return kindFiltered[0]!.id;
|
||||||
if (file.endsWith('.swift')) {
|
|
||||||
const nodes = context.getNodesInFile(file);
|
|
||||||
const viewNode = nodes.find(
|
|
||||||
(n) => (n.kind === 'struct' || n.kind === 'component') && n.name === name
|
|
||||||
);
|
|
||||||
if (viewNode) {
|
|
||||||
return viewNode.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveViewModel(name: string, context: ResolutionContext): string | null {
|
|
||||||
const vmDirs = ['ViewModels', 'ViewModel', 'Stores', 'Managers', 'Services'];
|
|
||||||
|
|
||||||
const allFiles = context.getAllFiles();
|
|
||||||
for (const file of allFiles) {
|
|
||||||
if (file.endsWith('.swift') && vmDirs.some((d) => file.includes(`/${d}/`))) {
|
|
||||||
const nodes = context.getNodesInFile(file);
|
|
||||||
const vmNode = nodes.find(
|
|
||||||
(n) => n.kind === 'class' && n.name === name
|
|
||||||
);
|
|
||||||
if (vmNode) {
|
|
||||||
return vmNode.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveModel(name: string, context: ResolutionContext): string | null {
|
|
||||||
const modelDirs = ['Models', 'Model', 'Entities', 'Domain'];
|
|
||||||
|
|
||||||
const allFiles = context.getAllFiles();
|
|
||||||
for (const file of allFiles) {
|
|
||||||
if (file.endsWith('.swift') && modelDirs.some((d) => file.includes(`/${d}/`))) {
|
|
||||||
const nodes = context.getNodesInFile(file);
|
|
||||||
const modelNode = nodes.find(
|
|
||||||
(n) => (n.kind === 'struct' || n.kind === 'class') && n.name === name
|
|
||||||
);
|
|
||||||
if (modelNode) {
|
|
||||||
return modelNode.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper functions for UIKit
|
|
||||||
|
|
||||||
function resolveViewController(name: string, context: ResolutionContext): string | null {
|
|
||||||
const vcDirs = ['ViewControllers', 'ViewController', 'Controllers', 'Screens'];
|
|
||||||
|
|
||||||
const allFiles = context.getAllFiles();
|
|
||||||
for (const file of allFiles) {
|
|
||||||
if (file.endsWith('.swift') && (vcDirs.some((d) => file.includes(`/${d}/`)) || file.includes(name))) {
|
|
||||||
const nodes = context.getNodesInFile(file);
|
|
||||||
const vcNode = nodes.find(
|
|
||||||
(n) => n.kind === 'class' && n.name === name
|
|
||||||
);
|
|
||||||
if (vcNode) {
|
|
||||||
return vcNode.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveUIView(name: string, context: ResolutionContext): string | null {
|
|
||||||
const viewDirs = ['Views', 'View', 'UI', 'Components'];
|
|
||||||
|
|
||||||
const allFiles = context.getAllFiles();
|
|
||||||
for (const file of allFiles) {
|
|
||||||
if (file.endsWith('.swift') && viewDirs.some((d) => file.includes(`/${d}/`))) {
|
|
||||||
const nodes = context.getNodesInFile(file);
|
|
||||||
const viewNode = nodes.find(
|
|
||||||
(n) => n.kind === 'class' && n.name === name
|
|
||||||
);
|
|
||||||
if (viewNode) {
|
|
||||||
return viewNode.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveCell(name: string, context: ResolutionContext): string | null {
|
|
||||||
const cellDirs = ['Cells', 'Cell', 'Views', 'TableViewCells', 'CollectionViewCells'];
|
|
||||||
|
|
||||||
const allFiles = context.getAllFiles();
|
|
||||||
for (const file of allFiles) {
|
|
||||||
if (file.endsWith('.swift') && cellDirs.some((d) => file.includes(`/${d}/`))) {
|
|
||||||
const nodes = context.getNodesInFile(file);
|
|
||||||
const cellNode = nodes.find(
|
|
||||||
(n) => n.kind === 'class' && n.name === name
|
|
||||||
);
|
|
||||||
if (cellNode) {
|
|
||||||
return cellNode.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveProtocol(name: string, context: ResolutionContext): string | null {
|
|
||||||
const allFiles = context.getAllFiles();
|
|
||||||
|
|
||||||
for (const file of allFiles) {
|
|
||||||
if (file.endsWith('.swift')) {
|
|
||||||
const nodes = context.getNodesInFile(file);
|
|
||||||
const protocolNode = nodes.find(
|
|
||||||
(n) => n.kind === 'protocol' && n.name === name
|
|
||||||
);
|
|
||||||
if (protocolNode) {
|
|
||||||
return protocolNode.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper functions for Vapor
|
|
||||||
|
|
||||||
function resolveVaporController(name: string, context: ResolutionContext): string | null {
|
|
||||||
const controllerDirs = ['Controllers', 'Controller', 'Routes'];
|
|
||||||
|
|
||||||
const allFiles = context.getAllFiles();
|
|
||||||
for (const file of allFiles) {
|
|
||||||
if (file.endsWith('.swift') && controllerDirs.some((d) => file.includes(`/${d}/`))) {
|
|
||||||
const nodes = context.getNodesInFile(file);
|
|
||||||
const controllerNode = nodes.find(
|
|
||||||
(n) => (n.kind === 'class' || n.kind === 'struct') && n.name === name
|
|
||||||
);
|
|
||||||
if (controllerNode) {
|
|
||||||
return controllerNode.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveFluentModel(name: string, context: ResolutionContext): string | null {
|
|
||||||
const modelDirs = ['Models', 'Model', 'Entities', 'Database'];
|
|
||||||
|
|
||||||
const allFiles = context.getAllFiles();
|
|
||||||
for (const file of allFiles) {
|
|
||||||
if (file.endsWith('.swift') && modelDirs.some((d) => file.includes(`/${d}/`))) {
|
|
||||||
const nodes = context.getNodesInFile(file);
|
|
||||||
const modelNode = nodes.find(
|
|
||||||
(n) => n.kind === 'class' && n.name === name
|
|
||||||
);
|
|
||||||
if (modelNode) {
|
|
||||||
return modelNode.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveVaporMiddleware(name: string, context: ResolutionContext): string | null {
|
|
||||||
const middlewareDirs = ['Middleware', 'Middlewares'];
|
|
||||||
|
|
||||||
const allFiles = context.getAllFiles();
|
|
||||||
for (const file of allFiles) {
|
|
||||||
if (file.endsWith('.swift') && middlewareDirs.some((d) => file.includes(`/${d}/`))) {
|
|
||||||
const nodes = context.getNodesInFile(file);
|
|
||||||
const mwNode = nodes.find(
|
|
||||||
(n) => (n.kind === 'class' || n.kind === 'struct') && n.name === name
|
|
||||||
);
|
|
||||||
if (mwNode) {
|
|
||||||
return mwNode.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,17 +25,39 @@ export const STOP_WORDS = new Set([
|
|||||||
'over', 'only', 'new', 'out', 'its', 'so', 'up', 'as', 'if',
|
'over', 'only', 'new', 'out', 'its', 'so', 'up', 'as', 'if',
|
||||||
// Code-specific noise
|
// Code-specific noise
|
||||||
'code', 'file', 'files', 'function', 'method', 'class', 'type',
|
'code', 'file', 'files', 'function', 'method', 'class', 'type',
|
||||||
'build', 'run', 'test', 'fix', 'bug', 'call', 'called', 'set', 'add',
|
'build', 'fix', 'bug', 'called', 'set', 'add',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract meaningful search terms from a natural language query.
|
* Extract meaningful search terms from a natural language query.
|
||||||
* Splits camelCase, PascalCase, snake_case, SCREAMING_SNAKE, and dot.notation
|
* Splits camelCase, PascalCase, snake_case, SCREAMING_SNAKE, and dot.notation
|
||||||
* into individual tokens before filtering.
|
* into individual tokens before filtering.
|
||||||
|
*
|
||||||
|
* Preserves original compound identifiers (e.g., "scrapeLoop") alongside
|
||||||
|
* their split parts so that FTS can match both the full symbol name and
|
||||||
|
* individual words within it.
|
||||||
*/
|
*/
|
||||||
export function extractSearchTerms(query: string): string[] {
|
export function extractSearchTerms(query: string): string[] {
|
||||||
const tokens = new Set<string>();
|
const tokens = new Set<string>();
|
||||||
|
|
||||||
|
// First, extract and preserve compound identifiers before splitting
|
||||||
|
// CamelCase: scrapeLoop, UserService, getCallGraph
|
||||||
|
const compoundPattern = /\b([a-zA-Z][a-zA-Z0-9]*(?:[A-Z][a-z]+)+|[A-Z][a-z]+(?:[A-Z][a-z]*)+)\b/g;
|
||||||
|
let match;
|
||||||
|
while ((match = compoundPattern.exec(query)) !== null) {
|
||||||
|
if (match[1] && match[1].length >= 3) {
|
||||||
|
tokens.add(match[1].toLowerCase()); // preserve full compound: "scrapeloop"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// snake_case: scrape_loop, user_service
|
||||||
|
const snakePattern = /\b([a-zA-Z][a-zA-Z0-9]*(?:_[a-zA-Z0-9]+)+)\b/g;
|
||||||
|
while ((match = snakePattern.exec(query)) !== null) {
|
||||||
|
if (match[1] && match[1].length >= 3) {
|
||||||
|
tokens.add(match[1].toLowerCase());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Split camelCase / PascalCase: "getUserName" → "get User Name"
|
// Split camelCase / PascalCase: "getUserName" → "get User Name"
|
||||||
const camelSplit = query
|
const camelSplit = query
|
||||||
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
||||||
|
|||||||
Reference in New Issue
Block a user