@angular-modernizer/plugin-solid
v1.2.0
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: Warning
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,Manager,Api,Client,Provider,Facade,Helper,UtilorUtils 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()"
}
}
]
}ServiceDipViolationRule
Rule ID: solid:service-dip-violation
Severity: Warning
Detects constructor parameters of @Injectable classes that inject concrete @Injectable classes rather than interfaces. The constructor option crossLayerOnly limits the results to injections between different path-based layers (shared, features, core, lib, app, config). Has a transform counterpart (ServiceDipViolationTransformRule) 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
},
},
},
});ISPViolationRule
Rule ID: solid:isp-violation
Severity: Warning
Detects interfaces with more than 5 methods, interfaces whose members mix concerns (UI, data, business logic), classes implementing more than 3 interfaces, and classes that implement an interface but declare no methods.
solid:import-direction-violation and solid:circular-dependency use the solid: prefix but belong to @angular-modernizer/plugin-architecture.
OCPViolationRule
Rule ID: solid:ocp-violation
Severity: Warning
Detects methods that require modification to extend behavior: switch statements with more than 5 cases, if/else chains with more than 4 branches, and methods with more than 4 parameters.
LSPViolationRule
Rule ID: solid:lsp-violation
Severity: Warning
Detects classes that break substitutability: missing members of implemented interfaces, unimplemented abstract methods, abstract method implementations that throw "not implemented", and instanceof checks (3 or more in one method count as excessive).
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
ServiceDipViolationTransformRule
Rule ID: solid:service-dip-violation-transform
Transformation Type: service-dip-violation-transform
Fixes solid:service-dip-violation findings. For each constructor parameter of an @Injectable class that injects a concrete @Injectable class, it adds an I{ClassName} interface (public method signatures) and a {CLASS_NAME}_TOKEN InjectionToken to the injected service file, changes the parameter type to the interface, adds @Inject(TOKEN) and the needed imports. Parameters that already have @Inject are skipped; an existing interface is not created again.
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
Most rules use fixed thresholds. Configurable are:
| Rule | Option | Default | Source |
|-|-|-|-|
| solid:god-switch | solid.godSwitch.maxCases | 10 | context.config |
| solid:god-switch-transform | godSwitch.minCaseCount, generateInterfaces, generateFactory, strategyNamingPattern, factoryNamingPattern | 10, true, true, '{CaseValue}Strategy', '{SwitchExpression}Factory' | context.config |
| solid:service-dip-violation | crossLayerOnly | false | ServiceDipViolationRule constructor |
Example Configuration
const context = ContextFactory.createAnalysisContext({
sourceFile,
project,
api: createPublicApi(project),
config: {
solid: { godSwitch: { maxCases: 15 } },
},
});Package Structure
packages/plugin-solid/
src/
rules/
dip-violation.rule.ts
srp-violation.rule.ts
god-switch-analysis.rule.ts
ocp-violation.rule.ts
lsp-violation.rule.ts
isp-violation.rule.ts
service-dip-violation.analysis.rule.ts
orchestrators/
god-switch-transform-orchestrator.ts
service-dip-violation-transform.orchestrator.ts
transform-rules/
god-switch-transform.rule.ts
service-dip-violation.transform.rule.ts
solid-plugin.ts
index.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
- packages/api/README.md - Public API tools
- packages/plugin-analyzer/README.md - Project analysis rules
