Compare commits

..
8 changed files with 77 additions and 142 deletions
@@ -3,7 +3,7 @@ import { ASTCache } from './ast-cache.js';
import { SymbolTable } from './symbol-table.js';
import { ImportMap } from './import-processor.js';
import Parser from 'tree-sitter';
import { loadParser, loadLanguage } from '../tree-sitter/parser-loader.js';
import { isLanguageAvailable, loadParser, loadLanguage } from '../tree-sitter/parser-loader.js';
import { LANGUAGE_QUERIES } from './tree-sitter-queries.js';
import { generateId } from '../../lib/utils.js';
import { getLanguageFromFilename, yieldToEventLoop } from './utils.js';
@@ -159,6 +159,7 @@ export const processCalls = async (
// 1. Check language support first
const language = getLanguageFromFilename(file.path);
if (!language) continue;
if (!isLanguageAvailable(language)) continue;
const queryStr = LANGUAGE_QUERIES[language];
if (!queryStr) continue;
@@ -10,7 +10,7 @@ import { KnowledgeGraph } from '../graph/types.js';
import { ASTCache } from './ast-cache.js';
import { SymbolTable } from './symbol-table.js';
import Parser from 'tree-sitter';
import { loadParser, loadLanguage } from '../tree-sitter/parser-loader.js';
import { isLanguageAvailable, loadParser, loadLanguage } from '../tree-sitter/parser-loader.js';
import { LANGUAGE_QUERIES } from './tree-sitter-queries.js';
import { generateId } from '../../lib/utils.js';
import { getLanguageFromFilename, yieldToEventLoop } from './utils.js';
@@ -33,6 +33,7 @@ export const processHeritage = async (
// 1. Check language support
const language = getLanguageFromFilename(file.path);
if (!language) continue;
if (!isLanguageAvailable(language)) continue;
const queryStr = LANGUAGE_QUERIES[language];
if (!queryStr) continue;
@@ -2,9 +2,8 @@ import fs from 'fs/promises';
import path from 'path';
import { KnowledgeGraph } from '../graph/types.js';
import { ASTCache } from './ast-cache.js';
import { SymbolTable } from './symbol-table.js';
import Parser from 'tree-sitter';
import { loadParser, loadLanguage } from '../tree-sitter/parser-loader.js';
import { isLanguageAvailable, loadParser, loadLanguage } from '../tree-sitter/parser-loader.js';
import { LANGUAGE_QUERIES } from './tree-sitter-queries.js';
import { generateId } from '../../lib/utils.js';
import { getLanguageFromFilename, yieldToEventLoop } from './utils.js';
@@ -729,7 +728,6 @@ export const processImports = async (
onProgress?: (current: number, total: number) => void,
repoRoot?: string,
allPaths?: string[],
symbolTable?: SymbolTable,
) => {
// Use allPaths (full repo) when available for cross-chunk resolution, else fall back to chunk files
const allFileList = allPaths ?? files.map(f => f.path);
@@ -775,57 +773,6 @@ export const processImports = async (
importMap.get(filePath)!.add(resolvedPath);
};
// Helper: add symbol-level IMPORTS edges for named imports
const addSymbolImportEdges = (filePath: string, resolvedPath: string, symbolNames?: string[]) => {
if (!symbolNames || !symbolTable) return;
const sourceId = generateId('File', filePath);
for (const name of symbolNames) {
const targetNodeId = symbolTable.lookupExact(resolvedPath, name);
if (!targetNodeId) continue;
const relId = generateId('IMPORTS', `${filePath}:${name}->${resolvedPath}`);
graph.addRelationship({
id: relId,
sourceId,
targetId: targetNodeId,
type: 'IMPORTS',
confidence: 1.0,
reason: '',
});
}
};
// Helper: extract imported symbol names from AST node (for sequential path)
const extractSymbolNames = (importNode: any, language: string): string[] => {
const names: string[] = [];
if (language === SupportedLanguages.Python) {
for (const child of importNode.namedChildren) {
if (child.type === 'module_name') continue;
if (child.type === 'wildcard_import') continue;
if (child.type === 'dotted_name' || child.type === 'identifier') {
names.push(child.text);
} else if (child.type === 'aliased_import') {
const nameNode = child.childForFieldName?.('name') || child.namedChildren?.[0];
if (nameNode) names.push(nameNode.text);
}
}
return names;
}
if (language === SupportedLanguages.TypeScript || language === SupportedLanguages.JavaScript) {
const importClause = importNode.namedChildren?.find((c: any) => c.type === 'import_clause');
const namedImports = importClause?.namedChildren?.find((c: any) => c.type === 'named_imports');
if (namedImports) {
for (const spec of namedImports.namedChildren) {
if (spec.type === 'import_specifier') {
const nameNode = spec.childForFieldName?.('name');
if (nameNode) names.push(nameNode.text);
}
}
}
return names;
}
return names;
};
for (let i = 0; i < files.length; i++) {
const file = files[i];
onProgress?.(i + 1, files.length);
@@ -834,6 +781,7 @@ export const processImports = async (
// 1. Check language support first
const language = getLanguageFromFilename(file.path);
if (!language) continue;
if (!isLanguageAvailable(language)) continue;
const queryStr = LANGUAGE_QUERIES[language];
if (!queryStr) continue;
@@ -897,9 +845,6 @@ export const processImports = async (
: sourceNode.text.replace(/['"<>]/g, '');
totalImportsFound++;
// Extract imported symbol names for symbol-level edges
const symbolNames = extractSymbolNames(captureMap['import'], language);
// ---- JVM languages (Java + Kotlin): handle wildcards and member imports ----
if (language === SupportedLanguages.Java || language === SupportedLanguages.Kotlin) {
const exts = language === SupportedLanguages.Java ? ['.java'] : KOTLIN_EXTENSIONS;
@@ -988,7 +933,6 @@ export const processImports = async (
if (resolvedPath) {
addImportEdge(file.path, resolvedPath);
addSymbolImportEdges(file.path, resolvedPath, symbolNames);
}
}
});
@@ -1013,7 +957,6 @@ export const processImportsFromExtracted = async (
onProgress?: (current: number, total: number) => void,
repoRoot?: string,
prebuiltCtx?: ImportResolutionContext,
symbolTable?: SymbolTable,
) => {
const ctx = prebuiltCtx ?? buildImportResolutionContext(files.map(f => f.path));
const { allFilePaths, allFileList, normalizedFileList, suffixIndex: index, resolveCache } = ctx;
@@ -1049,25 +992,6 @@ export const processImportsFromExtracted = async (
importMap.get(filePath)!.add(resolvedPath);
};
// Helper: add symbol-level IMPORTS edges for named imports
const addSymbolImportEdges = (filePath: string, resolvedPath: string, symbolNames?: string[]) => {
if (!symbolNames || !symbolTable) return;
const sourceId = generateId('File', filePath);
for (const name of symbolNames) {
const targetNodeId = symbolTable.lookupExact(resolvedPath, name);
if (!targetNodeId) continue;
const relId = generateId('IMPORTS', `${filePath}:${name}->${resolvedPath}`);
graph.addRelationship({
id: relId,
sourceId,
targetId: targetNodeId,
type: 'IMPORTS',
confidence: 1.0,
reason: '',
});
}
};
// Group by file for progress reporting (users see file count, not import count)
const importsByFile = new Map<string, ExtractedImport[]>();
for (const imp of extractedImports) {
@@ -1104,7 +1028,7 @@ export const processImportsFromExtracted = async (
await yieldToEventLoop();
}
for (const { rawImportPath, language, symbolNames } of fileImports) {
for (const { rawImportPath, language } of fileImports) {
totalImportsFound++;
// Check resolve cache first
@@ -1197,7 +1121,6 @@ export const processImportsFromExtracted = async (
if (resolvedPath) {
addImportEdge(filePath, resolvedPath);
addSymbolImportEdges(filePath, resolvedPath, symbolNames);
}
}
}
@@ -42,7 +42,6 @@ const DEFINITION_CAPTURE_KEYS = [
'definition.annotation',
'definition.constructor',
'definition.template',
'definition.instance',
] as const;
const getDefinitionNodeFromCaptures = (captureMap: Record<string, any>): any | null => {
@@ -366,7 +365,6 @@ const processParsingSequential = async (
else if (captureMap['definition.annotation']) nodeLabel = 'Annotation';
else if (captureMap['definition.constructor']) nodeLabel = 'Constructor';
else if (captureMap['definition.template']) nodeLabel = 'Template';
else if (captureMap['definition.instance']) nodeLabel = 'CodeElement';
const definitionNodeForRange = getDefinitionNodeFromCaptures(captureMap);
const startLine = definitionNodeForRange ? definitionNodeForRange.startPosition.row : (nameNode ? nameNode.startPosition.row : 0);
+2 -2
View File
@@ -213,7 +213,7 @@ export const runPipelineFromRepo = async (
if (chunkWorkerData) {
// Imports
await processImportsFromExtracted(graph, allPathObjects, chunkWorkerData.imports, importMap, undefined, repoPath, importCtx, symbolTable);
await processImportsFromExtracted(graph, allPathObjects, chunkWorkerData.imports, importMap, undefined, repoPath, importCtx);
// Calls — resolve immediately, then free the array
if (chunkWorkerData.calls.length > 0) {
await processCallsFromExtracted(graph, chunkWorkerData.calls, symbolTable, importMap);
@@ -227,7 +227,7 @@ export const runPipelineFromRepo = async (
await processRoutesFromExtracted(graph, chunkWorkerData.routes, symbolTable, importMap);
}
} else {
await processImports(graph, chunkFiles, astCache, importMap, undefined, repoPath, allPaths, symbolTable);
await processImports(graph, chunkFiles, astCache, importMap, undefined, repoPath, allPaths);
sequentialChunkPaths.push(chunkPaths);
}
@@ -141,13 +141,6 @@ export const PYTHON_QUERIES = `
function: (attribute
attribute: (identifier) @call.name)) @call
; Module-level singleton instances: service = ServiceClass()
(module
(expression_statement
(assignment
left: (identifier) @name
right: (call))) @definition.instance)
; Heritage queries - Python class inheritance
(class_definition
name: (identifier) @heritage.class
@@ -63,7 +63,6 @@ export interface ExtractedImport {
filePath: string;
rawImportPath: string;
language: string;
symbolNames?: string[];
}
export interface ExtractedCall {
@@ -281,47 +280,6 @@ const FUNCTION_NODE_TYPES = new Set([
'init_declaration', 'deinit_declaration',
]);
/** Extract the specific symbol names from an import AST node.
* Python: `from X import foo, bar` → ['foo', 'bar']
* JS/TS: `import { foo, bar } from 'X'` → ['foo', 'bar']
* Returns empty array for bare module imports or unsupported patterns. */
const extractImportedSymbolNames = (importNode: any, language: string): string[] => {
const names: string[] = [];
if (language === SupportedLanguages.Python) {
// import_from_statement children: module_name (dotted_name) + name fields (dotted_name | aliased_import)
for (const child of importNode.namedChildren) {
if (child.type === 'module_name') continue;
if (child.type === 'wildcard_import') continue;
if (child.type === 'dotted_name' || child.type === 'identifier') {
names.push(child.text);
} else if (child.type === 'aliased_import') {
// from X import foo as bar — use original name 'foo'
const nameNode = child.childForFieldName?.('name') || child.namedChildren?.[0];
if (nameNode) names.push(nameNode.text);
}
}
return names;
}
if (language === SupportedLanguages.TypeScript || language === SupportedLanguages.JavaScript) {
// import_statement > import_clause > named_imports > import_specifier*
const importClause = importNode.namedChildren?.find((c: any) => c.type === 'import_clause');
const namedImports = importClause?.namedChildren?.find((c: any) => c.type === 'named_imports');
if (namedImports) {
for (const spec of namedImports.namedChildren) {
if (spec.type === 'import_specifier') {
const nameNode = spec.childForFieldName?.('name');
if (nameNode) names.push(nameNode.text);
}
}
}
return names;
}
return names;
};
/** Walk up AST to find enclosing function, return its generateId or null for top-level */
const findEnclosingFunctionId = (node: any, filePath: string): string | null => {
let current = node.parent;
@@ -522,7 +480,6 @@ const getLabelFromCaptures = (captureMap: Record<string, any>): string | null =>
if (captureMap['definition.annotation']) return 'Annotation';
if (captureMap['definition.constructor']) return 'Constructor';
if (captureMap['definition.template']) return 'Template';
if (captureMap['definition.instance']) return 'CodeElement';
return 'CodeElement';
};
@@ -549,7 +506,6 @@ const DEFINITION_CAPTURE_KEYS = [
'definition.annotation',
'definition.constructor',
'definition.template',
'definition.instance',
] as const;
const getDefinitionNodeFromCaptures = (captureMap: Record<string, any>): any | null => {
@@ -1179,15 +1135,10 @@ const processFileGroup = (
const rawImportPath = language === SupportedLanguages.Kotlin
? appendKotlinWildcard(captureMap['import.source'].text.replace(/['"<>]/g, ''), captureMap['import'])
: captureMap['import.source'].text.replace(/['"<>]/g, '');
// Extract imported symbol names from the AST node
const symbolNames = extractImportedSymbolNames(captureMap['import'], language);
result.imports.push({
filePath: file.path,
rawImportPath,
language: language,
symbolNames: symbolNames.length > 0 ? symbolNames : undefined,
});
continue;
}
@@ -0,0 +1,68 @@
import { describe, expect, it, vi, beforeEach } from 'vitest';
vi.mock('../../src/core/tree-sitter/parser-loader.js', () => ({
loadParser: vi.fn(async () => ({
parse: vi.fn(),
getLanguage: vi.fn(),
})),
loadLanguage: vi.fn(async () => undefined),
isLanguageAvailable: vi.fn(() => true),
}));
import { createKnowledgeGraph } from '../../src/core/graph/graph.js';
import { createASTCache } from '../../src/core/ingestion/ast-cache.js';
import { createImportMap, processImports } from '../../src/core/ingestion/import-processor.js';
import { processCalls } from '../../src/core/ingestion/call-processor.js';
import { processHeritage } from '../../src/core/ingestion/heritage-processor.js';
import { createSymbolTable } from '../../src/core/ingestion/symbol-table.js';
import * as parserLoader from '../../src/core/tree-sitter/parser-loader.js';
describe('sequential native parser availability', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('skips Swift files in processImports when the native parser is unavailable', async () => {
vi.mocked(parserLoader.isLanguageAvailable).mockReturnValue(false);
await expect(processImports(
createKnowledgeGraph(),
[{ path: 'App.swift', content: 'import Foundation' }],
createASTCache(),
createImportMap(),
undefined,
'/tmp/repo',
['App.swift'],
)).resolves.toBeUndefined();
expect(parserLoader.loadLanguage).not.toHaveBeenCalled();
});
it('skips Swift files in processCalls when the native parser is unavailable', async () => {
vi.mocked(parserLoader.isLanguageAvailable).mockReturnValue(false);
await expect(processCalls(
createKnowledgeGraph(),
[{ path: 'App.swift', content: 'func demo() {}' }],
createASTCache(),
createSymbolTable(),
createImportMap(),
)).resolves.toBeUndefined();
expect(parserLoader.loadLanguage).not.toHaveBeenCalled();
});
it('skips Swift files in processHeritage when the native parser is unavailable', async () => {
vi.mocked(parserLoader.isLanguageAvailable).mockReturnValue(false);
await expect(processHeritage(
createKnowledgeGraph(),
[{ path: 'App.swift', content: 'class AppViewController: UIViewController {}' }],
createASTCache(),
createSymbolTable(),
)).resolves.toBeUndefined();
expect(parserLoader.loadLanguage).not.toHaveBeenCalled();
});
});