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 ❤️.

257 lines (256 loc) 11.5 kB
import { parse } from 'acorn'; import { ancestor as walkAncestor } from 'acorn-walk'; import path from 'path'; import chalk from 'chalk'; import { log } from '../../utils/log.js'; import { markFileAsSkippedTemplate, markFileAsExtractedTemplate, markFileAsFailedTemplate, insertFunctionTemplate, insertGraphClassTemplate, insertEdgeTemplate, insertGraphEntityTagTemplate, insertGraphTagTemplate, selectGraphTagIdTemplate, } from '../sqlTemplates.js'; import { getDbForRepo } from '../client.js'; import { kgModule } from '../../pipeline/modules/kgModule.js'; import { getUniqueId } from '../../utils/sharedUtils.js'; import { BUILTINS } from '../../fileRules/builtins.js'; /** Determine function name from AST node and its parent */ function getFunctionName(node, parent) { if (node.id?.name) return node.id.name; if (parent?.type === 'VariableDeclarator' && parent.id?.name) return parent.id?.name; if (parent?.type === 'Property' && parent.key?.name) return parent.key?.name; if (parent?.type === 'AssignmentExpression' && parent.left?.name) return parent.left?.name; if (parent?.type === 'MethodDefinition' && parent.key?.name) return parent.key?.name; return '<anon>'; } export async function extractFromJS(filePath, content, fileId) { const db = getDbForRepo(); const normalizedPath = path.normalize(filePath).replace(/\\/g, '/'); if (!fileId || typeof fileId !== 'number') { console.error(`❌ extractFromJS: invalid or missing fileId for ${filePath}`); return false; } if (!content || typeof content !== 'string' || !content.trim()) { console.error(`❌ extractFromJS: empty or invalid file content for ${filePath}`); db.prepare(markFileAsFailedTemplate).run({ id: fileId }); return false; } try { const ast = parse(content, { ecmaVersion: 'latest', sourceType: 'module', locations: true }); const functions = []; const classes = []; const imports = []; const exports = []; // --- Function info --- const handleFunctionNode = (node, ancestors) => { const parent = ancestors[ancestors.length - 2]; const name = getFunctionName(node, parent); const funcContent = content.slice(node.start, node.end); const unique_id = getUniqueId(name, filePath, node.loc?.start.line ?? -1, node.start, funcContent); functions.push({ name, start_line: node.loc?.start.line ?? -1, end_line: node.loc?.end.line ?? -1, content: funcContent, unique_id, }); }; // --- Class info --- const handleClassNode = (node) => { const className = node.id?.name || `${path.basename(filePath)}:<anon-class>`; const classContent = content.slice(node.start, node.end); const unique_id = node.id?.name ? `${className}@${normalizedPath}` : getUniqueId(className, filePath, node.loc?.start.line ?? -1, node.start, classContent); classes.push({ name: className, start_line: node.loc?.start.line ?? -1, end_line: node.loc?.end.line ?? -1, content: classContent, superClass: node.superClass?.name ?? null, unique_id, }); }; // --- AST traversal --- walkAncestor(ast, { ImportDeclaration(node) { if (node.source?.value) imports.push(node.source.value); }, ExportNamedDeclaration(node) { if (node.source?.value) exports.push(node.source.value); }, ExportAllDeclaration(node) { if (node.source?.value) exports.push(node.source.value); }, FunctionDeclaration: handleFunctionNode, FunctionExpression: handleFunctionNode, ArrowFunctionExpression: handleFunctionNode, ClassDeclaration: handleClassNode, ClassExpression: handleClassNode, }); if (functions.length === 0 && classes.length === 0) { log(`⚠️ No functions/classes found in JS file: ${filePath}`); try { db.prepare(markFileAsSkippedTemplate).run({ id: fileId }); } catch { } return false; } // --- KG tagging --- try { const kgInput = { fileId, filepath: filePath, summary: undefined }; const kgResult = await kgModule.run(kgInput, content); if (!kgResult.entities?.length) { console.log(chalk.yellow(`⚠️ [KG] Garbage or empty result for ${filePath}`)); db.prepare(markFileAsFailedTemplate).run({ id: fileId }); 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 }); return getTagIdStmt.get({ name: tag })?.id; } catch { return undefined; } }; const persistEntityTags = (entity) => { if (!entity.type || !Array.isArray(entity.tags) || !entity.tags.length) 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 { /* ignore */ } } }; kgResult.entities.forEach(persistEntityTags); } catch (kgErr) { console.log(chalk.yellow(`⚠️ [KG] Garbage or failed parse for ${filePath}: ${kgErr instanceof Error ? kgErr.message : kgErr}`)); db.prepare(markFileAsFailedTemplate).run({ id: fileId }); return false; } const seenEdges = new Set(); const jsBuiltins = BUILTINS.ts; // --- Insert functions --- for (const fn of functions) { db.prepare(insertFunctionTemplate).run({ file_id: fileId, name: fn.name, start_line: fn.start_line, end_line: fn.end_line, content: fn.content, embedding: null, lang: 'js', unique_id: fn.unique_id, }); const edgeKey = `file->${fn.unique_id}`; if (!seenEdges.has(edgeKey)) { db.prepare(insertEdgeTemplate).run({ source_type: 'file', source_unique_id: normalizedPath, target_type: 'function', target_unique_id: fn.unique_id, relation: 'contains', }); seenEdges.add(edgeKey); } // --- JS call edges --- try { const fnAst = parse(fn.content, { ecmaVersion: 'latest', sourceType: 'module', locations: true }); walkAncestor(fnAst, { CallExpression(node) { let calleeName; if (node.callee?.name) calleeName = node.callee.name; else if (node.callee?.property?.name) calleeName = node.callee.property.name; if (!calleeName || jsBuiltins.has(calleeName)) return; const targetUniqueId = `${calleeName}@${normalizedPath}`; const callEdgeKey = `${fn.unique_id}->${targetUniqueId}`; if (!seenEdges.has(callEdgeKey)) { try { db.prepare(insertEdgeTemplate).run({ source_type: 'function', source_unique_id: fn.unique_id, target_type: 'function', target_unique_id: targetUniqueId, relation: 'calls', }); seenEdges.add(callEdgeKey); } catch { /* ignore */ } } }, }); } catch (parseErr) { console.log(chalk.yellow(`⚠️ [Extract] Failed to parse function ${fn.name} in ${filePath}: ${parseErr.message}`)); } } // --- Insert classes --- for (const cls of classes) { db.prepare(insertGraphClassTemplate).run({ file_id: fileId, name: cls.name, start_line: cls.start_line, end_line: cls.end_line, content: cls.content, embedding: null, lang: 'js', unique_id: cls.unique_id, }); db.prepare(insertEdgeTemplate).run({ source_type: 'file', source_unique_id: normalizedPath, target_type: 'class', target_unique_id: cls.unique_id, relation: 'contains', }); if (cls.superClass) { db.prepare(insertEdgeTemplate).run({ source_type: 'class', source_unique_id: cls.unique_id, target_type: `unresolved:${cls.superClass}`, relation: 'inherits', }); } } // --- Imports/Exports edges --- const handleEdges = (items, relation) => { for (const item of items) { const resolved = item.startsWith('.') ? path.resolve(path.dirname(filePath), item) : item; const normalizedTarget = path.normalize(resolved).replace(/\\/g, '/'); db.prepare(insertEdgeTemplate).run({ source_type: 'file', source_unique_id: normalizedPath, target_type: 'file', target_unique_id: normalizedTarget, relation, }); } }; handleEdges(imports, 'imports'); handleEdges(exports, 'exports'); db.prepare(markFileAsExtractedTemplate).run({ id: fileId }); return true; } catch (err) { console.log(chalk.yellow(`⚠️ [Extract] Failed for ${filePath}: ${err.message}`)); db.prepare(markFileAsFailedTemplate).run({ id: fileId }); return false; } }