// Copyright 2025 Chris Bunting
// Brief: AST Processor for Code Complexity Analyzer MCP Server
// Scope: Handles AST parsing and processing for multiple programming languages
import * as fs from 'fs';
import * as path from 'path';
import { Logger } from '../utils/Logger.js';
import { Language } from '@mcp-code-analysis/shared-types';
export interface ASTNode {
type: string;
name?: string;
start: { line: number; column: number };
end: { line: number; column: number };
body?: ASTNode[];
children?: ASTNode[];
value?: any;
parent?: ASTNode;
}
export interface ASTProcessingResult {
ast: ASTNode;
language: Language;
filePath: string;
processingTime: number;
errors: string[];
}
export class ASTProcessor {
private logger: Logger;
private parsers: Map<Language, (content: string) => ASTNode>;
constructor() {
this.logger = new Logger();
this.parsers = new Map();
this.initializeParsers();
}
private initializeParsers(): void {
// Initialize parsers for different languages
this.parsers.set(Language.JAVASCRIPT, this.parseJavaScript.bind(this));
this.parsers.set(Language.TYPESCRIPT, this.parseTypeScript.bind(this));
this.parsers.set(Language.PYTHON, this.parsePython.bind(this));
this.parsers.set(Language.JAVA, this.parseJava.bind(this));
this.parsers.set(Language.C, this.parseC.bind(this));
this.parsers.set(Language.CPP, this.parseCpp.bind(this));
this.parsers.set(Language.GO, this.parseGo.bind(this));
this.parsers.set(Language.RUST, this.parseRust.bind(this));
}
public async processFile(filePath: string, language?: Language): Promise<ASTProcessingResult> {
const startTime = Date.now();
const errors: string[] = [];
try {
// Check if file exists
if (!fs.existsSync(filePath)) {
throw new Error(`File not found: ${filePath}`);
}
// Read file content
const content = fs.readFileSync(filePath, 'utf-8');
// Detect language if not provided
const detectedLanguage = language || this.detectLanguage(filePath, content);
// Get parser for the language
const parser = this.parsers.get(detectedLanguage);
if (!parser) {
throw new Error(`Unsupported language: ${detectedLanguage}`);
}
// Parse the content
const ast = parser(content);
const processingTime = Date.now() - startTime;
this.logger.info(`Successfully processed file: ${filePath}`, {
language: detectedLanguage,
processingTime,
nodes: this.countNodes(ast)
});
return {
ast,
language: detectedLanguage,
filePath,
processingTime,
errors
};
} catch (error) {
const processingTime = Date.now() - startTime;
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
errors.push(errorMessage);
this.logger.error(`Failed to process file: ${filePath}`, { error: errorMessage });
return {
ast: { type: 'error', start: { line: 0, column: 0 }, end: { line: 0, column: 0 } },
language: language || Language.JAVASCRIPT,
filePath,
processingTime,
errors
};
}
}
private detectLanguage(filePath: string, content: string): Language {
const ext = path.extname(filePath).toLowerCase();
switch (ext) {
case '.js':
case '.mjs':
case '.cjs':
return Language.JAVASCRIPT;
case '.ts':
case '.tsx':
return Language.TYPESCRIPT;
case '.py':
case '.pyx':
return Language.PYTHON;
case '.java':
return Language.JAVA;
case '.c':
case '.h':
return Language.C;
case '.cpp':
case '.cxx':
case '.cc':
case '.hpp':
case '.hxx':
return Language.CPP;
case '.go':
return Language.GO;
case '.rs':
return Language.RUST;
default:
// Fallback to content-based detection
if (content.includes('import ') && content.includes('function ')) {
return Language.JAVASCRIPT;
} else if (content.includes('def ') && content.includes(':')) {
return Language.PYTHON;
} else if (content.includes('public class ')) {
return Language.JAVA;
} else if (content.includes('#include') && content.includes('int main')) {
return Language.C;
} else if (content.includes('#include') && content.includes('using namespace')) {
return Language.CPP;
} else if (content.includes('package main') && content.includes('func ')) {
return Language.GO;
} else if (content.includes('fn ') && content.includes('let mut')) {
return Language.RUST;
}
return Language.JAVASCRIPT; // Default fallback
}
}
private parseJavaScript(content: string): ASTNode {
try {
// Use acorn for JavaScript parsing
const acorn = require('acorn');
const ast = acorn.parse(content, {
ecmaVersion: 2022,
sourceType: 'module',
locations: true
});
return this.convertAcornToCustomAST(ast);
} catch (error) {
throw new Error(`JavaScript parsing failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
private parseTypeScript(content: string): ASTNode {
try {
// For TypeScript, we'll use the same parser as JavaScript for now
// In a real implementation, you'd use TypeScript compiler API
return this.parseJavaScript(content);
} catch (error) {
throw new Error(`TypeScript parsing failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
private parsePython(content: string): ASTNode {
try {
// Use esprima-like parsing for Python (simplified)
// In a real implementation, you'd use a proper Python parser like ast-python
const lines = content.split('\n');
const root: ASTNode = {
type: 'module',
start: { line: 1, column: 0 },
end: { line: lines.length, column: lines[lines.length - 1]?.length || 0 },
body: []
};
let currentNode: ASTNode | null = null;
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
const lineNum = i + 1;
if (line.startsWith('def ')) {
const funcNode: ASTNode = {
type: 'function',
name: line.split('(')[0].replace('def ', '').trim(),
start: { line: lineNum, column: 0 },
end: { line: lineNum, column: line.length },
body: []
};
if (currentNode) {
currentNode.body?.push(funcNode);
} else {
root.body?.push(funcNode);
}
currentNode = funcNode;
} else if (line.startsWith('class ')) {
const classNode: ASTNode = {
type: 'class',
name: line.split(':')[0].replace('class ', '').trim(),
start: { line: lineNum, column: 0 },
end: { line: lineNum, column: line.length },
body: []
};
if (currentNode) {
currentNode.body?.push(classNode);
} else {
root.body?.push(classNode);
}
currentNode = classNode;
} else if (line && !line.startsWith('#') && currentNode) {
// Add statement to current function/class
const stmtNode: ASTNode = {
type: 'statement',
start: { line: lineNum, column: 0 },
end: { line: lineNum, column: line.length },
value: line
};
currentNode.body?.push(stmtNode);
}
}
return root;
} catch (error) {
throw new Error(`Python parsing failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
private parseJava(content: string): ASTNode {
try {
// Simplified Java parsing
const lines = content.split('\n');
const root: ASTNode = {
type: 'compilation_unit',
start: { line: 1, column: 0 },
end: { line: lines.length, column: lines[lines.length - 1]?.length || 0 },
body: []
};
let currentClass: ASTNode | null = null;
let currentMethod: ASTNode | null = null;
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
const lineNum = i + 1;
if (line.includes('class ')) {
const className = line.split('class ')[1].split('{')[0].trim();
const classNode: ASTNode = {
type: 'class',
name: className,
start: { line: lineNum, column: 0 },
end: { line: lineNum, column: line.length },
body: []
};
root.body?.push(classNode);
currentClass = classNode;
currentMethod = null;
} else if (line.includes('public ') || line.includes('private ') || line.includes('protected ')) {
if (line.includes('(') && line.includes(')')) {
const methodNode: ASTNode = {
type: 'method',
name: line.split('(')[0].split(' ').pop() || 'unknown',
start: { line: lineNum, column: 0 },
end: { line: lineNum, column: line.length },
body: []
};
if (currentClass) {
currentClass.body?.push(methodNode);
} else {
root.body?.push(methodNode);
}
currentMethod = methodNode;
}
} else if (line && currentMethod) {
const stmtNode: ASTNode = {
type: 'statement',
start: { line: lineNum, column: 0 },
end: { line: lineNum, column: line.length },
value: line
};
currentMethod.body?.push(stmtNode);
}
}
return root;
} catch (error) {
throw new Error(`Java parsing failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
private parseC(content: string): ASTNode {
// Simplified C parsing (similar to C++ but without classes)
return this.parseCppLike(content, 'c');
}
private parseCpp(content: string): ASTNode {
// Simplified C++ parsing
return this.parseCppLike(content, 'cpp');
}
private parseCppLike(content: string, language: 'c' | 'cpp'): ASTNode {
try {
const lines = content.split('\n');
const root: ASTNode = {
type: 'translation_unit',
start: { line: 1, column: 0 },
end: { line: lines.length, column: lines[lines.length - 1]?.length || 0 },
body: []
};
let currentFunction: ASTNode | null = null;
let currentClass: ASTNode | null = null;
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
const lineNum = i + 1;
if (language === 'cpp' && line.includes('class ')) {
const className = line.split('class ')[1].split('{')[0].trim();
const classNode: ASTNode = {
type: 'class',
name: className,
start: { line: lineNum, column: 0 },
end: { line: lineNum, column: line.length },
body: []
};
root.body?.push(classNode);
currentClass = classNode;
currentFunction = null;
} else if (line.includes('(') && line.includes(')') && line.includes('{')) {
const funcName = line.split('(')[0].split(' ').pop() || 'unknown';
const funcNode: ASTNode = {
type: 'function',
name: funcName,
start: { line: lineNum, column: 0 },
end: { line: lineNum, column: line.length },
body: []
};
if (currentClass) {
currentClass.body?.push(funcNode);
} else {
root.body?.push(funcNode);
}
currentFunction = funcNode;
} else if (line && currentFunction) {
const stmtNode: ASTNode = {
type: 'statement',
start: { line: lineNum, column: 0 },
end: { line: lineNum, column: line.length },
value: line
};
currentFunction.body?.push(stmtNode);
}
}
return root;
} catch (error) {
throw new Error(`${language.toUpperCase()} parsing failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
private parseGo(content: string): ASTNode {
try {
const lines = content.split('\n');
const root: ASTNode = {
type: 'package',
start: { line: 1, column: 0 },
end: { line: lines.length, column: lines[lines.length - 1]?.length || 0 },
body: []
};
let currentFunction: ASTNode | null = null;
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
const lineNum = i + 1;
if (line.startsWith('func ')) {
const funcName = line.split('(')[0].replace('func ', '').trim();
const funcNode: ASTNode = {
type: 'function',
name: funcName,
start: { line: lineNum, column: 0 },
end: { line: lineNum, column: line.length },
body: []
};
root.body?.push(funcNode);
currentFunction = funcNode;
} else if (line && currentFunction) {
const stmtNode: ASTNode = {
type: 'statement',
start: { line: lineNum, column: 0 },
end: { line: lineNum, column: line.length },
value: line
};
currentFunction.body?.push(stmtNode);
}
}
return root;
} catch (error) {
throw new Error(`Go parsing failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
private parseRust(content: string): ASTNode {
try {
const lines = content.split('\n');
const root: ASTNode = {
type: 'crate',
start: { line: 1, column: 0 },
end: { line: lines.length, column: lines[lines.length - 1]?.length || 0 },
body: []
};
let currentFunction: ASTNode | null = null;
let currentStruct: ASTNode | null = null;
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
const lineNum = i + 1;
if (line.startsWith('struct ')) {
const structName = line.split('{')[0].replace('struct ', '').trim();
const structNode: ASTNode = {
type: 'struct',
name: structName,
start: { line: lineNum, column: 0 },
end: { line: lineNum, column: line.length },
body: []
};
root.body?.push(structNode);
currentStruct = structNode;
currentFunction = null;
} else if (line.startsWith('fn ')) {
const funcName = line.split('(')[0].replace('fn ', '').trim();
const funcNode: ASTNode = {
type: 'function',
name: funcName,
start: { line: lineNum, column: 0 },
end: { line: lineNum, column: line.length },
body: []
};
if (currentStruct) {
currentStruct.body?.push(funcNode);
} else {
root.body?.push(funcNode);
}
currentFunction = funcNode;
} else if (line && currentFunction) {
const stmtNode: ASTNode = {
type: 'statement',
start: { line: lineNum, column: 0 },
end: { line: lineNum, column: line.length },
value: line
};
currentFunction.body?.push(stmtNode);
}
}
return root;
} catch (error) {
throw new Error(`Rust parsing failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
private convertAcornToCustomAST(acornAst: any): ASTNode {
// Convert acorn AST to our custom AST format
const convert = (node: any): ASTNode => {
if (!node) {
return { type: 'empty', start: { line: 0, column: 0 }, end: { line: 0, column: 0 } };
}
const customNode: ASTNode = {
type: node.type,
start: node.loc ? { line: node.loc.start.line, column: node.loc.start.column } : { line: 0, column: 0 },
end: node.loc ? { line: node.loc.end.line, column: node.loc.end.column } : { line: 0, column: 0 }
};
if (node.id?.name) {
customNode.name = node.id.name;
}
if (node.body) {
if (Array.isArray(node.body)) {
customNode.body = node.body.map(convert);
} else {
customNode.body = [convert(node.body)];
}
}
if (node.consequent) {
customNode.body = customNode.body || [];
customNode.body.push(convert(node.consequent));
}
if (node.alternate) {
customNode.body = customNode.body || [];
customNode.body.push(convert(node.alternate));
}
return customNode;
};
return convert(acornAst);
}
private countNodes(node: ASTNode): number {
let count = 1;
if (node.body) {
for (const child of node.body) {
count += this.countNodes(child);
}
}
if (node.children) {
for (const child of node.children) {
count += this.countNodes(child);
}
}
return count;
}
public traverseAST(ast: ASTNode, visitor: (node: ASTNode) => void): void {
visitor(ast);
if (ast.body) {
for (const child of ast.body) {
this.traverseAST(child, visitor);
}
}
if (ast.children) {
for (const child of ast.children) {
this.traverseAST(child, visitor);
}
}
}
public findNodesByType(ast: ASTNode, type: string): ASTNode[] {
const results: ASTNode[] = [];
this.traverseAST(ast, (node) => {
if (node.type === type) {
results.push(node);
}
});
return results;
}
public findNodeByName(ast: ASTNode, name: string): ASTNode | null {
let result: ASTNode | null = null;
this.traverseAST(ast, (node) => {
if (node.name === name && !result) {
result = node;
}
});
return result;
}
}