UNPKG

code-auditor-mcp

Version:

Multi-language code quality auditor with MCP server - Analyze TypeScript, JavaScript, and Go code for SOLID principles, DRY violations, security patterns, and more

405 lines 16 kB
/** * Schema Analyzer * Analyzes code against loaded database schemas to find violations and patterns */ import { CodeIndexDB } from '../codeIndexDB.js'; import { promises as fs } from 'fs'; import path from 'path'; import ts from 'typescript'; const DEFAULT_CONFIG = { enableTableUsageTracking: true, checkMissingReferences: true, checkNamingConventions: true, detectUnusedTables: false, validateQueryPatterns: true, maxQueriesPerFunction: 5, allowedQueryPatterns: ['SELECT', 'INSERT', 'UPDATE', 'DELETE'], requiredSchemas: [] }; export const analyzeSchema = async (files, config = {}, options = {}, progressCallback) => { const finalConfig = { ...DEFAULT_CONFIG, ...config }; const violations = []; const errors = []; let filesProcessed = 0; const startTime = Date.now(); // Get database handle const db = CodeIndexDB.getInstance(); await db.initialize(); // Get all loaded schemas const loadedSchemas = await db.getAllSchemas(); if (loadedSchemas.length === 0 && finalConfig.requiredSchemas.length > 0) { violations.push({ file: 'project', severity: 'warning', message: 'No database schemas loaded for analysis', schemaType: 'missing-reference', details: 'Load schema definitions using schema management tools' }); } // Build table name lookup for fast access const allTables = new Set(); const tableToSchema = new Map(); for (const { schema } of loadedSchemas) { for (const database of schema.databases) { for (const table of database.tables) { allTables.add(table.name); tableToSchema.set(table.name, schema.name); } } } // Analyze each file for (const file of files) { try { progressCallback?.({ current: filesProcessed, total: files.length, analyzer: 'schema', file: path.basename(file), phase: 'analyzing' }); // Skip non-source files if (!file.match(/\.(ts|tsx|js|jsx)$/)) { continue; } const content = await fs.readFile(file, 'utf-8'); const fileViolations = await analyzeFile(file, content, allTables, tableToSchema, finalConfig, db); violations.push(...fileViolations); filesProcessed++; } catch (error) { errors.push({ file, error: error instanceof Error ? error.message : 'Unknown error' }); } } return { violations, filesProcessed, executionTime: Date.now() - startTime, errors: errors.length > 0 ? errors : undefined, analyzerName: 'schema', metadata: { loadedSchemas: loadedSchemas.length, totalTables: allTables.size, config: finalConfig } }; }; async function analyzeFile(filePath, content, allTables, tableToSchema, config, db) { const violations = []; // Parse TypeScript/JavaScript const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true); // Track schema usage for this file const schemaUsages = []; // Analyze the AST function visit(node, currentFunction) { // Detect ORM-specific patterns // 1. Drizzle ORM - Functional table definitions if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) { const funcName = node.expression.text; if (['pgTable', 'mysqlTable', 'sqliteTable'].includes(funcName)) { const tableName = extractTableNameFromDrizzle(node); if (tableName) { const pos = sourceFile.getLineAndCharacterOfPosition(node.getStart()); schemaUsages.push({ tableName, functionName: currentFunction || 'schema-definition', usageType: 'reference', line: pos.line + 1, column: pos.character + 1 }); } } } // 2. TypeORM - Entity decorators if (ts.isDecorator(node) && ts.isCallExpression(node.expression)) { const decoratorName = getDecoratorName(node.expression); if (decoratorName === 'Entity') { // Extract table name from Entity decorator const tableName = extractTableNameFromTypeORM(node.expression); if (tableName) { const pos = sourceFile.getLineAndCharacterOfPosition(node.getStart()); schemaUsages.push({ tableName, functionName: currentFunction || 'entity-definition', usageType: 'reference', line: pos.line + 1, column: pos.character + 1 }); } } } // 3. Mongoose - Schema constructors if (ts.isNewExpression(node) && ts.isIdentifier(node.expression)) { if (node.expression.text === 'Schema') { // Analyze schema object literal for field references analyzeMongooseSchema(node, currentFunction); } } // 4. Prisma Client - Table access patterns if (ts.isPropertyAccessExpression(node)) { const propertyName = node.name.text; // Check if this looks like Prisma client table access (prisma.user.findMany) if (isPrismaBoundProperty(node)) { const pos = sourceFile.getLineAndCharacterOfPosition(node.getStart()); schemaUsages.push({ tableName: propertyName, functionName: currentFunction || 'unknown', usageType: 'query', line: pos.line + 1, column: pos.character + 1 }); } } // 5. Detect SQL queries and table references (existing logic) if (ts.isStringLiteral(node) || ts.isTemplateExpression(node)) { const queryText = getQueryText(node); if (queryText) { const queryAnalysis = analyzeQuery(queryText, allTables, filePath); violations.push(...queryAnalysis.violations); // Record schema usage for (const table of queryAnalysis.tables) { const pos = sourceFile.getLineAndCharacterOfPosition(node.getStart()); schemaUsages.push({ tableName: table, functionName: currentFunction || 'unknown', usageType: queryAnalysis.type, line: pos.line + 1, column: pos.character + 1, rawQuery: queryText }); } } } // Detect ORM/model references if (ts.isPropertyAccessExpression(node)) { const propertyName = node.name.text; if (allTables.has(propertyName)) { const pos = sourceFile.getLineAndCharacterOfPosition(node.getStart()); schemaUsages.push({ tableName: propertyName, functionName: currentFunction || 'unknown', usageType: 'reference', line: pos.line + 1, column: pos.character + 1 }); } } // Track current function context let funcName = currentFunction; if (ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node) || ts.isArrowFunction(node)) { if (ts.isFunctionDeclaration(node) && node.name) { funcName = node.name.text; } else if (ts.isMethodDeclaration(node) && ts.isIdentifier(node.name)) { funcName = node.name.text; } } ts.forEachChild(node, child => visit(child, funcName)); } visit(sourceFile); // Record schema usages in database if (config.enableTableUsageTracking) { for (const usage of schemaUsages) { await db.recordSchemaUsage({ tableName: usage.tableName, filePath, functionName: usage.functionName, usageType: usage.usageType, line: usage.line, column: usage.column, rawQuery: usage.rawQuery }); } } // Check for violations if (config.checkMissingReferences) { violations.push(...checkMissingTableReferences(schemaUsages, allTables, filePath)); } if (config.validateQueryPatterns) { violations.push(...validateQueryPatterns(schemaUsages, config, filePath)); } return violations; } function getQueryText(node) { if (ts.isStringLiteral(node)) { const text = node.text.toUpperCase(); // Check if it looks like SQL if (text.includes('SELECT') || text.includes('INSERT') || text.includes('UPDATE') || text.includes('DELETE') || text.includes('CREATE') || text.includes('DROP')) { return node.text; } } if (ts.isTemplateExpression(node)) { // For template literals, check the head const headText = node.head.text.toUpperCase(); if (headText.includes('SELECT') || headText.includes('INSERT') || headText.includes('UPDATE') || headText.includes('DELETE')) { // Return a simplified version for analysis return node.head.text + ' ...'; } } return null; } function analyzeQuery(query, allTables, filePath) { const violations = []; const tables = []; const queryUpper = query.toUpperCase(); let type = 'query'; if (queryUpper.includes('SELECT')) type = 'query'; else if (queryUpper.includes('INSERT')) type = 'insert'; else if (queryUpper.includes('UPDATE')) type = 'update'; else if (queryUpper.includes('DELETE')) type = 'delete'; // Extract table names (basic regex - could be enhanced) const tableMatches = query.match(/(?:FROM|JOIN|INTO|UPDATE)\s+([a-zA-Z_][a-zA-Z0-9_]*)/gi); if (tableMatches) { for (const match of tableMatches) { const tableName = match.split(/\s+/).pop(); if (tableName && !allTables.has(tableName)) { violations.push({ file: filePath, severity: 'warning', message: `Table '${tableName}' not found in loaded schemas`, schemaType: 'missing-reference', tableName, snippet: query.substring(0, 100), suggestion: 'Load the appropriate database schema or check table name spelling' }); } else if (tableName) { tables.push(tableName); } } } return { violations, tables, type }; } function checkMissingTableReferences(usages, allTables, filePath) { const violations = []; for (const usage of usages) { if (!allTables.has(usage.tableName)) { violations.push({ file: filePath, line: usage.line, severity: 'warning', message: `Reference to unknown table '${usage.tableName}'`, schemaType: 'missing-reference', tableName: usage.tableName, suggestion: 'Load the database schema that contains this table' }); } } return violations; } function validateQueryPatterns(usages, config, filePath) { const violations = []; // Count queries per function const queriesPerFunction = usages.reduce((acc, usage) => { if (usage.usageType !== 'reference') { acc[usage.functionName] = (acc[usage.functionName] || 0) + 1; } return acc; }, {}); // Check for functions with too many queries for (const [funcName, count] of Object.entries(queriesPerFunction)) { if (count > config.maxQueriesPerFunction) { violations.push({ file: filePath, severity: 'suggestion', message: `Function '${funcName}' has ${count} database queries (max recommended: ${config.maxQueriesPerFunction})`, schemaType: 'missing-reference', suggestion: 'Consider consolidating queries or extracting to a data access layer' }); } } return violations; } // ORM-specific helper functions function extractTableNameFromDrizzle(node) { // Drizzle: pgTable('table_name', { ... }) if (node.arguments.length > 0 && ts.isStringLiteral(node.arguments[0])) { return node.arguments[0].text; } return null; } function getDecoratorName(node) { if (ts.isIdentifier(node.expression)) { return node.expression.text; } return null; } function extractTableNameFromTypeORM(node) { // TypeORM: @Entity('table_name') or @Entity({ name: 'table_name' }) if (node.arguments.length > 0) { const firstArg = node.arguments[0]; if (ts.isStringLiteral(firstArg)) { return firstArg.text; } if (ts.isObjectLiteralExpression(firstArg)) { // Look for name property for (const prop of firstArg.properties) { if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name) && prop.name.text === 'name' && ts.isStringLiteral(prop.initializer)) { return prop.initializer.text; } } } } return null; } function analyzeMongooseSchema(node, currentFunction) { // Mongoose: new Schema({ field: { type: String, ref: 'OtherModel' } }) if (node.arguments && node.arguments.length > 0) { const schemaArg = node.arguments[0]; if (ts.isObjectLiteralExpression(schemaArg)) { // Look for ref properties that indicate relationships analyzeObjectForReferences(schemaArg, currentFunction); } } } function analyzeObjectForReferences(obj, currentFunction) { for (const prop of obj.properties) { if (ts.isPropertyAssignment(prop) && ts.isObjectLiteralExpression(prop.initializer)) { // Look for ref property for (const nestedProp of prop.initializer.properties) { if (ts.isPropertyAssignment(nestedProp) && ts.isIdentifier(nestedProp.name) && nestedProp.name.text === 'ref' && ts.isStringLiteral(nestedProp.initializer)) { // Found a reference to another model const referencedModel = nestedProp.initializer.text; // Add to schema usage tracking // This would need access to the parent scope's schemaUsages array } } } } } function isPrismaBoundProperty(node) { // Check if this looks like prisma.tableName.operation if (ts.isPropertyAccessExpression(node.expression) && ts.isIdentifier(node.expression.expression)) { const baseName = node.expression.expression.text; // Common Prisma client variable names return ['prisma', 'db', 'client'].includes(baseName.toLowerCase()); } return false; } /** * Schema Analyzer Definition */ export const schemaAnalyzer = { name: 'schema', analyze: analyzeSchema, defaultConfig: DEFAULT_CONFIG, description: 'Analyzes code against loaded database schemas to detect violations and track usage patterns', category: 'Data Access' }; //# sourceMappingURL=schemaAnalyzer.js.map