scai
Version:
> AI-powered CLI tool for commit messages **and** pull request reviews — using local models.
123 lines (122 loc) • 5.08 kB
JavaScript
import { parse } from 'acorn';
import { ancestor as walkAncestor } from 'acorn-walk';
import { generateEmbedding } from '../../lib/generateEmbedding.js';
import path from 'path';
import { log } from '../../utils/log.js';
import { markFileAsSkippedTemplate, markFileAsExtractedTemplate, markFileAsFailedTemplate } from '../sqlTemplates.js';
import { getDbForRepo } from '../client.js';
function getFunctionName(node, parent, fileName) {
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 `${fileName}:<anon>`;
}
export async function extractFromJS(filePath, content, fileId) {
const db = getDbForRepo();
try {
console.log(`[Debug] Attempting to parse: ${filePath}`);
console.log(`[Debug] First 3 lines:\n${content.split('\n').slice(0, 3).join('\n')}`);
const ast = parse(content, {
ecmaVersion: 'latest',
sourceType: 'module',
locations: true,
});
const functions = [];
walkAncestor(ast, {
FunctionDeclaration(node, ancestors) {
const parent = ancestors[ancestors.length - 2];
const name = getFunctionName(node, parent, path.basename(filePath));
functions.push({
name,
start_line: node.loc?.start.line ?? -1,
end_line: node.loc?.end.line ?? -1,
content: content.slice(node.start, node.end),
});
},
FunctionExpression(node, ancestors) {
const parent = ancestors[ancestors.length - 2];
const name = getFunctionName(node, parent, path.basename(filePath));
functions.push({
name,
start_line: node.loc?.start.line ?? -1,
end_line: node.loc?.end.line ?? -1,
content: content.slice(node.start, node.end),
});
},
ArrowFunctionExpression(node, ancestors) {
const parent = ancestors[ancestors.length - 2];
const name = getFunctionName(node, parent, path.basename(filePath));
functions.push({
name,
start_line: node.loc?.start.line ?? -1,
end_line: node.loc?.end.line ?? -1,
content: content.slice(node.start, node.end),
});
},
});
if (functions.length === 0) {
log(`⚠️ No functions found in: ${filePath}`);
db.prepare(markFileAsSkippedTemplate).run({ id: fileId });
return false;
}
log(`🔍 Found ${functions.length} functions in ${filePath}`);
for (const fn of functions) {
const embedding = await generateEmbedding(fn.content);
const result = db.prepare(`
INSERT INTO functions (
file_id, name, start_line, end_line, content, embedding, lang
) VALUES (
@file_id, @name, @start_line, @end_line, @content, @embedding, @lang
)
`).run({
file_id: fileId,
name: fn.name,
start_line: fn.start_line,
end_line: fn.end_line,
content: fn.content,
embedding: JSON.stringify(embedding),
lang: 'js'
});
const callerId = result.lastInsertRowid;
const fnAst = parse(fn.content, {
ecmaVersion: 'latest',
sourceType: 'module',
locations: true,
});
const calls = [];
walkAncestor(fnAst, {
CallExpression(node) {
if (node.callee?.type === 'Identifier' && node.callee.name) {
calls.push({ calleeName: node.callee.name });
}
}
});
for (const call of calls) {
db.prepare(`
INSERT INTO function_calls (caller_id, callee_name)
VALUES (@caller_id, @callee_name)
`).run({
caller_id: callerId,
callee_name: call.calleeName
});
}
log(`📌 Indexed function: ${fn.name} with ${calls.length} calls`);
}
db.prepare(markFileAsExtractedTemplate).run({ id: fileId });
log(`✅ Marked functions as extracted for ${filePath}`);
return true;
}
catch (err) {
log(`❌ Failed to extract from: ${filePath}`);
log(` ↳ ${String(err.message)}`);
db.prepare(markFileAsFailedTemplate).run({ id: fileId });
return false;
}
}