tsickle
Version:
Transpile TypeScript code to JavaScript with Closure annotations.
314 lines (312 loc) • 13.7 kB
JavaScript
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
"use strict";
var __extends = (this && this.__extends) || function (d, b) {
for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
var ts = require("typescript");
var decorators_1 = require("./decorators");
var rewriter_1 = require("./rewriter");
var type_translator_1 = require("./type-translator");
var util_1 = require("./util");
exports.ANNOTATION_SUPPORT_CODE = "\ninterface DecoratorInvocation {\n type: Function;\n args?: any[];\n}\n";
// ClassRewriter 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.
var ClassRewriter = (function (_super) {
__extends(ClassRewriter, _super);
function ClassRewriter(typeChecker, sourceFile) {
var _this = _super.call(this, sourceFile) || this;
_this.typeChecker = typeChecker;
return _this;
}
/**
* Determines whether the given decorator should be re-written as an annotation.
*/
ClassRewriter.prototype.shouldLower = function (decorator) {
for (var _i = 0, _a = decorators_1.getDecoratorDeclarations(decorator, this.typeChecker); _i < _a.length; _i++) {
var d = _a[_i];
// Switch to the TS JSDoc parser in the future to avoid false positives here.
// For example using '@Annotation' in a true comment.
// However, a new TS API would be needed, track at
// https://github.com/Microsoft/TypeScript/issues/7393.
var commentNode = d;
// Not handling PropertyAccess expressions here, because they are
// filtered earlier.
if (commentNode.kind === ts.SyntaxKind.VariableDeclaration) {
if (!commentNode.parent)
continue;
commentNode = commentNode.parent;
}
// Go up one more level to VariableDeclarationStatement, where usually
// the comment lives. If the declaration has an 'export', the
// VDList.getFullText will not contain the comment.
if (commentNode.kind === ts.SyntaxKind.VariableDeclarationList) {
if (!commentNode.parent)
continue;
commentNode = commentNode.parent;
}
var range = ts.getLeadingCommentRanges(commentNode.getFullText(), 0);
if (!range)
continue;
for (var _b = 0, range_1 = range; _b < range_1.length; _b++) {
var _c = range_1[_b], pos = _c.pos, end = _c.end;
var jsDocText = commentNode.getFullText().substring(pos, end);
if (jsDocText.includes('@Annotation'))
return true;
}
}
return false;
};
ClassRewriter.prototype.decoratorsToLower = function (n) {
var _this = this;
if (n.decorators) {
return n.decorators.filter(function (d) { return _this.shouldLower(d); });
}
return [];
};
/**
* process is the main entry point, rewriting a single class node.
*/
ClassRewriter.prototype.process = function (node) {
var _this = this;
if (node.decorators) {
var 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.)
var pos = node.getFullStart();
ts.forEachChild(node, function (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.
*/
ClassRewriter.prototype.gatherConstructor = function (ctor) {
var ctorParameters = [];
var hasDecoratedParam = false;
for (var _i = 0, _a = ctor.parameters; _i < _a.length; _i++) {
var param = _a[_i];
var paramCtor = void 0;
var decorators = void 0;
if (param.decorators) {
decorators = this.decoratorsToLower(param);
hasDecoratedParam = decorators.length > 0;
}
if (param.type) {
// param has a type provided, e.g. "foo: Bar".
// Verify that "Bar" is a value (e.g. a constructor) and not just a type.
var sym = this.typeChecker.getTypeAtLocation(param.type).getSymbol();
if (sym && (sym.flags & ts.SymbolFlags.Value)) {
paramCtor = new type_translator_1.TypeTranslator(this.typeChecker, param.type).symbolToString(sym);
}
}
if (paramCtor || decorators) {
ctorParameters.push([paramCtor, decorators]);
}
else {
ctorParameters.push(null);
}
}
// Use the ctor parameter metadata only if the class or the ctor was decorated.
if (this.decorators || hasDecoratedParam) {
this.ctorParameters = ctorParameters;
}
};
/**
* gatherMethod grabs the decorators off a class method and emits nothing.
*/
ClassRewriter.prototype.gatherMethodOrProperty = function (method) {
if (!method.decorators)
return;
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');
return;
}
var name = method.name.text;
var decorators = this.decoratorsToLower(method);
if (decorators.length === 0)
return;
if (!this.propDecorators)
this.propDecorators = new Map();
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.
*/
ClassRewriter.prototype.maybeProcess = function (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.
var _a = new ClassRewriter(this.typeChecker, this.file).process(node), output = _a.output, diagnostics = _a.diagnostics;
(_b = this.diagnostics).push.apply(_b, diagnostics);
this.emit(output);
return true;
case ts.SyntaxKind.Constructor:
this.gatherConstructor(node);
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);
return false; // Proceed with ordinary emit of the method.
case ts.SyntaxKind.Decorator:
if (this.shouldLower(node)) {
// 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;
}
var _b;
};
/**
* emitMetadata emits the various gathered metadata, as static fields.
*/
ClassRewriter.prototype.emitMetadata = function () {
if (this.decorators) {
this.emit("static decorators: DecoratorInvocation[] = [\n");
for (var _i = 0, _a = this.decorators; _i < _a.length; _i++) {
var annotation = _a[_i];
this.emitDecorator(annotation);
this.emit(',\n');
}
this.emit('];\n');
}
if (this.decorators || this.ctorParameters) {
this.emit("/** @nocollapse */\n");
// ctorParameters may contain forward references in the type: field, so wrap in a function
// closure
this.emit("static ctorParameters: () => ({type: any, decorators?: DecoratorInvocation[]}|null)[] = () => [\n");
for (var _b = 0, _c = this.ctorParameters || []; _b < _c.length; _b++) {
var param = _c[_b];
if (!param) {
this.emit('null,\n');
continue;
}
var ctor = param[0], decorators = param[1];
this.emit("{type: " + ctor + ", ");
if (decorators) {
this.emit('decorators: [');
for (var _d = 0, decorators_2 = decorators; _d < decorators_2.length; _d++) {
var decorator = decorators_2[_d];
this.emitDecorator(decorator);
this.emit(', ');
}
this.emit(']');
}
this.emit('},\n');
}
this.emit("];\n");
}
if (this.propDecorators) {
this.emit('static propDecorators: {[key: string]: DecoratorInvocation[]} = {\n');
for (var _e = 0, _f = util_1.toArray(this.propDecorators.keys()); _e < _f.length; _e++) {
var name_1 = _f[_e];
this.emit("'" + name_1 + "': [");
for (var _g = 0, _h = this.propDecorators.get(name_1); _g < _h.length; _g++) {
var decorator = _h[_g];
this.emitDecorator(decorator);
this.emit(',');
}
this.emit('],\n');
}
this.emit('};\n');
}
};
ClassRewriter.prototype.emitDecorator = function (decorator) {
this.emit('{ type: ');
var expr = decorator.expression;
switch (expr.kind) {
case ts.SyntaxKind.Identifier:
// The decorator was a plain @Foo.
this.visit(expr);
break;
case ts.SyntaxKind.CallExpression:
// The decorator was a call, like @Foo(bar).
var call = expr;
this.visit(call.expression);
if (call.arguments.length) {
this.emit(', args: [');
for (var _i = 0, _a = call.arguments; _i < _a.length; _i++) {
var arg = _a[_i];
this.emit(arg.getText());
this.emit(', ');
}
this.emit(']');
}
break;
default:
this.errorUnimplementedKind(expr, 'gathering metadata');
this.emit('undefined');
}
this.emit(' }');
};
return ClassRewriter;
}(rewriter_1.Rewriter));
var DecoratorRewriter = (function (_super) {
__extends(DecoratorRewriter, _super);
function DecoratorRewriter(typeChecker, sourceFile) {
var _this = _super.call(this, sourceFile) || this;
_this.typeChecker = typeChecker;
return _this;
}
DecoratorRewriter.prototype.process = function () {
this.visit(this.file);
return this.getOutput();
};
DecoratorRewriter.prototype.maybeProcess = function (node) {
switch (node.kind) {
case ts.SyntaxKind.ClassDeclaration:
var _a = new ClassRewriter(this.typeChecker, this.file).process(node), output = _a.output, diagnostics = _a.diagnostics;
(_b = this.diagnostics).push.apply(_b, diagnostics);
this.emit(output);
return true;
default:
return false;
}
var _b;
};
return DecoratorRewriter;
}(rewriter_1.Rewriter));
function convertDecorators(typeChecker, sourceFile) {
type_translator_1.assertTypeChecked(sourceFile);
return new DecoratorRewriter(typeChecker, sourceFile).process();
}
exports.convertDecorators = convertDecorators;
//# sourceMappingURL=decorator-annotator.js.map