Skip to content

Commit 5aa6990

Browse files
committed
poc: typescript transformer support for tsickle
1 parent 77de611 commit 5aa6990

24 files changed

+978
-401
lines changed

src/decorator-annotator.ts

+149-104
Original file line numberDiff line numberDiff line change
@@ -6,27 +6,32 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {SourceMapGenerator} from 'source-map';
109
import * as ts from 'typescript';
1110

1211
import {getDecoratorDeclarations} from './decorators';
13-
import {Rewriter} from './rewriter';
12+
import {getIdentifierText, Rewriter} from './rewriter';
13+
import {SourceMapper} from './source_map_utils';
1414
import {assertTypeChecked, TypeTranslator} from './type-translator';
1515
import {toArray} from './util';
1616

17-
// ClassRewriter rewrites a single "class Foo {...}" declaration.
17+
// DecoratorClassVisitor rewrites a single "class Foo {...}" declaration.
1818
// It's its own object because we collect decorators on the class and the ctor
1919
// separately for each class we encounter.
20-
class ClassRewriter extends Rewriter {
20+
export class DecoratorClassVisitor {
2121
/** Decorators on the class itself. */
2222
decorators: ts.Decorator[];
2323
/** The constructor parameter list and decorators on each param. */
2424
ctorParameters: ([string | undefined, ts.Decorator[]|undefined]|null)[];
2525
/** Per-method decorators. */
2626
propDecorators: Map<string, ts.Decorator[]>;
2727

28-
constructor(private typeChecker: ts.TypeChecker, sourceFile: ts.SourceFile) {
29-
super(sourceFile);
28+
constructor(
29+
private typeChecker: ts.TypeChecker, private rewriter: Rewriter,
30+
private classDecl: ts.ClassDeclaration) {
31+
if (classDecl.decorators) {
32+
let toLower = this.decoratorsToLower(classDecl);
33+
if (toLower.length > 0) this.decorators = toLower;
34+
}
3035
}
3136

3237
/**
@@ -69,40 +74,6 @@ class ClassRewriter extends Rewriter {
6974
return [];
7075
}
7176

72-
/**
73-
* process is the main entry point, rewriting a single class node.
74-
*/
75-
process(node: ts.ClassDeclaration): {output: string, diagnostics: ts.Diagnostic[]} {
76-
if (node.decorators) {
77-
let toLower = this.decoratorsToLower(node);
78-
if (toLower.length > 0) this.decorators = toLower;
79-
}
80-
81-
// Emit the class contents, but stop just before emitting the closing curly brace.
82-
// (This code is the same as Rewriter.writeNode except for the curly brace handling.)
83-
let pos = node.getFullStart();
84-
ts.forEachChild(node, child => {
85-
// This forEachChild handles emitting the text between each child, while child.visit
86-
// recursively emits the children themselves.
87-
this.writeRange(pos, child.getFullStart());
88-
this.visit(child);
89-
pos = child.getEnd();
90-
});
91-
92-
// At this point, we've emitted up through the final child of the class, so all that
93-
// remains is the trailing whitespace and closing curly brace.
94-
// The final character owned by the class node should always be a '}',
95-
// or we somehow got the AST wrong and should report an error.
96-
// (Any whitespace or semicolon following the '}' will be part of the next Node.)
97-
if (this.file.text[node.getEnd() - 1] !== '}') {
98-
this.error(node, 'unexpected class terminator');
99-
}
100-
this.writeRange(pos, node.getEnd() - 1);
101-
this.emitMetadata();
102-
this.emit('}');
103-
return this.getOutput();
104-
}
105-
10677
/**
10778
* gatherConstructor grabs the parameter list and decorators off the class
10879
* constructor, and emits nothing.
@@ -147,7 +118,7 @@ class ClassRewriter extends Rewriter {
147118
if (!method.name || method.name.kind !== ts.SyntaxKind.Identifier) {
148119
// Method has a weird name, e.g.
149120
// [Symbol.foo]() {...}
150-
this.error(method, 'cannot process decorators on strangely named method');
121+
this.rewriter.error(method, 'cannot process decorators on strangely named method');
151122
return;
152123
}
153124

@@ -158,153 +129,227 @@ class ClassRewriter extends Rewriter {
158129
this.propDecorators.set(name, decorators);
159130
}
160131

161-
/**
162-
* maybeProcess is called by the traversal of the AST.
163-
* @return True if the node was handled, false to have the node emitted as normal.
164-
*/
165-
protected maybeProcess(node: ts.Node): boolean {
132+
beforeProcessNode(node: ts.Node) {
166133
switch (node.kind) {
167-
case ts.SyntaxKind.ClassDeclaration:
168-
// Encountered a new class while processing this class; use a new separate
169-
// rewriter to gather+emit its metadata.
170-
let {output, diagnostics} =
171-
new ClassRewriter(this.typeChecker, this.file).process(node as ts.ClassDeclaration);
172-
this.diagnostics.push(...diagnostics);
173-
this.emit(output);
174-
return true;
175134
case ts.SyntaxKind.Constructor:
176135
this.gatherConstructor(node as ts.ConstructorDeclaration);
177-
return false; // Proceed with ordinary emit of the ctor.
136+
break;
178137
case ts.SyntaxKind.PropertyDeclaration:
179138
case ts.SyntaxKind.SetAccessor:
180139
case ts.SyntaxKind.GetAccessor:
181140
case ts.SyntaxKind.MethodDeclaration:
182141
this.gatherMethodOrProperty(node as ts.Declaration);
183-
return false; // Proceed with ordinary emit of the method.
184-
case ts.SyntaxKind.Decorator:
185-
if (this.shouldLower(node as ts.Decorator)) {
186-
// Return true to signal that this node should not be emitted,
187-
// but still emit the whitespace *before* the node.
188-
this.writeRange(node.getFullStart(), node.getStart());
189-
return true;
190-
}
191-
return false;
142+
break;
192143
default:
193-
return false;
144+
}
145+
}
146+
147+
maybeProcessDecorator(node: ts.Node, start?: number): boolean {
148+
if (this.shouldLower(node as ts.Decorator)) {
149+
// Return true to signal that this node should not be emitted,
150+
// but still emit the whitespace *before* the node.
151+
if (!start) {
152+
start = node.getFullStart();
153+
}
154+
this.rewriter.writeRange(node, start, node.getStart());
155+
return true;
156+
}
157+
return false;
158+
}
159+
160+
/**
161+
* emits the types for the various gathered metadata to be used
162+
* in the tsickle type annotations helper.
163+
*/
164+
emitMetadataTypeAnnotationsHelpers() {
165+
if (!this.classDecl.name) return;
166+
let className = getIdentifierText(this.classDecl.name);
167+
if (this.decorators) {
168+
this.rewriter.emit(`/** @type {!Array<{type: !Function, args: (undefined|!Array<?>)}>} */\n`);
169+
this.rewriter.emit(`${className}.decorators;\n`);
170+
}
171+
if (this.decorators || this.ctorParameters) {
172+
this.rewriter.emit(`/**\n`);
173+
this.rewriter.emit(` * @nocollapse\n`);
174+
this.rewriter.emit(
175+
` * @type {function(): !Array<(null|{type: ?, decorators: (undefined|!Array<{type: !Function, args: (undefined|!Array<?>)}>)})>}\n`);
176+
this.rewriter.emit(` */\n`);
177+
this.rewriter.emit(`${className}.ctorParameters;\n`);
178+
}
179+
if (this.propDecorators) {
180+
this.rewriter.emit(
181+
`/** @type {!Object<string,!Array<{type: !Function, args: (undefined|!Array<?>)}>>} */\n`);
182+
this.rewriter.emit(`${className}.propDecorators;\n`);
194183
}
195184
}
196185

197186
/**
198-
* emitMetadata emits the various gathered metadata, as static fields.
187+
* emits the various gathered metadata, as static fields.
199188
*/
200-
private emitMetadata() {
189+
emitMetadataAsStaticProperties() {
201190
const decoratorInvocations = '{type: Function, args?: any[]}[]';
202191
if (this.decorators) {
203-
this.emit(`static decorators: ${decoratorInvocations} = [\n`);
192+
this.rewriter.emit(`static decorators: ${decoratorInvocations} = [\n`);
204193
for (let annotation of this.decorators) {
205194
this.emitDecorator(annotation);
206-
this.emit(',\n');
195+
this.rewriter.emit(',\n');
207196
}
208-
this.emit('];\n');
197+
this.rewriter.emit('];\n');
209198
}
210199

211200
if (this.decorators || this.ctorParameters) {
212-
this.emit(`/** @nocollapse */\n`);
201+
this.rewriter.emit(`/** @nocollapse */\n`);
213202
// ctorParameters may contain forward references in the type: field, so wrap in a function
214203
// closure
215-
this.emit(
204+
this.rewriter.emit(
216205
`static ctorParameters: () => ({type: any, decorators?: ` + decoratorInvocations +
217206
`}|null)[] = () => [\n`);
218207
for (let param of this.ctorParameters || []) {
219208
if (!param) {
220-
this.emit('null,\n');
209+
this.rewriter.emit('null,\n');
221210
continue;
222211
}
223212
let [ctor, decorators] = param;
224-
this.emit(`{type: ${ctor}, `);
213+
this.rewriter.emit(`{type: ${ctor}, `);
225214
if (decorators) {
226-
this.emit('decorators: [');
215+
this.rewriter.emit('decorators: [');
227216
for (let decorator of decorators) {
228217
this.emitDecorator(decorator);
229-
this.emit(', ');
218+
this.rewriter.emit(', ');
230219
}
231-
this.emit(']');
220+
this.rewriter.emit(']');
232221
}
233-
this.emit('},\n');
222+
this.rewriter.emit('},\n');
234223
}
235-
this.emit(`];\n`);
224+
this.rewriter.emit(`];\n`);
236225
}
237226

238227
if (this.propDecorators) {
239-
this.emit(`static propDecorators: {[key: string]: ` + decoratorInvocations + `} = {\n`);
228+
this.rewriter.emit(
229+
`static propDecorators: {[key: string]: ` + decoratorInvocations + `} = {\n`);
240230
for (let name of toArray(this.propDecorators.keys())) {
241-
this.emit(`'${name}': [`);
231+
this.rewriter.emit(`'${name}': [`);
242232

243233
for (let decorator of this.propDecorators.get(name)!) {
244234
this.emitDecorator(decorator);
245-
this.emit(',');
235+
this.rewriter.emit(',');
246236
}
247-
this.emit('],\n');
237+
this.rewriter.emit('],\n');
248238
}
249-
this.emit('};\n');
239+
this.rewriter.emit('};\n');
250240
}
251241
}
252242

253243
private emitDecorator(decorator: ts.Decorator) {
254-
this.emit('{ type: ');
244+
this.rewriter.emit('{ type: ');
255245
let expr = decorator.expression;
256246
switch (expr.kind) {
257247
case ts.SyntaxKind.Identifier:
258248
// The decorator was a plain @Foo.
259-
this.visit(expr);
249+
this.rewriter.visit(expr);
260250
break;
261251
case ts.SyntaxKind.CallExpression:
262252
// The decorator was a call, like @Foo(bar).
263253
let call = expr as ts.CallExpression;
264-
this.visit(call.expression);
254+
this.rewriter.visit(call.expression);
265255
if (call.arguments.length) {
266-
this.emit(', args: [');
256+
this.rewriter.emit(', args: [');
267257
for (let arg of call.arguments) {
268-
this.emit(arg.getText());
269-
this.emit(', ');
258+
this.rewriter.emit(arg.getText());
259+
this.rewriter.emit(', ');
270260
}
271-
this.emit(']');
261+
this.rewriter.emit(']');
272262
}
273263
break;
274264
default:
275-
this.errorUnimplementedKind(expr, 'gathering metadata');
276-
this.emit('undefined');
265+
this.rewriter.errorUnimplementedKind(expr, 'gathering metadata');
266+
this.rewriter.emit('undefined');
277267
}
278-
this.emit(' }');
268+
this.rewriter.emit(' }');
279269
}
280270
}
281271

282272
class DecoratorRewriter extends Rewriter {
283-
constructor(private typeChecker: ts.TypeChecker, sourceFile: ts.SourceFile) {
284-
super(sourceFile);
273+
/** ComposableDecoratorRewriter when using tsickle as a TS transformer */
274+
private currentDecoratorConverter: DecoratorClassVisitor;
275+
276+
constructor(
277+
private typeChecker: ts.TypeChecker, file: ts.SourceFile, sourceMapper?: SourceMapper) {
278+
super(file, sourceMapper);
285279
}
286280

287-
process(): {output: string, diagnostics: ts.Diagnostic[], sourceMap: SourceMapGenerator} {
281+
process(): {output: string, diagnostics: ts.Diagnostic[]} {
288282
this.visit(this.file);
289283
return this.getOutput();
290284
}
291285

292286
protected maybeProcess(node: ts.Node): boolean {
287+
if (this.currentDecoratorConverter) {
288+
this.currentDecoratorConverter.beforeProcessNode(node);
289+
}
293290
switch (node.kind) {
291+
case ts.SyntaxKind.Decorator:
292+
return this.currentDecoratorConverter &&
293+
this.currentDecoratorConverter.maybeProcessDecorator(node);
294294
case ts.SyntaxKind.ClassDeclaration:
295-
let {output, diagnostics} =
296-
new ClassRewriter(this.typeChecker, this.file).process(node as ts.ClassDeclaration);
297-
this.diagnostics.push(...diagnostics);
298-
this.emit(output);
295+
const oldDecoratorConverter = this.currentDecoratorConverter;
296+
this.currentDecoratorConverter =
297+
new DecoratorClassVisitor(this.typeChecker, this, node as ts.ClassDeclaration);
298+
this.writeRange(node, node.getFullStart(), node.getStart());
299+
visitClassContent(node as ts.ClassDeclaration, this, this.currentDecoratorConverter);
300+
this.currentDecoratorConverter = oldDecoratorConverter;
299301
return true;
300302
default:
301303
return false;
302304
}
303305
}
304306
}
305307

306-
export function convertDecorators(typeChecker: ts.TypeChecker, sourceFile: ts.SourceFile):
307-
{output: string, diagnostics: ts.Diagnostic[], sourceMap: SourceMapGenerator} {
308+
export function convertDecorators(
309+
typeChecker: ts.TypeChecker, sourceFile: ts.SourceFile,
310+
sourceMapper?: SourceMapper): {output: string, diagnostics: ts.Diagnostic[]} {
308311
assertTypeChecked(sourceFile);
309-
return new DecoratorRewriter(typeChecker, sourceFile).process();
312+
return new DecoratorRewriter(typeChecker, sourceFile, sourceMapper).process();
313+
}
314+
315+
export function visitClassContent(
316+
classDecl: ts.ClassDeclaration, rewriter: Rewriter, decoratorVisitor?: DecoratorClassVisitor) {
317+
let pos = classDecl.getStart();
318+
if (decoratorVisitor) {
319+
// strip out decorators if needed
320+
ts.forEachChild(classDecl, child => {
321+
if (child.kind !== ts.SyntaxKind.Decorator) {
322+
return;
323+
}
324+
// Note: The getFullStart() of the first decorator is the same
325+
// as the getFullStart() of the class declaration.
326+
// Therefore, we need to use Math.max to not print the whitespace
327+
// of the class again.
328+
const childStart = Math.max(pos, child.getFullStart());
329+
rewriter.writeRange(classDecl, pos, childStart);
330+
if (decoratorVisitor.maybeProcessDecorator(child, childStart)) {
331+
pos = child.getEnd();
332+
}
333+
});
334+
}
335+
if (classDecl.members.length > 0) {
336+
rewriter.writeRange(classDecl, pos, classDecl.members[0].getFullStart());
337+
for (let member of classDecl.members) {
338+
rewriter.visit(member);
339+
}
340+
pos = classDecl.getLastToken().getFullStart();
341+
}
342+
// At this point, we've emitted up through the final child of the class, so all that
343+
// remains is the trailing whitespace and closing curly brace.
344+
// The final character owned by the class node should always be a '}',
345+
// or we somehow got the AST wrong and should report an error.
346+
// (Any whitespace or semicolon following the '}' will be part of the next Node.)
347+
if (rewriter.file.text[classDecl.getEnd() - 1] !== '}') {
348+
rewriter.error(classDecl, 'unexpected class terminator');
349+
}
350+
rewriter.writeRange(classDecl, pos, classDecl.getEnd() - 1);
351+
if (decoratorVisitor) {
352+
decoratorVisitor.emitMetadataAsStaticProperties();
353+
}
354+
rewriter.emit('}');
310355
}

0 commit comments

Comments
 (0)