Skip to content

Commit ca79e11

Browse files
alxhubmhevery
authored andcommitted
feat(ivy): a generic visitor which allows prefixing nodes for ngtsc (angular#24230)
This adds ngtsc/util/src/visitor, a utility for visiting TS ASTs that can add synthetic nodes immediately prior to certain types of nodes (e.g. class declarations). It's useful to lift definitions that need to be referenced repeatedly in generated code outside of the class that defines them. PR Close angular#24230
1 parent f781f74 commit ca79e11

File tree

9 files changed

+284
-18
lines changed

9 files changed

+284
-18
lines changed

packages/compiler-cli/src/ngtsc/metadata/test/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ ts_library(
1212
deps = [
1313
"//packages:types",
1414
"//packages/compiler-cli/src/ngtsc/metadata",
15+
"//packages/compiler-cli/src/ngtsc/testing",
1516
],
1617
)
1718

packages/compiler-cli/src/ngtsc/metadata/test/reflector_spec.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,13 @@
88

99
import * as ts from 'typescript';
1010

11+
import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript';
1112
import {Parameter, reflectConstructorParameters} from '../src/reflector';
1213

13-
import {getDeclaration, makeProgram} from './in_memory_typescript';
14-
1514
describe('reflector', () => {
1615
describe('ctor params', () => {
1716
it('should reflect a single argument', () => {
18-
const program = makeProgram([{
17+
const {program} = makeProgram([{
1918
name: 'entry.ts',
2019
contents: `
2120
class Bar {}
@@ -33,7 +32,7 @@ describe('reflector', () => {
3332
});
3433

3534
it('should reflect a decorated argument', () => {
36-
const program = makeProgram([
35+
const {program} = makeProgram([
3736
{
3837
name: 'dec.ts',
3938
contents: `
@@ -61,7 +60,7 @@ describe('reflector', () => {
6160
});
6261

6362
it('should reflect a decorated argument with a call', () => {
64-
const program = makeProgram([
63+
const {program} = makeProgram([
6564
{
6665
name: 'dec.ts',
6766
contents: `
@@ -89,7 +88,7 @@ describe('reflector', () => {
8988
});
9089

9190
it('should reflect a decorated argument with an indirection', () => {
92-
const program = makeProgram([
91+
const {program} = makeProgram([
9392
{
9493
name: 'bar.ts',
9594
contents: `

packages/compiler-cli/src/ngtsc/metadata/test/resolver_spec.ts

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

99
import * as ts from 'typescript';
1010

11+
import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript';
1112
import {ResolvedValue, staticallyResolve} from '../src/resolver';
1213

13-
import {getDeclaration, makeProgram} from './in_memory_typescript';
14-
1514
function makeSimpleProgram(contents: string): ts.Program {
16-
return makeProgram([{name: 'entry.ts', contents}]);
15+
return makeProgram([{name: 'entry.ts', contents}]).program;
1716
}
1817

1918
function makeExpression(
2019
code: string, expr: string): {expression: ts.Expression, checker: ts.TypeChecker} {
21-
const program = makeProgram([{name: 'entry.ts', contents: `${code}; const target$ = ${expr};`}]);
20+
const {program} =
21+
makeProgram([{name: 'entry.ts', contents: `${code}; const target$ = ${expr};`}]);
2222
const checker = program.getTypeChecker();
2323
const decl = getDeclaration(program, 'entry.ts', 'target$', ts.isVariableDeclaration);
2424
return {
@@ -34,7 +34,7 @@ function evaluate<T extends ResolvedValue>(code: string, expr: string): T {
3434

3535
describe('ngtsc metadata', () => {
3636
it('reads a file correctly', () => {
37-
const program = makeProgram([
37+
const {program} = makeProgram([
3838
{
3939
name: 'entry.ts',
4040
contents: `
@@ -117,7 +117,7 @@ describe('ngtsc metadata', () => {
117117
});
118118

119119
it('reads values from default exports', () => {
120-
const program = makeProgram([
120+
const {program} = makeProgram([
121121
{name: 'second.ts', contents: 'export default {property: "test"}'},
122122
{
123123
name: 'entry.ts',
@@ -135,7 +135,7 @@ describe('ngtsc metadata', () => {
135135
});
136136

137137
it('reads values from named exports', () => {
138-
const program = makeProgram([
138+
const {program} = makeProgram([
139139
{name: 'second.ts', contents: 'export const a = {property: "test"};'},
140140
{
141141
name: 'entry.ts',
@@ -152,7 +152,7 @@ describe('ngtsc metadata', () => {
152152
});
153153

154154
it('chain of re-exports works', () => {
155-
const program = makeProgram([
155+
const {program} = makeProgram([
156156
{name: 'const.ts', contents: 'export const value = {property: "test"};'},
157157
{name: 'def.ts', contents: `import {value} from './const'; export default value;`},
158158
{name: 'indirect-reexport.ts', contents: `import value from './def'; export {value};`},
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package(default_visibility = ["//visibility:public"])
2+
3+
load("//tools:defaults.bzl", "ts_library")
4+
load("@build_bazel_rules_nodejs//:defs.bzl", "jasmine_node_test")
5+
6+
ts_library(
7+
name = "testing",
8+
testonly = 1,
9+
srcs = glob([
10+
"**/*.ts",
11+
]),
12+
deps = [
13+
"//packages:types",
14+
],
15+
)

packages/compiler-cli/src/ngtsc/metadata/test/in_memory_typescript.ts renamed to packages/compiler-cli/src/ngtsc/testing/in_memory_typescript.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,19 @@
99
import * as path from 'path';
1010
import * as ts from 'typescript';
1111

12-
export function makeProgram(files: {name: string, contents: string}[]): ts.Program {
12+
export function makeProgram(files: {name: string, contents: string}[]):
13+
{program: ts.Program, host: ts.CompilerHost} {
1314
const host = new InMemoryHost();
1415
files.forEach(file => host.writeFile(file.name, file.contents));
1516

1617
const rootNames = files.map(file => host.getCanonicalFileName(file.name));
1718
const program = ts.createProgram(rootNames, {noLib: true, experimentalDecorators: true}, host);
1819
const diags = [...program.getSyntacticDiagnostics(), ...program.getSemanticDiagnostics()];
1920
if (diags.length > 0) {
20-
fail(diags.map(diag => diag.messageText).join(', '));
21-
throw new Error(`Typescript diagnostics failed!`);
21+
throw new Error(
22+
`Typescript diagnostics failed! ${diags.map(diag => diag.messageText).join(', ')}`);
2223
}
23-
return program;
24+
return {program, host};
2425
}
2526

2627
export class InMemoryHost implements ts.CompilerHost {
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package(default_visibility = ["//visibility:public"])
2+
3+
load("//tools:defaults.bzl", "ts_library")
4+
5+
ts_library(
6+
name = "util",
7+
srcs = glob([
8+
"index.ts",
9+
"src/**/*.ts",
10+
]),
11+
module_name = "@angular/compiler-cli/src/ngtsc/util",
12+
)
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. 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.io/license
7+
*/
8+
9+
import * as ts from 'typescript';
10+
11+
/**
12+
* Result type of visiting a node that's typically an entry in a list, which allows specifying that
13+
* nodes should be added before the visited node in the output.
14+
*/
15+
export type VisitListEntryResult<B extends ts.Node, T extends B> = {
16+
node: T,
17+
before?: B[]
18+
};
19+
20+
/**
21+
* Visit a node with the given visitor and return a transformed copy.
22+
*/
23+
export function visit<T extends ts.Node>(
24+
node: T, visitor: Visitor, context: ts.TransformationContext): T {
25+
return visitor._visit(node, context);
26+
}
27+
28+
/**
29+
* Abstract base class for visitors, which processes certain nodes specially to allow insertion
30+
* of other nodes before them.
31+
*/
32+
export abstract class Visitor {
33+
/**
34+
* Maps statements to an array of statements that should be inserted before them.
35+
*/
36+
private _before = new Map<ts.Statement, ts.Statement[]>();
37+
38+
/**
39+
* Visit a class declaration, returning at least the transformed declaration and optionally other
40+
* nodes to insert before the declaration.
41+
*/
42+
visitClassDeclaration(node: ts.ClassDeclaration):
43+
VisitListEntryResult<ts.Statement, ts.ClassDeclaration> {
44+
return {node};
45+
}
46+
47+
private _visitClassDeclaration(node: ts.ClassDeclaration, context: ts.TransformationContext):
48+
ts.ClassDeclaration {
49+
const result = this.visitClassDeclaration(node);
50+
const visited = ts.visitEachChild(result.node, child => this._visit(child, context), context);
51+
if (result.before !== undefined) {
52+
// Record that some nodes should be inserted before the given declaration. The declaration's
53+
// parent's _visit call is responsible for performing this insertion.
54+
this._before.set(visited, result.before);
55+
}
56+
return visited;
57+
}
58+
59+
/**
60+
* Visit types of nodes which don't have their own explicit visitor.
61+
*/
62+
visitOtherNode<T extends ts.Node>(node: T): T { return node; }
63+
64+
private _visitOtherNode<T extends ts.Node>(node: T, context: ts.TransformationContext): T {
65+
return ts.visitEachChild(
66+
this.visitOtherNode(node), child => this._visit(child, context), context);
67+
}
68+
69+
/**
70+
* @internal
71+
*/
72+
_visit<T extends ts.Node>(node: T, context: ts.TransformationContext): T {
73+
// First, visit the node. visitedNode starts off as `null` but should be set after visiting
74+
// is completed.
75+
let visitedNode: T|null = null;
76+
if (ts.isClassDeclaration(node)) {
77+
visitedNode = this._visitClassDeclaration(node, context) as typeof node;
78+
} else {
79+
visitedNode = this._visitOtherNode(node, context);
80+
}
81+
82+
// If the visited node has a `statements` array then process them, maybe replacing the visited
83+
// node and adding additional statements.
84+
if (hasStatements(visitedNode)) {
85+
visitedNode = this._maybeProcessStatements(visitedNode);
86+
}
87+
88+
return visitedNode;
89+
}
90+
91+
private _maybeProcessStatements<T extends ts.Node&{statements: ts.NodeArray<ts.Statement>}>(
92+
node: T): T {
93+
// Shortcut - if every statement doesn't require nodes to be prepended, this is a no-op.
94+
if (node.statements.every(stmt => !this._before.has(stmt))) {
95+
return node;
96+
}
97+
98+
// There are statements to prepend, so clone the original node.
99+
const clone = ts.getMutableClone(node);
100+
101+
// Build a new list of statements and patch it onto the clone.
102+
const newStatements: ts.Statement[] = [];
103+
clone.statements.forEach(stmt => {
104+
if (this._before.has(stmt)) {
105+
newStatements.push(...(this._before.get(stmt) !as ts.Statement[]));
106+
this._before.delete(stmt);
107+
}
108+
newStatements.push(stmt);
109+
});
110+
clone.statements = ts.createNodeArray(newStatements, node.statements.hasTrailingComma);
111+
return clone;
112+
}
113+
}
114+
115+
function hasStatements(node: ts.Node): node is ts.Node&{statements: ts.NodeArray<ts.Statement>} {
116+
const block = node as{statements?: any};
117+
return block.statements !== undefined && Array.isArray(block.statements);
118+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package(default_visibility = ["//visibility:public"])
2+
3+
load("//tools:defaults.bzl", "ts_library")
4+
load("@build_bazel_rules_nodejs//:defs.bzl", "jasmine_node_test")
5+
6+
ts_library(
7+
name = "test_lib",
8+
testonly = 1,
9+
srcs = glob([
10+
"**/*.ts",
11+
]),
12+
deps = [
13+
"//packages:types",
14+
"//packages/compiler-cli/src/ngtsc/testing",
15+
"//packages/compiler-cli/src/ngtsc/util",
16+
],
17+
)
18+
19+
jasmine_node_test(
20+
name = "test",
21+
bootstrap = ["angular/tools/testing/init_node_no_angular_spec.js"],
22+
deps = [
23+
":test_lib",
24+
"//tools/testing:node_no_angular",
25+
],
26+
)

0 commit comments

Comments
 (0)