@angular-modernizer/plugin-solid
v0.1.3
Published
Analysis plugin for detecting SOLID principle violations.
Maintainers
Readme
@angular-modernizer/plugin-solid
SOLID principle violation detection and architectural analysis for Angular codebases.
Overview
The @angular-modernizer/plugin-solid package provides analysis rules for detecting violations of SOLID principles and other architectural issues. This plugin powers the scan-solid tool in the MCP server.
SOLID Principles Coverage
Single Responsibility Principle (SRP)
Detects classes that have too many responsibilities by examining class sizes, method counts, and coupling metrics. Also detects single methods that mix 4 or more concern categories across 20 or more lines.
Open/Closed Principle (OCP)
Identifies code that requires modification to extend behavior, including switch statements and if-else chains that grow with each new case.
Liskov Substitution Principle (LSP)
Finds inheritance hierarchies where subclasses break substitutability by overriding behavior in incompatible ways.
Interface Segregation Principle (ISP)
Detects fat interfaces that force unnecessary dependencies on implementing classes.
Dependency Inversion Principle (DIP)
Finds direct instantiation and tight coupling violations, including new expressions for services and concrete constructor dependencies.
Analysis Rules
SRPSingleResponsibilityViolationRule
Rule ID: solid:srp-single-responsibility-violation
Severity: Warning
Detects classes with more than 15 public methods or mixed responsibility categories across 3 or more distinct concern domains. Also detects single methods that mix 4 or more concern categories across 20 or more lines.
DIPViolationRule
Rule ID: solid:dip-violation-direct-instantiation
Severity: High
Detects direct instantiation of services and the service locator anti-pattern.
Detection patterns:
new MyService()in constructors or methodsnew MyRepository()in component methods- Direct instantiation of classes ending with "Service", "Repository", "Store"
injector.get()service locator calls
Before:
// Direct instantiation
@Component({...})
export class UserComponent {
private userService = new UserService(); // DIP violation
private httpClient = new HttpClient(); // DIP violation
}
// Service locator
constructor(private injector: Injector) {
this.service = injector.get(UserService); // DIP violation
}After:
@Component({...})
export class UserComponent {
private userService = inject(UserService);
private httpClient = inject(HttpClient);
}Analysis Result:
{
"ruleId": "solid:dip-violation-direct-instantiation",
"violations": [
{
"id": "dip-violation-1",
"severity": "high",
"message": "Direct instantiation of UserService violates Dependency Inversion Principle",
"location": {
"filePath": "src/app/user.component.ts",
"startLine": 5,
"startColumn": 25
},
"category": "solid-violation",
"autoFix": {
"type": "inject-conversion",
"description": "Convert to dependency injection using inject()"
}
}
]
}DIPConcreteDependencyRule
Rule ID: solid:dip-violation-concrete-dependency
Severity: Warning
Detects constructor parameters that inject concrete @Injectable classes rather than interfaces. Has a transform counterpart (ServiceDipViolationTransformOrchestrator) in this plugin that creates an I{ClassName} interface and InjectionToken in the service file, and updates the consuming file with an @Inject decorator.
GodSwitchAnalysisRule
Rule ID: solid:god-switch
Severity: Warning
Detects switch statements with too many cases (default threshold: 10), classified by switch type.
Detection patterns:
- Switch statements exceeding the configurable threshold
- Classification by switch type: type-based, status-based, action-based, event-based, command-based, state-based, kind-based, category-based
- Confidence scoring for violations (0.7 to 1.0)
Before:
// God Switch with 15 cases
export class OrderProcessor {
processOrder(status: string): void {
switch (status) {
case 'pending': break;
case 'processing': break;
case 'shipped': break;
case 'delivered': break;
case 'cancelled': break;
case 'refunded': break;
case 'returned': break;
case 'failed': break;
case 'on-hold': break;
case 'completed': break;
case 'archived': break;
case 'disputed': break;
case 'escalated': break;
case 'resolved': break;
case 'closed': break;
default: break;
}
}
}After (Factory Pattern):
interface OrderStatusStrategy {
execute(): void;
}
class PendingStrategy implements OrderStatusStrategy {
execute(): void { /* ... */ }
}
class ProcessingStrategy implements OrderStatusStrategy {
execute(): void { /* ... */ }
}
// ... more strategy classes
class OrderStatusFactory {
private strategies = new Map<string, OrderStatusStrategy>([
['pending', new PendingStrategy()],
['processing', new ProcessingStrategy()],
// ... more strategies
]);
createStrategy(status: string): OrderStatusStrategy {
return this.strategies.get(status) ?? new DefaultStrategy();
}
}Analysis Result:
{
"ruleId": "solid:god-switch",
"message": "God Switch detected: 15 cases (status-based). Consider Factory Pattern.",
"filePath": "src/app/order-processor.ts",
"line": 3,
"column": 4,
"suggestedFix": "Refactor to Factory Pattern with strategy objects for each case type",
"metadata": {
"caseCount": 15,
"switchType": "status-based",
"switchExpression": "status",
"hasDefault": true,
"confidence": 0.9,
"caseLabels": ["pending", "processing", "shipped", "..."],
"violationType": "god-switch",
"principle": "Open/Closed Principle"
}
}Configuration:
interface GodSwitchConfig {
/**
* Maximum allowed case clauses before violation
* Default: 10
*/
maxCases?: number;
}
// Usage
const context = ContextFactory.createAnalysisContext({
sourceFile,
project,
api: createPublicApi(),
config: {
solid: {
godSwitch: {
maxCases: 15, // Custom threshold
},
},
},
});ImportDirectionViolationRule
Rule ID: solid:import-direction-violation
Severity: Error
Detects imports that violate the configured layer direction rules. Requires layers configuration in .angular-modernizer.json. Silently skips violations when layer detection returns null for either file to avoid noise from unrecognized paths.
OCPViolationRule
Rule ID: solid:ocp-violation
Severity: Warning
Detects classes that require modification to extend behavior, including type-checking patterns (instanceof chains, type switches) and hardcoded branching on type identifiers.
LSPViolationRule
Rule ID: solid:lsp-violation
Severity: Warning
Detects subclasses that break Liskov Substitution by overriding methods in ways that violate the base class contract.
GodSwitchTransformRule
Rule ID: solid:god-switch-transform
Transformation Type: god-switch-transform
Transforms God Switches into Factory Pattern implementations.
Transformation steps:
- Detect switch statements exceeding the threshold
- Extract metadata: case logic, parameters, return types, mutations
- Generate strategy interface for polymorphic dispatch
- Generate one strategy class per case with extracted logic
- Generate factory class with strategy map and factory method
- Replace switch statement with factory call
- Update imports for generated code
Before:
export class PaymentProcessor {
processPayment(method: string, amount: number): string {
let result = '';
switch (method) {
case 'credit-card':
result = `Processing credit card payment of ${amount}`;
break;
case 'debit-card':
result = `Processing debit card payment of ${amount}`;
break;
case 'paypal':
result = `Processing PayPal payment of ${amount}`;
break;
case 'bank-transfer':
result = `Processing bank transfer of ${amount}`;
break;
case 'crypto':
result = `Processing crypto payment of ${amount}`;
break;
// ... 10+ more cases
default:
result = 'Unknown payment method';
break;
}
return result;
}
}After:
interface MethodStrategy {
execute(amount: number): string;
}
class CreditCardStrategy implements MethodStrategy {
execute(amount: number): string {
return `Processing credit card payment of ${amount}`;
}
}
class DebitCardStrategy implements MethodStrategy {
execute(amount: number): string {
return `Processing debit card payment of ${amount}`;
}
}
// ... more strategy classes
class MethodFactory {
private strategies = new Map<string, MethodStrategy>([
['credit-card', new CreditCardStrategy()],
['debit-card', new DebitCardStrategy()],
['paypal', new PaypalStrategy()],
// ... more strategies
]);
createStrategy(method: string): MethodStrategy {
return this.strategies.get(method) ?? new DefaultStrategy();
}
}
export class PaymentProcessor {
private factory = new MethodFactory();
processPayment(method: string, amount: number): string {
const strategy = this.factory.createStrategy(method);
return strategy.execute(amount);
}
}Configuration:
interface GodSwitchTransformConfig {
/**
* Minimum case count to trigger transformation
* Default: 10
*/
minCaseCount?: number;
/**
* Generate strategy interface
* Default: true
*/
generateInterfaces?: boolean;
/**
* Generate factory class
* Default: true
*/
generateFactory?: boolean;
/**
* Strategy class naming pattern
* Placeholders: {CaseValue}, {SwitchExpression}
* Default: "{CaseValue}Strategy"
*/
strategyNamingPattern?: string;
/**
* Factory class naming pattern
* Default: "{SwitchExpression}Factory"
*/
factoryNamingPattern?: string;
}
// Usage via MCP tool
const result = await transformCode({
filePath: 'src/app/payment-processor.ts',
transformationType: 'god-switch-transform',
config: {
'@angular-modernizer/plugin-solid': {
transformationType: 'god-switch-transform',
godSwitch: {
minCaseCount: 10,
generateInterfaces: true,
generateFactory: true,
strategyNamingPattern: '{CaseValue}Strategy',
factoryNamingPattern: '{SwitchExpression}Factory',
},
},
},
});Transformation guarantees:
- Idempotent: safe to run multiple times, skips already-transformed switches
- Atomic: all-or-nothing semantics with automatic rollback on failure
- Mutation handling: preserves variable mutations across case boundaries
- Fallthrough support: groups fallthrough cases into a single strategy
- Type safety: generates TypeScript interfaces and typed factory methods
Factory Pattern Comparison
| Aspect | God Switch | Factory Pattern | |-|-|-| | Extensibility | Modify switch for new cases | Add new strategy class | | Testability | Test entire switch | Test strategies independently | | Maintainability | Large, complex switch | Small, focused classes | | OCP Compliance | Violates OCP | Follows OCP | | Code Reuse | Duplicate logic | Reusable strategies | | Type Safety | String-based cases | Interface-based polymorphism |
Usage Examples
Basic SOLID Analysis
import { Kernel, RealFileSystemAdapter } from '@angular-modernizer/core';
import { SolidPlugin } from '@angular-modernizer/plugin-solid';
const kernel = new Kernel({
tsConfigPath: './tsconfig.json',
fileSystem: new RealFileSystemAdapter(),
plugins: [new SolidPlugin()],
});
await kernel.initialize();
// Get SOLID analysis rules
const plugin = kernel.getPlugin('@angular-modernizer/plugin-solid');
const rules = plugin.getAnalysisRules();
const dipRule = rules.find(
(r) => r.id === 'solid:dip-violation-direct-instantiation',
);
// Analyze a file
const project = kernel.getProject();
const sourceFile = project.addSourceFileAtPath('src/app/user.component.ts');
const context = ContextFactory.createAnalysisContext({
sourceFile,
project,
api: createPublicApi(),
config: {},
});
const result = await dipRule.analyze(context);
console.log('DIP Violations found:', result.violations.length);
result.violations.forEach((v) => {
console.log(`- ${v.message} at line ${v.location.startLine}`);
});Detection + Transformation (God Switch)
// Get both analysis and transform rules
const analysisRule = plugin
.getAnalysisRules()
.find((r) => r.id === 'solid:god-switch');
const transformRule = plugin
.getTransformRules()
.find((r) => r.id === 'solid:god-switch-transform');
// 1. Detect violations
const analysisContext = ContextFactory.createAnalysisContext({
sourceFile,
project,
api: createPublicApi(project),
config: {},
});
const violations = await analysisRule.analyze(analysisContext);
if (violations.length > 0) {
console.log(`Found ${violations.length} God Switches, transforming...`);
// 2. Transform violations
const transformContext = ContextFactory.createTransformContext({
sourceFile,
project,
api: createPublicApi(project),
config: {
'@angular-modernizer/plugin-solid': {
transformationType: 'god-switch-transform',
godSwitch: {
minCaseCount: 10,
generateInterfaces: true,
generateFactory: true,
},
},
},
});
const result = await transformRule.transform(transformContext);
if (result.modified) {
await sourceFile.save();
}
}MCP Tool Integration
// MCP tool usage (handled by adapter-mcp)
const violations = await callTool("scan-solid", {
rootPath: "/path/to/project",
rules: ["solid:dip-violation-direct-instantiation"],
includeAutoFixes: true
});Batch Analysis
import { glob } from 'glob';
const files = await glob('src/**/*.{ts,tsx}', {
ignore: ['**/node_modules/**', '**/dist/**'],
});
const allViolations: Violation[] = [];
for (const filePath of files) {
const sourceFile = project.addSourceFileAtPath(filePath);
const context = ContextFactory.createAnalysisContext({
sourceFile,
project,
api: createPublicApi(),
});
for (const rule of rules) {
const result = await rule.analyze(context);
allViolations.push(...result.violations);
}
}
// Group by severity
const bySeverity = allViolations.reduce(
(acc, v) => {
acc[v.severity] = (acc[v.severity] || 0) + 1;
return acc;
},
{} as Record<string, number>,
);
console.log('SOLID Analysis Summary:');
console.log('High:', bySeverity.high || 0);
console.log('Medium:', bySeverity.medium || 0);
console.log('Low:', bySeverity.low || 0);Configuration Options
DIP Violation Config
interface DIPViolationConfig {
/**
* Service class suffixes to detect
* Default: ['Service', 'Repository', 'Store', 'Manager']
*/
serviceSuffixes?: string[];
/**
* Exclude patterns for files/directories
* Default: ['**/node_modules/**', '**/dist/**']
*/
excludePatterns?: string[];
/**
* Include auto-fix suggestions
* Default: true
*/
includeAutoFixes?: boolean;
/**
* Custom service detection patterns
*/
customPatterns?: RegExp[];
}Example Configuration
const context = ContextFactory.createAnalysisContext({
sourceFile,
project,
api: createPublicApi(),
config: {
serviceSuffixes: ['Service', 'Repository', 'Store', 'Api'],
excludePatterns: ['**/generated/**', '**/vendor/**'],
includeAutoFixes: true,
customPatterns: [/.*Client$/, /.*Provider$/],
},
});Package Structure
packages/plugin-solid/
src/
rules/
dip-violation.rule.ts
srp-single-responsibility-violation.rule.ts
god-switch-analysis.rule.ts
ocp-violation.rule.ts
lsp-violation.rule.ts
import-direction-violation.rule.ts
dip-violation-concrete-dependency.rule.ts
orchestrators/
god-switch-transform-orchestrator.ts
service-dip-violation-transform.orchestrator.ts
transform-rules/
god-switch-transform.rule.ts
solid-plugin.ts
index.ts
types.ts
__tests__/
rules/
dip-violation.rule.test.ts
god-switch-analysis.rule.test.ts
(and more)
orchestrators/
god-switch-transform-orchestrator.test.ts
god-switch-mutation-preservation.test.ts
transform-rules/
god-switch-transform.rule.test.ts
solid-plugin.test.ts
package.json
tsconfig.json
jest.config.js
README.mdTesting
# Run SOLID plugin tests
pnpm test --filter @angular-modernizer/plugin-solid
# Run with coverage
pnpm test --filter @angular-modernizer/plugin-solid --coverageTest Examples
describe('DIPViolationRule', () => {
let rule: DIPViolationRule;
beforeEach(() => {
rule = new DIPViolationRule();
});
it('should detect direct service instantiation', async () => {
const sourceFile = project.createSourceFile(
'test.component.ts',
`
@Component({})
export class TestComponent {
private service = new UserService(); // Violation
private repo = new UserRepository(); // Violation
}
`,
);
const context = ContextFactory.createAnalysisContext({
sourceFile,
project,
api: createPublicApi(),
});
const result = await rule.analyze(context);
expect(result.violations).toHaveLength(2);
expect(result.violations[0].message).toContain('UserService');
expect(result.violations[1].message).toContain('UserRepository');
});
it('should not flag dependency injection', async () => {
const sourceFile = project.createSourceFile(
'test.component.ts',
`
@Component({})
export class TestComponent {
private service = inject(UserService); // OK
private repo = inject(UserRepository); // OK
}
`,
);
const context = ContextFactory.createAnalysisContext({
sourceFile,
project,
api: createPublicApi(),
});
const result = await rule.analyze(context);
expect(result.violations).toHaveLength(0);
});
});Dependencies
@angular-modernizer/api- Public API with analysis tools@angular-modernizer/core- Kernel and infrastructure@angular-modernizer/plugin-system- Plugin contracts and contexts
SOLID Principle Reference
Dependency Inversion Principle (DIP)
High-level modules should not depend on low-level modules. Both should depend on abstractions.
Common violations:
- Direct instantiation of services (
new MyService()) - Missing interfaces for services
- Tight coupling between high-level and low-level modules
Best practices:
- Use dependency injection
- Program to interfaces, not implementations
- Create abstractions for volatile dependencies
Single Responsibility Principle (SRP)
A class should have only one reason to change. One responsibility per class.
Open/Closed Principle (OCP)
Open for extension, closed for modification. Use polymorphism instead of conditionals.
Liskov Substitution Principle (LSP)
Subtypes must be substitutable for their base types. Inheritance should preserve behavior.
Interface Segregation Principle (ISP)
Clients should not be forced to depend on interfaces they don't use. Prefer small, focused interfaces.
Contributing
When adding new SOLID analysis rules:
- Focus on one principle - each rule should address one SOLID principle
- Use PublicApi tools - leverage existing analysis infrastructure
- Include auto-fixes when possible
- Write comprehensive tests covering various violation scenarios
- Document with before/after examples
Adding a New SOLID Rule
// 1. Implement the rule
class MySolidRule implements AnalysisRule {
readonly id = 'solid:my-solid-rule';
readonly name = 'My SOLID Rule';
readonly description = 'Detects violations of a specific SOLID principle';
async analyze(context: AnalysisContext): Promise<AnalysisResult> {
const { api, sourceFile } = context;
const violations: Violation[] = [];
// Use PublicApi for analysis
// Detect violations and create Violation objects
return {
ruleId: this.id,
violations,
metadata: {
totalViolations: violations.length,
},
};
}
}
// 2. Add to plugin
class SolidPlugin implements Plugin {
getAnalysisRules(): AnalysisRule[] {
return [
new DIPViolationRule(),
new MySolidRule(), // Add here
];
}
}See Also
- README.md - Project overview
- DEVELOPMENT_GUIDE.md - Architecture details
- packages/api/README.md - Public API tools
- packages/plugin-analyzer/README.md - Project analysis rules
