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
821 lines • 34 kB
JavaScript
/**
* SOLID Principles Analyzer - Functional Implementation
* Detects violations of SOLID principles in TypeScript/JavaScript code
*/
import * as ts from 'typescript';
import { ResponsibilityType } from '../types.js';
import { createViolation, getNodePosition, findNodesOfType, getNodeName, traverseAST, processFiles } from './analyzerUtils.js';
import { isReactComponent, getComponentName, detectComponentType, extractHooks, extractPropTypes } from '../utils/reactDetection.js';
import { detectComponentResponsibilities, areResponsibilitiesRelated } from '../utils/componentResponsibility.js';
import { detectComponentPattern, getUnrelatedResponsibilities, DEFAULT_PATTERNS } from '../utils/componentPatterns.js';
import { CodeIndexDB } from '../codeIndexDB.js';
import { WhitelistType } from '../types/whitelist.js';
/**
* Default configuration
*/
// Database instance for whitelist checking
let dbInstance = null;
async function getDB() {
if (!dbInstance) {
dbInstance = CodeIndexDB.getInstance();
await dbInstance.initialize();
}
return dbInstance;
}
const DEFAULT_CONFIG = {
maxMethodsPerClass: 15, // Increased from 10 - reasonable for service classes
maxLinesPerMethod: 50,
maxParametersPerMethod: 4,
maxClassComplexity: 50,
maxInterfaceMembers: 20, // Increased from 15 - configuration interfaces need more flexibility
checkDependencyInversion: false, // Disabled by default - DI is not standard pattern
checkInterfaceSegregation: true,
checkLiskovSubstitution: true,
// Component-specific defaults
enableComponentSRP: true,
componentPatterns: {
patterns: [],
customPatterns: undefined,
enablePatternDetection: true
},
maxUnrelatedResponsibilities: 3, // Increased from 2 - allow URL management + UI + state
contextAwareThresholds: true,
checkUnrelatedResponsibilities: true // Check for unrelated responsibilities by default
};
/**
* Helper: Check if import is from a framework/library
*/
function isFrameworkImport(importPath) {
const frameworkPatterns = [
// React ecosystem
/^(react|react-dom|react-router|react-redux)/,
// Next.js
/^(next|@next)\//,
// Vue ecosystem
/^(vue|vue-router|vuex|@vue)/,
// Angular
/^(@angular|angular)/,
// UI frameworks
/^@(mui|mantine|chakra-ui|ant-design|arco-design)/,
// Node frameworks
/^(express|fastify|koa|hapi|@nestjs)/,
// Auth libraries
/^(@clerk|@auth0|@stackframe|next-auth)/,
// Database clients
/^(pg|mongodb|@prisma|redis|ioredis)/,
// HTTP clients
/^(axios|node-fetch|got|ky)/,
// Testing
/^(@testing-library|jest|vitest|mocha|chai)/,
// Build tools
/^(webpack|vite|rollup|parcel|esbuild)/,
// Common utilities that shouldn't trigger DI violations
/^(lodash|underscore|ramda|date-fns|moment|dayjs)/,
// Node modules indicator
/node_modules/,
// Anything from npm scope
/^@[\w-]+\//
];
return frameworkPatterns.some(pattern => pattern.test(importPath));
}
/**
* Helper: Find import declaration for an identifier
*/
function findImportForIdentifier(sourceFile, identifier) {
let importPath;
ts.forEachChild(sourceFile, node => {
if (ts.isImportDeclaration(node) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) {
const moduleSpecifier = node.moduleSpecifier.text;
// Check named imports
if (node.importClause && node.importClause.namedBindings) {
if (ts.isNamedImports(node.importClause.namedBindings)) {
for (const element of node.importClause.namedBindings.elements) {
if (element.name.text === identifier ||
(element.propertyName && element.propertyName.text === identifier)) {
importPath = moduleSpecifier;
return;
}
}
}
}
// Check default import
if (node.importClause && node.importClause.name && node.importClause.name.text === identifier) {
importPath = moduleSpecifier;
}
}
});
return importPath;
}
/**
* Architectural layer detection for context-aware analysis
*/
var ArchitecturalLayer;
(function (ArchitecturalLayer) {
ArchitecturalLayer["Route"] = "route";
ArchitecturalLayer["Middleware"] = "middleware";
ArchitecturalLayer["Component"] = "component";
ArchitecturalLayer["Library"] = "library";
ArchitecturalLayer["Repository"] = "repository";
ArchitecturalLayer["Service"] = "service";
ArchitecturalLayer["Hook"] = "hook";
ArchitecturalLayer["Utility"] = "utility";
ArchitecturalLayer["Config"] = "config";
})(ArchitecturalLayer || (ArchitecturalLayer = {}));
/**
* Helper: Detect architectural layer from file path
*/
function detectArchitecturalLayer(filePath) {
const patterns = [
// Next.js App Router patterns
[ArchitecturalLayer.Route, /\/(app|pages)\/.*\/(route|page|layout|error|loading|not-found)\.(ts|tsx|js|jsx)$/],
[ArchitecturalLayer.Route, /\/api\/.*\/route\.(ts|js)$/],
// Middleware patterns
[ArchitecturalLayer.Middleware, /middleware\.(ts|js)$/],
[ArchitecturalLayer.Middleware, /\/(middleware|middlewares)\/.*\.(ts|js)$/],
// Component patterns
[ArchitecturalLayer.Component, /\/(components?|ui)\/.*\.(tsx|jsx)$/],
// Repository patterns
[ArchitecturalLayer.Repository, /\/(repositories|repository|repos?|dal|data-access)\/.*\.(ts|js)$/],
// Service patterns
[ArchitecturalLayer.Service, /\/(services?|use-cases|domain|business)\/.*\.(ts|js)$/],
// Hook patterns
[ArchitecturalLayer.Hook, /\/(hooks?)\/.*\.(ts|tsx|js|jsx)$/],
[ArchitecturalLayer.Hook, /use[A-Z]\w+\.(ts|tsx|js|jsx)$/],
// Utility patterns
[ArchitecturalLayer.Utility, /\/(utils?|utilities|helpers?|lib)\/.*\.(ts|js)$/],
// Config patterns
[ArchitecturalLayer.Config, /\/(config|constants)\/.*\.(ts|js)$/],
[ArchitecturalLayer.Config, /\.(config|constants)\.(ts|js)$/]
];
for (const [layer, pattern] of patterns) {
if (pattern.test(filePath)) {
return layer;
}
}
// Default to library for unmatched files
return ArchitecturalLayer.Library;
}
/**
* Helper: Get context-aware thresholds based on architectural layer
*/
function getLayerThresholds(layer, config) {
const layerConfigs = {
[ArchitecturalLayer.Route]: {
maxMethodsPerClass: 20, // Routes can have more methods
maxUnrelatedResponsibilities: 4 // Routes often coordinate multiple concerns
},
[ArchitecturalLayer.Component]: {
maxUnrelatedResponsibilities: 3, // Components can manage UI + state + events
maxMethodsPerClass: 12
},
[ArchitecturalLayer.Repository]: {
maxMethodsPerClass: 25, // Repositories can have many data access methods
maxUnrelatedResponsibilities: 1 // Should focus on data access only
},
[ArchitecturalLayer.Service]: {
maxMethodsPerClass: 15,
maxUnrelatedResponsibilities: 2
},
[ArchitecturalLayer.Hook]: {
maxMethodsPerClass: 5,
maxUnrelatedResponsibilities: 1 // Hooks should have single purpose
},
[ArchitecturalLayer.Middleware]: {
maxMethodsPerClass: 8,
maxUnrelatedResponsibilities: 1
},
[ArchitecturalLayer.Utility]: {
maxMethodsPerClass: 20, // Utility modules can have many functions
maxUnrelatedResponsibilities: 3 // Utilities may group related functions
},
[ArchitecturalLayer.Config]: {
maxMethodsPerClass: 50, // Config files may export many settings
maxInterfaceMembers: 30 // Config interfaces need flexibility
},
[ArchitecturalLayer.Library]: {
// Use defaults from config
}
};
return config.contextAwareThresholds ? (layerConfigs[layer] || {}) : {};
}
/**
* Helper: Check if file is a test file
*/
function isTestFile(filePath) {
const testPatterns = [
/\.test\.[jt]sx?$/,
/\.spec\.[jt]sx?$/,
/\.test-d\.ts$/,
/__tests__\//,
/test\//,
/tests\//,
/spec\//,
/\.e2e\.[jt]s$/,
];
return testPatterns.some(pattern => pattern.test(filePath));
}
/**
* Analyze a file for SOLID violations
*/
const analyzeFile = async (filePath, sourceFile, config) => {
const violations = [];
const isTest = isTestFile(filePath);
// Detect architectural layer for context-aware analysis
const layer = detectArchitecturalLayer(filePath);
const layerThresholds = getLayerThresholds(layer, config);
// Layer thresholds should not override explicit user configuration
// Only apply layer defaults if the user hasn't configured them
const effectiveConfig = { ...layerThresholds, ...config };
// Check Single Responsibility Principle for classes
const classes = findNodesOfType(sourceFile, ts.isClassDeclaration);
classes.forEach(cls => {
violations.push(...checkSingleResponsibility(cls, sourceFile, filePath, effectiveConfig));
});
// Check Single Responsibility Principle for React components
if (config.enableComponentSRP) {
const processedComponents = new Set();
traverseAST(sourceFile, node => {
if (isReactComponent(node)) {
// Skip if this is an arrow function that's part of a variable declaration
// that we've already processed
if (ts.isArrowFunction(node) && node.parent &&
ts.isVariableDeclaration(node.parent) &&
processedComponents.has(node.parent)) {
return;
}
// For variable declarations with arrow functions, process the declaration
// not the arrow function itself
if (ts.isVariableDeclaration(node) && node.initializer &&
ts.isArrowFunction(node.initializer)) {
if (!processedComponents.has(node)) {
processedComponents.add(node);
violations.push(...checkComponentSRP(node, sourceFile, filePath, effectiveConfig));
}
}
else if (!processedComponents.has(node)) {
processedComponents.add(node);
violations.push(...checkComponentSRP(node, sourceFile, filePath, effectiveConfig));
}
}
});
}
// Check Open/Closed Principle
traverseAST(sourceFile, node => {
violations.push(...checkOpenClosed(node, sourceFile, filePath, effectiveConfig));
});
// Check Liskov Substitution Principle
if (effectiveConfig.checkLiskovSubstitution) {
violations.push(...checkLiskovSubstitution(sourceFile, filePath, effectiveConfig));
}
// Check Interface Segregation Principle
if (effectiveConfig.checkInterfaceSegregation) {
const interfaces = findNodesOfType(sourceFile, ts.isInterfaceDeclaration);
interfaces.forEach(iface => {
violations.push(...checkInterfaceSegregation(iface, sourceFile, filePath, effectiveConfig));
});
}
// Check Dependency Inversion Principle (skip for test files)
if (effectiveConfig.checkDependencyInversion && !isTest) {
violations.push(...await checkDependencyInversion(sourceFile, filePath, effectiveConfig));
}
return violations;
};
/**
* Check Single Responsibility Principle
*/
function checkSingleResponsibility(node, sourceFile, filePath, config) {
const violations = [];
const className = getNodeName(node) || 'Anonymous';
// Count methods
const methods = findNodesOfType(node, ts.isMethodDeclaration);
const publicMethods = methods.filter(m => !m.modifiers?.some(mod => mod.kind === ts.SyntaxKind.PrivateKeyword));
if (publicMethods.length > config.maxMethodsPerClass) {
const { line, column } = getNodePosition(sourceFile, node);
violations.push(createViolation({
analyzer: 'solid',
file: filePath,
line,
column,
severity: 'warning',
message: `Class "${className}" has ${publicMethods.length} public methods (max: ${config.maxMethodsPerClass})`,
type: 'solid',
principle: 'single-responsibility',
recommendation: 'Consider splitting this class into smaller, more focused classes',
details: {
className,
methodCount: publicMethods.length,
threshold: config.maxMethodsPerClass
}
}));
}
// Check for multiple responsibilities
const responsibilities = detectResponsibilities(node);
if (responsibilities.length > 1) {
const { line, column } = getNodePosition(sourceFile, node);
violations.push(createViolation({
analyzer: 'solid',
file: filePath,
line,
column,
severity: 'warning',
message: `Class "${className}" appears to have multiple responsibilities`,
type: 'solid',
principle: 'single-responsibility',
recommendation: `Consider splitting based on: ${responsibilities.join(', ')}`,
details: {
className,
responsibilities
}
}));
}
return violations;
}
/**
* Check Open/Closed Principle
*/
function checkOpenClosed(node, sourceFile, filePath, config) {
const violations = [];
// Check for long if-else chains
if (ts.isIfStatement(node)) {
const chainLength = countIfElseChain(node);
if (chainLength > 3) {
const { line, column } = getNodePosition(sourceFile, node);
violations.push(createViolation({
analyzer: 'solid',
file: filePath,
line,
column,
severity: 'warning',
message: `Long if-else chain (${chainLength} branches) violates Open/Closed Principle`,
type: 'solid',
principle: 'open-closed',
recommendation: 'Consider using polymorphism, strategy pattern, or a mapping object',
details: {
chainLength
}
}));
}
}
// Check for switch statements with many cases
if (ts.isSwitchStatement(node)) {
const cases = findNodesOfType(node, ts.isCaseClause);
if (cases.length > 5) {
const { line, column } = getNodePosition(sourceFile, node);
violations.push(createViolation({
analyzer: 'solid',
file: filePath,
line,
column,
severity: 'warning',
message: `Switch statement with ${cases.length} cases violates Open/Closed Principle`,
type: 'solid',
principle: 'open-closed',
recommendation: 'Consider using polymorphism or a strategy pattern',
details: {
caseCount: cases.length
}
}));
}
}
return violations;
}
/**
* Check Liskov Substitution Principle
*/
function checkLiskovSubstitution(sourceFile, filePath, config) {
const violations = [];
// Find method overrides that throw errors
traverseAST(sourceFile, node => {
if (ts.isMethodDeclaration(node) && node.modifiers?.some(m => m.kind === ts.SyntaxKind.OverrideKeyword)) {
const hasThrow = findNodesOfType(node, ts.isThrowStatement).length > 0;
const isNotImplemented = node.body?.statements.length === 0 ||
(node.body?.statements.length === 1 && ts.isThrowStatement(node.body.statements[0]));
if (hasThrow || isNotImplemented) {
const { line, column } = getNodePosition(sourceFile, node);
const methodName = getNodeName(node) || 'unknown';
violations.push(createViolation({
analyzer: 'solid',
file: filePath,
line,
column,
severity: 'critical',
message: 'Method override throws error or is not implemented',
type: 'solid',
principle: 'liskov-substitution',
recommendation: 'All overridden methods should maintain behavioral compatibility with base class',
details: {
methodName
}
}));
}
}
});
return violations;
}
/**
* Check Interface Segregation Principle
*/
function checkInterfaceSegregation(node, sourceFile, filePath, config) {
const violations = [];
const interfaceName = getNodeName(node) || 'Anonymous';
if (node.members.length > config.maxInterfaceMembers) {
const { line, column } = getNodePosition(sourceFile, node);
violations.push(createViolation({
analyzer: 'solid',
file: filePath,
line,
column,
severity: 'critical',
message: `Interface "${interfaceName}" has too many members (${node.members.length})`,
type: 'solid',
principle: 'interface-segregation',
recommendation: 'Split this interface into smaller, more focused interfaces',
details: {
interfaceName,
memberCount: node.members.length,
threshold: config.maxInterfaceMembers
}
}));
}
return violations;
}
/**
* Check Dependency Inversion Principle
*/
async function checkDependencyInversion(sourceFile, filePath, config) {
const violations = [];
// Skip import checking - DI principle should focus on instantiation, not imports
// Imports are necessary and checking them creates too many false positives
// Check for 'new' expressions (direct instantiation)
const newExpressions = findNodesOfType(sourceFile, ts.isNewExpression);
for (const expr of newExpressions) {
if (ts.isIdentifier(expr.expression)) {
const className = expr.expression.text;
// Skip if this is inside a singleton pattern
if (isInsideSingletonPattern(expr)) {
continue;
}
// Skip if this is inside a factory method
if (isInsideFactoryMethod(expr)) {
continue;
}
// Skip Error classes - they need to be instantiated
if (isErrorClass(className)) {
continue;
}
// Check if this class is imported from a framework/library
const importPath = findImportForIdentifier(sourceFile, className);
if (importPath && isFrameworkImport(importPath)) {
continue; // Skip framework classes
}
if (await isConcreteClass(className)) {
const { line, column } = getNodePosition(sourceFile, expr);
violations.push(createViolation({
analyzer: 'solid',
file: filePath,
line,
column,
severity: 'warning',
message: `Direct instantiation of concrete class "${className}"`,
type: 'solid',
principle: 'dependency-inversion',
recommendation: 'Consider using dependency injection or factory pattern',
details: {
className,
importPath: importPath || 'local'
}
}));
}
}
}
return violations;
}
/**
* Helper: Detect responsibilities in a class
*/
function detectResponsibilities(node) {
const responsibilities = new Set();
const methods = findNodesOfType(node, ts.isMethodDeclaration);
methods.forEach(method => {
const name = getNodeName(method)?.toLowerCase() || '';
// Detect different concerns
if (name.includes('save') || name.includes('load') || name.includes('persist')) {
responsibilities.add('data persistence');
}
if (name.includes('validate') || name.includes('check')) {
responsibilities.add('validation');
}
if (name.includes('render') || name.includes('display') || name.includes('show')) {
responsibilities.add('presentation');
}
if (name.includes('send') || name.includes('fetch') || name.includes('request')) {
responsibilities.add('communication');
}
if (name.includes('calculate') || name.includes('compute') || name.includes('process')) {
responsibilities.add('business logic');
}
if (name.includes('log') || name.includes('track') || name.includes('monitor')) {
responsibilities.add('logging/monitoring');
}
});
// Check for too many public methods as a responsibility
const publicMethods = methods.filter(m => !m.modifiers?.some(mod => mod.kind === ts.SyntaxKind.PrivateKeyword));
if (publicMethods.length > 10) {
responsibilities.add('too many methods');
}
return Array.from(responsibilities);
}
/**
* Helper: Count if-else chain length
*/
function countIfElseChain(node) {
let count = 1;
let current = node.elseStatement;
while (current) {
count++;
if (ts.isIfStatement(current)) {
current = current.elseStatement;
}
else {
break;
}
}
return count;
}
/**
* Helper: Check if import is a concrete dependency
*/
async function isConcreteDependency(importPath) {
try {
const db = await getDB();
// Skip relative paths (they're project internal) - don't flag these
if (importPath.startsWith('.') || importPath.startsWith('/')) {
return false; // Internal modules are fine
}
// Check if it's whitelisted directly
if (db.isWhitelisted(importPath, WhitelistType.NodeBuiltin) ||
db.isWhitelisted(importPath, WhitelistType.SharedLibrary) ||
db.isWhitelisted(importPath, WhitelistType.ProjectDependency) ||
db.isWhitelisted(importPath, WhitelistType.FrameworkClass)) {
return false;
}
// For Node.js built-ins, also check the base module name
// e.g., "fs/promises" -> check "fs", "node:fs/promises" -> check "fs"
const baseModule = importPath.replace(/^node:/, '').split('/')[0];
if (baseModule !== importPath) {
if (db.isWhitelisted(baseModule, WhitelistType.NodeBuiltin)) {
return false;
}
}
// If not whitelisted, it's potentially a concrete dependency
return true;
}
catch (error) {
// If database is not available, be conservative and don't flag
console.warn('Database not available for dependency check:', error);
return false;
}
}
/**
* Helper: Check if class name is an Error class
*/
function isErrorClass(className) {
return className.endsWith('Error') ||
className.endsWith('Exception') ||
className === 'Error';
}
/**
* Helper: Check if node is inside a singleton pattern
*/
function isInsideSingletonPattern(node) {
let parent = node.parent;
while (parent) {
// Check if we're inside a method named getInstance, instance, singleton, etc.
if (ts.isMethodDeclaration(parent) || ts.isFunctionDeclaration(parent)) {
const methodName = getNodeName(parent)?.toLowerCase();
if (methodName && (methodName.includes('getinstance') ||
methodName === 'instance' ||
methodName === 'singleton' ||
methodName === 'create' && parent.parent && ts.isClassDeclaration(parent.parent))) {
return true;
}
}
// Check if we're inside a static property initialization
if (ts.isPropertyDeclaration(parent) &&
parent.modifiers?.some(m => m.kind === ts.SyntaxKind.StaticKeyword)) {
const propName = getNodeName(parent)?.toLowerCase();
if (propName && (propName === 'instance' || propName === '_instance')) {
return true;
}
}
parent = parent.parent;
}
return false;
}
/**
* Helper: Check if node is inside a factory method
*/
function isInsideFactoryMethod(node) {
let parent = node.parent;
while (parent) {
// Check if we're inside a method with 'create' or 'factory' in its name
if (ts.isMethodDeclaration(parent) || ts.isFunctionDeclaration(parent)) {
const methodName = getNodeName(parent)?.toLowerCase();
if (methodName && (methodName.includes('create') ||
methodName.includes('factory') ||
methodName.includes('make') ||
methodName.includes('build'))) {
return true;
}
}
// Check if we're inside a class with Factory in its name
if (ts.isClassDeclaration(parent)) {
const className = getNodeName(parent);
if (className && className.toLowerCase().includes('factory')) {
return true;
}
}
parent = parent.parent;
}
return false;
}
/**
* Helper: Check if class name represents concrete implementation
*/
async function isConcreteClass(className) {
try {
const db = await getDB();
// Check if it's whitelisted (platform API or framework class)
if (db.isWhitelisted(className, WhitelistType.PlatformAPI) ||
db.isWhitelisted(className, WhitelistType.FrameworkClass)) {
return false;
}
// Skip if it looks like a type/interface name
if (className.startsWith('I') && className.length > 2 && className[1] === className[1].toUpperCase()) {
return false; // e.g., IUserService
}
if (className.endsWith('Interface') || className.endsWith('Type') || className.endsWith('Schema')) {
return false;
}
return true;
}
catch (error) {
// If database is not available, be conservative and don't flag
console.warn('Database not available for class check:', error);
return false;
}
}
/**
* Check Component Single Responsibility Principle
*/
function checkComponentSRP(node, sourceFile, filePath, config) {
const violations = [];
// Extract component metadata
const componentType = detectComponentType(node);
if (!componentType)
return violations;
const componentName = getComponentName(node) || 'AnonymousComponent';
const hooks = extractHooks(node, sourceFile);
const props = extractPropTypes(node, sourceFile);
const componentInfo = {
name: componentName,
filePath,
lineNumber: getNodePosition(sourceFile, node).line,
language: 'typescript',
dependencies: [],
purpose: '',
context: '',
entityType: 'component',
componentType,
hooks,
props,
isExported: true
};
// Detect component responsibilities
const responsibilities = detectComponentResponsibilities(sourceFile, node, componentInfo);
// Detect component pattern
const patterns = config.componentPatterns?.patterns || DEFAULT_PATTERNS;
const pattern = detectComponentPattern(componentName, filePath, componentInfo.props?.map(p => p.name) || [], componentInfo.hooks?.map(h => h.name) || [], [] // TODO: Extract imports if needed
);
// Apply context-aware thresholds
let effectiveMaxResponsibilities = config.maxUnrelatedResponsibilities;
if (config.contextAwareThresholds && pattern) {
// Check for pattern-specific override first
if (config.patternThresholds?.[pattern.name]?.maxResponsibilities) {
effectiveMaxResponsibilities = config.patternThresholds[pattern.name].maxResponsibilities;
}
else {
// Fall back to multiplier-based calculation
effectiveMaxResponsibilities = Math.ceil(config.maxUnrelatedResponsibilities * pattern.complexityMultiplier);
}
}
// Get unrelated responsibilities
const uniqueTypes = [...new Set(responsibilities.map(r => r.type))];
const unrelatedGroups = [];
if (pattern) {
// Use pattern-specific analysis
const patternUnrelated = getUnrelatedResponsibilities(uniqueTypes, pattern);
unrelatedGroups.push(...patternUnrelated);
}
else {
// Generic analysis - check for unrelated responsibility pairs
for (let i = 0; i < uniqueTypes.length; i++) {
for (let j = i + 1; j < uniqueTypes.length; j++) {
if (!areResponsibilitiesRelated(uniqueTypes[i], uniqueTypes[j])) {
unrelatedGroups.push([uniqueTypes[i], uniqueTypes[j]]);
}
}
}
}
// Create violations only if there are actually unrelated responsibilities
// Skip if there's only one responsibility or all responsibilities are related
// Filter out single-element groups which aren't violations
const realUnrelatedGroups = unrelatedGroups.filter(group => group.length >= 2);
// Only check for unrelated groups if enabled in config
if (config.checkUnrelatedResponsibilities && realUnrelatedGroups.length > 0 && uniqueTypes.length > 1) {
const { line, column } = getNodePosition(sourceFile, node);
// Main violation for mixed responsibilities
violations.push(createViolation({
analyzer: 'solid',
file: filePath,
line,
column,
severity: 'warning',
message: `Component "${componentName}" has ${uniqueTypes.length} responsibilities with ${realUnrelatedGroups.length} unrelated groups`,
type: 'solid',
principle: 'single-responsibility',
componentName,
recommendation: generateComponentRefactoringRecommendation(responsibilities, pattern),
details: {
componentName,
componentType: componentInfo.componentType,
pattern: pattern?.name,
responsibilities: uniqueTypes,
unrelatedGroups: realUnrelatedGroups,
hookCount: componentInfo.hooks?.length || 0,
effectCount: responsibilities.filter(r => r.type === ResponsibilityType.SideEffects).length
}
}));
}
// Check for too many responsibilities overall
if (uniqueTypes.length > effectiveMaxResponsibilities) {
const { line, column } = getNodePosition(sourceFile, node);
violations.push(createViolation({
analyzer: 'solid',
file: filePath,
line,
column,
severity: 'critical',
message: `Component "${componentName}" has too many responsibilities (${uniqueTypes.length} > ${effectiveMaxResponsibilities})`,
type: 'solid',
principle: 'single-responsibility',
componentName,
recommendation: 'Split this component into smaller, focused components',
details: {
componentName,
responsibilityCount: uniqueTypes.length,
threshold: effectiveMaxResponsibilities,
detectedPattern: pattern?.name
}
}));
}
return violations;
}
/**
* Generate component-specific refactoring recommendations
*/
function generateComponentRefactoringRecommendation(responsibilities, pattern) {
const types = [...new Set(responsibilities.map(r => r.type))];
// Check for common anti-patterns
if (types.includes(ResponsibilityType.DataFetching) && types.includes(ResponsibilityType.UIState)) {
return 'Consider using container/presenter pattern: extract data fetching into a container component';
}
if (types.includes(ResponsibilityType.BusinessLogic) && types.includes(ResponsibilityType.FormHandling)) {
return 'Extract business logic into custom hooks or utility functions';
}
if (types.includes(ResponsibilityType.SideEffects) && types.includes(ResponsibilityType.UIState)) {
return 'Move side effects (analytics, logging) to a higher-level component or custom hook';
}
if (pattern?.name === 'Form' && types.includes(ResponsibilityType.DataFetching)) {
return 'Forms should not fetch data directly. Move data fetching to parent component';
}
if (pattern?.name === 'SimpleUI' && types.length > 2) {
return 'Simple UI components should only handle presentation. Extract logic to parent or hooks';
}
// Generic recommendation
return 'Consider splitting by responsibility: ' + types.join(', ');
}
/**
* SOLID analyzer definition
*/
export const solidAnalyzer = {
name: 'solid',
defaultConfig: DEFAULT_CONFIG,
analyze: async (files, config, options, progressCallback) => {
const mergedConfig = { ...DEFAULT_CONFIG, ...config };
return processFiles(files, analyzeFile, 'solid', mergedConfig, progressCallback ?
(current, total, file) => progressCallback({ current, total, analyzer: 'solid', file }) :
undefined);
}
};
//# sourceMappingURL=solidAnalyzer.js.map