# @angular-modernizer/plugin-angular

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

## Overview

This plugin provides 30 analysis rules and multiple transformation types 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)

## Installation

```bash
pnpm add @angular-modernizer/plugin-angular
```

## Analysis Rules

### RxJSOptimizationRule

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

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

#### Detected Patterns

##### Multiple Chained Pipe Calls

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

**Before:**

```typescript
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:**

```typescript
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

---

##### Missing shareReplay for Expensive Operations

HTTP calls or computed observables subscribed multiple times without caching.

**Before:**

```typescript
// 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:**

```typescript
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

---

##### Redundant Sequential Operators

Consecutive identical operators that should be merged.

**Before:**

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

**After:**

```typescript
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

---

##### Missing Debounce on User Input

FormControl `valueChanges` without `debounceTime()` or `throttleTime()`.

**Before:**

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

**After:**

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

**Impact:** High | **Refactoring Complexity:** Low

---

##### Logic in Subscribe Blocks

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

**Before:**

```typescript
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:**

```typescript
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

---

##### Subscribe in Subscribe (Nested Subscriptions)

Nested observable subscriptions without flattening operators.

**Before:**

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

**After:**

```typescript
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

---

##### Silent HTTP catchError

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

---

##### Incorrect Subject Usage for State

Using `Subject` instead of `BehaviorSubject` for state management.

**Before:**

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

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

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

**After:**

```typescript
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

---

#### Configuration

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

---

### PerformanceViolationRule

**Rule ID:** `angular:performance-pattern`
**Severity:** Warning/Error

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

**Missing takeUntil Before:**

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

**Missing takeUntil After:**

```typescript
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();
  }
}
```

---

### MissingOnPushRule

**Rule ID:** `angular:missing-onpush`
**Severity:** High

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

**Before:**

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

**After:**

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

---

### ServiceWithoutInjectableRule

**Rule ID:** `angular:service-without-injectable`
**Severity:** High

Detects services missing `@Injectable()` decorator.

---

### ComponentWithoutSelectorRule

**Rule ID:** `angular:component-without-selector`
**Severity:** Medium

Detects components missing selector property.

---

### DirectiveWithoutSelectorRule

**Rule ID:** `angular:directive-without-selector`
**Severity:** Medium

Detects directives missing selector property.

---

### PipeWithoutNameRule

**Rule ID:** `angular:pipe-without-name`
**Severity:** Medium

Detects pipes missing name property.

---

### AsyncPipeMisuseRule

**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

---

### UnnecessaryChangeDetectionRule

**Rule ID:** `angular:unnecessary-change-detection`
**Severity:** Low

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

---

### MissingReturnTypeRule

**Rule ID:** `angular:missing-return-type`
**Severity:** Low

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

**Before:**

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

**After:**

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

---

### TooManyInputsRule

**Rule ID:** `angular:too-many-inputs`
**Severity:** Low

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

**Configuration:**

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

---

### LifecycleHookRule

**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:**

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

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

**After:**

```typescript
@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();
  }
}
```

---

### TemplateComplexityRule

**Rule ID:** `angular:template-complexity`
**Severity:** Medium

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:**

```typescript
@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:**

```typescript
@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;
  }
}
```

---

### ImmutableInputViolationRule

**Rule ID:** `angular:immutable-input-violation`
**Severity:** High

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

**Before:**

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

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

**After:**

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

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

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

---

### MissingTrackByRule

**Rule ID:** `angular:missing-trackby`
**Severity:** Medium

Detects ngFor directives without trackBy functions.

**Before:**

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

**After:**

```typescript
@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;
  }
}
```

---

### PresentationalComponentViolationRule

**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.

---

### ServiceMutableStateRule

**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`.

---

### PromiseVsObservableRule

**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

---

### ComponentWrapperInstantiationRule

**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`.

---

### DtoDirectInstantiationRule

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

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

---

### MagicStringSelectorRule

**Rule ID:** `angular:magic-string-selector`
**Severity:** Warning

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

---

## Transformation Rules

### ConstructorToInjectRule

**Transformation Type:** `constructor-to-inject`

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

**Before:**

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

**After:**

```typescript
@Component({...})
export class UserComponent {
  private userService = inject(UserService);
  private router = inject(Router);
  private 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

---

### InterfaceExtractionRule

**Transformation Type:** `interface-extraction`

Extracts inline type literals into named interfaces.

**Before:**

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

**After:**

```typescript
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

---

### DependencyInjectionMigrationRule

**Transformation Type:** `dependency-injection-migration`

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

**Before:**

```typescript
// 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:**

```typescript
// 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 }]);
```

---

### ConstructorInjectionTransformRule

**Transformation Type:** `constructor-injection-transform`

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

**Before:**

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

  private httpClient: HttpClient;
  private userService: UserService;
}
```

**After:**

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

**Configuration:**

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

---

### ComponentInputsInterfaceExtractionRule

**Transformation Type:** `component-inputs-interface-extraction`

Extracts component input properties into dedicated interfaces.

**Before:**

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

**After:**

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

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

**Configuration:**

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

---

### ServiceInjectionCleanupRule

**Transformation Type:** `service-injection-cleanup`

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

**Before:**

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

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

**After:**

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

**Configuration:**

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

---

### Additional Transformation Types

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-transform` - Converts static utility classes to standalone functions

---

## Usage

### With MCP Tools

```bash
# Start MCP server
pnpm run start:mcp

# 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
```

### With Direct API

```typescript
import { Kernel } from '@angular-modernizer/core';
import { AngularPlugin } from '@angular-modernizer/plugin-angular';
import { ContextFactory } from '@angular-modernizer/plugin-system';

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

await kernel.initialize();

const plugin = kernel.getPlugin('@angular-modernizer/plugin-angular');
const transformRules = plugin.getTransformRules();

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

// Transform a file
const sourceFile = kernel.getProject().addSourceFileAtPath('user.component.ts');
const context = ContextFactory.createTransformContext({
  sourceFile,
  project: kernel.getProject(),
  api: createPublicApi(),
  config: {},
});

const result = await constructorToInjectRule.transform(context);

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

---

## Testing

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

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

# Run with coverage
pnpm test:coverage
```

---

## Performance

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 |

---

## Contributing

See [DEVELOPMENT_GUIDE.md](../../DEVELOPMENT_GUIDE.md) for architecture details and [TESTING_GUIDE.md](../../TESTING_GUIDE.md) for testing conventions.

---

## License

MIT
