@aaswe/codebase-ai
Version:
AI-Assisted Software Engineering (AASWE) - Rich codebase context for IDE LLMs
573 lines (572 loc) • 21.9 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.CodeGraphAnalyzer = void 0;
const ts_morph_1 = require("ts-morph");
const dependency_cruiser_1 = require("dependency-cruiser");
const path = __importStar(require("path"));
const fs = __importStar(require("fs"));
/**
* CodeGraphAnalyzer - Advanced code graph analysis with versioning and TTL synchronization
*
* This service creates a comprehensive code graph by:
* 1. Using ts-morph for TypeScript AST analysis
* 2. Using dependency-cruiser for module dependency analysis
* 3. Storing actual source code content in Neo4j
* 4. Maintaining version synchronization with TTL files
* 5. Providing rich context for LLM queries
*/
class CodeGraphAnalyzer {
project;
neo4jService;
currentVersion;
constructor(neo4jService) {
this.neo4jService = neo4jService;
this.project = new ts_morph_1.Project({
tsConfigFilePath: 'tsconfig.json',
skipAddingFilesFromTsConfig: false
});
this.currentVersion = this.generateVersion();
}
/**
* Analyze entire codebase and create versioned code graph
*/
async analyzeCodebase(projectPath) {
console.log('🔍 Starting comprehensive code graph analysis...');
const startTime = Date.now();
const entities = [];
const relationships = [];
try {
// 1. Analyze TypeScript/JavaScript files with ts-morph
console.log('📝 Analyzing TypeScript/JavaScript files...');
const tsEntities = await this.analyzeTypeScriptFiles();
entities.push(...tsEntities.entities);
relationships.push(...tsEntities.relationships);
// 2. Analyze module dependencies with dependency-cruiser
console.log('🔗 Analyzing module dependencies...');
const depEntities = await this.analyzeDependencies(projectPath);
relationships.push(...depEntities);
// 3. Store in Neo4j with versioning
console.log('💾 Storing code graph in Neo4j...');
await this.storeVersionedGraph(entities, relationships);
// 4. Generate TTL sync hash
const ttlSyncHash = await this.generateTTLSyncHash();
const codeGraph = {
version: this.currentVersion,
timestamp: new Date(),
entities,
relationships,
ttlSyncHash,
gitCommit: await this.getCurrentGitCommit()
};
const duration = Date.now() - startTime;
console.log(`✅ Code graph analysis completed in ${duration}ms`);
console.log(`📊 Entities: ${entities.length}, Relationships: ${relationships.length}`);
return codeGraph;
}
catch (error) {
console.error('❌ Code graph analysis failed:', error);
throw error;
}
}
/**
* Analyze TypeScript files using ts-morph
*/
async analyzeTypeScriptFiles() {
const entities = [];
const relationships = [];
const sourceFiles = this.project.getSourceFiles();
for (const sourceFile of sourceFiles) {
// Skip node_modules and test files
if (sourceFile.getFilePath().includes('node_modules') ||
sourceFile.getFilePath().includes('.test.') ||
sourceFile.getFilePath().includes('.spec.')) {
continue;
}
// Create file entity
const fileEntity = await this.createFileEntity(sourceFile);
entities.push(fileEntity);
// Analyze classes
const classes = sourceFile.getClasses();
for (const classDecl of classes) {
const classEntity = await this.createClassEntity(classDecl, sourceFile);
entities.push(classEntity);
// Create file-class relationship
relationships.push({
from: fileEntity.id,
to: classEntity.id,
type: 'CONTAINS',
metadata: { line: classDecl.getStartLineNumber() },
version: this.currentVersion
});
// Analyze methods
const methods = classDecl.getMethods();
for (const method of methods) {
const methodEntity = await this.createMethodEntity(method, classDecl, sourceFile);
entities.push(methodEntity);
// Create class-method relationship
relationships.push({
from: classEntity.id,
to: methodEntity.id,
type: 'CONTAINS',
metadata: {
line: method.getStartLineNumber(),
visibility: method.getModifiers().map(m => m.getText()).join(' ')
},
version: this.currentVersion
});
}
// Analyze inheritance
const heritage = classDecl.getHeritageClauses();
for (const clause of heritage) {
const types = clause.getTypeNodes();
for (const type of types) {
const typeName = type.getText();
relationships.push({
from: classEntity.id,
to: `class:${typeName}`,
type: clause.getToken().toString().includes('extends') ? 'EXTENDS' : 'IMPLEMENTS',
metadata: { typeName },
version: this.currentVersion
});
}
}
}
// Analyze imports
const imports = sourceFile.getImportDeclarations();
for (const importDecl of imports) {
const importRel = await this.createImportRelationship(importDecl, fileEntity);
if (importRel) {
relationships.push(importRel);
}
}
// Analyze function calls (simplified)
const callRelationships = await this.analyzeFunctionCalls(sourceFile, fileEntity);
relationships.push(...callRelationships);
}
return { entities, relationships };
}
/**
* Create file entity from source file
*/
async createFileEntity(sourceFile) {
const filePath = sourceFile.getFilePath();
const relativePath = path.relative(process.cwd(), filePath);
const sourceCode = sourceFile.getFullText();
const lines = sourceCode.split('\n');
return {
id: `file:${relativePath}`,
name: path.basename(filePath),
type: 'File',
sourceCode,
filePath: relativePath,
startLine: 1,
endLine: lines.length,
version: this.currentVersion,
lastModified: new Date(),
metadata: {
language: 'TypeScript',
size: sourceCode.length,
lineCount: lines.length,
extension: path.extname(filePath)
}
};
}
/**
* Create class entity from class declaration
*/
async createClassEntity(classDecl, sourceFile) {
const className = classDecl.getName() || 'Anonymous';
const sourceCode = classDecl.getFullText();
const filePath = path.relative(process.cwd(), sourceFile.getFilePath());
return {
id: `class:${filePath}:${className}`,
name: className,
type: 'Class',
sourceCode,
filePath,
startLine: classDecl.getStartLineNumber(),
endLine: classDecl.getEndLineNumber(),
version: this.currentVersion,
lastModified: new Date(),
metadata: {
isExported: classDecl.isExported(),
isAbstract: classDecl.isAbstract(),
modifiers: classDecl.getModifiers().map(m => m.getText()),
methodCount: classDecl.getMethods().length,
propertyCount: classDecl.getProperties().length
}
};
}
/**
* Create method entity from method declaration
*/
async createMethodEntity(method, classDecl, sourceFile) {
const methodName = method.getName();
const className = classDecl.getName() || 'Anonymous';
const sourceCode = method.getFullText();
const filePath = path.relative(process.cwd(), sourceFile.getFilePath());
return {
id: `method:${filePath}:${className}:${methodName}`,
name: methodName,
type: 'Method',
sourceCode,
filePath,
startLine: method.getStartLineNumber(),
endLine: method.getEndLineNumber(),
version: this.currentVersion,
lastModified: new Date(),
metadata: {
className,
isAsync: method.isAsync(),
isStatic: method.isStatic(),
visibility: method.getModifiers().map(m => m.getText()).join(' '),
returnType: method.getReturnTypeNode()?.getText(),
parameters: method.getParameters().map(p => ({
name: p.getName(),
type: p.getTypeNode()?.getText(),
isOptional: p.isOptional()
})),
complexity: this.calculateCyclomaticComplexity(sourceCode)
}
};
}
/**
* Create import relationship
*/
async createImportRelationship(importDecl, fileEntity) {
const moduleSpecifier = importDecl.getModuleSpecifierValue();
// Skip external modules (node_modules)
if (!moduleSpecifier.startsWith('.') && !moduleSpecifier.startsWith('/')) {
return null;
}
return {
from: fileEntity.id,
to: `file:${moduleSpecifier}`,
type: 'IMPORTS',
metadata: {
moduleSpecifier,
importClause: importDecl.getImportClause()?.getText(),
line: importDecl.getStartLineNumber()
},
version: this.currentVersion
};
}
/**
* Analyze function calls (simplified pattern matching)
*/
async analyzeFunctionCalls(sourceFile, fileEntity) {
const relationships = [];
const sourceCode = sourceFile.getFullText();
// Simple regex patterns for common call patterns
const callPatterns = [
/(\w+)\.(\w+)\(/g, // object.method()
/new\s+(\w+)\(/g, // new Constructor()
/(\w+)\(/g // function()
];
for (const pattern of callPatterns) {
let match;
while ((match = pattern.exec(sourceCode)) !== null) {
const [fullMatch, object, method] = match;
relationships.push({
from: fileEntity.id,
to: method ? `method:${object}:${method}` : `function:${object}`,
type: 'CALLS',
metadata: {
callExpression: fullMatch,
line: this.getLineNumber(sourceCode, match.index)
},
version: this.currentVersion
});
}
}
return relationships;
}
/**
* Analyze module dependencies using dependency-cruiser
*/
async analyzeDependencies(_projectPath) {
const relationships = [];
try {
const cruiseResult = await (0, dependency_cruiser_1.cruise)(['src/**/*.ts', 'src/**/*.js'], {
exclude: {
path: 'node_modules|test|spec'
},
outputType: 'json'
}, {
bustTheCache: true
});
if (cruiseResult && typeof cruiseResult === 'object') {
const modules = cruiseResult.modules || [];
for (const module of modules) {
const fromId = `file:${module.source}`;
for (const dependency of module.dependencies || []) {
relationships.push({
from: fromId,
to: `file:${dependency.resolved}`,
type: 'DEPENDS_ON',
metadata: {
dependencyType: dependency.dependencyTypes?.join(','),
circular: dependency.circular,
dynamic: dependency.dynamic
},
version: this.currentVersion
});
}
}
}
}
catch (error) {
console.warn('⚠️ Dependency analysis failed:', error instanceof Error ? error.message : String(error));
}
return relationships;
}
/**
* Store versioned graph in Neo4j
*/
async storeVersionedGraph(entities, relationships) {
const session = this.neo4jService.getSession();
try {
// Create version node
await session.run(`
MERGE (v:Version {version: $version})
SET v.timestamp = datetime($timestamp),
v.entityCount = $entityCount,
v.relationshipCount = $relationshipCount
`, {
version: this.currentVersion,
timestamp: new Date().toISOString(),
entityCount: entities.length,
relationshipCount: relationships.length
});
// Store entities
for (const entity of entities) {
await session.run(`
MERGE (e:${entity.type} {id: $id, version: $version})
SET e.name = $name,
e.sourceCode = $sourceCode,
e.filePath = $filePath,
e.startLine = $startLine,
e.endLine = $endLine,
e.lastModified = datetime($lastModified),
e.metadata = $metadata
WITH e
MATCH (v:Version {version: $version})
MERGE (v)-[:CONTAINS]->(e)
`, {
id: entity.id,
version: entity.version,
name: entity.name,
sourceCode: entity.sourceCode,
filePath: entity.filePath,
startLine: entity.startLine,
endLine: entity.endLine,
lastModified: entity.lastModified.toISOString(),
metadata: JSON.stringify(entity.metadata)
});
}
// Store relationships
for (const rel of relationships) {
await session.run(`
MATCH (from {id: $fromId, version: $version})
MATCH (to {id: $toId, version: $version})
MERGE (from)-[r:${rel.type} {version: $version}]->(to)
SET r.metadata = $metadata
`, {
fromId: rel.from,
toId: rel.to,
version: rel.version,
metadata: JSON.stringify(rel.metadata)
});
}
}
finally {
await session.close();
}
}
/**
* Generate TTL sync hash for version correlation
*/
async generateTTLSyncHash() {
// Find all TTL files
const ttlFiles = await this.findTTLFiles();
let combinedContent = '';
for (const ttlFile of ttlFiles) {
if (fs.existsSync(ttlFile)) {
combinedContent += fs.readFileSync(ttlFile, 'utf8');
}
}
// Simple hash generation (could use crypto for production)
return Buffer.from(combinedContent).toString('base64').slice(0, 16);
}
/**
* Find all TTL files in the project
*/
async findTTLFiles() {
const ttlFiles = [];
const searchDirs = ['src', 'scripts', '.aaswe'];
for (const dir of searchDirs) {
if (fs.existsSync(dir)) {
const files = this.findFilesRecursive(dir, '.ttl');
ttlFiles.push(...files);
}
}
return ttlFiles;
}
/**
* Find files recursively
*/
findFilesRecursive(dir, extension) {
const files = [];
try {
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
files.push(...this.findFilesRecursive(fullPath, extension));
}
else if (entry.name.endsWith(extension)) {
files.push(fullPath);
}
}
}
catch (error) {
// Ignore permission errors
}
return files;
}
/**
* Get current Git commit hash
*/
async getCurrentGitCommit() {
try {
const { execSync } = require('child_process');
return execSync('git rev-parse HEAD', { encoding: 'utf8' }).trim();
}
catch {
return undefined;
}
}
/**
* Generate version string
*/
generateVersion() {
return `v${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
/**
* Calculate cyclomatic complexity (simplified)
*/
calculateCyclomaticComplexity(code) {
const complexityKeywords = ['if', 'else', 'for', 'while', 'switch', 'case', 'catch', '&&', '||', '?'];
let complexity = 1;
for (const keyword of complexityKeywords) {
const matches = code.match(new RegExp(`\\b${keyword}\\b`, 'g'));
if (matches) {
complexity += matches.length;
}
}
return complexity;
}
/**
* Get line number from character index
*/
getLineNumber(text, index) {
return text.substring(0, index).split('\n').length;
}
/**
* Query code graph
*/
async queryCodeGraph(query, version) {
const session = this.neo4jService.getSession();
try {
const versionFilter = version ? `{version: "${version}"}` : '';
const cypherQuery = query.replace(/\{version\}/g, versionFilter);
const result = await session.run(cypherQuery);
return result.records.map(record => record.toObject());
}
finally {
await session.close();
}
}
/**
* Get source code for entity
*/
async getSourceCode(entityId, version) {
const session = this.neo4jService.getSession();
try {
const query = `
MATCH (e {id: $entityId${version ? ', version: $version' : ''}})
RETURN e.sourceCode as sourceCode
ORDER BY e.lastModified DESC
LIMIT 1
`;
const result = await session.run(query, { entityId, version });
return result.records.length > 0 ? result.records[0].get('sourceCode') : null;
}
finally {
await session.close();
}
}
/**
* Search code patterns across versions
*/
async searchCodePatterns(pattern, entityType, version) {
const session = this.neo4jService.getSession();
try {
let query = `
MATCH (e${entityType ? `:${entityType}` : ''}${version ? ' {version: $version}' : ''})
WHERE e.sourceCode CONTAINS $pattern
OR e.name CONTAINS $pattern
RETURN e.id as id,
e.name as name,
labels(e)[0] as type,
e.filePath as filePath,
e.startLine as startLine,
e.endLine as endLine,
substring(e.sourceCode, 0, 300) as preview,
e.version as version
ORDER BY e.lastModified DESC
LIMIT 20
`;
const result = await session.run(query, { pattern, version });
return result.records.map(record => record.toObject());
}
finally {
await session.close();
}
}
}
exports.CodeGraphAnalyzer = CodeGraphAnalyzer;
//# sourceMappingURL=CodeGraphAnalyzer.js.map