Angular Modernizer
    Preparing search index...

    Module @angular-modernizer/plugin-solid - v1.2.0

    SOLID principles plugin for the Angular Modernizer.

    SolidPlugin registers seven analysis rules and two transform rules:

    @angular-modernizer/plugin-solid

    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 methods
    • new MyRepository() in component methods
    • Direct instantiation of classes ending with Service, Repository, Store, Manager, Api, Client, Provider, Facade, Helper, Util or Utils
    • 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()"
    }
    }
    ]
    }

    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:

    • 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
    },
    },
    },
    });

    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:

    1. Detect switch statements exceeding the threshold
    2. Extract metadata: case logic, parameters, return types, mutations
    3. Generate strategy interface for polymorphic dispatch
    4. Generate one strategy class per case with extracted logic
    5. Generate factory class with strategy map and factory method
    6. Replace switch statement with factory call
    7. 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

    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 contexts

    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

    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. Focus on one principle - each rule should address one SOLID principle
    2. Use PublicApi tools - leverage existing analysis infrastructure
    3. Include auto-fixes when possible
    4. Write comprehensive tests covering various violation scenarios
    5. Document with before/after examples
    // 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
    ];
    }
    }

    Classes

    DIPViolationRule
    GodSwitchAnalysisRule
    GodSwitchTransformOrchestrator
    GodSwitchTransformRule
    ISPViolationRule
    LSPViolationRule
    OCPViolationRule
    ServiceDipViolationRule
    ServiceDipViolationTransformOrchestrator
    ServiceDipViolationTransformRule
    SingleResponsibilityRule
    SolidPlugin

    Interfaces

    GodSwitchTransformConfig
    ServiceDipTransformResult
    ServiceDipViolationConfig
    ServiceDipViolationMetadata