Angular Modernizer
    Preparing search index...

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

    Angular-specific analysis and transform rules for the Angular Modernizer kernel.

    The main entry point is AngularPlugin: register it with the kernel to get 28 analysis rules and 25 transform rules. The rule classes and their orchestrators are also exported for direct use, for example MissingOnPushRule, RxJSOptimizationRule, TypeSafetyAnalysisRule, ServiceBagTransformRule with ServiceBagTransformOrchestrator, and LibraryExtractionTransformRule with LibraryExtractionOrchestrator.

    Transform rules follow the "thin rule, thick orchestrator" pattern: the rule checks that its transformationType was requested in the plugin config (key @angular-modernizer/plugin-angular) and delegates the work to an orchestrator class.

    @angular-modernizer/plugin-angular

    Angular-specific analysis and transform rules for detecting and fixing anti-patterns, performance issues, and best practice violations in Angular applications.

    This plugin provides 28 analysis rules and 25 transform rules (one per transformation type) for analyzing and modernizing Angular codebases, with a focus on:

    • Change Detection Strategy optimization
    • RxJS operator efficiency and memory leak prevention
    • Angular decorator validation
    • Component/Service architecture best practices
    • Automated code transformations (constructor injection, interface extraction, DI migration)
    pnpm add @angular-modernizer/plugin-angular
    

    Rule ID: plugin-angular:rxjs-optimization Severity: Warning

    Detects inefficient RxJS operator usage across multiple anti-patterns with impact analysis and refactoring complexity estimation.

    Consolidate multiple .pipe().pipe() chains into a single pipe operator.

    Before:

    this.data$ = this.http
    .get('/api/data')
    .pipe(map((data) => data.items))
    .pipe(filter((items) => items.length > 0))
    .pipe(tap((items) => console.log(items)));

    After:

    this.data$ = this.http.get('/api/data').pipe(
    map((data) => data.items),
    filter((items) => items.length > 0),
    tap((items) => console.log(items)),
    );

    Impact: Low | Refactoring Complexity: Low


    HTTP calls or computed observables subscribed multiple times without caching.

    Before:

    // In component
    this.userData$ = this.http.get<User>('/api/user');

    // In template - triggers 3 HTTP requests
    <div>{{ (userData$ | async)?.name }}</div>
    <div>{{ (userData$ | async)?.email }}</div>
    <div>{{ (userData$ | async)?.role }}</div>

    After:

    this.userData$ = this.http
    .get<User>('/api/user')
    .pipe(shareReplay({ bufferSize: 1, refCount: true }));

    // Now only 1 HTTP request for all subscriptions

    Impact: High | Refactoring Complexity: Low


    Consecutive identical operators that should be merged.

    Before:

    this.filtered$ = this.items$.pipe(
    map((items) => items.filter((i) => i.active)),
    map((items) => items.sort((a, b) => a.name.localeCompare(b.name))),
    );

    After:

    this.filtered$ = this.items$.pipe(
    map((items) =>
    items.filter((i) => i.active).sort((a, b) => a.name.localeCompare(b.name)),
    ),
    );

    Impact: Low | Refactoring Complexity: Low


    FormControl valueChanges without debounceTime() or throttleTime().

    Before:

    this.searchControl.valueChanges
    .pipe(switchMap((term) => this.searchService.search(term)))
    .subscribe((results) => (this.results = results));

    After:

    this.searchControl.valueChanges
    .pipe(
    debounceTime(300),
    switchMap((term) => this.searchService.search(term)),
    )
    .subscribe((results) => (this.results = results));

    Impact: High | Refactoring Complexity: Low


    Business logic inside .subscribe() that should use pipe operators.

    Before:

    this.users$.subscribe((users) => {
    const activeUsers = users.filter((u) => u.active);
    const sortedUsers = activeUsers.sort((a, b) => a.name.localeCompare(b.name));
    this.displayUsers = sortedUsers.slice(0, 10);
    });

    After:

    this.displayUsers$ = this.users$.pipe(
    map(users => users.filter(u => u.active)),
    map(users => users.sort((a, b) => a.name.localeCompare(b.name))),
    map(users => users.slice(0, 10))
    );

    // In template
    <div *ngFor="let user of displayUsers$ | async">{{ user.name }}</div>

    Impact: Medium | Refactoring Complexity: Medium


    Nested observable subscriptions without flattening operators.

    Before:

    this.route.params.subscribe((params) => {
    this.userService.getUser(params['id']).subscribe((user) => {
    this.user = user;
    });
    });

    After:

    this.user$ = this.route.params.pipe(
    switchMap(params => this.userService.getUser(params['id']))
    );

    // In template
    <div>{{ (user$ | async)?.name }}</div>

    Impact: High | Refactoring Complexity: Medium


    HTTP observables using catchError that returns of(null), of([]), or EMPTY without re-throwing, preventing callers from distinguishing success from failure.

    Impact: High | Refactoring Complexity: Medium


    Using Subject instead of BehaviorSubject for state management.

    Before:

    export class StateService {
    private state$ = new Subject<AppState>();

    setState(state: AppState) {
    this.state$.next(state);
    }

    getState() {
    return this.state$.asObservable();
    }
    }

    After:

    export class StateService {
    private state$ = new BehaviorSubject<AppState>(initialState);

    setState(state: AppState) {
    this.state$.next(state);
    }

    getState() {
    return this.state$.asObservable();
    }
    }

    Impact: High | Refactoring Complexity: Low


    {
    "rxjs-optimization": {
    "maxPipeChains": 1, // Max allowed chained .pipe() calls
    "maxSubscribeComplexity": 2, // Max statements in subscribe blocks
    "requireDebounceOnUserInput": true // Enforce debounce on FormControl valueChanges
    }
    }

    Rule ID: angular:performance-violation Severity: Error

    Detects missing subscription cleanup patterns in component subscriptions and complex template getters.

    Missing takeUntil Before:

    export class UserComponent implements OnInit {
    ngOnInit() {
    this.userService.currentUser$.subscribe((user) => {
    this.user = user;
    }); // Memory leak - survives component destruction
    }
    }

    Missing takeUntil After:

    export class UserComponent implements OnInit, OnDestroy {
    private destroy$ = new Subject<void>();

    ngOnInit() {
    this.userService.currentUser$
    .pipe(takeUntil(this.destroy$))
    .subscribe((user) => {
    this.user = user;
    });
    }

    ngOnDestroy() {
    this.destroy$.next();
    this.destroy$.complete();
    }
    }

    Rule ID: angular:missing-onpush-change-detection Severity: Warning

    Detects components with @Input() properties using default change detection.

    Before:

    @Component({
    selector: 'app-user-card',
    // Missing: changeDetection: ChangeDetectionStrategy.OnPush
    })
    export class UserCardComponent {
    @Input() user!: User;
    }

    After:

    @Component({
    selector: 'app-user-card',
    changeDetection: ChangeDetectionStrategy.OnPush,
    })
    export class UserCardComponent {
    @Input() user!: User;
    }

    Rule ID: angular:service-without-injectable Severity: Error

    Detects services missing @Injectable() decorator.


    Rule ID: angular:component-without-selector Severity: Error

    Detects components missing selector property.


    Rule ID: angular:directive-without-selector Severity: Error

    Detects directives missing selector property.


    Rule ID: angular:pipe-without-name Severity: Error

    Detects pipes missing name property.


    Rule ID: angular:async-pipe-misuse Severity: Warning

    Detects incorrect usage of the async pipe in templates. Two violation types:

    • double-async-subscription: observable used with | async in template AND manually subscribed in a lifecycle hook
    • multiple-async-subscriptions: same observable used with | async two or more times in the same template

    Rule ID: angular:unnecessary-change-detection Severity: Warning

    Detects components using Default change detection when OnPush would suffice, and components calling detectChanges() without using OnPush strategy.


    Rule ID: angular:missing-return-type Severity: Warning

    Detects methods in Angular classes that are missing explicit return type annotations.

    Before:

    @Component({...})
    export class UserComponent {
    // Missing return type
    getUserName() {
    return this.user?.name || 'Unknown';
    }
    }

    After:

    @Component({...})
    export class UserComponent {
    // Explicit return type
    getUserName(): string {
    return this.user?.name || 'Unknown';
    }
    }

    Rule ID: angular:too-many-inputs Severity: Warning

    Detects components with excessive @Input() properties (default threshold: 10).

    Configuration:

    {
    "too-many-inputs": {
    "maxInputs": 5
    }
    }

    Rule ID: angular:lifecycle-hook-violation Severity: Warning/Error

    Detects improper lifecycle hook usage and missing cleanup. Also detects heavy ngOnInit with 5 or more service calls (warning), escalating to error at 8 or more.

    Before:

    @Component({...})
    export class TimerComponent {
    private intervalId: number;

    ngOnInit() {
    // Missing cleanup - interval continues after component destruction
    this.intervalId = window.setInterval(() => {
    console.log('tick');
    }, 1000);
    }
    }

    After:

    @Component({...})
    export class TimerComponent implements OnDestroy {
    private destroy$ = new Subject<void>();

    ngOnInit() {
    interval(1000).pipe(
    takeUntil(this.destroy$)
    ).subscribe(() => console.log('tick'));
    }

    ngOnDestroy() {
    this.destroy$.next();
    this.destroy$.complete();
    }
    }

    Rule ID: angular:template-complexity Severity: Warning

    Detects complex operations in templates that cause side effects on every change detection cycle. Also detects component getters with 3 or more non-trivial statements (complex-template-getter violation type).

    Before:

    @Component({
    template: `
    <div>{{ getExpensiveComputation() }}</div>
    <button (click)="users.push(newUser)">Add User</button>
    `,
    })
    export class UserListComponent {
    getExpensiveComputation() {
    return this.users.filter((u) => u.active).length * 42;
    }
    }

    After:

    @Component({
    template: `
    <div>{{ activeUserCount }}</div>
    <button (click)="addUser()">Add User</button>
    `,
    })
    export class UserListComponent {
    activeUserCount = this.users.filter((u) => u.active).length;

    addUser() {
    this.users.push(this.newUser);
    this.activeUserCount = this.users.filter((u) => u.active).length;
    }
    }

    Rule ID: angular:immutable-input-violation Severity: Error

    Detects mutation of @Input properties that break OnPush change detection.

    Before:

    @Component({
    changeDetection: ChangeDetectionStrategy.OnPush,
    })
    export class UserCardComponent {
    @Input() user: User;

    updateUser() {
    this.user.name = 'New Name'; // Mutates input - breaks OnPush
    }
    }

    After:

    @Component({
    changeDetection: ChangeDetectionStrategy.OnPush,
    })
    export class UserCardComponent {
    @Input() user: User;

    updateUser() {
    this.userChange.emit({ ...this.user, name: 'New Name' });
    }

    @Output() userChange = new EventEmitter<User>();
    }

    Rule ID: angular:missing-trackby Severity: Warning

    Detects ngFor directives without trackBy functions.

    Before:

    @Component({
    template: `
    <li *ngFor="let user of users">{{ user.name }}</li>
    `,
    })
    export class UserListComponent {
    users: User[] = [];
    }

    After:

    @Component({
    template: `
    <li *ngFor="let user of users; trackBy: trackByUserId">{{ user.name }}</li>
    `,
    })
    export class UserListComponent {
    users: User[] = [];

    trackByUserId(index: number, user: User): string {
    return user.id;
    }
    }

    Rule ID: angular:presentational-component-violation Severity: Warning

    Detects presentational (dumb) components that have taken on responsibilities belonging to container components:

    • service-dependency: constructor or inject() service dependency
    • subscription-in-lifecycle: subscriptions without takeUntilDestroyed
    • missing-output: service call without a corresponding @Output
    • business-logic: keyword prefix combined with cyclomatic complexity above 3
    • complex-state-management: 3 or more non-Input array/Map/Set properties

    Configurable dialog class detection via .angular-modernizer.json rules section.


    Rule ID: angular:service-mutable-state Severity: Warning

    Detects @Injectable services that mutate public array, Map, or Set fields via push, splice, pop, sort, or reverse.


    Rule ID: angular:promise-vs-observable-anti-pattern Severity: Warning

    Detects 5 anti-patterns:

    • .toPromise() calls
    • Async component methods
    • Promise-wrapping of HttpClient services
    • Sequential awaits that could run in parallel
    • Manual detectChanges() calls

    Rule ID: angular:component-wrapper-instantiation Severity: Warning

    Detects direct new X() instantiation of wrapper or adapter classes that should be injected. Configurable class name patterns via .angular-modernizer.json.


    Rule ID: angular:dto-direct-instantiation Severity: Warning

    Detects direct new X() instantiation of DTO, Response, or Result classes. Configurable via .angular-modernizer.json.


    Rule ID: angular:magic-string-selector Severity: Error (Warning for selectors read from a property)

    Detects string-literal selector arguments passed to dynamic component loading methods. Target method patterns are configurable via .angular-modernizer.json.


    Transformation Type: constructor-to-inject

    Converts Angular constructor injection to the modern inject() function.

    Before:

    @Component({...})
    export class UserComponent {
    constructor(
    private userService: UserService,
    private router: Router,
    @Inject(APP_CONFIG) private config: AppConfig
    ) {}
    }

    After:

    @Component({...})
    export class UserComponent {
    private readonly userService = inject(UserService);
    private readonly router = inject(Router);
    private readonly config = inject(APP_CONFIG);
    }

    Features:

    • Converts all constructor parameters to inject() calls
    • Handles @Inject() decorators with string tokens
    • Automatically adds inject import from @angular/core
    • Preserves parameter access modifiers and names
    • Makes a field readonly when it is never reassigned (api MemberWriteFinder: a private field is checked in its class, a protected or public one also through its references in the project, so a subclass assigning it keeps it mutable; templates are not checked); readonly parameters stay readonly; option readonlyInjectFields: false turns it off

    Transformation Type: interface-extraction

    Extracts inline type literals into named interfaces.

    Before:

    @Component({...})
    export class UserFormComponent {
    @Input() user: {
    id: number;
    name: string;
    email: string;
    profile: {
    avatar: string;
    bio?: string;
    };
    };
    }

    After:

    interface UserProfile {
    avatar: string;
    bio?: string;
    }

    interface User {
    id: number;
    name: string;
    email: string;
    profile: UserProfile;
    }

    @Component({...})
    export class UserFormComponent {
    @Input() user: User;
    }

    Features:

    • Extracts complex inline type literals
    • Handles nested object types and arrays
    • Supports optional properties and union types
    • Generates unique interface names to avoid conflicts

    Transformation Type: dependency-injection-migration

    Modernizes legacy Angular dependency injection patterns including string tokens and ReflectiveInjector.

    Before:

    // String token injection
    @Injectable()
    export class ApiService {
    constructor(@Inject('API_URL') private apiUrl: string) {}
    }

    // ReflectiveInjector usage
    const injector = ReflectiveInjector.resolveAndCreate([
    { provide: 'API_CONFIG', useValue: config },
    ]);

    After:

    // Modern injection token
    export const API_URL = new InjectionToken<string>('API_URL');

    @Injectable()
    export class ApiService {
    private apiUrl = inject(API_URL);
    }

    // Modern injector usage
    const injector = Injector.create([{ provide: API_CONFIG, useValue: config }]);

    Transformation Type: constructor-injection-transform

    Converts manual service instantiation (new Service()) to proper inject(Service) calls.

    Before:

    @Component({...})
    export class UserComponent {
    constructor() {
    this.httpClient = new HttpClient();
    this.userService = new UserService();
    }

    private httpClient: HttpClient;
    private userService: UserService;
    }

    After:

    @Component({...})
    export class UserComponent {
    private httpClient = inject(HttpClient);
    private userService = inject(UserService);
    }

    Configuration:

    {
    "constructor-injection-transform": {
    "enabled": true,
    "serviceSuffixes": ["Service", "Repository", "Client", "Store"],
    "handleComplexExpressions": true
    }
    }

    Transformation Type: component-inputs-interface-extraction

    Extracts component input properties into dedicated interfaces.

    Before:

    @Component({...})
    export class UserCardComponent {
    @Input() userId: number;
    @Input() userName: string;
    @Input() isActive: boolean;
    @Input() avatarUrl?: string;
    }

    After:

    interface UserCardInputs {
    userId: number;
    userName: string;
    isActive: boolean;
    avatarUrl?: string;
    }

    @Component({...})
    export class UserCardComponent {
    @Input() inputs: UserCardInputs;
    }

    Configuration:

    {
    "component-inputs-interface-extraction": {
    "enabled": true,
    "minInputsThreshold": 3,
    "generateUniqueNames": true,
    "addToSeparateFile": false
    }
    }

    Transformation Type: service-injection-cleanup

    Converts manual service instantiation in property initializers and constructors to inject() calls.

    Before:

    @Component({...})
    export class DataComponent {
    private http = new HttpClient();
    private router: Router = new Router();

    constructor() {
    this.http = new HttpClient();
    }
    }

    After:

    @Component({...})
    export class DataComponent {
    private http = inject(HttpClient);
    private router = inject(Router);
    }

    Configuration:

    {
    "service-injection-cleanup": {
    "enabled": true,
    "servicePatterns": ["Service$", "Client$", "Repository$"],
    "handleComplexExpressions": true
    }
    }

    The plugin also exposes the following transformation types via the transform-code MCP tool:

    • promise-service-to-observable - Converts Promise-based service methods to Observable equivalents
    • promise-component-to-reactive - Converts async component methods to reactive patterns
    • promise-cleanup - Removes .toPromise() and related promise bridge code
    • sequential-await-to-forkjoin - Converts independent sequential awaits to parallel forkJoin()
    • missing-output-transform - Adds output() signals to presentational components that call services directly
    • subscription-transform - Converts raw .subscribe() in lifecycle hooks to takeUntilDestroyed() pattern
    • library-extraction - Executes a 7-phase library extraction workflow (use after extract-libraries detection)
    • static-class-to-functions - Converts static-only utility classes to module-level functions and constants
    • extract-static-from-stateful - Moves public static methods of a stateful class to module-level functions above it
    • any-to-interface - Replaces any-typed parameters and properties with interfaces inferred from member accesses
    • type-safety - Adds missing parameter, return and property types (implicit any becomes unknown)
    • dto-object-literal - Rewrites new X() followed by property assignments into a typed object literal
    • interface-duplication - Consolidates interface families into a base interface plus branded type aliases
    • facade-pattern - Extracts complex component logic into a facade service
    • form-modernization - Modernizes reactive form code
    • container-presentational - Turns components with service dependencies into presentational components with inputs and outputs
    • core-shared-modules - Organizes the application into Core and Shared modules
    • inheritance-to-composition - Replaces class inheritance with injected services
    • service-bag-transform - Splits a Service Bag into focused services (rule ID refactor:service-bag-split)

    Each transform rule acts only when its type is requested: transform-code passes transformationType (and the call's options) under the '@angular-modernizer/plugin-angular' key of the transform config.


    # Start MCP server
    pnpm run mcp:start

    # Transform constructor injection
    transform-code --filePath src/app/user.component.ts --transformation constructor-to-inject

    # Extract interfaces
    transform-code --filePath src/app/data.models.ts --transformation interface-extraction

    # Migrate DI patterns
    transform-code --filePath src/app/legacy.service.ts --transformation dependency-injection-migration

    # Convert subscriptions to takeUntilDestroyed
    transform-code --filePath src/app/user.component.ts --transformation subscription-transform

    # Parallelize sequential awaits
    transform-code --filePath src/app/data.service.ts --transformation sequential-await-to-forkjoin
    import { Kernel } from '@angular-modernizer/core';
    import { createPublicApi } from '@angular-modernizer/api';
    import { AngularPlugin } from '@angular-modernizer/plugin-angular';
    import { ContextFactory } from '@angular-modernizer/plugin-system';

    const plugin = new AngularPlugin();
    const kernel = new Kernel({ plugins: [plugin] });

    await kernel.initialize();

    // Get specific transformation rule
    const constructorToInjectRule = plugin
    .getTransformRules()
    .find((rule) => rule.id === 'angular:constructor-to-inject')!;

    // Transform a file; the rule acts only when its transformation type is set
    const project = kernel.getProject();
    const sourceFile = project.addSourceFileAtPath('user.component.ts');
    const context = ContextFactory.createTransformContext({
    sourceFile,
    project,
    api: createPublicApi(project),
    config: {
    '@angular-modernizer/plugin-angular': {
    transformationType: 'constructor-to-inject',
    },
    },
    });

    const result = await constructorToInjectRule.transform(context);

    if (result.modified) {
    await sourceFile.save();
    }

    # Run all plugin tests (analysis + transformation)
    pnpm test

    # Run transformation rule tests specifically
    pnpm test transform-rules/

    Tested on production Angular codebases:

    Metric Analysis Transformation
    Files Processed 2,753 500+
    Execution Time ~3.2s ~1.5s
    Memory Usage ~185 MB ~95 MB

    MIT

    Classes

    AngularPlugin
    AnyToInterfaceAnalysisRule
    AnyToInterfaceOrchestrator
    AnyToInterfaceTransformRule
    BundleSizeOptimizationRule
    ComponentWrapperInstantiationRule
    DataHandlingStrategyRule
    DtoDirectInstantiationRule
    DtoObjectLiteralOrchestrator
    DtoObjectLiteralTransformRule
    InheritanceToCompositionRule
    InterfaceDuplicationAnalysisRule
    InterfaceDuplicationTransformOrchestrator
    InterfaceDuplicationTransformRule
    LibraryExtractionOrchestrator
    LibraryExtractionTransformRule
    MagicStringSelectorRule
    MissingOnPushRule
    MissingOutputTransformOrchestrator
    MissingOutputTransformRule
    PresentationalComponentViolationRule
    PromiseCleanupTransformOrchestrator
    PromiseCleanupTransformRule
    PromiseComponentTransformOrchestrator
    PromiseComponentTransformRule
    PromiseObservableAnalysisRule
    PromiseServiceTransformOrchestrator
    PromiseServiceTransformRule
    RxJSOptimizationRule
    SequentialAwaitTransformOrchestrator
    SequentialAwaitTransformRule
    ServiceBagTransformOrchestrator
    ServiceBagTransformRule
    StaticClassAnalysisRule
    StaticClassTransformOrchestrator
    StaticClassTransformRule
    StaticExtractTransformOrchestrator
    StaticExtractTransformRule
    SubscriptionTransformOrchestrator
    SubscriptionTransformRule
    TypeSafetyAnalysisRule
    TypeSafetyTransformOrchestrator
    TypeSafetyTransformRule
    UntypedDynamicComponentRule

    Interfaces

    BundleSizeOptimizationConfig
    ComponentWrapperInstantiationConfig
    ComponentWrapperInstantiationMetadata
    DataHandlingStrategyConfig
    DialogDetectionConfig
    DtoDetectionConfig
    DtoDirectInstantiationConfig
    DtoDirectInstantiationMetadata
    DtoTransformResult
    LibraryExtractionOptions
    MagicStringSelectorConfig
    MagicStringSelectorMetadata
    PromiseServiceTransformOptions
    RxJSOptimizationConfig
    StaticClassMetadata
    StaticClassTransformResult
    StaticExtractResult
    UntypedDynamicComponentConfig
    UntypedDynamicComponentMetadata