SOLID principle violation detection and architectural analysis for Angular codebases.
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.
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.
Identifies code that requires modification to extend behavior, including switch statements and if-else chains that grow with each new case.
Finds inheritance hierarchies where subclasses break substitutability by overriding behavior in incompatible ways.
Detects fat interfaces that force unnecessary dependencies on implementing classes.
Finds direct instantiation and tight coupling violations, including new expressions for services and concrete constructor dependencies.
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.
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 methodsService, Repository, Store, Manager, Api, Client, Provider, Facade, Helper, Util or Utilsinjector.get() service locator callsBefore:
// 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()"
}
}
]
}
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.
Rule ID: solid:god-switch
Severity: Warning
Detects switch statements with too many cases (default threshold: 10), classified by switch type.
Detection patterns:
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
},
},
},
});
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.
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.
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).
Rule ID: solid:god-switch-transform
Transformation Type: god-switch-transform
Transforms God Switches into Factory Pattern implementations.
Transformation steps:
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:
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.
| 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 |
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}`);
});
// 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 usage (handled by adapter-mcp)
const violations = await callTool("scan-solid", {
rootPath: "/path/to/project",
rules: ["solid:dip-violation-direct-instantiation"],
includeAutoFixes: true
});
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);
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 |
const context = ContextFactory.createAnalysisContext({
sourceFile,
project,
api: createPublicApi(project),
config: {
solid: { godSwitch: { maxCases: 15 } },
},
});
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.md
# Run SOLID plugin tests
pnpm test --filter @angular-modernizer/plugin-solid
# Run with coverage
pnpm test --filter @angular-modernizer/plugin-solid --coverage
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);
});
});
@angular-modernizer/api - Public API with analysis tools@angular-modernizer/core - Kernel and infrastructure@angular-modernizer/plugin-system - Plugin contracts and contextsHigh-level modules should not depend on low-level modules. Both should depend on abstractions.
Common violations:
new MyService())Best practices:
A class should have only one reason to change. One responsibility per class.
Open for extension, closed for modification. Use polymorphism instead of conditionals.
Subtypes must be substitutable for their base types. Inheritance should preserve behavior.
Clients should not be forced to depend on interfaces they don't use. Prefer small, focused interfaces.
When adding new SOLID analysis rules:
// 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
];
}
}
SOLID principles plugin for the Angular Modernizer.
SolidPlugin registers seven analysis rules and two transform rules:
InjectionToken(ServiceDipViolationTransformOrchestrator)