arela
Version:
AI-powered CTO with multi-agent orchestration, code summarization, visual testing (web + mobile) for blazing fast development.
516 lines • 18.2 kB
JavaScript
import Parser from "tree-sitter";
import TypeScript from "tree-sitter-typescript";
const TS_LANGUAGE = TypeScript.typescript;
export class ASTExtractor {
parser;
constructor() {
this.parser = new Parser();
this.parser.setLanguage(TS_LANGUAGE);
}
/**
* Extract semantic contract from code
*/
async extract(code, filePath) {
// Parse with tree-sitter for future, richer AST-based analysis.
// Current implementation primarily uses lightweight textual parsing
// to keep logic robust without deep grammar coupling.
this.parser.parse(code);
const lines = code.split(/\r?\n/);
const lineOffsets = this.computeLineOffsets(lines);
const exports = this.extractExports(code, lines, lineOffsets);
const imports = this.extractImports(code);
return {
filePath,
description: this.extractFileDescription(lines),
exports,
imports,
metadata: {
language: this.detectLanguage(filePath),
linesOfCode: lines.length,
extractedAt: new Date().toISOString(),
},
};
}
detectLanguage(filePath) {
if (filePath.endsWith(".ts") || filePath.endsWith(".tsx")) {
return "typescript";
}
if (filePath.endsWith(".js") || filePath.endsWith(".jsx")) {
return "javascript";
}
return "typescript";
}
computeLineOffsets(lines) {
const offsets = [];
let current = 0;
for (const line of lines) {
offsets.push(current);
current += line.length + 1; // assume \n
}
return offsets;
}
indexToLine(index, lineOffsets) {
// Binary search for the greatest offset <= index
let low = 0;
let high = lineOffsets.length - 1;
while (low <= high) {
const mid = (low + high) >> 1;
const offset = lineOffsets[mid];
if (offset === index)
return mid;
if (offset < index) {
low = mid + 1;
}
else {
high = mid - 1;
}
}
return Math.max(0, high);
}
extractFileDescription(lines) {
// Look for a leading file-level JSDoc before any non-comment, non-empty line.
let firstCodeLine = 0;
for (let i = 0; i < lines.length; i++) {
const trimmed = lines[i].trim();
if (!trimmed || trimmed.startsWith("//"))
continue;
if (trimmed.startsWith("/*")) {
// Might be JSDoc; handled below.
firstCodeLine = i;
break;
}
firstCodeLine = i;
break;
}
if (firstCodeLine === 0)
return undefined;
return this.extractJsDocForLine(firstCodeLine, lines);
}
extractExports(code, lines, lineOffsets) {
const exports = [];
const exportRegex = /^export\s+(default\s+)?(async\s+)?(function|class|const|let|var|type|interface)\s+([A-Za-z0-9_$]*)/gm;
let match;
while ((match = exportRegex.exec(code)) !== null) {
const [full, defaultKeyword, asyncKeyword, kindToken, nameRaw] = match;
const startIndex = match.index;
const startLine = this.indexToLine(startIndex, lineOffsets);
const jsDoc = this.extractJsDocForLine(startLine, lines);
const isAsync = Boolean(asyncKeyword);
const name = nameRaw || "default";
if (kindToken === "function") {
const signature = this.extractFunctionSignatureFromText(code.slice(startIndex + full.length), isAsync);
exports.push({
name,
kind: "function",
jsDoc,
signature,
});
}
else if (kindToken === "class") {
const classBodyText = this.extractBlockText(code, startIndex + full.length);
const methods = this.extractMethodsFromClassBody(classBodyText, lines);
exports.push({
name,
kind: "class",
jsDoc,
methods,
});
}
else if (kindToken === "type") {
exports.push({
name,
kind: "type",
jsDoc,
});
}
else if (kindToken === "interface") {
exports.push({
name,
kind: "interface",
jsDoc,
});
}
else {
// const / let / var
const signature = this.extractArrowFunctionSignatureFromExport(code, match.index);
exports.push({
name,
kind: "const",
jsDoc,
signature,
});
}
}
// Handle named exports: export { foo, bar as baz }
const namedExportRegex = /^export\s*\{([^}]+)\}(?:\s*from\s*["'][^"']+["'])?/gm;
while ((match = namedExportRegex.exec(code)) !== null) {
const namesPart = match[1];
const startIndex = match.index;
const startLine = this.indexToLine(startIndex, lineOffsets);
const jsDoc = this.extractJsDocForLine(startLine, lines);
const specs = namesPart.split(",").map((part) => part.trim());
for (const spec of specs) {
if (!spec)
continue;
const pieces = spec.split(/\s+as\s+/i);
const exportedName = (pieces[1] || pieces[0]).trim();
exports.push({
name: exportedName,
kind: "const",
jsDoc,
});
}
}
// Handle export * from './mod'
const starExportRegex = /^export\s+\*\s+from\s+["']([^"']+)["']/gm;
while ((match = starExportRegex.exec(code)) !== null) {
exports.push({
name: "*",
kind: "const",
});
}
return exports;
}
extractImports(code) {
const imports = [];
// import ... from 'module';
const importFromRegex = /^import\s+([^'";]+?)\s+from\s+["']([^"']+)["'];?/gm;
let match;
while ((match = importFromRegex.exec(code)) !== null) {
const bindings = match[1].trim();
const module = match[2].trim();
const cleaned = bindings.replace(/^type\s+/, "").trim();
// default + named: defaultName, { a, b as c }
let defaultPart;
let namedPart;
if (cleaned.includes(",")) {
const [first, rest] = cleaned.split(",", 2);
defaultPart = first.trim();
namedPart = rest.trim();
}
else if (cleaned.startsWith("{") || cleaned.startsWith("*")) {
namedPart = cleaned;
}
else if (cleaned) {
defaultPart = cleaned;
}
if (defaultPart) {
imports.push({
module,
names: [defaultPart],
isDefault: true,
});
}
if (namedPart) {
if (namedPart.startsWith("{")) {
const inner = namedPart.slice(1, -1);
const specs = inner.split(",").map((p) => p.trim());
const names = [];
for (const spec of specs) {
if (!spec)
continue;
const pieces = spec.split(/\s+as\s+/i);
const localName = (pieces[1] || pieces[0]).trim();
names.push(localName);
}
if (names.length > 0) {
imports.push({
module,
names,
isDefault: false,
});
}
}
else if (namedPart.startsWith("*")) {
imports.push({
module,
names: [namedPart],
isDefault: false,
});
}
}
}
// import 'side-effect';
const sideEffectRegex = /^import\s+["']([^"']+)["'];?/gm;
while ((match = sideEffectRegex.exec(code)) !== null) {
const module = match[1].trim();
imports.push({
module,
names: [],
isDefault: false,
});
}
return imports;
}
extractJsDocForLine(lineIndex, lines) {
let i = lineIndex - 1;
if (i < 0)
return undefined;
// Skip empty lines immediately above
while (i >= 0 && !lines[i].trim()) {
i--;
}
if (i < 0)
return undefined;
const current = lines[i].trim();
// Check if this line ends the JSDoc comment
if (!current.endsWith("*/"))
return undefined;
// Find the start of the JSDoc comment
let startLine = i;
while (startLine >= 0 && !lines[startLine].trim().startsWith("/**")) {
startLine--;
}
if (startLine < 0)
return undefined;
const commentLines = [];
// Collect all lines from start to end of JSDoc
for (let j = startLine; j <= i; j++) {
commentLines.push(lines[j]);
}
if (commentLines.length === 0)
return undefined;
const cleaned = [];
for (const raw of commentLines) {
let line = raw.trim();
if (line.startsWith("/**")) {
line = line.slice(3);
}
else if (line.startsWith("*/")) {
line = line.slice(2);
}
else if (line.startsWith("*")) {
line = line.slice(1);
}
const trimmed = line.trim();
if (trimmed)
cleaned.push(trimmed);
}
if (cleaned.length === 0)
return undefined;
return cleaned.join("\n");
}
extractFunctionSignatureFromText(text, isAsync) {
const firstBrace = text.indexOf("{");
const firstArrow = text.indexOf("=>");
let endOfSignature = text.length;
if (firstArrow !== -1) {
endOfSignature = firstArrow;
}
if (firstBrace !== -1 && firstBrace < endOfSignature) {
endOfSignature = firstBrace;
}
const sigText = text.slice(0, endOfSignature);
const openParen = sigText.indexOf("(");
const closeParen = openParen !== -1 ? this.findMatchingParen(sigText, openParen) : -1;
let params = [];
if (openParen !== -1 && closeParen !== -1) {
const inner = sigText.slice(openParen + 1, closeParen).trim();
params = this.parseParams(inner);
}
let returnType;
const rest = sigText.slice(closeParen + 1);
const colonIndex = rest.indexOf(":");
if (colonIndex !== -1) {
let rt = rest.slice(colonIndex + 1).trim();
const braceIdx = rt.indexOf("{");
const arrowIdx = rt.indexOf("=>");
let cut = rt.length;
if (braceIdx !== -1 && braceIdx < cut)
cut = braceIdx;
if (arrowIdx !== -1 && arrowIdx < cut)
cut = arrowIdx;
rt = rt.slice(0, cut).trim();
if (rt)
returnType = rt;
}
return {
params,
returnType,
isAsync,
};
}
extractArrowFunctionSignatureFromExport(code, exportIndex) {
const afterExport = code.slice(exportIndex);
const arrowIndex = afterExport.indexOf("=>");
if (arrowIndex === -1)
return undefined;
const beforeArrow = afterExport.slice(0, arrowIndex + 2);
const openParen = beforeArrow.indexOf("(");
if (openParen === -1) {
// Single parameter without parentheses
const nameMatch = beforeArrow.match(/=\s*([A-Za-z0-9_$]+)/);
if (!nameMatch) {
return {
params: [],
isAsync: beforeArrow.includes("async"),
};
}
const name = nameMatch[1];
const param = {
name,
optional: false,
};
return {
params: [param],
isAsync: beforeArrow.includes("async"),
};
}
const closeParen = this.findMatchingParen(beforeArrow, openParen);
if (closeParen === -1)
return undefined;
const inner = beforeArrow.slice(openParen + 1, closeParen).trim();
const params = this.parseParams(inner);
const rest = beforeArrow.slice(closeParen + 1);
let returnType;
const colonIndex = rest.indexOf(":");
if (colonIndex !== -1) {
let rt = rest.slice(colonIndex + 1).trim();
const arrowIdx = rt.indexOf("=>");
if (arrowIdx !== -1) {
rt = rt.slice(0, arrowIdx).trim();
}
if (rt)
returnType = rt;
}
return {
params,
returnType,
isAsync: beforeArrow.includes("async"),
};
}
findMatchingParen(text, startIndex) {
let depth = 0;
for (let i = startIndex; i < text.length; i++) {
const ch = text[i];
if (ch === "(")
depth++;
else if (ch === ")") {
depth--;
if (depth === 0)
return i;
}
}
return -1;
}
parseParams(text) {
if (!text.trim())
return [];
const params = [];
const parts = this.splitTopLevel(text, ",");
for (const part of parts) {
const raw = part.trim();
if (!raw)
continue;
let nameAndRest = raw;
let defaultValue;
const eqIndex = raw.indexOf("=");
if (eqIndex !== -1) {
nameAndRest = raw.slice(0, eqIndex).trim();
defaultValue = raw.slice(eqIndex + 1).trim();
}
let namePart = nameAndRest;
let typePart;
const colonIndex = nameAndRest.indexOf(":");
if (colonIndex !== -1) {
namePart = nameAndRest.slice(0, colonIndex).trim();
typePart = nameAndRest.slice(colonIndex + 1).trim();
}
let name = namePart.trim();
const isRest = name.startsWith("...");
if (isRest) {
name = name.slice(3).trim();
}
let optional = false;
if (name.endsWith("?")) {
optional = true;
name = name.slice(0, -1);
}
const param = {
name,
type: typePart,
optional,
defaultValue,
};
params.push(param);
}
return params;
}
splitTopLevel(text, delimiter) {
const result = [];
let current = "";
let depthParen = 0;
let depthAngle = 0;
let depthBracket = 0;
for (let i = 0; i < text.length; i++) {
const ch = text[i];
if (ch === "(")
depthParen++;
else if (ch === ")")
depthParen--;
else if (ch === "<")
depthAngle++;
else if (ch === ">")
depthAngle--;
else if (ch === "[")
depthBracket++;
else if (ch === "]")
depthBracket--;
if (ch === delimiter &&
depthParen === 0 &&
depthAngle === 0 &&
depthBracket === 0) {
result.push(current);
current = "";
}
else {
current += ch;
}
}
if (current)
result.push(current);
return result;
}
extractBlockText(code, startIndex) {
const braceIndex = code.indexOf("{", startIndex);
if (braceIndex === -1)
return "";
let depth = 0;
for (let i = braceIndex; i < code.length; i++) {
const ch = code[i];
if (ch === "{")
depth++;
else if (ch === "}") {
depth--;
if (depth === 0) {
return code.slice(braceIndex, i + 1);
}
}
}
return code.slice(braceIndex);
}
extractMethodsFromClassBody(classBodyText, lines) {
const methods = [];
if (!classBodyText)
return methods;
// Roughly match method declarations: name(...) { ... }
const methodRegex = /(\w+)\s*\([^)]*\)\s*:\s*[^({]+(?=\{)|(\w+)\s*\([^)]*\)\s*(?=\{)/g;
let match;
while ((match = methodRegex.exec(classBodyText)) !== null) {
const name = (match[1] || match[2] || "").trim();
if (!name)
continue;
const before = classBodyText.slice(0, match.index);
const lineIndex = before.split(/\r?\n/).length - 1;
const jsDoc = this.extractJsDocForLine(lineIndex, lines);
const sig = this.extractFunctionSignatureFromText(classBodyText.slice(match.index), false);
methods.push({
name,
jsDoc,
signature: sig,
});
}
return methods;
}
}
//# sourceMappingURL=ast-extractor.js.map