Skip to content

Commit 7396021

Browse files
crisbetoatscott
authored andcommitted
fix(core): avoid duplicating comments in TestBed teardown migration (#43776)
Currently the TestBed teardown migration is set up in a similar way to all other migrations where we take a `CallExpression`, add a parameter to it, print it, and replace the existing call. The problem is that doing so while preserving the `expression` of the original `CallExpression` can cause comments to be duplicated. This can happen quite frequently, because by default the CLI generates comments before `initTestModule` calls. To work around it, these changes make the migration more precise by inserting a new parameter or replacing and existing one using string manipulation. This requires a bit more code, but it's more reliable than the following alternatives: 1. Using `getFullStart` and `getFullWidth` to replace the node. This would work with our current setup, but the problem is that `getFullStart` also includes whitespace and newlines before the leading comment. This can cause us to mess up the user's formatting and figuring out which whitespace to keep and which one to remove is tricky. 2. Recreating the `CallExpression.expression` when constructing the new node. This would also work since it'll drop any existing comments, but the problem is that `CallExpression.expression` can be a wide variety of nodes which we would have to account for. We can't use `getMutableClone`, because it preserves the comments. Fixes #43739. PR Close #43776
1 parent 6b90452 commit 7396021

File tree

5 files changed

+117
-41
lines changed

5 files changed

+117
-41
lines changed

packages/core/schematics/migrations/google3/testbedTeardownRule.ts

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import {Replacement, RuleFailure, Rules} from 'tslint';
1010
import ts from 'typescript';
11-
import {findInitTestEnvironmentCalls, findTestModuleMetadataNodes, InitTestEnvironmentAnalysis, migrateInitTestEnvironment, migrateTestModuleMetadataLiteral} from '../testbed-teardown/util';
11+
import {findInitTestEnvironmentCalls, findTestModuleMetadataNodes, getInitTestEnvironmentLiteralReplacement, InitTestEnvironmentAnalysis, migrateTestModuleMetadataLiteral} from '../testbed-teardown/util';
1212

1313
/** TSLint rule that adds the `teardown` flag to `TestBed` calls. */
1414
export class Rule extends Rules.TypedRule {
@@ -42,31 +42,31 @@ export class Rule extends Rules.TypedRule {
4242
if (initTestEnvironmentResult.totalCalls > 0) {
4343
// Migrate all of the unmigrated calls `initTestEnvironment` in this file. This could be zero
4444
// if the user has already opted into the new teardown behavior themselves.
45-
initTestEnvironmentResult.callsToMigrate.forEach(call => {
45+
initTestEnvironmentResult.callsToMigrate.forEach(node => {
4646
// This analysis is global so we need to check that the call is within this file.
47-
if (call.getSourceFile() === sourceFile) {
48-
failures.push(this._getFailure(call, migrateInitTestEnvironment, printer));
47+
if (node.getSourceFile() === sourceFile) {
48+
const {span, text} = getInitTestEnvironmentLiteralReplacement(node, printer);
49+
50+
failures.push(new RuleFailure(
51+
sourceFile, span.start, span.end, 'Teardown behavior has to be configured.',
52+
this.ruleName, new Replacement(span.start, span.length, text)));
4953
}
5054
});
5155
} else {
5256
// Otherwise migrate the metadata passed into the `configureTestingModule` and `withModule`
5357
// calls. This scenario is less likely, but it could happen if `initTestEnvironment` has been
5458
// abstracted away or is inside a .js file.
55-
findTestModuleMetadataNodes(typeChecker, sourceFile).forEach(literal => {
56-
failures.push(this._getFailure(literal, migrateTestModuleMetadataLiteral, printer));
59+
findTestModuleMetadataNodes(typeChecker, sourceFile).forEach(node => {
60+
const sourceFile = node.getSourceFile();
61+
const migrated = migrateTestModuleMetadataLiteral(node);
62+
const replacementText = printer.printNode(ts.EmitHint.Unspecified, migrated, sourceFile);
63+
64+
failures.push(new RuleFailure(
65+
sourceFile, node.getStart(), node.getEnd(), 'Teardown behavior has to be configured.',
66+
this.ruleName, new Replacement(node.getStart(), node.getWidth(), replacementText)));
5767
});
5868
}
5969

6070
return failures;
6171
}
62-
63-
private _getFailure<T extends ts.Node>(node: T, migrator: (node: T) => T, printer: ts.Printer) {
64-
const sourceFile = node.getSourceFile();
65-
const migrated = migrator(node);
66-
const replacementText = printer.printNode(ts.EmitHint.Unspecified, migrated, sourceFile);
67-
68-
return new RuleFailure(
69-
sourceFile, node.getStart(), node.getEnd(), 'Teardown behavior has to be configured.',
70-
this.ruleName, new Replacement(node.getStart(), node.getWidth(), replacementText));
71-
}
7272
}

packages/core/schematics/migrations/testbed-teardown/index.ts

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import ts from 'typescript';
1212

1313
import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths';
1414
import {canMigrateFile, createMigrationProgram} from '../../utils/typescript/compiler_host';
15-
import {findInitTestEnvironmentCalls, findTestModuleMetadataNodes, migrateInitTestEnvironment, migrateTestModuleMetadataLiteral} from './util';
15+
import {findInitTestEnvironmentCalls, findTestModuleMetadataNodes, getInitTestEnvironmentLiteralReplacement, migrateTestModuleMetadataLiteral} from './util';
1616

1717

1818
/** Migration that adds the `teardown` flag to `TestBed` calls. */
@@ -48,28 +48,30 @@ function runTestbedTeardownMigration(tree: Tree, tsconfigPath: string, basePath:
4848
if (initTestEnvironmentResult.totalCalls > 0) {
4949
// Migrate all of the unmigrated calls `initTestEnvironment`. This could be zero
5050
// if the user has already opted into the new teardown behavior themselves.
51-
initTestEnvironmentResult.callsToMigrate.forEach(call => {
52-
migrate(call, migrateInitTestEnvironment, tree, basePath, printer);
51+
initTestEnvironmentResult.callsToMigrate.forEach(node => {
52+
const {span, text} = getInitTestEnvironmentLiteralReplacement(node, printer);
53+
const update = tree.beginUpdate(relative(basePath, node.getSourceFile().fileName));
54+
// The update appears to break if we try to call `remove` with a zero length.
55+
if (span.length > 0) {
56+
update.remove(span.start, span.length);
57+
}
58+
update.insertRight(span.start, text);
59+
tree.commitUpdate(update);
5360
});
5461
} else {
5562
// Otherwise migrate the metadata passed into the `configureTestingModule` and `withModule`
5663
// calls. This scenario is less likely, but it could happen if `initTestEnvironment` has been
5764
// abstracted away or is inside a .js file.
5865
sourceFiles.forEach(sourceFile => {
59-
findTestModuleMetadataNodes(typeChecker, sourceFile).forEach(literal => {
60-
migrate(literal, migrateTestModuleMetadataLiteral, tree, basePath, printer);
66+
findTestModuleMetadataNodes(typeChecker, sourceFile).forEach(node => {
67+
const migrated = migrateTestModuleMetadataLiteral(node);
68+
const update = tree.beginUpdate(relative(basePath, node.getSourceFile().fileName));
69+
update.remove(node.getStart(), node.getWidth());
70+
update.insertRight(
71+
node.getStart(),
72+
printer.printNode(ts.EmitHint.Unspecified, migrated, node.getSourceFile()));
73+
tree.commitUpdate(update);
6174
});
6275
});
6376
}
6477
}
65-
66-
67-
function migrate<T extends ts.Node>(
68-
node: T, migrator: (node: T) => T, tree: Tree, basePath: string, printer: ts.Printer) {
69-
const migrated = migrator(node);
70-
const update = tree.beginUpdate(relative(basePath, node.getSourceFile().fileName));
71-
update.remove(node.getStart(), node.getWidth());
72-
update.insertRight(
73-
node.getStart(), printer.printNode(ts.EmitHint.Unspecified, migrated, node.getSourceFile()));
74-
tree.commitUpdate(update);
75-
}

packages/core/schematics/migrations/testbed-teardown/util.ts

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -82,26 +82,45 @@ export function findTestModuleMetadataNodes(
8282
return sortInReverseSourceOrder(Array.from(testModuleMetadataLiterals));
8383
}
8484

85-
/** Migrates a call to `TestBed.initTestEnvironment`. */
86-
export function migrateInitTestEnvironment(node: ts.CallExpression): ts.CallExpression {
85+
/**
86+
* Gets data that can be used to migrate a call to `TestBed.initTestEnvironment`.
87+
* The returned `span` is used to mark the text that should be replaced while the `text`
88+
* is the code that should be inserted instead.
89+
*/
90+
export function getInitTestEnvironmentLiteralReplacement(
91+
node: ts.CallExpression, printer: ts.Printer) {
8792
const literalProperties: ts.ObjectLiteralElementLike[] = [];
93+
const lastArg = node.arguments[node.arguments.length - 1];
94+
let span: {start: number, end: number, length: number};
95+
let prefix: string;
8896

8997
if (node.arguments.length > 2) {
90-
if (isFunction(node.arguments[2])) {
98+
if (isFunction(lastArg)) {
9199
// If the last argument is a function, add the function as the `aotSummaries` property.
92-
literalProperties.push(ts.createPropertyAssignment('aotSummaries', node.arguments[2]));
93-
} else if (ts.isObjectLiteralExpression(node.arguments[2])) {
100+
literalProperties.push(ts.createPropertyAssignment('aotSummaries', lastArg));
101+
} else if (ts.isObjectLiteralExpression(lastArg)) {
94102
// If the property is an object literal, copy over all the properties.
95-
literalProperties.push(...node.arguments[2].properties);
103+
literalProperties.push(...lastArg.properties);
96104
}
105+
106+
prefix = '';
107+
span = {start: lastArg.getStart(), end: lastArg.getEnd(), length: lastArg.getWidth()};
108+
} else {
109+
const start = lastArg.getEnd();
110+
prefix = ', ';
111+
span = {start, end: start, length: 0};
97112
}
98113

99114
// Finally push the teardown object so that it appears last.
100115
literalProperties.push(createTeardownAssignment());
101116

102-
return ts.createCall(
103-
node.expression, node.typeArguments,
104-
[...node.arguments.slice(0, 2), ts.createObjectLiteral(literalProperties, true)]);
117+
return {
118+
span,
119+
text: prefix +
120+
printer.printNode(
121+
ts.EmitHint.Unspecified, ts.createObjectLiteral(literalProperties, true),
122+
node.getSourceFile())
123+
};
105124
}
106125

107126
/** Migrates an object literal that is passed into `configureTestingModule` or `withModule`. */

packages/core/schematics/test/google3/testbed_teardown_spec.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -704,4 +704,32 @@ describe('Google3 TestBed teardown TSLint rule', () => {
704704
}));
705705
`));
706706
});
707+
708+
it('should not duplicate comments on initTestEnvironment calls', () => {
709+
writeFile('/index.ts', `
710+
import { TestBed } from '@angular/core/testing';
711+
import {
712+
BrowserDynamicTestingModule,
713+
platformBrowserDynamicTesting
714+
} from '@angular/platform-browser-dynamic/testing';
715+
716+
// Hello
717+
TestBed.initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting());
718+
`);
719+
720+
runTSLint(true);
721+
722+
expect(stripWhitespace(getFile('/index.ts'))).toContain(stripWhitespace(`
723+
import { TestBed } from '@angular/core/testing';
724+
import {
725+
BrowserDynamicTestingModule,
726+
platformBrowserDynamicTesting
727+
} from '@angular/platform-browser-dynamic/testing';
728+
729+
// Hello
730+
TestBed.initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting(), {
731+
teardown: { destroyAfterEach: false }
732+
});
733+
`));
734+
});
707735
});

packages/core/schematics/test/testbed_teardown_spec.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -649,6 +649,33 @@ describe('TestBed teardown migration', () => {
649649
`));
650650
});
651651

652+
it('should not duplicate comments on initTestEnvironment calls', async () => {
653+
writeFile('/index.ts', `
654+
import { TestBed } from '@angular/core/testing';
655+
import {
656+
BrowserDynamicTestingModule,
657+
platformBrowserDynamicTesting
658+
} from '@angular/platform-browser-dynamic/testing';
659+
660+
// Hello
661+
TestBed.initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting());
662+
`);
663+
664+
await runMigration();
665+
666+
expect(stripWhitespace(tree.readContent('/index.ts'))).toContain(stripWhitespace(`
667+
import { TestBed } from '@angular/core/testing';
668+
import {
669+
BrowserDynamicTestingModule,
670+
platformBrowserDynamicTesting
671+
} from '@angular/platform-browser-dynamic/testing';
672+
673+
// Hello
674+
TestBed.initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting(), {
675+
teardown: { destroyAfterEach: false }
676+
});
677+
`));
678+
});
652679

653680
function writeFile(filePath: string, contents: string) {
654681
host.sync.write(normalize(filePath), virtualFs.stringToFileBuffer(contents));

0 commit comments

Comments
 (0)