Skip to content

Commit d298d25

Browse files
crisbetothePunderWoman
authored andcommitted
feat(migrations): add schematic to clean up unused imports (#59353)
In v19 we added a warning about unused standalone imports, however we didn't do anything about existing code which means that users have to clean it up manually. These changes add the `ng g @angular/core:cleanup-unused-imports` schematic which will remove the unused dependencies automatically. There isn't any new detection code since all the manipulations are based on the produced diagnostics, but there's a bit of code to remove the import declarations from the file as well. Fixes #58849. PR Close #59353
1 parent d6ca669 commit d298d25

File tree

9 files changed

+763
-1
lines changed

9 files changed

+763
-1
lines changed

packages/core/schematics/BUILD.bazel

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
load("//tools:defaults.bzl", "pkg_npm")
21
load("@npm//@bazel/rollup:index.bzl", "rollup_bundle")
2+
load("//tools:defaults.bzl", "pkg_npm")
33

44
exports_files([
55
"tsconfig.json",
@@ -13,6 +13,7 @@ pkg_npm(
1313
"collection.json",
1414
"migrations.json",
1515
"package.json",
16+
"//packages/core/schematics/ng-generate/cleanup-unused-imports:static_files",
1617
"//packages/core/schematics/ng-generate/control-flow-migration:static_files",
1718
"//packages/core/schematics/ng-generate/inject-migration:static_files",
1819
"//packages/core/schematics/ng-generate/output-migration:static_files",
@@ -37,6 +38,7 @@ rollup_bundle(
3738
"//packages/core/schematics/ng-generate/inject-migration:index.ts": "inject-migration",
3839
"//packages/core/schematics/ng-generate/route-lazy-loading:index.ts": "route-lazy-loading",
3940
"//packages/core/schematics/ng-generate/standalone-migration:index.ts": "standalone-migration",
41+
"//packages/core/schematics/ng-generate/cleanup-unused-imports:index.ts": "cleanup-unused-imports",
4042
"//packages/core/schematics/ng-generate/signals:index.ts": "signals",
4143
"//packages/core/schematics/ng-generate/signal-input-migration:index.ts": "signal-input-migration",
4244
"//packages/core/schematics/ng-generate/signal-queries-migration:index.ts": "signal-queries-migration",
@@ -56,6 +58,7 @@ rollup_bundle(
5658
"//packages/core/schematics/migrations/explicit-standalone-flag",
5759
"//packages/core/schematics/migrations/pending-tasks",
5860
"//packages/core/schematics/migrations/provide-initializer",
61+
"//packages/core/schematics/ng-generate/cleanup-unused-imports",
5962
"//packages/core/schematics/ng-generate/control-flow-migration",
6063
"//packages/core/schematics/ng-generate/inject-migration",
6164
"//packages/core/schematics/ng-generate/output-migration",

packages/core/schematics/collection.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@
4646
"description": "Combines all signals-related migrations into a single migration",
4747
"factory": "./bundles/signals#migrate",
4848
"schema": "./ng-generate/signals/schema.json"
49+
},
50+
"cleanup-unused-imports": {
51+
"description": "Removes unused imports from standalone components.",
52+
"factory": "./bundles/cleanup-unused-imports#migrate",
53+
"schema": "./ng-generate/cleanup-unused-imports/schema.json"
4954
}
5055
}
5156
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
load("//tools:defaults.bzl", "ts_library")
2+
3+
package(
4+
default_visibility = [
5+
"//packages/core/schematics:__pkg__",
6+
"//packages/core/schematics/migrations/google3:__pkg__",
7+
"//packages/core/schematics/test:__pkg__",
8+
],
9+
)
10+
11+
filegroup(
12+
name = "static_files",
13+
srcs = ["schema.json"],
14+
)
15+
16+
ts_library(
17+
name = "cleanup-unused-imports",
18+
srcs = glob(["**/*.ts"]),
19+
tsconfig = "//packages/core/schematics:tsconfig.json",
20+
deps = [
21+
"//packages/compiler-cli",
22+
"//packages/compiler-cli/private",
23+
"//packages/compiler-cli/src/ngtsc/core:api",
24+
"//packages/compiler-cli/src/ngtsc/file_system",
25+
"//packages/core/schematics/utils",
26+
"//packages/core/schematics/utils/tsurge",
27+
"//packages/core/schematics/utils/tsurge/helpers/angular_devkit",
28+
"@npm//@angular-devkit/schematics",
29+
"@npm//@types/node",
30+
"@npm//typescript",
31+
],
32+
)
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
## Cleanup unused imports migration
2+
Automated migration that removes all unused standalone imports across the entire project. It can be
3+
run using:
4+
5+
```bash
6+
ng generate @angular/core:cleanup-unused-imports
7+
```
8+
9+
**Before:**
10+
```typescript
11+
import { Component } from '@angular/core';
12+
import { UnusedDirective } from './unused';
13+
14+
@Component({
15+
template: 'Hello',
16+
imports: [UnusedDirective],
17+
})
18+
export class MyComp {}
19+
```
20+
21+
**After:**
22+
```typescript
23+
import { Component } from '@angular/core';
24+
25+
@Component({
26+
template: 'Hello',
27+
imports: [],
28+
})
29+
export class MyComp {}
30+
```
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import {Rule, SchematicsException} from '@angular-devkit/schematics';
10+
11+
import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths';
12+
import {DevkitMigrationFilesystem} from '../../utils/tsurge/helpers/angular_devkit/devkit_filesystem';
13+
import {groupReplacementsByFile} from '../../utils/tsurge/helpers/group_replacements';
14+
import {setFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system';
15+
import {ProjectRootRelativePath, TextUpdate} from '../../utils/tsurge';
16+
import {synchronouslyCombineUnitData} from '../../utils/tsurge/helpers/combine_units';
17+
import {CompilationUnitData, UnusedImportsMigration} from './unused_imports_migration';
18+
19+
export function migrate(): Rule {
20+
return async (tree, context) => {
21+
const {buildPaths, testPaths} = await getProjectTsConfigPaths(tree);
22+
23+
if (!buildPaths.length && !testPaths.length) {
24+
throw new SchematicsException(
25+
'Could not find any tsconfig file. Cannot clean up unused imports.',
26+
);
27+
}
28+
29+
const fs = new DevkitMigrationFilesystem(tree);
30+
setFileSystem(fs);
31+
32+
const migration = new UnusedImportsMigration();
33+
const unitResults: CompilationUnitData[] = [];
34+
const programInfos = [...buildPaths, ...testPaths].map((tsconfigPath) => {
35+
context.logger.info(`Preparing analysis for ${tsconfigPath}`);
36+
37+
const baseInfo = migration.createProgram(tsconfigPath, fs);
38+
const info = migration.prepareProgram(baseInfo);
39+
40+
return {info, tsconfigPath};
41+
});
42+
43+
for (const {info, tsconfigPath} of programInfos) {
44+
context.logger.info(`Scanning for unused imports using ${tsconfigPath}`);
45+
unitResults.push(await migration.analyze(info));
46+
}
47+
48+
const combined = await synchronouslyCombineUnitData(migration, unitResults);
49+
if (combined === null) {
50+
context.logger.error('Schematic failed unexpectedly with no analysis data');
51+
return;
52+
}
53+
54+
const globalMeta = await migration.globalMeta(combined);
55+
const replacementsPerFile: Map<ProjectRootRelativePath, TextUpdate[]> = new Map();
56+
const {replacements} = await migration.migrate(globalMeta);
57+
const changesPerFile = groupReplacementsByFile(replacements);
58+
59+
for (const [file, changes] of changesPerFile) {
60+
if (!replacementsPerFile.has(file)) {
61+
replacementsPerFile.set(file, changes);
62+
}
63+
}
64+
65+
for (const [file, changes] of replacementsPerFile) {
66+
const recorder = tree.beginUpdate(file);
67+
for (const c of changes) {
68+
recorder
69+
.remove(c.data.position, c.data.end - c.data.position)
70+
.insertLeft(c.data.position, c.data.toInsert);
71+
}
72+
tree.commitUpdate(recorder);
73+
}
74+
75+
const {
76+
counters: {removedImports, changedFiles},
77+
} = await migration.stats(globalMeta);
78+
let statsMessage: string;
79+
80+
if (removedImports === 0) {
81+
statsMessage = 'Schematic could not find unused imports in the project';
82+
} else {
83+
statsMessage =
84+
`Removed ${removedImports} import${removedImports !== 1 ? 's' : ''} ` +
85+
`in ${changedFiles} file${changedFiles !== 1 ? 's' : ''}`;
86+
}
87+
88+
context.logger.info('');
89+
context.logger.info(statsMessage);
90+
};
91+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema",
3+
"$id": "AngularCleanupUnusedImportsMigration",
4+
"title": "Angular Cleanup Unused Imports Schema",
5+
"type": "object",
6+
"properties": {}
7+
}

0 commit comments

Comments
 (0)