Skip to content

Commit f82bdc4

Browse files
crisbetothePunderWoman
authored andcommitted
fix(migrations): don't delete classes that may provide dependencies transitively (#48866)
Fixes that we would incorrectly remove a module that imports another module which has providers. This is a follow-up from the following discussion: #48832 (comment) PR Close #48866
1 parent c865b8b commit f82bdc4

File tree

2 files changed

+138
-10
lines changed

2 files changed

+138
-10
lines changed

packages/core/schematics/ng-generate/standalone-migration/prune-modules.ts

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@
99
import {NgtscProgram} from '@angular/compiler-cli';
1010
import ts from 'typescript';
1111

12-
import {getAngularDecorators} from '../../utils/ng_decorators';
12+
import {getAngularDecorators, NgDecorator} from '../../utils/ng_decorators';
1313
import {closestNode} from '../../utils/typescript/nodes';
1414

15-
import {ChangeTracker, createLanguageService, findLiteralProperty, getNodeLookup, offsetsToNodes, UniqueItemTracker} from './util';
15+
import {ChangeTracker, createLanguageService, findClassDeclaration, findLiteralProperty, getNodeLookup, offsetsToNodes, UniqueItemTracker} from './util';
1616

1717
/** Mapping between a file name and spans for node references inside of it. */
1818
type ReferencesByFile = Map<string, [start: number, end: number][]>;
@@ -199,9 +199,8 @@ function removeExportReferences(
199199
* @param typeChecker
200200
*/
201201
function canRemoveClass(node: ts.ClassDeclaration, typeChecker: ts.TypeChecker): boolean {
202-
const decorator = getAngularDecorators(typeChecker, ts.getDecorators(node) || [])
203-
.find(decorator => decorator.name === 'NgModule')
204-
?.node;
202+
const decorator = findNgModuleDecorator(node, typeChecker)?.node;
203+
205204
// We can't remove a declaration if it's not a valid `NgModule`.
206205
if (!decorator || !ts.isCallExpression(decorator.expression)) {
207206
return false;
@@ -227,11 +226,25 @@ function canRemoveClass(node: ts.ClassDeclaration, typeChecker: ts.TypeChecker):
227226
const literal = decorator.expression.arguments[0] as ts.ObjectLiteralExpression;
228227
const imports = findLiteralProperty(literal, 'imports');
229228

230-
// We can't remove classes where the `imports` contain something different
231-
// from an identifier, because it may be a `ModuleWithProviders`.
232-
if (imports && isNonEmptyNgModuleProperty(imports) &&
233-
imports.initializer.elements.some(el => !ts.isIdentifier(el))) {
234-
return false;
229+
if (imports && isNonEmptyNgModuleProperty(imports)) {
230+
// We can't remove the class if at least one import isn't identifier, because it may be a
231+
// `ModuleWithProviders` which is the equivalent of having something in the `providers` array.
232+
for (const dep of imports.initializer.elements) {
233+
if (!ts.isIdentifier(dep)) {
234+
return false;
235+
}
236+
237+
const depDeclaration = findClassDeclaration(dep, typeChecker);
238+
const depNgModule =
239+
depDeclaration ? findNgModuleDecorator(depDeclaration, typeChecker) : null;
240+
241+
// If any of the dependencies of the class is an `NgModule` that can't be removed, the class
242+
// itself can't be removed either, because it may be part of a transitive dependency chain.
243+
if (depDeclaration !== null && depNgModule !== null &&
244+
!canRemoveClass(depDeclaration, typeChecker)) {
245+
return false;
246+
}
247+
}
235248
}
236249

237250
// We can't remove classes that have any `declarations`, `providers` or `bootstrap` elements.
@@ -364,3 +377,10 @@ function addRemovalTodos(nodes: Set<ts.Node>, tracker: ChangeTracker) {
364377
` /* TODO(standalone-migration): clean up removed NgModule reference manually. */ `);
365378
}
366379
}
380+
381+
/** Finds the `NgModule` decorator in a class, if it exists. */
382+
function findNgModuleDecorator(node: ts.ClassDeclaration, typeChecker: ts.TypeChecker): NgDecorator|
383+
null {
384+
const decorators = getAngularDecorators(typeChecker, ts.getDecorators(node) || []);
385+
return decorators.find(decorator => decorator.name === 'NgModule') || null;
386+
}

packages/core/schematics/test/standalone_migration_spec.ts

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1413,6 +1413,114 @@ describe('standalone migration', () => {
14131413
`));
14141414
});
14151415

1416+
it('should not remove a chain of modules if a module in the chain cannot be removed because it has providers',
1417+
async () => {
1418+
const moduleAContent = `
1419+
import {NgModule, InjectionToken} from '@angular/core';
1420+
1421+
export const token = new InjectionToken<any>('token');
1422+
1423+
@NgModule({providers: [{provide: token, useValue: 123}]})
1424+
export class ModuleA {}
1425+
`;
1426+
1427+
const moduleBContent = `
1428+
import {NgModule} from '@angular/core';
1429+
import {ModuleA} from './a.module';
1430+
1431+
@NgModule({imports: [ModuleA]})
1432+
export class ModuleB {}
1433+
`;
1434+
1435+
const moduleCContent = `
1436+
import {NgModule} from '@angular/core';
1437+
import {ModuleB} from './b.module';
1438+
1439+
@NgModule({imports: [ModuleB]})
1440+
export class ModuleC {}
1441+
`;
1442+
1443+
const appModuleContent = `
1444+
import {NgModule} from '@angular/core';
1445+
import {MyDir} from './dir';
1446+
import {ModuleC} from './c.module';
1447+
1448+
@NgModule({imports: [ModuleC], declarations: [MyDir]})
1449+
export class AppModule {}
1450+
`;
1451+
1452+
writeFile('a.module.ts', moduleAContent);
1453+
writeFile('b.module.ts', moduleBContent);
1454+
writeFile('c.module.ts', moduleCContent);
1455+
writeFile('app.module.ts', appModuleContent);
1456+
writeFile('dir.ts', `
1457+
import {Directive} from '@angular/core';
1458+
1459+
@Directive({selector: '[myDir]'})
1460+
export class MyDir {}
1461+
`);
1462+
1463+
await runMigration('prune-ng-modules');
1464+
1465+
expect(tree.readContent('a.module.ts')).toBe(moduleAContent);
1466+
expect(tree.readContent('b.module.ts')).toBe(moduleBContent);
1467+
expect(tree.readContent('c.module.ts')).toBe(moduleCContent);
1468+
expect(tree.readContent('app.module.ts')).toBe(appModuleContent);
1469+
});
1470+
1471+
it('should not remove a chain of modules if a module in the chain cannot be removed because it is importing a ModuleWithProviders',
1472+
async () => {
1473+
const moduleAContent = `
1474+
import {NgModule} from '@angular/core';
1475+
1476+
@NgModule({imports: [RouterModule.forRoot([{path: '/foo'}])]})
1477+
export class ModuleA {}
1478+
`;
1479+
1480+
const moduleBContent = `
1481+
import {NgModule} from '@angular/core';
1482+
import {ModuleA} from './a.module';
1483+
1484+
@NgModule({imports: [ModuleA]})
1485+
export class ModuleB {}
1486+
`;
1487+
1488+
const moduleCContent = `
1489+
import {NgModule} from '@angular/core';
1490+
import {ModuleB} from './b.module';
1491+
1492+
@NgModule({imports: [ModuleB]})
1493+
export class ModuleC {}
1494+
`;
1495+
1496+
const appModuleContent = `
1497+
import {NgModule} from '@angular/core';
1498+
import {MyDir} from './dir';
1499+
import {ModuleC} from './c.module';
1500+
1501+
@NgModule({imports: [ModuleC], declarations: [MyDir]})
1502+
export class AppModule {}
1503+
`;
1504+
1505+
writeFile('a.module.ts', moduleAContent);
1506+
writeFile('b.module.ts', moduleBContent);
1507+
writeFile('c.module.ts', moduleCContent);
1508+
writeFile('app.module.ts', appModuleContent);
1509+
writeFile('dir.ts', `
1510+
import {Directive} from '@angular/core';
1511+
1512+
@Directive({selector: '[myDir]'})
1513+
export class MyDir {}
1514+
`);
1515+
1516+
await runMigration('prune-ng-modules');
1517+
1518+
expect(tree.readContent('a.module.ts')).toBe(moduleAContent);
1519+
expect(tree.readContent('b.module.ts')).toBe(moduleBContent);
1520+
expect(tree.readContent('c.module.ts')).toBe(moduleCContent);
1521+
expect(tree.readContent('app.module.ts')).toBe(appModuleContent);
1522+
});
1523+
14161524
it('should not remove the module file if it contains other exported code', async () => {
14171525
writeFile('app.module.ts', `
14181526
import {NgModule} from '@angular/core';

0 commit comments

Comments
 (0)