Skip to content

Commit 34f0453

Browse files
crisbetopkozlowski-opensource
authored andcommitted
refactor(migrations): add migration for TestBed.get (#60414)
Adds a migration that will move users off the deprecated `TestBed.get` method. PR Close #60414
1 parent 5e209cb commit 34f0453

File tree

7 files changed

+336
-0
lines changed

7 files changed

+336
-0
lines changed

packages/core/schematics/BUILD.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ rollup_bundle(
4646
"//packages/core/schematics/ng-generate/output-migration:index.ts": "output-migration",
4747
"//packages/core/schematics/ng-generate/self-closing-tags-migration:index.ts": "self-closing-tags-migration",
4848
"//packages/core/schematics/migrations/inject-flags:index.ts": "inject-flags",
49+
"//packages/core/schematics/migrations/test-bed-get:index.ts": "test-bed-get",
4950
},
5051
format = "cjs",
5152
link_workspace_root = True,
@@ -56,6 +57,7 @@ rollup_bundle(
5657
],
5758
deps = [
5859
"//packages/core/schematics/migrations/inject-flags",
60+
"//packages/core/schematics/migrations/test-bed-get",
5961
"//packages/core/schematics/ng-generate/cleanup-unused-imports",
6062
"//packages/core/schematics/ng-generate/control-flow-migration",
6163
"//packages/core/schematics/ng-generate/inject-migration",

packages/core/schematics/migrations.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@
44
"version": "20.0.0",
55
"description": "Replaces usages of the deprecated InjectFlags enum",
66
"factory": "./bundles/inject-flags#migrate"
7+
},
8+
"test-bed-get": {
9+
"version": "20.0.0",
10+
"description": "Replaces usages of the deprecated TestBed.get method with TestBed.inject",
11+
"factory": "./bundles/test-bed-get#migrate"
712
}
813
}
914
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
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+
ts_library(
12+
name = "test-bed-get",
13+
srcs = glob(["**/*.ts"]),
14+
tsconfig = "//packages/core/schematics:tsconfig.json",
15+
deps = [
16+
"//packages/compiler-cli/private",
17+
"//packages/compiler-cli/src/ngtsc/file_system",
18+
"//packages/core/schematics/utils",
19+
"//packages/core/schematics/utils/tsurge",
20+
"//packages/core/schematics/utils/tsurge/helpers/angular_devkit",
21+
"@npm//@angular-devkit/schematics",
22+
"@npm//@types/node",
23+
"@npm//typescript",
24+
],
25+
)
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
## Remove `TestBed.get` migration
2+
Replaces the usages of the deprecated `TestBed.get` method with the non-deprecated `TestBed.inject`:
3+
4+
### Before
5+
```typescript
6+
import { TestBed } from '@angular/core/testing';
7+
8+
describe('test', () => {
9+
it('should inject', () => {
10+
console.log(TestBed.get(SOME_TOKEN));
11+
});
12+
});
13+
```
14+
15+
### After
16+
```typescript
17+
import { TestBed } from '@angular/core/testing';
18+
19+
describe('test', () => {
20+
it('should inject', () => {
21+
console.log(TestBed.inject(SOME_TOKEN));
22+
});
23+
});
24+
```
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
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} from '@angular-devkit/schematics';
10+
import {TestBedGetMigration} from './test_bed_get_migration';
11+
import {runMigrationInDevkit} from '../../utils/tsurge/helpers/angular_devkit';
12+
13+
export function migrate(): Rule {
14+
return async (tree) => {
15+
await runMigrationInDevkit({
16+
tree,
17+
getMigration: () => new TestBedGetMigration(),
18+
});
19+
};
20+
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
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 ts from 'typescript';
10+
import {
11+
confirmAsSerializable,
12+
ProgramInfo,
13+
ProjectFile,
14+
projectFile,
15+
Replacement,
16+
Serializable,
17+
TextUpdate,
18+
TsurgeFunnelMigration,
19+
} from '../../utils/tsurge';
20+
import {getImportSpecifier} from '../../utils/typescript/imports';
21+
import {isReferenceToImport} from '../../utils/typescript/symbol';
22+
23+
export interface CompilationUnitData {
24+
locations: Location[];
25+
}
26+
27+
/** Information about the `get` identifier in `TestBed.get`. */
28+
interface Location {
29+
/** File in which the expression is defined. */
30+
file: ProjectFile;
31+
32+
/** Start of the `get` identifier. */
33+
position: number;
34+
}
35+
36+
/** Name of the method being replaced. */
37+
const METHOD_NAME = 'get';
38+
39+
/** Migration that replaces `TestBed.get` usages with `TestBed.inject`. */
40+
export class TestBedGetMigration extends TsurgeFunnelMigration<
41+
CompilationUnitData,
42+
CompilationUnitData
43+
> {
44+
override async analyze(info: ProgramInfo): Promise<Serializable<CompilationUnitData>> {
45+
const locations: Location[] = [];
46+
47+
for (const sourceFile of info.sourceFiles) {
48+
const specifier = getImportSpecifier(sourceFile, '@angular/core/testing', 'TestBed');
49+
50+
if (specifier === null) {
51+
continue;
52+
}
53+
54+
const typeChecker = info.program.getTypeChecker();
55+
sourceFile.forEachChild(function walk(node) {
56+
if (
57+
ts.isPropertyAccessExpression(node) &&
58+
node.name.text === METHOD_NAME &&
59+
ts.isIdentifier(node.expression) &&
60+
isReferenceToImport(typeChecker, node.expression, specifier)
61+
) {
62+
locations.push({file: projectFile(sourceFile, info), position: node.name.getStart()});
63+
} else {
64+
node.forEachChild(walk);
65+
}
66+
});
67+
}
68+
69+
return confirmAsSerializable({locations});
70+
}
71+
72+
override async migrate(globalData: CompilationUnitData) {
73+
const replacements = globalData.locations.map(({file, position}) => {
74+
return new Replacement(
75+
file,
76+
new TextUpdate({
77+
position: position,
78+
end: position + METHOD_NAME.length,
79+
toInsert: 'inject',
80+
}),
81+
);
82+
});
83+
84+
return confirmAsSerializable({replacements});
85+
}
86+
87+
override async combine(
88+
unitA: CompilationUnitData,
89+
unitB: CompilationUnitData,
90+
): Promise<Serializable<CompilationUnitData>> {
91+
const seen = new Set<string>();
92+
const locations: Location[] = [];
93+
const combined = [...unitA.locations, ...unitB.locations];
94+
95+
for (const location of combined) {
96+
const key = `${location.file.id}#${location.position}`;
97+
if (!seen.has(key)) {
98+
seen.add(key);
99+
locations.push(location);
100+
}
101+
}
102+
103+
return confirmAsSerializable({locations});
104+
}
105+
106+
override async globalMeta(
107+
combinedData: CompilationUnitData,
108+
): Promise<Serializable<CompilationUnitData>> {
109+
return confirmAsSerializable(combinedData);
110+
}
111+
112+
override async stats() {
113+
return {counters: {}};
114+
}
115+
}
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
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 {getSystemPath, normalize, virtualFs} from '@angular-devkit/core';
10+
import {TempScopedNodeJsSyncHost} from '@angular-devkit/core/node/testing';
11+
import {HostTree} from '@angular-devkit/schematics';
12+
import {SchematicTestRunner, UnitTestTree} from '@angular-devkit/schematics/testing';
13+
import {runfiles} from '@bazel/runfiles';
14+
import shx from 'shelljs';
15+
16+
describe('test-bed-get migration', () => {
17+
let runner: SchematicTestRunner;
18+
let host: TempScopedNodeJsSyncHost;
19+
let tree: UnitTestTree;
20+
let tmpDirPath: string;
21+
22+
function writeFile(filePath: string, contents: string) {
23+
host.sync.write(normalize(filePath), virtualFs.stringToFileBuffer(contents));
24+
}
25+
26+
function runMigration() {
27+
return runner.runSchematic('test-bed-get', {}, tree);
28+
}
29+
30+
beforeEach(() => {
31+
runner = new SchematicTestRunner('test', runfiles.resolvePackageRelative('../migrations.json'));
32+
host = new TempScopedNodeJsSyncHost();
33+
tree = new UnitTestTree(new HostTree(host));
34+
tmpDirPath = getSystemPath(host.root);
35+
36+
writeFile('/tsconfig.json', '{}');
37+
writeFile(
38+
'/angular.json',
39+
JSON.stringify({
40+
version: 1,
41+
projects: {t: {root: '', architect: {build: {options: {tsConfig: './tsconfig.json'}}}}},
42+
}),
43+
);
44+
45+
writeFile(
46+
'/node_modules/@angular/core/testing/index.d.ts',
47+
`
48+
export declare class TestBed {
49+
static get(token: any): any;
50+
}
51+
`,
52+
);
53+
54+
shx.cd(tmpDirPath);
55+
});
56+
57+
it('should migrate a usage of TestBed.get', async () => {
58+
writeFile(
59+
'/test.ts',
60+
`
61+
import { TestBed } from '@angular/core/testing';
62+
63+
const SOME_TOKEN = {};
64+
65+
describe('test', () => {
66+
it('should inject', () => {
67+
console.log(TestBed.get(SOME_TOKEN, null));
68+
});
69+
});
70+
`,
71+
);
72+
73+
await runMigration();
74+
expect(tree.readContent('/test.ts')).toContain(
75+
'console.log(TestBed.inject(SOME_TOKEN, null));',
76+
);
77+
});
78+
79+
it('should migrate a usage of an aliased TestBed.get', async () => {
80+
writeFile(
81+
'/test.ts',
82+
`
83+
import { TestBed as Alias } from '@angular/core/testing';
84+
85+
const SOME_TOKEN = {};
86+
87+
describe('test', () => {
88+
it('should inject', () => {
89+
console.log(Alias.get(SOME_TOKEN, null));
90+
});
91+
});
92+
`,
93+
);
94+
95+
await runMigration();
96+
expect(tree.readContent('/test.ts')).toContain('console.log(Alias.inject(SOME_TOKEN, null));');
97+
});
98+
99+
it('should migrate a usage of TestBed.get that is not in a call', async () => {
100+
writeFile(
101+
'/test.ts',
102+
`
103+
import { TestBed } from '@angular/core/testing';
104+
105+
export const GET = TestBed.get;
106+
`,
107+
);
108+
109+
await runMigration();
110+
expect(tree.readContent('/test.ts')).toContain('export const GET = TestBed.inject;');
111+
});
112+
113+
it('should handle a file that is present in multiple projects', async () => {
114+
writeFile('/tsconfig-2.json', '{}');
115+
writeFile(
116+
'/angular.json',
117+
JSON.stringify({
118+
version: 1,
119+
projects: {
120+
a: {root: '', architect: {build: {options: {tsConfig: './tsconfig.json'}}}},
121+
b: {root: '', architect: {build: {options: {tsConfig: './tsconfig-2.json'}}}},
122+
},
123+
}),
124+
);
125+
126+
writeFile(
127+
'test.ts',
128+
`
129+
import { TestBed } from '@angular/core/testing';
130+
131+
const SOME_TOKEN = {};
132+
133+
describe('test', () => {
134+
it('should inject', () => {
135+
console.log(TestBed.get(SOME_TOKEN));
136+
});
137+
});
138+
`,
139+
);
140+
141+
await runMigration();
142+
const content = tree.readContent('/test.ts');
143+
expect(content).toContain('console.log(TestBed.inject(SOME_TOKEN));');
144+
});
145+
});

0 commit comments

Comments
 (0)