Hone logo
Hone
Problems

Angular Unused Code Detector

Imagine you're working on a large Angular project. Over time, code accumulates, and it's easy for components, services, pipes, directives, and other elements to become unused but still clutter the codebase. This leads to longer build times, increased maintenance overhead, and potential confusion for developers. Your task is to create a tool that can identify these unused Angular code artifacts within a given project structure.

Problem Description

You need to develop a TypeScript-based script that analyzes an Angular project's source files and identifies components, services, pipes, and directives that are not being referenced or imported elsewhere in the project.

Key Requirements:

  1. File Scanning: The script should be able to scan a specified directory (representing the Angular project root) for TypeScript files (.ts).
  2. Angular Artifact Identification: It needs to identify Angular decorated classes (using @Component, @Injectable, @Pipe, @Directive).
  3. Usage Analysis: For each identified Angular artifact, the script must determine if it's:
    • Imported: Is it imported by another TypeScript file?
    • Used in Templates: For components and directives, are they used in any HTML templates (.html) within the project? (Consider simple attribute selectors for directives and tag selectors for components).
  4. Output: The script should output a list of unused artifacts, categorized by type (e.g., "Unused Components," "Unused Services").

Expected Behavior:

The script should accept a project directory path as an argument. It will then traverse the directory, parse the relevant files, and generate a report listing all identified Angular artifacts that are not being used.

Edge Cases:

  • Private Classes: Ignore classes that are not exported.
  • Modules: For simplicity, focus on individual components, services, pipes, and directives rather than module-level usage.
  • Dynamic Imports/Usage: Assume direct imports and template usage. Dynamic imports or reflection-based usage detection is out of scope for this challenge.
  • Third-Party Libraries: The analysis should be limited to the provided project directory and not extend to node_modules.
  • Shared/Library Code: The script should flag code as unused even if it's intended for a library that might be used elsewhere, as long as it's not referenced within the analyzed project.

Examples

Example 1:

Input Directory Structure and Contents:

my-angular-app/
├── src/
│   ├── app/
│   │   ├── app.component.ts
│   │   ├── app.module.ts
│   │   ├── dashboard/
│   │   │   ├── dashboard.component.ts  <-- This component is never used
│   │   │   ├── dashboard.service.ts
│   │   │   └── dashboard.component.html
│   │   ├── shared/
│   │   │   ├── unused.service.ts    <-- This service is never used
│   │   │   └── common.directive.ts  <-- This directive is never used
│   │   └── lazy-loaded-feature/
│   │       ├── lazy-feature.component.ts
│   │       └── lazy-feature.module.ts
│   └── main.ts
└── package.json

my-angular-app/src/app/app.component.ts:

import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = 'my-app';
}

my-angular-app/src/app/dashboard/dashboard.component.ts:

import { Component } from '@angular/core';

@Component({
  selector: 'app-dashboard',
  templateUrl: './dashboard.component.html',
  styleUrls: ['./dashboard.component.css']
})
export class DashboardComponent {
  // ...
}

my-angular-app/src/app/dashboard/dashboard.component.html:

<div>
  <h2>Dashboard</h2>
</div>

my-angular-app/src/app/shared/unused.service.ts:

import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class UnusedService {
  constructor() { }
  log() { console.log('This service is not used.'); }
}

my-angular-app/src/app/shared/common.directive.ts:

import { Directive } from '@angular/core';

@Directive({
  selector: '[appCommon]'
})
export class CommonDirective {
  constructor() { }
}

my-angular-app/src/app/app.module.ts:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppComponent } from './app.component';
// DashboardComponent is not imported here
// CommonDirective is not imported here
// UnusedService is not imported here (but providedIn: 'root')

@NgModule({
  declarations: [
    AppComponent,
    // DashboardComponent should be declared if used directly here
    // CommonDirective should be declared if used directly here
  ],
  imports: [
    BrowserModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Output:

Unused Components:
- src/app/dashboard/dashboard.component.ts

Unused Services:
- src/app/shared/unused.service.ts

Unused Directives:
- src/app/shared/common.directive.ts

Explanation: DashboardComponent is not imported in app.module.ts or app.component.ts, and its selector app-dashboard is not found in any .html files. UnusedService is not injected anywhere and its method log is not called. CommonDirective is not imported and its selector [appCommon] is not present in any templates.

Example 2:

Input Directory Structure and Contents:

my-angular-app/
├── src/
│   ├── app/
│   │   ├── app.component.ts
│   │   ├── app.module.ts
│   │   ├── greeting.pipe.ts      <-- This pipe is used
│   │   └── user-profile/
│   │       ├── user-profile.component.ts
│   │       └── user-profile.component.html
│   └── main.ts
└── package.json

my-angular-app/src/app/user-profile/user-profile.component.ts:

import { Component } from '@angular/core';

@Component({
  selector: 'app-user-profile',
  templateUrl: './user-profile.component.html',
  styleUrls: ['./user-profile.component.css']
})
export class UserProfileComponent {
  userName: string = 'Alice';
}

my-angular-app/src/app/user-profile/user-profile.component.html:

<div>
  <h2>User Profile</h2>
  <p>Name: {{ userName | greeting }}</p>
</div>

my-angular-app/src/app/greeting.pipe.ts:

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'greeting'
})
export class GreetingPipe implements PipeTransform {
  transform(value: string, ...args: unknown[]): string {
    return `Hello, ${value}!`;
  }
}

my-angular-app/src/app/app.module.ts:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppComponent } from './app.component';
import { UserProfileComponent } from './user-profile/user-profile.component';
import { GreetingPipe } from './greeting.pipe'; // Imported

@NgModule({
  declarations: [
    AppComponent,
    UserProfileComponent,
    GreetingPipe // Declared
  ],
  imports: [
    BrowserModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Output:

(No unused artifacts found in this example)

Explanation: UserProfileComponent is declared in AppModule and its selector app-user-profile is not used, but it's also not imported anywhere. GreetingPipe is imported and declared in AppModule, and its name greeting is used in the user-profile.component.html template.

Constraints

  • The script should run in Node.js.
  • Input is a single string representing the path to the Angular project's root directory.
  • The script should be able to handle a project with up to 1000 TypeScript files and 500 HTML files.
  • Performance: The script should ideally complete its analysis within 30 seconds for the specified file count.
  • Focus only on .ts and .html files within the specified directory and its subdirectories. Ignore node_modules and other build-related directories.

Notes

  • You'll likely need to use Node.js's fs module for file system operations and potentially a parser for TypeScript and HTML.
  • For TypeScript parsing, consider using the built-in ts-morph library or the TypeScript Compiler API.
  • For HTML parsing, libraries like parse5 or htmlparser2 could be useful, though for this challenge, simple regular expressions might suffice for basic tag/attribute matching.
  • Pay close attention to how Angular defines and uses components, directives, services, and pipes.
  • Remember that providedIn: 'root' services don't necessarily need to be explicitly imported or injected to be "used" by Angular's DI system, but they are used if Angular deems them necessary. For this challenge, we'll consider them "used" if they are not explicitly declared/imported and not injected. However, a more robust solution would track their presence in providedIn or providers arrays. For simplicity in this challenge, if a service is not imported/injected, and not in providedIn: 'root' or providers, it's a candidate for being flagged. Correction for this challenge: We will consider @Injectable({ providedIn: 'root' }) services as potentially unused if they are never explicitly injected into a constructor.
  • Components and directives are considered used if their selectors appear in any HTML template.
  • Pipes are considered used if their names appear in any pipe expressions (| pipeName) within HTML templates.
Loading editor...
typescript