UNPKG

scai

Version:

> **AI-powered CLI for local code analysis, commit message suggestions, and natural-language queries.** 100% local, private, GDPR-friendly, made in Denmark/EU with ❤️.

267 lines (266 loc) 12 kB
import { Project, SyntaxKind, } from 'ts-morph'; import path from 'path'; import { log } from '../../utils/log.js'; import { getDbForRepo } from '../client.js'; import { markFileAsSkippedTemplate, markFileAsExtractedTemplate, markFileAsFailedTemplate, insertFunctionTemplate, insertGraphClassTemplate, insertEdgeTemplate, insertGraphEntityTagTemplate, insertGraphTagTemplate, selectGraphTagIdTemplate, } from '../sqlTemplates.js'; import { kgModule } from '../../pipeline/modules/kgModule.js'; import { BUILTINS } from '../../fileRules/builtins.js'; import { getUniqueId } from '../../utils/sharedUtils.js'; export async function extractFromTS(filePath, content, fileId) { const db = getDbForRepo(); const normalizedPath = path.normalize(filePath).replace(/\\/g, '/'); try { const project = new Project({ useInMemoryFileSystem: true }); const sourceFile = project.createSourceFile(filePath, content); const functions = []; const classes = []; // --- Gather AST nodes --- const allFuncNodes = [ ...sourceFile.getDescendantsOfKind(SyntaxKind.FunctionDeclaration), ...sourceFile.getDescendantsOfKind(SyntaxKind.FunctionExpression), ...sourceFile.getDescendantsOfKind(SyntaxKind.ArrowFunction), ]; const allClassNodes = [ ...sourceFile.getDescendantsOfKind(SyntaxKind.ClassDeclaration), ...sourceFile.getDescendantsOfKind(SyntaxKind.ClassExpression), ]; if (allFuncNodes.length === 0 && allClassNodes.length === 0) { log(`⚠️ No functions/classes found in TS file: ${filePath}`); try { db.prepare(markFileAsSkippedTemplate).run({ id: fileId }); } catch { } return false; } log(`🔍 Found ${allFuncNodes.length} functions and ${allClassNodes.length} classes in ${filePath}`); // --- Knowledge Graph tagging --- try { const kgInput = { fileId, filepath: filePath, summary: undefined }; const kgResult = await kgModule.run(kgInput, content); if (!kgResult.entities?.length) { log(`⚠️ KG returned no entities for ${filePath}`); try { db.prepare(markFileAsFailedTemplate).run({ id: fileId }); } catch { } return false; } const insertTagStmt = db.prepare(insertGraphTagTemplate); const getTagIdStmt = db.prepare(selectGraphTagIdTemplate); const insertEntityTagStmt = db.prepare(insertGraphEntityTagTemplate); const persistTag = (tag) => { try { insertTagStmt.run({ name: tag }); const tagRow = getTagIdStmt.get({ name: tag }); return tagRow?.id; } catch { return undefined; } }; const persistEntityTags = (entity) => { if (!entity.type || !Array.isArray(entity.tags) || entity.tags.length === 0) return; for (const tag of entity.tags) { if (!tag || typeof tag !== 'string') continue; const tagId = persistTag(tag); if (!tagId) continue; const matchedUniqueId = functions.find(f => f.name === entity.name)?.unique_id || classes.find(c => c.name === entity.name)?.unique_id || `${entity.name}@${filePath}`; try { insertEntityTagStmt.run({ entity_type: entity.type, entity_unique_id: matchedUniqueId, tag_id: tagId, }); } catch { } } }; kgResult.entities.forEach(persistEntityTags); log(`🏷 Persisted LLM-generated tags for ${filePath}`); } catch (kgErr) { log(`⚠️ KG tagging failed for ${filePath}: ${kgErr.message}`); try { db.prepare(markFileAsFailedTemplate).run({ id: fileId }); } catch { } return false; } // --- Collect functions --- const tsBuiltins = BUILTINS.ts; for (const funcNode of allFuncNodes) { const name = funcNode.getSymbol()?.getName() ?? '<anon>'; const start = funcNode.getStartLineNumber(); const end = funcNode.getEndLineNumber(); const code = funcNode.getText(); const unique_id = getUniqueId(name, filePath, start, funcNode.getStart(), code); functions.push({ name, start_line: start, end_line: end, content: code, unique_id }); try { const callExprs = funcNode.getDescendantsOfKind(SyntaxKind.CallExpression); const edgeSet = new Set(); for (const callExpr of callExprs) { let calleeName; try { const expr = callExpr.getExpression(); calleeName = expr.getSymbol()?.getName() ?? expr.getText(); } catch { /* ignore symbol resolution errors */ } if (!calleeName || tsBuiltins.has(calleeName)) continue; const targetUniqueId = `${calleeName}@${normalizedPath}`; const edgeKey = `${unique_id}->${targetUniqueId}`; if (!edgeSet.has(edgeKey)) { edgeSet.add(edgeKey); try { db.prepare(insertEdgeTemplate).run({ source_type: 'function', source_unique_id: unique_id, target_type: 'function', target_unique_id: targetUniqueId, relation: 'calls', }); } catch { /* ignore DB insert errors */ } } } } catch (funcErr) { log(`⚠️ Failed to process call expressions in function ${name} of ${filePath}: ${funcErr.message}`); } } // --- Collect classes --- for (const clsNode of allClassNodes) { const name = clsNode.getName() ?? `${path.basename(filePath)}:<anon-class>`; const start = clsNode.getStartLineNumber(); const end = clsNode.getEndLineNumber(); const code = clsNode.getText(); let superClass = null; try { superClass = clsNode.getExtends()?.getText() ?? null; } catch { /* ignore */ } const unique_id = getUniqueId(name, filePath, start, clsNode.getStart(), code); classes.push({ name, start_line: start, end_line: end, content: code, superClass, unique_id }); if (superClass) { try { db.prepare(insertEdgeTemplate).run({ source_type: 'class', source_unique_id: unique_id, target_type: 'class', target_unique_id: `unresolved:${superClass}`, relation: 'inherits', }); } catch { /* ignore DB insert errors */ } } } const seenEdges = new Set(); // --- Insert functions --- for (const fn of functions) { db.prepare(insertFunctionTemplate).run({ file_id: fileId, name: fn.name ?? '<anon>', start_line: fn.start_line ?? -1, end_line: fn.end_line ?? -1, content: fn.content ?? '', embedding: null, lang: 'ts', unique_id: fn.unique_id, }); const containsEdgeKey = `file->${fn.unique_id}`; if (!seenEdges.has(containsEdgeKey)) { db.prepare(insertEdgeTemplate).run({ source_type: 'file', source_unique_id: normalizedPath, target_type: 'function', target_unique_id: fn.unique_id, relation: 'contains', }); seenEdges.add(containsEdgeKey); } } // --- Insert classes --- for (const cls of classes) { db.prepare(insertGraphClassTemplate).run({ file_id: fileId, name: cls.name, start_line: cls.start_line ?? -1, end_line: cls.end_line ?? -1, content: cls.content ?? '', embedding: null, lang: 'ts', unique_id: cls.unique_id, }); const containsEdgeKey = `file->${cls.unique_id}`; if (!seenEdges.has(containsEdgeKey)) { db.prepare(insertEdgeTemplate).run({ source_type: 'file', source_unique_id: normalizedPath, target_type: 'class', target_unique_id: cls.unique_id, relation: 'contains', }); seenEdges.add(containsEdgeKey); } } // --- Imports --- const importDecls = sourceFile.getDescendantsOfKind(SyntaxKind.ImportDeclaration); for (const imp of importDecls) { const moduleSpecifier = imp.getModuleSpecifierValue(); if (!moduleSpecifier) continue; const resolvedPath = moduleSpecifier.startsWith('.') ? path.resolve(path.dirname(filePath), moduleSpecifier) : moduleSpecifier; const normalizedTarget = path.normalize(resolvedPath).replace(/\\/g, '/'); try { db.prepare(insertEdgeTemplate).run({ source_type: 'file', source_unique_id: normalizedPath, target_type: 'file', target_unique_id: normalizedTarget, relation: 'imports', }); } catch { /* ignore */ } } // --- Exports --- const exportDecls = sourceFile.getDescendantsOfKind(SyntaxKind.ExportDeclaration); for (const exp of exportDecls) { const moduleSpecifier = exp.getModuleSpecifierValue(); if (!moduleSpecifier) continue; const resolvedPath = moduleSpecifier.startsWith('.') ? path.resolve(path.dirname(filePath), moduleSpecifier) : moduleSpecifier; const normalizedTarget = path.normalize(resolvedPath).replace(/\\/g, '/'); try { db.prepare(insertEdgeTemplate).run({ source_type: 'file', source_unique_id: normalizedPath, target_type: 'file', target_unique_id: normalizedTarget, relation: 'exports', }); } catch { /* ignore */ } } db.prepare(markFileAsExtractedTemplate).run({ id: fileId }); log(`📊 Extraction summary for ${filePath}: ${functions.length} functions, ${classes.length} classes`); return true; } catch (err) { log(`❌ Failed to extract from TS file: ${filePath}`); log(` ↳ ${err.message}`); try { db.prepare(markFileAsFailedTemplate).run({ id: fileId }); } catch { } return false; } }