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

603 lines 29.1 kB
/** * Function Scanner * Scans directories for functions using AST parsing */ import { discoverFiles } from './utils/fileDiscovery.js'; import { parseTypeScriptFile } from './utils/astParser.js'; import { findNodesByKind, getLineAndColumn } from './utils/astUtils.js'; import { getImports, getImportsDetailed, extractIdentifierUsage, getReExports } from './utils/astUtils.js'; import { isReactComponent, detectComponentType, getComponentName, extractHooks, extractPropTypes } from './utils/reactDetection.js'; import { buildImportMap, extractFunctionCalls, getLocalFunctionNames, normalizeCallTarget } from './utils/dependencyExtractor.js'; import * as ts from 'typescript'; import path from 'path'; /** * Scan directory for functions */ export async function scanDirectoryForFunctions(dirPath, options) { const functions = []; try { // Discover TypeScript and JavaScript files const fileExtensions = options?.fileExtensions || ['.ts', '.js', '.tsx', '.jsx']; const files = await discoverFiles(dirPath, { includePaths: options?.includePaths || ['**/*'], excludePaths: options?.excludePaths || ['**/node_modules/**', '**/dist/**', '**/build/**'] }); // Filter by extensions const targetFiles = files.filter(file => fileExtensions.some(ext => file.endsWith(ext))); // Process each file for (const filePath of targetFiles) { try { const fileFunctions = await extractFunctionsFromFile(filePath, { unusedImportsConfig: options?.unusedImportsConfig }); functions.push(...fileFunctions); } catch (error) { console.error(`Error processing ${filePath}:`, error); } } return functions; } catch (error) { throw new Error(`Failed to scan directory: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Extract functions from a single file */ export async function extractFunctionsFromFile(filePath, options) { const functions = []; // Parse file const parseResult = await parseTypeScriptFile(filePath); if (!parseResult.sourceFile) { return functions; } const sourceFile = parseResult.sourceFile; // Get file dependencies const imports = getImports(sourceFile); const dependencies = imports .map(imp => imp.moduleSpecifier) .filter(spec => !spec.startsWith('.') && !spec.startsWith('/')) .filter((v, i, a) => a.indexOf(v) === i); // Unique only // Build import map for dependency tracking const importMap = buildImportMap(sourceFile); const detailedImports = getImportsDetailed(sourceFile); const localFunctions = getLocalFunctionNames(sourceFile); // Track import usage across the file const importNames = new Set(detailedImports.map(imp => imp.localName)); const fileUsageMap = extractIdentifierUsage(sourceFile, sourceFile, importNames); // Track re-exports - these imports are used even if not referenced in code const reExports = getReExports(sourceFile); for (const reExport of reExports) { // Find imports that match re-exported names for (const imp of detailedImports) { if (imp.importedName === reExport.name || (reExport.name === '*' && imp.modulePath === reExport.module)) { // Mark this import as used for re-export if (!fileUsageMap.has(imp.localName)) { fileUsageMap.set(imp.localName, { usageType: 'reexport', usageCount: 1, lineNumbers: [] }); } else { const usage = fileUsageMap.get(imp.localName); if (usage.usageType !== 'reexport') { usage.usageType = 'reexport'; } } } } } // Find all function declarations const functionDeclarations = findNodesByKind(sourceFile, ts.SyntaxKind.FunctionDeclaration); for (const func of functionDeclarations) { const funcDecl = func; if (funcDecl.name) { const { line } = getLineAndColumn(sourceFile, func.getStart()); // Extract function calls const functionCalls = funcDecl.body ? extractFunctionCalls(funcDecl.body, sourceFile, importMap) : []; const normalizedCalls = functionCalls.map(call => normalizeCallTarget(call.callee, filePath, localFunctions)); // Track which imports this function uses const functionUsageMap = funcDecl ? extractIdentifierUsage(funcDecl, sourceFile, importNames) : new Map(); const usedImports = Array.from(functionUsageMap.keys()); // Apply unused imports configuration const config = options?.unusedImportsConfig; let unusedImports = detailedImports .filter(imp => { // Skip side-effect imports - they're never "unused" if (imp.importType === 'side-effect') return false; // Check if import is used in this function OR at module level if (functionUsageMap.has(imp.localName) || fileUsageMap.has(imp.localName)) return false; // Apply type-only configuration if (!config?.includeTypeOnlyImports && imp.isTypeOnly) return false; // Apply ignore patterns if (config?.ignorePatterns?.some(pattern => imp.localName.match(new RegExp(pattern)))) return false; return true; }) .map(imp => imp.localName); functions.push({ name: funcDecl.name.text, filePath, lineNumber: line, language: getLanguageFromPath(filePath), dependencies, purpose: `Function ${funcDecl.name.text} implementation`, context: `Located in ${path.basename(filePath)}`, metadata: { kind: 'function', isAsync: !!funcDecl.modifiers?.some(m => m.kind === ts.SyntaxKind.AsyncKeyword), isExported: !!funcDecl.modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword), parameterCount: funcDecl.parameters.length, functionCalls: normalizedCalls, usedImports, unusedImports: unusedImports.length > 0 ? unusedImports : undefined, body: funcDecl.body ? funcDecl.body.getText(sourceFile) : undefined } }); } } // Find arrow functions assigned to variables const variableStatements = findNodesByKind(sourceFile, ts.SyntaxKind.VariableStatement); for (const varStmt of variableStatements) { const varDeclarations = varStmt.declarationList.declarations; for (const varDecl of varDeclarations) { if (varDecl.initializer && varDecl.initializer.kind === ts.SyntaxKind.ArrowFunction && ts.isIdentifier(varDecl.name)) { const { line } = getLineAndColumn(sourceFile, varDecl.getStart()); const arrowFunc = varDecl.initializer; const varStmtNode = varStmt; // Extract function calls const functionCalls = arrowFunc.body ? extractFunctionCalls(arrowFunc.body, sourceFile, importMap) : []; const normalizedCalls = functionCalls.map(call => normalizeCallTarget(call.callee, filePath, localFunctions)); // Track which imports this function uses const functionUsageMap = arrowFunc ? extractIdentifierUsage(arrowFunc, sourceFile, importNames) : new Map(); const usedImports = Array.from(functionUsageMap.keys()); // Apply unused imports configuration const config = options?.unusedImportsConfig; let unusedImports = detailedImports .filter(imp => { // Skip side-effect imports - they're never "unused" if (imp.importType === 'side-effect') return false; // Check if import is used in this function OR at module level if (functionUsageMap.has(imp.localName) || fileUsageMap.has(imp.localName)) return false; // Apply type-only configuration if (!config?.includeTypeOnlyImports && imp.isTypeOnly) return false; // Apply ignore patterns if (config?.ignorePatterns?.some(pattern => imp.localName.match(new RegExp(pattern)))) return false; return true; }) .map(imp => imp.localName); functions.push({ name: varDecl.name.text, filePath, lineNumber: line, language: getLanguageFromPath(filePath), dependencies, purpose: `Arrow function ${varDecl.name.text}`, context: `Defined in ${path.basename(filePath)}`, metadata: { kind: 'arrow', isAsync: arrowFunc.modifiers?.some(m => m.kind === ts.SyntaxKind.AsyncKeyword) || false, isExported: varStmtNode.modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword) || false, parameterCount: arrowFunc.parameters.length, functionCalls: normalizedCalls, usedImports, unusedImports: unusedImports.length > 0 ? unusedImports : undefined, body: arrowFunc.body ? arrowFunc.body.getText(sourceFile) : undefined } }); } } } // Find class methods const classDeclarations = findNodesByKind(sourceFile, ts.SyntaxKind.ClassDeclaration); for (const classDecl of classDeclarations) { const className = classDecl.name?.text || 'AnonymousClass'; const methods = classDecl.members.filter(member => member.kind === ts.SyntaxKind.MethodDeclaration); for (const method of methods) { if (ts.isIdentifier(method.name)) { const { line } = getLineAndColumn(sourceFile, method.getStart()); // Extract function calls const functionCalls = method.body ? extractFunctionCalls(method.body, sourceFile, importMap) : []; const normalizedCalls = functionCalls.map(call => normalizeCallTarget(call.callee, filePath, localFunctions)); // Track which imports this method uses const functionUsageMap = method ? extractIdentifierUsage(method, sourceFile, importNames) : new Map(); const usedImports = Array.from(functionUsageMap.keys()); // Apply unused imports configuration const config = options?.unusedImportsConfig; let unusedImports = detailedImports .filter(imp => { // Skip side-effect imports - they're never "unused" if (imp.importType === 'side-effect') return false; // Check if import is used in this method OR at module level if (functionUsageMap.has(imp.localName) || fileUsageMap.has(imp.localName)) return false; // Apply type-only configuration if (!config?.includeTypeOnlyImports && imp.isTypeOnly) return false; // Apply ignore patterns if (config?.ignorePatterns?.some(pattern => imp.localName.match(new RegExp(pattern)))) return false; return true; }) .map(imp => imp.localName); functions.push({ name: `${className}.${method.name.text}`, filePath, lineNumber: line, language: getLanguageFromPath(filePath), dependencies, purpose: `Method ${method.name.text} of class ${className}`, context: `Class method in ${path.basename(filePath)}`, metadata: { kind: 'method', className, isAsync: !!method.modifiers?.some(m => m.kind === ts.SyntaxKind.AsyncKeyword), isStatic: !!method.modifiers?.some(m => m.kind === ts.SyntaxKind.StaticKeyword), isPrivate: !!method.modifiers?.some(m => m.kind === ts.SyntaxKind.PrivateKeyword), parameterCount: method.parameters.length, functionCalls: normalizedCalls, usedImports, unusedImports: unusedImports.length > 0 ? unusedImports : undefined, body: method.body ? method.body.getText(sourceFile) : undefined } }); } } } // Check if this is a React file and scan for components if (filePath.endsWith('.tsx') || filePath.endsWith('.jsx') || (filePath.endsWith('.js') && dependencies.includes('react'))) { // Check all nodes for React components const checkNode = (node) => { if (isReactComponent(node)) { const componentType = detectComponentType(node); const componentName = getComponentName(node); const { line } = getLineAndColumn(sourceFile, node.getStart()); const endLine = getLineAndColumn(sourceFile, node.getEnd()).line; // Skip if we already indexed this as a regular function if (functions.some(f => f.name === componentName && f.lineNumber === line)) { // Update the existing function with component metadata const existingFunc = functions.find(f => f.name === componentName && f.lineNumber === line); existingFunc.purpose = `React ${componentType} component`; // For arrow functions, we need to check the parent variable declaration for types let nodeForProps = node; if (ts.isArrowFunction(node) && ts.isVariableDeclaration(node.parent)) { nodeForProps = node.parent; } existingFunc.metadata = { ...existingFunc.metadata, entityType: 'component', componentType, props: extractPropTypes(nodeForProps, sourceFile), hooks: extractHooks(node, sourceFile), jsxElements: extractJSXElements(node), isExported: isComponentExported(node), complexity: calculateComponentComplexity(node), body: getComponentBody(node, sourceFile) }; } else { // Track which imports this component uses const componentUsageMap = extractIdentifierUsage(node, sourceFile, importNames); const usedImports = Array.from(componentUsageMap.keys()); // Apply unused imports configuration const config = options?.unusedImportsConfig; let unusedImports = detailedImports .filter(imp => { // Skip side-effect imports - they're never "unused" if (imp.importType === 'side-effect') return false; // Check if import is used in this component OR at module level if (componentUsageMap.has(imp.localName) || fileUsageMap.has(imp.localName)) return false; // Apply type-only configuration if (!config?.includeTypeOnlyImports && imp.isTypeOnly) return false; // Apply ignore patterns if (config?.ignorePatterns?.some(pattern => imp.localName.match(new RegExp(pattern)))) return false; return true; }) .map(imp => imp.localName); // Add new component functions.push({ name: componentName, filePath, lineNumber: line, startLine: line, endLine: endLine, language: getLanguageFromPath(filePath), dependencies, purpose: `React ${componentType} component`, context: `Located in ${path.basename(filePath)}`, metadata: { entityType: 'component', componentType, props: extractPropTypes(node, sourceFile), hooks: extractHooks(node, sourceFile), jsxElements: extractJSXElements(node), isExported: isComponentExported(node), complexity: calculateComponentComplexity(node), body: getComponentBody(node, sourceFile), usedImports, unusedImports: unusedImports.length > 0 ? unusedImports : undefined, calledBy: [] } }); } } ts.forEachChild(node, checkNode); }; checkNode(sourceFile); } // Add file-level unused import analysis if configured if (options?.unusedImportsConfig?.checkLevel === 'file' && functions.length > 0) { // Get all imports used across all functions in the file const allUsedImports = new Set(); for (const func of functions) { if (func.metadata?.usedImports) { for (const imp of func.metadata.usedImports) { allUsedImports.add(imp); } } } // Calculate file-level unused imports const config = options.unusedImportsConfig; const fileUnusedImports = detailedImports .filter(imp => { // Check if import is used anywhere in the file if (allUsedImports.has(imp.localName)) return false; // Apply type-only configuration if (!config?.includeTypeOnlyImports && imp.isTypeOnly) return false; // Apply ignore patterns if (config?.ignorePatterns?.some(pattern => imp.localName.match(new RegExp(pattern)))) return false; return true; }) .map(imp => imp.localName); // Add a special file-level entry if there are unused imports if (fileUnusedImports.length > 0) { functions.push({ name: `[File-Level Analysis] ${path.basename(filePath)}`, filePath, lineNumber: 1, language: getLanguageFromPath(filePath), dependencies, purpose: 'File-level unused imports analysis', context: `File ${path.basename(filePath)} has unused imports at the file level`, metadata: { kind: 'file-analysis', unusedImports: fileUnusedImports, totalImports: detailedImports.length, usedImportsCount: allUsedImports.size } }); } } return functions; } // Helper functions for React component extraction function extractJSXElements(node) { const elements = new Set(); function visit(child) { if (ts.isJsxElement(child) || ts.isJsxSelfClosingElement(child)) { const tagName = ts.isJsxElement(child) ? child.openingElement.tagName : child.tagName; if (ts.isIdentifier(tagName)) { elements.add(tagName.text); } else if (ts.isPropertyAccessExpression(tagName)) { elements.add(tagName.getText()); } } ts.forEachChild(child, visit); } ts.forEachChild(node, visit); return Array.from(elements); } function isComponentExported(node) { // Check for export modifier if (ts.canHaveModifiers(node)) { const modifiers = ts.getModifiers(node); if (modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword)) { return true; } } // Check if parent is an export statement let parent = node.parent; while (parent) { if (ts.isExportAssignment(parent) || ts.isExportDeclaration(parent)) { return true; } parent = parent.parent; } return false; } function calculateComponentComplexity(node) { let complexity = 1; // Base complexity function visit(child) { // Control flow statements if (ts.isIfStatement(child) || ts.isConditionalExpression(child)) { complexity++; } else if (ts.isForStatement(child) || ts.isForInStatement(child) || ts.isForOfStatement(child) || ts.isWhileStatement(child)) { complexity += 2; } else if (ts.isSwitchStatement(child)) { complexity += child.caseBlock.clauses.length; } // Callbacks and event handlers if (ts.isCallExpression(child)) { const expression = child.expression; if (ts.isPropertyAccessExpression(expression) && expression.name.text === 'map') { complexity++; } } ts.forEachChild(child, visit); } ts.forEachChild(node, visit); return complexity; } /** * Get language from file path */ function getLanguageFromPath(filePath) { const ext = path.extname(filePath).toLowerCase(); switch (ext) { case '.ts': case '.tsx': return 'typescript'; case '.js': case '.jsx': return 'javascript'; default: return 'unknown'; } } // Aliases for MCP server compatibility export const scanFunctionsInFile = extractFunctionsFromFile; export async function scanFunctionsInDirectory(dirPath, options) { return scanDirectoryForFunctions(dirPath, { fileExtensions: options?.fileTypes, includePaths: options?.recursive !== false ? ['**/*'] : ['*'] }); } /** * Function Scanner class for compatibility */ export class FunctionScanner { async scanFunctions(content, filePath, language) { // Parse the content directly const functions = []; // Create a TypeScript source file from content const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true); // Get file dependencies const imports = getImports(sourceFile); const dependencies = imports .map(imp => imp.moduleSpecifier) .filter(spec => !spec.startsWith('.') && !spec.startsWith('/')) .filter((v, i, a) => a.indexOf(v) === i); // Unique only // Find all function declarations const functionDeclarations = findNodesByKind(sourceFile, ts.SyntaxKind.FunctionDeclaration); for (const func of functionDeclarations) { const funcNode = func; if (!funcNode.name) continue; const { line } = getLineAndColumn(sourceFile, funcNode.getStart()); functions.push({ name: funcNode.name.text, filePath: filePath, lineNumber: line, language: language, dependencies, purpose: `Function ${funcNode.name.text} implementation`, context: `Located in ${path.basename(filePath)}`, metadata: { kind: 'function', isAsync: !!funcNode.modifiers?.some(m => m.kind === ts.SyntaxKind.AsyncKeyword), isExported: !!funcNode.modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword), parameterCount: funcNode.parameters.length } }); } // Find arrow functions and variable declarations const varDeclarations = findNodesByKind(sourceFile, ts.SyntaxKind.VariableStatement); for (const varStmt of varDeclarations) { const varStmtNode = varStmt; for (const declaration of varStmtNode.declarationList.declarations) { if (declaration.initializer && declaration.initializer.kind === ts.SyntaxKind.ArrowFunction && ts.isIdentifier(declaration.name)) { const arrowFunc = declaration.initializer; const { line } = getLineAndColumn(sourceFile, declaration.getStart()); functions.push({ name: declaration.name.text, filePath: filePath, lineNumber: line, language: language, dependencies, purpose: `Arrow function ${declaration.name.text}`, context: `Defined in ${path.basename(filePath)}`, metadata: { kind: 'arrow', isAsync: !!arrowFunc.modifiers?.some(m => m.kind === ts.SyntaxKind.AsyncKeyword), isExported: !!varStmtNode.modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword), parameterCount: arrowFunc.parameters.length } }); } } } // Find class methods const classDeclarations = findNodesByKind(sourceFile, ts.SyntaxKind.ClassDeclaration); for (const classDecl of classDeclarations) { const classNode = classDecl; if (!classNode.name) continue; const className = classNode.name.text; for (const member of classNode.members) { if (ts.isMethodDeclaration(member) && member.name && ts.isIdentifier(member.name)) { const method = member; const { line } = getLineAndColumn(sourceFile, method.getStart()); functions.push({ name: `${className}.${method.name.text}`, filePath: filePath, lineNumber: line, language: language, dependencies, purpose: `Method ${method.name.text} of class ${className}`, context: `Class method in ${path.basename(filePath)}`, metadata: { kind: 'method', className, isAsync: !!method.modifiers?.some(m => m.kind === ts.SyntaxKind.AsyncKeyword), isStatic: !!method.modifiers?.some(m => m.kind === ts.SyntaxKind.StaticKeyword), isPrivate: !!method.modifiers?.some(m => m.kind === ts.SyntaxKind.PrivateKeyword), parameterCount: method.parameters.length } }); } } } return functions; } } // Helper function to get component body function getComponentBody(node, sourceFile) { if (ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node)) { return node.body ? node.body.getText(sourceFile) : undefined; } else if (ts.isArrowFunction(node)) { return node.body ? node.body.getText(sourceFile) : undefined; } else if (ts.isClassDeclaration(node)) { // For class components, get the render method body const renderMethod = node.members.find(member => ts.isMethodDeclaration(member) && member.name && ts.isIdentifier(member.name) && member.name.text === 'render'); return renderMethod?.body ? renderMethod.body.getText(sourceFile) : undefined; } return undefined; } //# sourceMappingURL=functionScanner.js.map