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
JavaScript
/**
* 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