Skip to content

Commit 504f25c

Browse files
committed
feat(migrations): Add migration for CanMatchFn snapshot parameter
the third partial match snapshot parameter is now required in the types since the Router always providers it
1 parent e9046e9 commit 504f25c

File tree

6 files changed

+294
-0
lines changed

6 files changed

+294
-0
lines changed

packages/core/schematics/BUILD.bazel

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,10 @@ bundle_entrypoints = [
129129
"strict-templates",
130130
"packages/core/schematics/migrations/strict-template/index.js",
131131
],
132+
[
133+
"can-match-snapshot-required",
134+
"packages/core/schematics/migrations/can-match-snapshot-required/index.js",
135+
],
132136
]
133137

134138
rollup.rollup(
@@ -140,6 +144,7 @@ rollup.rollup(
140144
"//:node_modules/magic-string",
141145
"//:node_modules/semver",
142146
"//packages/core/schematics:tsconfig_build",
147+
"//packages/core/schematics/migrations/can-match-snapshot-required",
143148
"//packages/core/schematics/migrations/change-detection-eager",
144149
"//packages/core/schematics/migrations/http-xhr-backend",
145150
"//packages/core/schematics/migrations/strict-template",

packages/core/schematics/migrations.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@
1414
"version": "22.0.0",
1515
"description": "Adds 'strictTemplates: true' in tsconfig.json.",
1616
"factory": "./bundles/strict-templates.cjs#migrate"
17+
},
18+
"can-match-snapshot-required": {
19+
"version": "22.0.0",
20+
"description": "Adds the required third argument to canMatch callsites.",
21+
"factory": "./bundles/can-match-snapshot-required.cjs#migrate"
1722
}
1823
}
1924
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
load("//tools:defaults.bzl", "ts_project", "zoneless_jasmine_test")
2+
3+
package(
4+
default_visibility = [
5+
"//packages/core/schematics:__pkg__",
6+
"//packages/core/schematics/test:__pkg__",
7+
],
8+
)
9+
10+
ts_project(
11+
name = "can-match-snapshot-required",
12+
srcs = glob(
13+
["**/*.ts"],
14+
exclude = ["*.spec.ts"],
15+
),
16+
deps = [
17+
"//:node_modules/@angular-devkit/schematics",
18+
"//:node_modules/@types/node",
19+
"//:node_modules/typescript",
20+
"//packages/compiler-cli",
21+
"//packages/compiler-cli/private",
22+
"//packages/core/schematics/utils",
23+
"//packages/core/schematics/utils/tsurge",
24+
"//packages/core/schematics/utils/tsurge/helpers/angular_devkit",
25+
],
26+
)
27+
28+
ts_project(
29+
name = "test_lib",
30+
testonly = True,
31+
srcs = glob(["*.spec.ts"]),
32+
deps = [
33+
":can-match-snapshot-required",
34+
"//:node_modules/typescript",
35+
"//packages/compiler-cli/private",
36+
"//packages/core/schematics/utils/tsurge",
37+
],
38+
)
39+
40+
zoneless_jasmine_test(
41+
name = "test",
42+
data = [":test_lib"],
43+
env = {"FORCE_COLOR": "3"},
44+
)
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
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 {runMigrationInDevkit} from '../../utils/tsurge/helpers/angular_devkit';
11+
import {CanMatchSnapshotRequiredMigration} from './migration';
12+
13+
interface Options {
14+
path: string;
15+
}
16+
17+
export function migrate(options: Options): Rule {
18+
return async (tree, context) => {
19+
await runMigrationInDevkit({
20+
tree,
21+
getMigration: (fs) => new CanMatchSnapshotRequiredMigration(),
22+
});
23+
};
24+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
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 {absoluteFrom} from '@angular/compiler-cli';
10+
import {initMockFileSystem} from '@angular/compiler-cli/private/testing';
11+
import {runTsurgeMigration} from '../../utils/tsurge/testing';
12+
import {CanMatchSnapshotRequiredMigration} from './migration';
13+
14+
describe('CanMatchSnapshotRequired migration (Type-Based)', () => {
15+
beforeEach(() => {
16+
initMockFileSystem('Native');
17+
});
18+
19+
it('should add the third argument to canMatch calls in classes implementing CanMatch', async () => {
20+
const {fs} = await runTsurgeMigration(new CanMatchSnapshotRequiredMigration(), [
21+
{
22+
name: absoluteFrom('/index.ts'),
23+
isProgramRootFile: true,
24+
contents: `
25+
interface CanMatch {}
26+
class MyGuard implements CanMatch {
27+
canMatch(route: any, segments: any) {}
28+
}
29+
const guard = new MyGuard();
30+
guard.canMatch({}, []);
31+
`,
32+
},
33+
]);
34+
35+
const content = fs.readFile(absoluteFrom('/index.ts'));
36+
expect(content).toContain('guard.canMatch({}, [], {} as any /* added by migration */)');
37+
});
38+
39+
it('should NOT add argument if the class does NOT implement CanMatch', async () => {
40+
const {fs} = await runTsurgeMigration(new CanMatchSnapshotRequiredMigration(), [
41+
{
42+
name: absoluteFrom('/index.ts'),
43+
isProgramRootFile: true,
44+
contents: `
45+
class MyGuard {
46+
canMatch(route: any, segments: any) {}
47+
}
48+
const guard = new MyGuard();
49+
guard.canMatch({}, []);
50+
`,
51+
},
52+
]);
53+
54+
const content = fs.readFile(absoluteFrom('/index.ts'));
55+
expect(content).not.toContain('{} as any /* added by migration */');
56+
});
57+
58+
it('should add the third argument to canMatch calls for functions typed as CanMatchFn', async () => {
59+
const {fs} = await runTsurgeMigration(new CanMatchSnapshotRequiredMigration(), [
60+
{
61+
name: absoluteFrom('/index.ts'),
62+
isProgramRootFile: true,
63+
contents: `
64+
type CanMatchFn = (route: any, segments: any) => boolean;
65+
const canMatch: CanMatchFn = (route, segments) => true;
66+
canMatch({}, []);
67+
`,
68+
},
69+
]);
70+
71+
const content = fs.readFile(absoluteFrom('/index.ts'));
72+
expect(content).toContain('canMatch({}, [], {} as any /* added by migration */)');
73+
});
74+
75+
it('should NOT add argument if the function is NOT typed as CanMatchFn', async () => {
76+
const {fs} = await runTsurgeMigration(new CanMatchSnapshotRequiredMigration(), [
77+
{
78+
name: absoluteFrom('/index.ts'),
79+
isProgramRootFile: true,
80+
contents: `
81+
function canMatch(route: any, segments: any) { return true; }
82+
canMatch({}, []);
83+
`,
84+
},
85+
]);
86+
87+
const content = fs.readFile(absoluteFrom('/index.ts'));
88+
expect(content).not.toContain('{} as any /* added by migration */');
89+
});
90+
});
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
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+
Replacement,
15+
Serializable,
16+
TextUpdate,
17+
TsurgeFunnelMigration,
18+
} from '../../utils/tsurge';
19+
20+
export interface UnitAnalysisMetadata {
21+
replacements: Replacement[];
22+
}
23+
24+
export class CanMatchSnapshotRequiredMigration extends TsurgeFunnelMigration<
25+
UnitAnalysisMetadata,
26+
UnitAnalysisMetadata
27+
> {
28+
override async analyze(info: ProgramInfo): Promise<Serializable<UnitAnalysisMetadata>> {
29+
const replacements: Replacement[] = [];
30+
const {sourceFiles, program} = info;
31+
const typeChecker = program.getTypeChecker();
32+
33+
for (const sourceFile of sourceFiles) {
34+
const walk = (node: ts.Node): void => {
35+
ts.forEachChild(node, walk);
36+
37+
if (ts.isCallExpression(node)) {
38+
let shouldMigrate = false;
39+
40+
// 1. Method calls objective: obj.canMatch(a, b)
41+
if (
42+
ts.isPropertyAccessExpression(node.expression) &&
43+
node.expression.name.text === 'canMatch'
44+
) {
45+
const type = typeChecker.getTypeAtLocation(node.expression.expression);
46+
const classSymbol = type.getSymbol();
47+
if (classSymbol && classSymbol.declarations) {
48+
const decl = classSymbol.declarations[0];
49+
if (ts.isClassDeclaration(decl)) {
50+
if (implementsInterface(decl, 'CanMatch')) {
51+
shouldMigrate = true;
52+
}
53+
}
54+
}
55+
}
56+
57+
// 2. Function calls objective: canMatch(a, b)
58+
if (ts.isIdentifier(node.expression) && node.expression.text === 'canMatch') {
59+
const type = typeChecker.getTypeAtLocation(node.expression);
60+
const typeStr = typeChecker.typeToString(type);
61+
if (typeStr.includes('CanMatchFn')) {
62+
shouldMigrate = true;
63+
}
64+
}
65+
66+
if (shouldMigrate && node.arguments.length === 2) {
67+
const lastArg = node.arguments[1];
68+
replacements.push(
69+
new Replacement(
70+
projectFile(sourceFile, info),
71+
new TextUpdate({
72+
position: lastArg.getEnd(),
73+
end: lastArg.getEnd(),
74+
toInsert: ', {} as any /* added by migration */',
75+
}),
76+
),
77+
);
78+
}
79+
}
80+
};
81+
82+
ts.forEachChild(sourceFile, walk);
83+
}
84+
85+
return confirmAsSerializable({replacements});
86+
}
87+
88+
override async combine(
89+
unitA: UnitAnalysisMetadata,
90+
unitB: UnitAnalysisMetadata,
91+
): Promise<Serializable<UnitAnalysisMetadata>> {
92+
return confirmAsSerializable({
93+
replacements: [...unitA.replacements, ...unitB.replacements],
94+
});
95+
}
96+
97+
override async globalMeta(
98+
combinedData: UnitAnalysisMetadata,
99+
): Promise<Serializable<UnitAnalysisMetadata>> {
100+
return confirmAsSerializable(combinedData);
101+
}
102+
103+
override async stats(globalMetadata: UnitAnalysisMetadata): Promise<Serializable<unknown>> {
104+
return confirmAsSerializable({});
105+
}
106+
107+
override async migrate(globalData: UnitAnalysisMetadata): Promise<{replacements: Replacement[]}> {
108+
return {replacements: globalData.replacements};
109+
}
110+
}
111+
112+
function implementsInterface(decl: ts.ClassDeclaration, interfaceName: string): boolean {
113+
if (!decl.heritageClauses) return false;
114+
115+
for (const clause of decl.heritageClauses) {
116+
if (clause.token === ts.SyntaxKind.ImplementsKeyword) {
117+
for (const expr of clause.types) {
118+
if (ts.isIdentifier(expr.expression) && expr.expression.text === interfaceName) {
119+
return true;
120+
}
121+
}
122+
}
123+
}
124+
125+
return false;
126+
}

0 commit comments

Comments
 (0)