Skip to content

Commit

Permalink
poc: typescript transformer support for tsickle
Browse files Browse the repository at this point in the history
  • Loading branch information
tbosch committed May 31, 2017
1 parent 77de611 commit 802eb2f
Show file tree
Hide file tree
Showing 24 changed files with 1,062 additions and 407 deletions.
252 changes: 147 additions & 105 deletions src/decorator-annotator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,32 @@
* found in the LICENSE file at https://angular.io/license
*/

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

import {getDecoratorDeclarations} from './decorators';
import {Rewriter} from './rewriter';
import {getIdentifierText, Rewriter} from './rewriter';
import {SourceMapper, SourcePosition} from './source_map_utils';
import {assertTypeChecked, TypeTranslator} from './type-translator';
import {toArray} from './util';

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

constructor(private typeChecker: ts.TypeChecker, sourceFile: ts.SourceFile) {
super(sourceFile);
constructor(
private typeChecker: ts.TypeChecker, private rewriter: Rewriter,
private classDecl: ts.ClassDeclaration) {
if (classDecl.decorators) {
let toLower = this.decoratorsToLower(classDecl);
if (toLower.length > 0) this.decorators = toLower;
}
}

/**
Expand Down Expand Up @@ -69,40 +74,6 @@ class ClassRewriter extends Rewriter {
return [];
}

/**
* process is the main entry point, rewriting a single class node.
*/
process(node: ts.ClassDeclaration): {output: string, diagnostics: ts.Diagnostic[]} {
if (node.decorators) {
let toLower = this.decoratorsToLower(node);
if (toLower.length > 0) this.decorators = toLower;
}

// Emit the class contents, but stop just before emitting the closing curly brace.
// (This code is the same as Rewriter.writeNode except for the curly brace handling.)
let pos = node.getFullStart();
ts.forEachChild(node, child => {
// This forEachChild handles emitting the text between each child, while child.visit
// recursively emits the children themselves.
this.writeRange(pos, child.getFullStart());
this.visit(child);
pos = child.getEnd();
});

// At this point, we've emitted up through the final child of the class, so all that
// remains is the trailing whitespace and closing curly brace.
// The final character owned by the class node should always be a '}',
// or we somehow got the AST wrong and should report an error.
// (Any whitespace or semicolon following the '}' will be part of the next Node.)
if (this.file.text[node.getEnd() - 1] !== '}') {
this.error(node, 'unexpected class terminator');
}
this.writeRange(pos, node.getEnd() - 1);
this.emitMetadata();
this.emit('}');
return this.getOutput();
}

/**
* gatherConstructor grabs the parameter list and decorators off the class
* constructor, and emits nothing.
Expand Down Expand Up @@ -147,7 +118,7 @@ class ClassRewriter extends Rewriter {
if (!method.name || method.name.kind !== ts.SyntaxKind.Identifier) {
// Method has a weird name, e.g.
// [Symbol.foo]() {...}
this.error(method, 'cannot process decorators on strangely named method');
this.rewriter.error(method, 'cannot process decorators on strangely named method');
return;
}

Expand All @@ -158,153 +129,224 @@ class ClassRewriter extends Rewriter {
this.propDecorators.set(name, decorators);
}

/**
* maybeProcess is called by the traversal of the AST.
* @return True if the node was handled, false to have the node emitted as normal.
*/
protected maybeProcess(node: ts.Node): boolean {
beforeProcessNode(node: ts.Node) {
switch (node.kind) {
case ts.SyntaxKind.ClassDeclaration:
// Encountered a new class while processing this class; use a new separate
// rewriter to gather+emit its metadata.
let {output, diagnostics} =
new ClassRewriter(this.typeChecker, this.file).process(node as ts.ClassDeclaration);
this.diagnostics.push(...diagnostics);
this.emit(output);
return true;
case ts.SyntaxKind.Constructor:
this.gatherConstructor(node as ts.ConstructorDeclaration);
return false; // Proceed with ordinary emit of the ctor.
case ts.SyntaxKind.PropertyDeclaration:
case ts.SyntaxKind.SetAccessor:
case ts.SyntaxKind.GetAccessor:
case ts.SyntaxKind.MethodDeclaration:
this.gatherMethodOrProperty(node as ts.Declaration);
return false; // Proceed with ordinary emit of the method.
case ts.SyntaxKind.Decorator:
if (this.shouldLower(node as ts.Decorator)) {
// Return true to signal that this node should not be emitted,
// but still emit the whitespace *before* the node.
this.writeRange(node.getFullStart(), node.getStart());
return true;
}
return false;
default:
return false;
}
}

maybeProcessDecorator(node: ts.Node, start?: number): boolean {
if (this.shouldLower(node as ts.Decorator)) {
// Return true to signal that this node should not be emitted,
// but still emit the whitespace *before* the node.
if (!start) {
start = node.getFullStart();
}
this.rewriter.writeRange(node, start, node.getStart());
return true;
}
return false;
}

/**
* emitMetadata emits the various gathered metadata, as static fields.
* emits the types for the various gathered metadata to be used
* in the tsickle type annotations helper.
*/
private emitMetadata() {
emitMetadataTypeAnnotationsHelpers() {
if (!this.classDecl.name) return;
let className = getIdentifierText(this.classDecl.name);
if (this.decorators) {
this.rewriter.emit(`/** @type {!Array<{type: !Function, args: (undefined|!Array<?>)}>} */\n`);
this.rewriter.emit(`${className}.decorators;\n`);
}
if (this.decorators || this.ctorParameters) {
this.rewriter.emit(`/**\n`);
this.rewriter.emit(` * @nocollapse\n`);
this.rewriter.emit(
` * @type {function(): !Array<(null|{type: ?, decorators: (undefined|!Array<{type: !Function, args: (undefined|!Array<?>)}>)})>}\n`);
this.rewriter.emit(` */\n`);
this.rewriter.emit(`${className}.ctorParameters;\n`);
}
if (this.propDecorators) {
this.rewriter.emit(
`/** @type {!Object<string,!Array<{type: !Function, args: (undefined|!Array<?>)}>>} */\n`);
this.rewriter.emit(`${className}.propDecorators;\n`);
}
}

/**
* emits the various gathered metadata, as static fields.
*/
emitMetadataAsStaticProperties() {
const decoratorInvocations = '{type: Function, args?: any[]}[]';
if (this.decorators) {
this.emit(`static decorators: ${decoratorInvocations} = [\n`);
this.rewriter.emit(`static decorators: ${decoratorInvocations} = [\n`);
for (let annotation of this.decorators) {
this.emitDecorator(annotation);
this.emit(',\n');
this.rewriter.emit(',\n');
}
this.emit('];\n');
this.rewriter.emit('];\n');
}

if (this.decorators || this.ctorParameters) {
this.emit(`/** @nocollapse */\n`);
this.rewriter.emit(`/** @nocollapse */\n`);
// ctorParameters may contain forward references in the type: field, so wrap in a function
// closure
this.emit(
this.rewriter.emit(
`static ctorParameters: () => ({type: any, decorators?: ` + decoratorInvocations +
`}|null)[] = () => [\n`);
for (let param of this.ctorParameters || []) {
if (!param) {
this.emit('null,\n');
this.rewriter.emit('null,\n');
continue;
}
let [ctor, decorators] = param;
this.emit(`{type: ${ctor}, `);
this.rewriter.emit(`{type: ${ctor}, `);
if (decorators) {
this.emit('decorators: [');
this.rewriter.emit('decorators: [');
for (let decorator of decorators) {
this.emitDecorator(decorator);
this.emit(', ');
this.rewriter.emit(', ');
}
this.emit(']');
this.rewriter.emit(']');
}
this.emit('},\n');
this.rewriter.emit('},\n');
}
this.emit(`];\n`);
this.rewriter.emit(`];\n`);
}

if (this.propDecorators) {
this.emit(`static propDecorators: {[key: string]: ` + decoratorInvocations + `} = {\n`);
this.rewriter.emit(
`static propDecorators: {[key: string]: ` + decoratorInvocations + `} = {\n`);
for (let name of toArray(this.propDecorators.keys())) {
this.emit(`'${name}': [`);
this.rewriter.emit(`'${name}': [`);

for (let decorator of this.propDecorators.get(name)!) {
this.emitDecorator(decorator);
this.emit(',');
this.rewriter.emit(',');
}
this.emit('],\n');
this.rewriter.emit('],\n');
}
this.emit('};\n');
this.rewriter.emit('};\n');
}
}

private emitDecorator(decorator: ts.Decorator) {
this.emit('{ type: ');
this.rewriter.emit('{ type: ');
let expr = decorator.expression;
switch (expr.kind) {
case ts.SyntaxKind.Identifier:
// The decorator was a plain @Foo.
this.visit(expr);
this.rewriter.visit(expr);
break;
case ts.SyntaxKind.CallExpression:
// The decorator was a call, like @Foo(bar).
let call = expr as ts.CallExpression;
this.visit(call.expression);
this.rewriter.visit(call.expression);
if (call.arguments.length) {
this.emit(', args: [');
this.rewriter.emit(', args: [');
for (let arg of call.arguments) {
this.emit(arg.getText());
this.emit(', ');
this.rewriter.emit(arg.getText());
this.rewriter.emit(', ');
}
this.emit(']');
this.rewriter.emit(']');
}
break;
default:
this.errorUnimplementedKind(expr, 'gathering metadata');
this.emit('undefined');
this.rewriter.errorUnimplementedKind(expr, 'gathering metadata');
this.rewriter.emit('undefined');
}
this.emit(' }');
this.rewriter.emit(' }');
}
}

class DecoratorRewriter extends Rewriter {
constructor(private typeChecker: ts.TypeChecker, sourceFile: ts.SourceFile) {
super(sourceFile);
/** ComposableDecoratorRewriter when using tsickle as a TS transformer */
private currentDecoratorConverter: DecoratorClassVisitor;

constructor(
private typeChecker: ts.TypeChecker, file: ts.SourceFile, sourceMapper?: SourceMapper) {
super(file, sourceMapper);
}

process(): {output: string, diagnostics: ts.Diagnostic[], sourceMap: SourceMapGenerator} {
process(): {output: string, diagnostics: ts.Diagnostic[]} {
this.visit(this.file);
return this.getOutput();
}

protected maybeProcess(node: ts.Node): boolean {
if (this.currentDecoratorConverter) {
this.currentDecoratorConverter.beforeProcessNode(node);
}
switch (node.kind) {
case ts.SyntaxKind.Decorator:
return this.currentDecoratorConverter &&
this.currentDecoratorConverter.maybeProcessDecorator(node);
case ts.SyntaxKind.ClassDeclaration:
let {output, diagnostics} =
new ClassRewriter(this.typeChecker, this.file).process(node as ts.ClassDeclaration);
this.diagnostics.push(...diagnostics);
this.emit(output);
const oldDecoratorConverter = this.currentDecoratorConverter;
this.currentDecoratorConverter =
new DecoratorClassVisitor(this.typeChecker, this, node as ts.ClassDeclaration);
this.writeRange(node, node.getFullStart(), node.getStart());
visitClassContent(node as ts.ClassDeclaration, this, this.currentDecoratorConverter);
this.currentDecoratorConverter = oldDecoratorConverter;
return true;
default:
return false;
}
}
}

export function convertDecorators(typeChecker: ts.TypeChecker, sourceFile: ts.SourceFile):
{output: string, diagnostics: ts.Diagnostic[], sourceMap: SourceMapGenerator} {
export function convertDecorators(
typeChecker: ts.TypeChecker, sourceFile: ts.SourceFile,
sourceMapper?: SourceMapper): {output: string, diagnostics: ts.Diagnostic[]} {
assertTypeChecked(sourceFile);
return new DecoratorRewriter(typeChecker, sourceFile).process();
return new DecoratorRewriter(typeChecker, sourceFile, sourceMapper).process();
}

export function visitClassContent(
classDecl: ts.ClassDeclaration, rewriter: Rewriter, decoratorVisitor?: DecoratorClassVisitor) {
let pos = classDecl.getStart();
if (decoratorVisitor) {
// strip out decorators if needed
ts.forEachChild(classDecl, child => {
if (child.kind !== ts.SyntaxKind.Decorator) {
return;
}
// Note: The getFullStart() of the first decorator is the same
// as the getFullStart() of the class declaration.
// Therefore, we need to use Math.max to not print the whitespace
// of the class again.
const childStart = Math.max(pos, child.getFullStart());
rewriter.writeRange(classDecl, pos, childStart);
if (decoratorVisitor.maybeProcessDecorator(child, childStart)) {
pos = child.getEnd();
}
});
}
if (classDecl.members.length > 0) {
rewriter.writeRange(classDecl, pos, classDecl.members[0].getFullStart());
for (let member of classDecl.members) {
rewriter.visit(member);
}
pos = classDecl.getLastToken().getFullStart();
}
// At this point, we've emitted up through the final child of the class, so all that
// remains is the trailing whitespace and closing curly brace.
// The final character owned by the class node should always be a '}',
// or we somehow got the AST wrong and should report an error.
// (Any whitespace or semicolon following the '}' will be part of the next Node.)
if (rewriter.file.text[classDecl.getEnd() - 1] !== '}') {
rewriter.error(classDecl, 'unexpected class terminator');
}
rewriter.writeRange(classDecl, pos, classDecl.getEnd() - 1);
if (decoratorVisitor) {
decoratorVisitor.emitMetadataAsStaticProperties();
}
rewriter.emit('}');
}
Loading

0 comments on commit 802eb2f

Please sign in to comment.