├── .prettierignore ├── bin └── cli.js ├── lib ├── findUsages │ ├── index.ts │ ├── hasUsagesByPipeName.ts │ ├── hasUsagesBySelectors.ts │ ├── getPropertyFromDecorator.ts │ ├── templateService.ts │ ├── findUnusedClasses.ts │ └── hasUsagesInTs.ts ├── constants.ts ├── types.ts ├── cli.ts ├── createProject.ts ├── index.ts └── output.ts ├── .gitignore ├── .lintstagedrc.json ├── commitlint.config.cjs ├── .npmignore ├── .husky ├── commit-msg ├── pre-commit └── prepare-commit-msg ├── .prettierrc ├── tsconfig.json ├── .eslintrc.json ├── package.json └── README.md /.prettierignore: -------------------------------------------------------------------------------- 1 | **/*.js 2 | .husky 3 | .* -------------------------------------------------------------------------------- /bin/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import '../dist/index.js'; 3 | -------------------------------------------------------------------------------- /lib/findUsages/index.ts: -------------------------------------------------------------------------------- 1 | export * from './findUnusedClasses.js'; 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | !bin/*.js 3 | node_modules 4 | dist 5 | .idea 6 | -------------------------------------------------------------------------------- /.lintstagedrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "*": ["prettier --write"], 3 | "*.ts": ["eslint --fix"] 4 | } 5 | -------------------------------------------------------------------------------- /commitlint.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ['@commitlint/config-conventional'] }; 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !dist 3 | .* 4 | tsconfig.json 5 | commitlint.config.cjs 6 | README.md 7 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx commitlint --edit $1 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | npx tsc --noEmit 6 | -------------------------------------------------------------------------------- /.husky/prepare-commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | exec < /dev/tty && npx -y cz --hook || true -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "endOfLine": "lf", 4 | "bracketSpacing": true, 5 | "tabWidth": 2, 6 | "useTabs": true, 7 | "singleQuote": true 8 | } 9 | -------------------------------------------------------------------------------- /lib/constants.ts: -------------------------------------------------------------------------------- 1 | import { ClassType } from './types'; 2 | 3 | export const RELEVANT_DECORATOR_NAMES: (ClassType | string)[] = [ 4 | 'Component', 5 | 'Injectable', 6 | 'Pipe', 7 | 'Directive', 8 | ]; 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES6", 4 | "module": "ES6", 5 | "strict": true, 6 | "types": ["node"], 7 | "lib": ["ES2019"], 8 | "esModuleInterop": true, 9 | "moduleResolution": "node", 10 | "resolveJsonModule": true, 11 | "skipLibCheck": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "outDir": "dist" 14 | }, 15 | "include": ["lib/**/*.ts"] 16 | } 17 | -------------------------------------------------------------------------------- /lib/findUsages/hasUsagesByPipeName.ts: -------------------------------------------------------------------------------- 1 | import { Decorator } from 'ts-morph'; 2 | import { getPropertyFromDecoratorCall } from './getPropertyFromDecorator.js'; 3 | import { TemplateService } from './templateService.js'; 4 | 5 | export function hasUsagesByPipeName( 6 | decorator: Decorator, 7 | templateService: TemplateService 8 | ): boolean { 9 | const name = getPropertyFromDecoratorCall(decorator, 'name'); 10 | return templateService.matchesPipeName(name ?? ''); 11 | } 12 | -------------------------------------------------------------------------------- /lib/findUsages/hasUsagesBySelectors.ts: -------------------------------------------------------------------------------- 1 | import { Decorator } from 'ts-morph'; 2 | import { getPropertyFromDecoratorCall } from './getPropertyFromDecorator.js'; 3 | import { TemplateService } from './templateService.js'; 4 | 5 | export function hasUsagesBySelectors( 6 | decorator: Decorator, 7 | templateService: TemplateService 8 | ): boolean { 9 | const selector = getPropertyFromDecoratorCall(decorator, 'selector'); 10 | return templateService.matchesSelectors(selector?.split(',') ?? []); 11 | } 12 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier"], 7 | "parser": "@typescript-eslint/parser", 8 | "parserOptions": { 9 | "ecmaVersion": "latest", 10 | "sourceType": "module" 11 | }, 12 | "plugins": ["@typescript-eslint"], 13 | "rules": { 14 | "indent": ["error", "tab"], 15 | "linebreak-style": ["error", "unix"], 16 | "quotes": ["error", "single"], 17 | "semi": ["error", "always"] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/types.ts: -------------------------------------------------------------------------------- 1 | import { ParsedArgs } from 'minimist'; 2 | 3 | export interface RuntimeConfig { 4 | sourceRoots: string[]; 5 | tsConfigFilePath: string; 6 | decorateOutput: boolean; 7 | // decorate cli 8 | // ci -mode , whatever 9 | } 10 | 11 | export type ClassType = 'Component' | 'Injectable' | 'Pipe' | 'Directive'; 12 | 13 | export interface Result { 14 | fileName: string; 15 | directory: string; 16 | className: string; 17 | classType: ClassType; 18 | } 19 | 20 | export interface CliArgs extends ParsedArgs { 21 | help?: boolean; 22 | project?: string; 23 | sourceRoots?: string[]; 24 | decorateOutput?: string; 25 | } 26 | -------------------------------------------------------------------------------- /lib/cli.ts: -------------------------------------------------------------------------------- 1 | import { existsSync } from 'fs'; 2 | import { stdout } from 'process'; 3 | import { CliArgs, RuntimeConfig } from './types'; 4 | 5 | interface InputValidation { 6 | tsConfigFilePath: boolean; 7 | sourceRoots: boolean; 8 | valid: boolean; 9 | } 10 | 11 | export function getRuntimeConfig(cliArgs: CliArgs): RuntimeConfig { 12 | return { 13 | tsConfigFilePath: cliArgs.project || '', 14 | sourceRoots: cliArgs._ || [], 15 | decorateOutput: stdout.isTTY, 16 | }; 17 | } 18 | 19 | export function validate(cliArgs: CliArgs): InputValidation { 20 | return { 21 | tsConfigFilePath: !!cliArgs.project && existsSync(cliArgs.project), 22 | sourceRoots: !!cliArgs._.length, 23 | valid: !!cliArgs.project && !!cliArgs._.length, 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /lib/findUsages/getPropertyFromDecorator.ts: -------------------------------------------------------------------------------- 1 | import { Decorator, ObjectLiteralExpression, SyntaxKind } from 'ts-morph'; 2 | 3 | export function getPropertyFromDecoratorCall( 4 | decorator: Decorator, 5 | propertyName: 'selector' | 'name' | 'template' | 'templateUrl' 6 | ) { 7 | const decoratorCallArguments = decorator.getArguments(); 8 | const matchedProperty = decoratorCallArguments 9 | .flatMap(argument => 10 | (argument as ObjectLiteralExpression) 11 | .getProperties() 12 | .map(prop => prop.asKind(SyntaxKind.PropertyAssignment)) 13 | .filter(value => value !== undefined) 14 | ) 15 | .find(structure => structure!.getName() === propertyName); 16 | 17 | return ( 18 | matchedProperty 19 | ?.getInitializerIfKind(SyntaxKind.StringLiteral) 20 | ?.getLiteralValue() || matchedProperty?.getText() 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /lib/createProject.ts: -------------------------------------------------------------------------------- 1 | import { existsSync } from 'fs'; 2 | import { globSync } from 'glob'; 3 | import path from 'path'; 4 | import { Project } from 'ts-morph'; 5 | import { RuntimeConfig } from './types'; 6 | 7 | export function createProject({ 8 | tsConfigFilePath, 9 | sourceRoots, 10 | }: RuntimeConfig): Project { 11 | const project: Project = new Project({ 12 | tsConfigFilePath, 13 | skipAddingFilesFromTsConfig: true, 14 | skipFileDependencyResolution: true, 15 | skipLoadingLibFiles: true, 16 | }); 17 | project.addSourceFilesAtPaths(getSourceFiles(sourceRoots)); 18 | return project; 19 | } 20 | 21 | function getSourceFiles(sourceRoots: string[]): string[] { 22 | const existingDirectories = sourceRoots.filter(directory => 23 | existsSync(directory) 24 | ); 25 | const includePatterns = existingDirectories.map( 26 | directory => `${path.resolve(directory)}/**/*.ts` 27 | ); 28 | const excludePatterns = existingDirectories.flatMap(directory => [ 29 | `${directory}/**/node_modules/**`, 30 | `${directory}/**/*.spec.ts`, 31 | `${directory}/**/*.d.ts`, 32 | ]); 33 | 34 | return globSync(includePatterns, { 35 | ignore: [...excludePatterns, 'node_modules/**'], 36 | }); 37 | } 38 | -------------------------------------------------------------------------------- /lib/index.ts: -------------------------------------------------------------------------------- 1 | import minimist from 'minimist'; 2 | import { argv, exit } from 'process'; 3 | import { getRuntimeConfig, validate } from './cli.js'; 4 | import { createProject } from './createProject.js'; 5 | import { findUnusedClasses } from './findUsages/index.js'; 6 | import { 7 | help, 8 | invalidTsConfig, 9 | printNoFiles, 10 | printResults, 11 | usage, 12 | } from './output.js'; 13 | import { CliArgs } from './types.js'; 14 | 15 | const cliArgs: CliArgs = minimist(argv.slice(2), { 16 | alias: { 17 | decorateOutput: 'd', 18 | project: 'p', 19 | help: 'h', 20 | } as Record, 21 | }); 22 | 23 | if (cliArgs.help || cliArgs.h) { 24 | help(); 25 | exit(0); 26 | } 27 | 28 | const inputValidation = validate(cliArgs); 29 | if (!inputValidation.tsConfigFilePath) { 30 | invalidTsConfig(); 31 | exit(2); 32 | } 33 | if (!inputValidation.valid) { 34 | usage(); 35 | exit(2); 36 | } 37 | const { tsConfigFilePath, sourceRoots, decorateOutput } = 38 | getRuntimeConfig(cliArgs); 39 | 40 | const project = createProject({ 41 | tsConfigFilePath, 42 | sourceRoots, 43 | decorateOutput, 44 | }); 45 | const sourceFiles = project.getSourceFiles(); 46 | 47 | if (sourceFiles.length === 0) { 48 | printNoFiles(); 49 | exit(0); 50 | } 51 | const results = findUnusedClasses(sourceFiles); 52 | 53 | printResults(results); 54 | exit(results.length ? 1 : 0); 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngx-unused", 3 | "version": "1.3.0", 4 | "description": "Detect unused classes (components, pipes, services and directives) defined in Angular workspace.", 5 | "bugs": { 6 | "url": "https://github.com/wgrabowski/ngx-unused/issues" 7 | }, 8 | "main": "./bin/cli.js", 9 | "bin": "./bin/cli.js", 10 | "type": "module", 11 | "keywords": [ 12 | "angular", 13 | "ngx", 14 | "unused code", 15 | "unused angular classes", 16 | "unused angular component", 17 | "unused angular pipe", 18 | "unused angular directive", 19 | "unused angular service" 20 | ], 21 | "dependencies": { 22 | "minimist": "^1.2.8", 23 | "node-html-parser": "^6.1.11", 24 | "ts-morph": "^21.0.1", 25 | "typescript": "^4.3.4", 26 | "glob": "^10.3.10" 27 | }, 28 | "devDependencies": { 29 | "@commitlint/cli": "^17.7.2", 30 | "@commitlint/config-conventional": "^17.7.0", 31 | "@types/node": "^15.12.5", 32 | "@typescript-eslint/eslint-plugin": "^6.7.5", 33 | "@typescript-eslint/parser": "^6.7.5", 34 | "commitizen": "^4.3.0", 35 | "cz-conventional-changelog": "^3.3.0", 36 | "eslint": "^8.51.0", 37 | "eslint-config-prettier": "^9.0.0", 38 | "husky": "^8.0.0", 39 | "lint-staged": "^15.2.9", 40 | "prettier": "^2.8.8", 41 | "prettier-plugin-organize-imports": "^2.2.0" 42 | }, 43 | "scripts": { 44 | "start": "tsc -p tsconfig.json --watch --removeComments", 45 | "prepareOnly": "tsc -p tsconfig.json --removeComments", 46 | "format": "prettier --write .", 47 | "prepare": "husky install" 48 | }, 49 | "author": "Wojciech Grabowski ", 50 | "repository": { 51 | "type": "git", 52 | "url": "https://github.com/wgrabowski/ngx-unused.git" 53 | }, 54 | "config": { 55 | "commitizen": { 56 | "path": "./node_modules/cz-conventional-changelog" 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /lib/findUsages/templateService.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, readFileSync } from 'fs'; 2 | import { parse } from 'node-html-parser'; 3 | import { dirname, join, resolve } from 'path'; 4 | import { Decorator } from 'ts-morph'; 5 | import { getPropertyFromDecoratorCall } from './getPropertyFromDecorator.js'; 6 | 7 | export class TemplateService { 8 | private templates: string[]; 9 | 10 | constructor(componentDecorators: Decorator[]) { 11 | this.templates = componentDecorators.map(decorator => 12 | this.getTemplateHtmlFromDecorator(decorator) 13 | ); 14 | } 15 | 16 | matchesSelectors(selectors: string[]) { 17 | return this.templates.some(template => 18 | this.templateMatchesSelectors(template, selectors) 19 | ); 20 | } 21 | 22 | private templateMatchesSelectors( 23 | template: string, 24 | selectors: string[] 25 | ): boolean { 26 | const root = parse(template); 27 | 28 | // directive selector can be used as input or attribute 29 | const normalizeSelectors = selectors.flatMap(selector => { 30 | const withParensEscaped = selector 31 | .replace(/^\[/, '[\\[') 32 | .replace(/]$/, '\\]]') 33 | .trim(); 34 | 35 | return [withParensEscaped, selector.trim()]; 36 | }); 37 | 38 | return normalizeSelectors.some(selector => { 39 | return root.querySelectorAll(selector).length > 0; 40 | }); 41 | } 42 | 43 | private getTemplateHtmlFromDecorator(decorator: Decorator): string { 44 | const templateString = getPropertyFromDecoratorCall(decorator, 'template'); 45 | if (templateString) { 46 | return templateString; 47 | } 48 | const templateUrl = getPropertyFromDecoratorCall(decorator, 'templateUrl'); 49 | if (templateUrl) { 50 | return this.getTemplateFromFile( 51 | decorator.getSourceFile().getFilePath(), 52 | templateUrl 53 | ); 54 | } 55 | return ''; 56 | } 57 | 58 | private getTemplateFromFile( 59 | decoratorSourceFilePath: string, 60 | templateUrl: string 61 | ): string { 62 | const templateFilePath = resolve( 63 | join(dirname(decoratorSourceFilePath), templateUrl) 64 | ); 65 | return existsSync(templateFilePath) 66 | ? readFileSync(templateFilePath, { encoding: 'utf-8' }) 67 | : ''; 68 | } 69 | 70 | matchesPipeName(name: string) { 71 | const pipeSelectorRegexp = new RegExp(`\\|\\s*${name}\\b`, 'gm'); 72 | return this.templates.some(template => pipeSelectorRegexp.test(template)); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /lib/output.ts: -------------------------------------------------------------------------------- 1 | import { relative } from 'path'; 2 | import { cwd, stdout } from 'process'; 3 | import { Result } from './types'; 4 | 5 | export const CLI_DECORATOR = '[ngx-unused]'; 6 | 7 | export function usage() { 8 | stdout.write('ngx-unused -p \n'); 9 | stdout.write('Invoke ngx-unused --help for more details\n'); 10 | } 11 | 12 | export function help() { 13 | stdout.write(`ngx-unused - find unused classes in Angular codebase 14 | 15 | 16 | \rUsage: ngx-unused [-p | --project] 17 | 18 | \r - directory to be scanned 19 | \r to scan multiple directories pass names separated by space 20 | \r (usages of classes from source roots will be also searched in source roots) 21 | 22 | \r - main tsconfig file 23 | \r should be one containing @paths definitions 24 | \r for NX projects its usually tsconfig.base.json\n 25 | 26 | Options: 27 | \r-p | --project - tsconfig file path (required) 28 | \r-h | --help - print this help 29 | Source root directories and tsconfig file must be under the same root directory. 30 | 31 | Examples: 32 | ngx-unused . -p tsconfig.base.json 33 | ngx-unused libs apps/my-app -p tsconfig.base.json 34 | `); 35 | } 36 | 37 | export function invalidTsConfig() { 38 | stdout.write("Provided tsconfig file doesn't exists\n"); //eslint-disable-line 39 | } 40 | 41 | export function printNoFiles() { 42 | stdout.write('No relevant .ts files found in provided source root(s)\n'); 43 | } 44 | 45 | export function print(content: string, progress: boolean = false) { 46 | if (stdout.isTTY) { 47 | if (progress) { 48 | stdout.write('\r'); 49 | } 50 | stdout.write(`${CLI_DECORATOR} `); 51 | } 52 | 53 | stdout.write(content); 54 | } 55 | 56 | export function printResults(results: Result[]) { 57 | if (results.length === 0) { 58 | print('No unused Angular classes found\n'); 59 | return; 60 | } 61 | 62 | let output = ''; 63 | const groupedResults = results.reduce((grouped, result) => { 64 | const groupName = `${relative(cwd(), result.directory)}/${result.fileName}`; 65 | if (grouped[groupName] !== undefined) { 66 | grouped[groupName]?.push(result); 67 | } else { 68 | grouped[groupName] = [result]; 69 | } 70 | return grouped; 71 | }, {} as Record); 72 | 73 | Object.entries(groupedResults).map(group => { 74 | output += group[0]; 75 | output += '\n'; 76 | output += 77 | group[1]?.map(result => `- ${result.className}\n`).join('') + '\n'; 78 | }); 79 | output += '\n\n'; 80 | stdout.write('\n'); 81 | print(`${results.length} (probably) unused classes\n`); 82 | stdout.write(output); 83 | } 84 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ngx-unused 2 | 3 | Find declared but unused Angular classes in your codebase. 4 | 5 | This tool recognizes components, directives, pipes and services in Angular code and checks if they are used in provided 6 | project. 7 | 8 | # Usage 9 | 10 | Simplest way to use is via npx: 11 | 12 | `npx ngx-unused -p ` 13 | 14 | ```shell 15 | ngx-unused - find unused classes in Angular codebase 16 | 17 | 18 | Usage: ngx-unused [-p | --project] 19 | 20 | - directory to be scanned 21 | to scan multiple directories pass names separated by space 22 | (usages of classes from source roots will be also searched in source roots) 23 | 24 | - main tsconfig file 25 | should be one containing @paths definitions 26 | for NX projects its usually tsconfig.base.json 27 | 28 | 29 | Options: 30 | -p | --project - tsconfig file path (required) 31 | -h | --help - print this help 32 | Source root directories and tsconfig file must be under the same root directory. 33 | 34 | Examples: 35 | ngx-unused . -p tsconfig.base.json 36 | ngx-unused libs apps/my-app -p tsconfig.base.json 37 | ``` 38 | 39 | # How does it work? 40 | 41 | Code from provided source root directory (or directories) is analyzed to find [relevant classes](#relevant-classes). 42 | Relevant class is class with on of following Angular decorators: `@Component`,`@Directory`,`@Pipe`,`@Injectable`. 43 | Each class is checked for [relevant usages](#relevant-usages) in codebase. When it has no relevant usages it is 44 | considered unused. 45 | 46 | ## Relevant classes 47 | 48 | Class decorated with one of Angular decorators 49 | 50 | - `@Component` 51 | - `@Directive` 52 | - `@Pipe` 53 | - `@Injectable` 54 | 55 | Classes declared in [ignored files](#ignored-files) will be ignored. 56 | 57 | ## Relevant usages 58 | 59 | Relevant usage is any usage that is not one of following: 60 | 61 | - import 62 | - export 63 | - usage in `@NgModule` decorator (in `imports`,`exports`, `declarations`, `providers` properties) 64 | - with exception for `useClass` and `useExisting` in provider object 65 | - usage in any of [ignored files](#ignored-files) 66 | 67 | ## Ignored files 68 | 69 | Files matching `*.spec.ts` glob will be ignored. 70 | Future version may have option to configure that. 71 | 72 | # Output 73 | 74 | Output is printed to standard output. If `process.stdout.isTTY` is false no decorative texts and no progress will be 75 | printed, so it can be safely piped. 76 | 77 | ## Output formatting 78 | 79 | Output contains progress information and formatted results. 80 | Formatted results is a list of unused classes, grouped by files. 81 | 82 | ## Exit codes 83 | 84 | `0` No unused classes detected. 85 | 86 | `1` Detected unused component, directive, pipe, or service. 87 | 88 | `2`: Invalid configuration 89 | -------------------------------------------------------------------------------- /lib/findUsages/findUnusedClasses.ts: -------------------------------------------------------------------------------- 1 | import { stdout } from 'process'; 2 | import { ClassDeclaration, Decorator, SourceFile } from 'ts-morph'; 3 | import { RELEVANT_DECORATOR_NAMES } from '../constants.js'; 4 | import { print } from '../output.js'; 5 | import { ClassType, Result } from '../types'; 6 | import { hasUsagesByPipeName } from './hasUsagesByPipeName.js'; 7 | import { hasUsagesBySelectors } from './hasUsagesBySelectors.js'; 8 | import { hasUsagesInTs } from './hasUsagesInTs.js'; 9 | import { TemplateService } from './templateService.js'; 10 | 11 | export function findUnusedClasses(sourceFiles: SourceFile[]): Result[] { 12 | const classes = sourceFiles 13 | .filter(file => !file.getBaseName().includes('.spec.ts')) 14 | .flatMap(file => file.getClasses()) 15 | .filter(declaration => getRelevantDecorator(declaration) !== undefined); 16 | 17 | const componentClasses = classes 18 | .filter( 19 | declaration => 20 | getRelevantDecorator(declaration)?.getFullName() === 'Component' 21 | ) 22 | .map(getRelevantDecorator) 23 | .filter(decorator => decorator !== undefined); 24 | const templateService = new TemplateService(componentClasses as Decorator[]); 25 | 26 | if (stdout.isTTY) { 27 | print( 28 | `Found ${classes.length} classes from ${sourceFiles.length} files.\n` 29 | ); 30 | } 31 | return classes 32 | .filter((declaration, index, { length }) => { 33 | const percentage = Math.round(((index + 1) / length) * 100); 34 | if (stdout.isTTY) { 35 | print(`Analyzing ${index + 1}/${length} (${percentage}%)`, true); 36 | if (index === length - 1) stdout.write('\n'); 37 | } 38 | return !isUsed(declaration, templateService); 39 | }) 40 | .map(asResult); 41 | } 42 | 43 | function isUsed( 44 | declaration: ClassDeclaration, 45 | templateService: TemplateService 46 | ): boolean { 47 | const relevantDecorator = getRelevantDecorator(declaration)!; 48 | const classType = relevantDecorator.getFullName(); 49 | const hasTsUsages = hasUsagesInTs(declaration); 50 | if (hasTsUsages) { 51 | return true; 52 | } 53 | 54 | if (classType === 'Component' || classType === 'Directive') { 55 | return hasUsagesBySelectors(relevantDecorator, templateService); 56 | } 57 | if (classType === 'Pipe') { 58 | return hasUsagesByPipeName(relevantDecorator, templateService); 59 | } 60 | 61 | return false; 62 | } 63 | 64 | function asResult(declaration: ClassDeclaration): Result { 65 | const sourceFile = declaration.getSourceFile(); 66 | return { 67 | // classess without decorators will not be passed to this function 68 | classType: getRelevantDecorator(declaration)!.getFullName() as ClassType, 69 | className: declaration.getName() ?? '', 70 | fileName: sourceFile.getBaseName(), 71 | directory: sourceFile.getDirectory().getPath().toString(), 72 | }; 73 | } 74 | 75 | function getRelevantDecorator(classDeclaration: ClassDeclaration) { 76 | return classDeclaration.getDecorator(decorator => 77 | RELEVANT_DECORATOR_NAMES.includes(decorator.getFullName() as ClassType) 78 | ); 79 | } 80 | -------------------------------------------------------------------------------- /lib/findUsages/hasUsagesInTs.ts: -------------------------------------------------------------------------------- 1 | // if class is used in ts - excluding imports, exports, and declarations in NgModule decorator 2 | // with exception useClass,useExisting in providers 3 | import { ClassDeclaration, Node, SourceFile, SyntaxKind } from 'ts-morph'; 4 | 5 | export function hasUsagesInTs(declaration: ClassDeclaration): boolean { 6 | const referencingNodes = declaration.findReferencesAsNodes().filter(node => { 7 | const sourceFile = node.getSourceFile(); 8 | return !isFileIrrelevant(sourceFile); 9 | }); 10 | const isDynamicallyImported = 11 | isClassReferencedInDynamicImportCallback(declaration); 12 | 13 | return ( 14 | referencingNodes.some(node => isReferecingNodeRelevant(node)) || 15 | isDynamicallyImported 16 | ); 17 | } 18 | 19 | function isFileIrrelevant(sourceFile: SourceFile): boolean { 20 | return ( 21 | sourceFile.isDeclarationFile() || 22 | sourceFile.getBaseName().includes('.spec.ts') 23 | ); 24 | } 25 | function isDynamicImport(node: Node): boolean { 26 | return ( 27 | node.isKind(SyntaxKind.CallExpression) && 28 | !!node.getFirstChildIfKind(SyntaxKind.ImportKeyword) 29 | ); 30 | } 31 | 32 | // assuming dynamic imports looks like this 33 | // import("../file/path.ts").then(x=>x.ClassName) 34 | function isDynamicImportReferencingClass( 35 | node: Node, 36 | declaration: ClassDeclaration 37 | ): boolean { 38 | const importCallback = node 39 | .getFirstAncestorByKind(SyntaxKind.PropertyAccessExpression) 40 | ?.getFirstAncestorByKind(SyntaxKind.ArrowFunction) 41 | ?.getFirstDescendantByKind(SyntaxKind.ArrowFunction); 42 | 43 | return !!importCallback 44 | ?.getDescendants() 45 | .some(descendant => descendant.getFullText() === declaration.getName()); 46 | } 47 | 48 | function isClassReferencedInDynamicImportCallback( 49 | declaration: ClassDeclaration 50 | ): boolean { 51 | return declaration 52 | .getSourceFile() 53 | .getReferencingNodesInOtherSourceFiles() 54 | .filter(reference => !isFileIrrelevant(reference.getSourceFile())) 55 | .filter(isDynamicImport) 56 | .some(node => isDynamicImportReferencingClass(node, declaration)); 57 | } 58 | function isReferecingNodeRelevant(node: Node): boolean { 59 | const irrelevantNodeKinds = [ 60 | SyntaxKind.ImportSpecifier, 61 | SyntaxKind.ExportSpecifier, 62 | ]; 63 | const irrelevantParentNodeKinds = [ 64 | SyntaxKind.ExportAssignment, 65 | SyntaxKind.ClassDeclaration, 66 | SyntaxKind.ImportSpecifier, 67 | SyntaxKind.ExportSpecifier, 68 | ]; 69 | const isOfIrrelevantKind = irrelevantNodeKinds.includes(node.getKind()); 70 | const hasParentOfIrrelevantKind = 71 | node.getParent() !== undefined && 72 | irrelevantParentNodeKinds.includes(node.getParent()!.getKind()); 73 | 74 | return ( 75 | (!isOfIrrelevantKind && 76 | !hasParentOfIrrelevantKind && 77 | !isInNgModuleDecoratorCall(node)) || 78 | node.getParent()!.isKind(SyntaxKind.PropertyAssignment) 79 | ); 80 | } 81 | 82 | function isInNgModuleDecoratorCall(node: Node): boolean { 83 | if (node.getParent() === undefined) { 84 | return false; 85 | } 86 | // when used with useClass, useExisting it is relevant as it will not be injected by its own name 87 | if (node.getParent()!.isKind(SyntaxKind.PropertyAssignment)) { 88 | return false; 89 | } 90 | // bootstrap 91 | if ( 92 | node.getFirstAncestor( 93 | ancestor => 94 | ancestor.isKind(SyntaxKind.PropertyAssignment) && 95 | ancestor.getName() === 'bootstrap' 96 | ) !== undefined 97 | ) { 98 | return false; 99 | } 100 | 101 | return ( 102 | node.getFirstAncestor( 103 | ancestor => 104 | ancestor.isKind(SyntaxKind.Decorator) && 105 | ancestor.getFullName() === 'NgModule' 106 | ) !== undefined 107 | ); 108 | } 109 | --------------------------------------------------------------------------------