tsickle
Version:
Transpile TypeScript code to JavaScript with Closure annotations.
1,032 lines (1,031 loc) • 62.2 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 jsdoc = require("./jsdoc");
var rewriter_1 = require("./rewriter");
var type_translator_1 = require("./type-translator");
var util_1 = require("./util");
var decorator_annotator_1 = require("./decorator-annotator");
exports.convertDecorators = decorator_annotator_1.convertDecorators;
var es5processor_1 = require("./es5processor");
exports.processES5 = es5processor_1.processES5;
var modules_manifest_1 = require("./modules_manifest");
exports.ModulesManifest = modules_manifest_1.ModulesManifest;
var tsickle_compiler_host_1 = require("./tsickle_compiler_host");
exports.Pass = tsickle_compiler_host_1.Pass;
exports.TsickleCompilerHost = tsickle_compiler_host_1.TsickleCompilerHost;
/**
* Symbols that are already declared as externs in Closure, that should
* be avoided by tsickle's "declare ..." => externs.js conversion.
*/
exports.closureExternsBlacklist = [
'exports',
'global',
'module',
// ErrorConstructor is the interface of the Error object itself.
// tsickle detects that this is part of the TypeScript standard library
// and assumes it's part of the Closure standard library, but this
// assumption is wrong for ErrorConstructor. To properly handle this
// we'd somehow need to map methods defined on the ErrorConstructor
// interface into properties on Closure's Error object, but for now it's
// simpler to just blacklist it.
'ErrorConstructor',
'Symbol',
'WorkerGlobalScope',
];
function formatDiagnostics(diags) {
return diags
.map(function (d) {
var res = ts.DiagnosticCategory[d.category];
if (d.file) {
res += ' at ' + d.file.fileName + ':';
var _a = d.file.getLineAndCharacterOfPosition(d.start), line = _a.line, character = _a.character;
res += (line + 1) + ':' + (character + 1) + ':';
}
res += ' ' + ts.flattenDiagnosticMessageText(d.messageText, '\n');
return res;
})
.join('\n');
}
exports.formatDiagnostics = formatDiagnostics;
/** @return true if node has the specified modifier flag set. */
function hasModifierFlag(node, flag) {
return (ts.getCombinedModifierFlags(node) & flag) !== 0;
}
/**
* TypeScript allows you to write identifiers quoted, like:
* interface Foo {
* 'bar': string;
* 'complex name': string;
* }
* Foo.bar; // ok
* Foo['bar'] // ok
* Foo['complex name'] // ok
*
* In Closure-land, we want identify that the legal name 'bar' can become an
* ordinary field, but we need to skip strings like 'complex name'.
*/
function isValidClosurePropertyName(name) {
// In local experimentation, it appears that reserved words like 'var' and
// 'if' are legal JS and still accepted by Closure.
return /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name);
}
function isDtsFileName(fileName) {
return /\.d\.ts$/.test(fileName);
}
exports.isDtsFileName = isDtsFileName;
/** Returns the Closure name of a function parameter, special-casing destructuring. */
function getParameterName(param, index) {
switch (param.name.kind) {
case ts.SyntaxKind.Identifier:
var name_1 = rewriter_1.getIdentifierText(param.name);
// TypeScript allows parameters named "arguments", but Closure
// disallows this, even in externs.
if (name_1 === 'arguments')
name_1 = 'tsickle_arguments';
return name_1;
case ts.SyntaxKind.ArrayBindingPattern:
case ts.SyntaxKind.ObjectBindingPattern:
// Closure crashes if you put a binding pattern in the externs.
// Avoid this by just generating an unused name; the name is
// ignored anyway.
return "__" + index;
default:
// The above list of kinds is exhaustive. param.name is 'never' at this point.
var paramName = param.name;
throw new Error("unhandled function parameter kind: " + ts.SyntaxKind[paramName.kind]);
}
}
var VISIBILITY_FLAGS = ts.ModifierFlags.Private | ts.ModifierFlags.Protected | ts.ModifierFlags.Public;
/**
* A Rewriter subclass that adds Tsickle-specific (Closure translation) functionality.
*
* One Rewriter subclass manages .ts => .ts+Closure translation.
* Another Rewriter subclass manages .ts => externs translation.
*/
var ClosureRewriter = (function (_super) {
__extends(ClosureRewriter, _super);
function ClosureRewriter(program, file, options) {
var _this = _super.call(this, file) || this;
_this.program = program;
_this.options = options;
return _this;
}
/**
* Handles emittng the jsdoc for methods, including overloads.
* If overloaded, merges the signatures in the list of SignatureDeclarations into a single jsdoc.
* - Total number of parameters will be the maximum count found across all variants.
* - Different names at the same parameter index will be joined with "_or_"
* - Variable args (...type[] in TypeScript) will be output as "...type",
* except if found at the same index as another argument.
* @param fnDecls Pass > 1 declaration for overloads of same name
* @return The list of parameter names that should be used to emit the actual
* function statement; for overloads, name will have been merged.
*/
ClosureRewriter.prototype.emitFunctionType = function (fnDecls, extraTags) {
if (extraTags === void 0) { extraTags = []; }
var typeChecker = this.program.getTypeChecker();
var newDoc = extraTags;
var lens = fnDecls.map(function (fnDecl) { return fnDecl.parameters.length; });
var minArgsCount = Math.min.apply(Math, lens);
var maxArgsCount = Math.max.apply(Math, lens);
var isConstructor = fnDecls.find(function (d) { return d.kind === ts.SyntaxKind.Constructor; }) !== undefined;
// For each parameter index i, paramTags[i] is an array of parameters
// that can be found at index i. E.g.
// function foo(x: string)
// function foo(y: number, z: string)
// then paramTags[0] = [info about x, info about y].
var paramTags = [];
var returnTags = [];
for (var _i = 0, fnDecls_1 = fnDecls; _i < fnDecls_1.length; _i++) {
var fnDecl = fnDecls_1[_i];
// Construct the JSDoc comment by reading the existing JSDoc, if
// any, and merging it with the known types of the function
// parameters and return type.
var jsDoc = this.getJSDoc(fnDecl) || [];
// Copy all the tags other than @param/@return into the new
// JSDoc without any change; @param/@return are handled specially.
// TODO: there may be problems if an annotation doesn't apply to all overloads;
// is it worth checking for this and erroring?
for (var _a = 0, jsDoc_1 = jsDoc; _a < jsDoc_1.length; _a++) {
var tag = jsDoc_1[_a];
if (tag.tagName === 'param' || tag.tagName === 'return')
continue;
newDoc.push(tag);
}
// Add @abstract on "abstract" declarations.
if (hasModifierFlag(fnDecl, ts.ModifierFlags.Abstract)) {
newDoc.push({ tagName: 'abstract' });
}
// Merge the parameters into a single list of merged names and list of types
var sig = typeChecker.getSignatureFromDeclaration(fnDecl);
for (var i = 0; i < sig.declaration.parameters.length; i++) {
var paramNode = sig.declaration.parameters[i];
var name_2 = getParameterName(paramNode, i);
var isThisParam = name_2 === 'this';
var newTag = {
tagName: isThisParam ? 'this' : 'param',
optional: paramNode.initializer !== undefined || paramNode.questionToken !== undefined,
parameterName: isThisParam ? undefined : name_2,
};
var type = typeChecker.getTypeAtLocation(paramNode);
if (paramNode.dotDotDotToken !== undefined) {
newTag.restParam = true;
// In TypeScript you write "...x: number[]", but in Closure
// you don't write the array: "@param {...number} x". Unwrap
// the Array<> wrapper.
type = type.typeArguments[0];
}
newTag.type = this.typeToClosure(fnDecl, type);
for (var _b = 0, jsDoc_2 = jsDoc; _b < jsDoc_2.length; _b++) {
var _c = jsDoc_2[_b], tagName = _c.tagName, parameterName = _c.parameterName, text = _c.text;
if (tagName === 'param' && parameterName === newTag.parameterName) {
newTag.text = text;
break;
}
}
if (!paramTags[i])
paramTags.push([]);
paramTags[i].push(newTag);
}
// Return type.
if (!isConstructor) {
var retType = typeChecker.getReturnTypeOfSignature(sig);
var retTypeString = this.typeToClosure(fnDecl, retType);
var returnDoc = void 0;
for (var _d = 0, jsDoc_3 = jsDoc; _d < jsDoc_3.length; _d++) {
var _e = jsDoc_3[_d], tagName = _e.tagName, text = _e.text;
if (tagName === 'return') {
returnDoc = text;
break;
}
}
returnTags.push({
tagName: 'return',
type: retTypeString,
text: returnDoc,
});
}
}
// Merge the JSDoc tags for each overloaded parameter.
var foundOptional = false;
for (var i = 0; i < maxArgsCount; i++) {
var paramTag = jsdoc.merge(paramTags[i]);
// If any overload marks this param as a ..., mark it ... in the
// merged output.
if (paramTags[i].find(function (t) { return t.restParam === true; }) !== undefined) {
paramTag.restParam = true;
}
// If any overload marks this param optional, mark it optional in the
// merged output. Also mark parameters following optional as optional,
// even if they are not, since Closure restricts this, see
// https://github.com/google/closure-compiler/issues/2314
var optional = paramTags[i].find(function (t) { return t.optional === true; }) !== undefined || foundOptional;
if (!paramTag.restParam && (optional || i >= minArgsCount)) {
foundOptional = true;
paramTag.type += '=';
}
newDoc.push(paramTag);
}
// Merge the JSDoc tags for each overloaded return.
if (!isConstructor) {
newDoc.push(jsdoc.merge(returnTags));
}
this.emit('\n' + jsdoc.toString(newDoc));
return newDoc.filter(function (t) { return t.tagName === 'param'; }).map(function (t) { return t.parameterName; });
};
/**
* Returns null if there is no existing comment.
*/
ClosureRewriter.prototype.getJSDoc = function (node) {
var text = node.getFullText();
var comments = ts.getLeadingCommentRanges(text, 0);
if (!comments || comments.length === 0)
return null;
// JS compiler only considers the last comment significant.
var _a = comments[comments.length - 1], pos = _a.pos, end = _a.end;
var comment = text.substring(pos, end);
var parsed = jsdoc.parse(comment);
if (!parsed)
return null;
if (parsed.warnings) {
var start = node.getFullStart() + pos;
this.diagnostics.push({
file: this.file,
start: start,
length: node.getStart() - start,
messageText: parsed.warnings.join('\n'),
category: ts.DiagnosticCategory.Warning,
code: 0,
});
}
return parsed.tags;
};
/** Emits a type annotation in JSDoc, or {?} if the type is unavailable. */
ClosureRewriter.prototype.emitJSDocType = function (node, additionalDocTag, type) {
this.emit(' /**');
if (additionalDocTag) {
this.emit(' ' + additionalDocTag);
}
this.emit(" @type {" + this.typeToClosure(node, type) + "} */");
};
/**
* Convert a TypeScript ts.Type into the equivalent Closure type.
*
* @param context The ts.Node containing the type reference; used for resolving symbols
* in context.
* @param type The type to translate; if not provided, the Node's type will be used.
*/
ClosureRewriter.prototype.typeToClosure = function (context, type) {
var _this = this;
if (this.options.untyped) {
return '?';
}
var typeChecker = this.program.getTypeChecker();
if (!type) {
type = typeChecker.getTypeAtLocation(context);
}
var translator = new type_translator_1.TypeTranslator(typeChecker, context, this.options.typeBlackListPaths);
translator.warn = function (msg) { return _this.debugWarn(context, msg); };
return translator.translate(type);
};
/**
* debug logs a debug warning. These should only be used for cases
* where tsickle is making a questionable judgement about what to do.
* By default, tsickle does not report any warnings to the caller,
* and warnings are hidden behind a debug flag, as warnings are only
* for tsickle to debug itself.
*/
ClosureRewriter.prototype.debugWarn = function (node, messageText) {
if (!this.options.logWarning)
return;
// Use a ts.Diagnosic so that the warning includes context and file offets.
var diagnostic = {
file: this.file,
start: node.getStart(),
length: node.getEnd() - node.getStart(), messageText: messageText,
category: ts.DiagnosticCategory.Warning,
code: 0,
};
this.options.logWarning(diagnostic);
};
return ClosureRewriter;
}(rewriter_1.Rewriter));
/** Annotator translates a .ts to a .ts with Closure annotations. */
var Annotator = (function (_super) {
__extends(Annotator, _super);
function Annotator(program, file, options, host, tsOpts) {
var _this = _super.call(this, program, file, options) || this;
_this.host = host;
_this.tsOpts = tsOpts;
/** Exported symbol names that have been generated by expanding an "export * from ...". */
_this.generatedExports = new Set();
/** Externs determined by an exporting decorator. */
_this.exportingDecoratorExterns = [];
_this.externsWriter = new ExternsWriter(program, file, options);
return _this;
}
Annotator.prototype.annotate = function () {
this.visit(this.file);
var externs = this.externsWriter.getOutput();
var annotated = this.getOutput();
var externsSource = null;
if (externs.output.length > 0 || this.exportingDecoratorExterns.length > 0) {
externsSource = "/**\n * @externs\n * @suppress {duplicate}\n */\n// NOTE: generated by tsickle, do not edit.\n" + externs.output +
this.formatExportingDecoratorExterns();
}
return {
output: annotated.output,
externs: externsSource,
diagnostics: externs.diagnostics.concat(annotated.diagnostics),
sourceMap: annotated.sourceMap,
};
};
Annotator.prototype.getExportDeclarationNames = function (node) {
var _this = this;
switch (node.kind) {
case ts.SyntaxKind.VariableStatement:
var varDecl = node;
return varDecl.declarationList.declarations.map(function (d) { return _this.getExportDeclarationNames(d)[0]; });
case ts.SyntaxKind.VariableDeclaration:
case ts.SyntaxKind.FunctionDeclaration:
case ts.SyntaxKind.InterfaceDeclaration:
case ts.SyntaxKind.ClassDeclaration:
var decl = node;
if (!decl.name || decl.name.kind !== ts.SyntaxKind.Identifier) {
break;
}
return [decl.name];
case ts.SyntaxKind.TypeAliasDeclaration:
var typeAlias = node;
return [typeAlias.name];
default:
break;
}
this.error(node, "unsupported export declaration " + ts.SyntaxKind[node.kind] + ": " + node.getText());
return [];
};
/**
* Emits an ES6 export for the ambient declaration behind node, if it is indeed exported.
*/
Annotator.prototype.maybeEmitAmbientDeclarationExport = function (node) {
// In TypeScript, `export declare` simply generates no code in the exporting module, but does
// generate a regular import in the importing module.
// For Closure Compiler, such declarations must still be exported, so that importing code in
// other modules can reference them. Because tsickle generates global symbols for such types,
// the appropriate semantics are referencing the global name.
if (this.options.untyped || !hasModifierFlag(node, ts.ModifierFlags.Export)) {
return;
}
var declNames = this.getExportDeclarationNames(node);
for (var _i = 0, declNames_1 = declNames; _i < declNames_1.length; _i++) {
var decl = declNames_1[_i];
var sym = this.program.getTypeChecker().getSymbolAtLocation(decl);
var isValue = sym.flags & ts.SymbolFlags.Value;
var declName = rewriter_1.getIdentifierText(decl);
if (node.kind === ts.SyntaxKind.VariableStatement) {
// For variables, TypeScript rewrites every reference to the variable name as an
// "exports." access, to maintain mutable ES6 exports semantics. Indirecting through the
// window object means we reference the correct global symbol. Closure Compiler does
// understand that "var foo" in externs corresponds to "window.foo".
this.emit("\nexports." + declName + " = window." + declName + ";\n");
}
else if (!isValue) {
// Non-value objects do not exist at runtime, so we cannot access the symbol (it only
// exists in externs). Export them as a typedef, which forwards to the type in externs.
this.emit("\n/** @typedef {" + declName + "} */\nexports." + declName + ";\n");
}
else {
this.emit("\nexports." + declName + " = " + declName + ";\n");
}
}
};
Annotator.prototype.formatExportingDecoratorExterns = function () {
if (this.exportingDecoratorExterns.length === 0) {
return '';
}
return '\n' + this.exportingDecoratorExterns.map(function (name) { return "var " + name + ";\n"; }).join('');
};
/**
* Examines a ts.Node and decides whether to do special processing of it for output.
*
* @return True if the ts.Node has been handled, false if we should
* emit it as is and visit its children.
*/
Annotator.prototype.maybeProcess = function (node) {
if (hasModifierFlag(node, ts.ModifierFlags.Ambient) || isDtsFileName(this.file.fileName)) {
this.externsWriter.visit(node);
// An ambient declaration declares types for TypeScript's benefit, so we want to skip Tsickle
// conversion of its contents.
this.writeRange(node.getFullStart(), node.getEnd());
// ... but it might need to be exported for downstream importing code.
this.maybeEmitAmbientDeclarationExport(node);
return true;
}
if (decorators_1.hasExportingDecorator(node, this.program.getTypeChecker())) {
var name_3 = node.name;
if (name_3) {
this.exportingDecoratorExterns.push(name_3.getText());
}
}
switch (node.kind) {
case ts.SyntaxKind.ImportDeclaration:
return this.emitImportDeclaration(node);
case ts.SyntaxKind.ExportDeclaration:
var exportDecl = node;
this.writeRange(node.getFullStart(), node.getStart());
this.emit('export');
if (!exportDecl.exportClause && exportDecl.moduleSpecifier) {
// It's an "export * from ..." statement.
// Rewrite it to re-export each exported symbol directly.
var exports_1 = this.expandSymbolsFromExportStar(exportDecl);
this.emit(" {" + exports_1.join(',') + "}");
}
else {
if (exportDecl.exportClause)
this.visit(exportDecl.exportClause);
}
if (exportDecl.moduleSpecifier) {
this.emit(' from');
this.writeModuleSpecifier(exportDecl.moduleSpecifier);
}
this.emit(';');
return true;
case ts.SyntaxKind.InterfaceDeclaration:
this.emitInterface(node);
// Emit the TS interface verbatim, with no tsickle processing of properties.
this.writeRange(node.getFullStart(), node.getEnd());
return true;
case ts.SyntaxKind.VariableDeclaration:
var varDecl = node;
// Only emit a type annotation when it's a plain variable and
// not a binding pattern, as Closure doesn't(?) have a syntax
// for annotating binding patterns. See issue #128.
if (varDecl.name.kind === ts.SyntaxKind.Identifier) {
this.emitJSDocType(varDecl);
}
return false;
case ts.SyntaxKind.ClassDeclaration:
var classNode = node;
this.visitClassDeclaration(classNode);
return true;
case ts.SyntaxKind.PublicKeyword:
case ts.SyntaxKind.PrivateKeyword:
// The "public"/"private" keywords are encountered in two places:
// 1) In class fields (which don't appear in the transformed output).
// 2) In "parameter properties", e.g.
// constructor(/** @export */ public foo: string).
// In case 2 it's important to not emit that JSDoc in the generated
// constructor, as this is illegal for Closure. It's safe to just
// always skip comments preceding the 'public' keyword.
// See test_files/parameter_properties.ts.
this.writeNode(node, /* skipComments */ true);
return true;
case ts.SyntaxKind.Constructor:
var ctor = node;
this.emitFunctionType([ctor]);
// Write the "constructor(...) {" bit, but iterate through any
// parameters if given so that we can examine them more closely.
var offset = ctor.getStart();
if (ctor.parameters.length) {
for (var _i = 0, _a = ctor.parameters; _i < _a.length; _i++) {
var param = _a[_i];
this.writeRange(offset, param.getFullStart());
this.visit(param);
offset = param.getEnd();
}
}
this.writeRange(offset, node.getEnd());
return true;
case ts.SyntaxKind.ArrowFunction:
// It's difficult to annotate arrow functions due to a bug in
// TypeScript (see tsickle issue 57). For now, just pass them
// through unannotated.
return false;
case ts.SyntaxKind.FunctionDeclaration:
case ts.SyntaxKind.MethodDeclaration:
case ts.SyntaxKind.GetAccessor:
case ts.SyntaxKind.SetAccessor:
var fnDecl = node;
if (!fnDecl.body) {
if (hasModifierFlag(fnDecl, ts.ModifierFlags.Abstract)) {
this.emitFunctionType([fnDecl]);
// Abstract functions look like
// abstract foo();
// Emit the function as normal, except:
// - remove the "abstract"
// - change the return type to "void"
// - replace the trailing semicolon with an empty block {}
// To do so, skip all modifiers before the function name, and
// emit up to the end of the parameter list / return type.
if (!fnDecl.name) {
// Can you even have an unnamed abstract function?
this.error(fnDecl, 'anonymous abstract function');
return false;
}
this.writeRange(fnDecl.name.getStart(), fnDecl.parameters.end);
this.emit(') {}');
return true;
}
// Functions are allowed to not have bodies in the presence
// of overloads. It's not clear how to translate these overloads
// into Closure types, so skip them for now.
return false;
}
this.emitFunctionType([fnDecl]);
this.writeRange(fnDecl.getStart(), fnDecl.body.getFullStart());
this.visit(fnDecl.body);
return true;
case ts.SyntaxKind.TypeAliasDeclaration:
this.writeNode(node);
this.visitTypeAlias(node);
return true;
case ts.SyntaxKind.EnumDeclaration:
return this.maybeProcessEnum(node);
case ts.SyntaxKind.TypeAssertionExpression:
case ts.SyntaxKind.AsExpression:
// Both of these cases are AssertionExpressions.
var typeAssertion = node;
this.emitJSDocType(typeAssertion);
// When TypeScript emits JS, it removes one layer of "redundant"
// parens, but we need them for the Closure type assertion. Work
// around this by using two parens. See test_files/coerce.*.
// TODO: the comment is currently dropped from pure assignments due to
// https://github.com/Microsoft/TypeScript/issues/9873
this.emit('((');
this.writeNode(node);
this.emit('))');
return true;
case ts.SyntaxKind.NonNullExpression:
var nnexpr = node;
var type = this.program.getTypeChecker().getTypeAtLocation(nnexpr.expression);
if (type.flags & ts.TypeFlags.Union) {
var nonNullUnion = type
.types.filter(function (t) { return (t.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined)) === 0; });
var typeCopy = Object.assign({}, type);
typeCopy.types = nonNullUnion;
type = typeCopy;
}
this.emitJSDocType(nnexpr, undefined, type);
// See comment above.
this.emit('((');
this.writeNode(nnexpr.expression);
this.emit('))');
return true;
case ts.SyntaxKind.PropertyDeclaration:
var jsDoc = this.getJSDoc(node);
if (jsDoc && jsDoc.length > 0 && node.getFirstToken()) {
this.emit('\n');
this.emit(jsdoc.toString(jsDoc));
this.writeRange(node.getFirstToken().getStart(), node.getEnd());
return true;
}
break;
default:
break;
}
return false;
};
/**
* Given a "export * from ..." statement, gathers the symbol names it actually
* exports to be used in a statement like "export {foo, bar, baz} from ...".
*
* This is necessary because TS transpiles "export *" by just doing a runtime loop
* over the target module's exports, which means Closure won't see the declarations/types
* that are exported.
*/
Annotator.prototype.expandSymbolsFromExportStar = function (exportDecl) {
// You can't have an "export *" without a module specifier.
var moduleSpecifier = exportDecl.moduleSpecifier;
var typeChecker = this.program.getTypeChecker();
// Gather the names of local exports, to avoid reexporting any
// names that are already locally exported.
// To find symbols declared like
// export {foo} from ...
// we must also query for "Alias", but that unfortunately also brings in
// import {foo} from ...
// so the latter is filtered below.
var locals = typeChecker.getSymbolsInScope(this.file, ts.SymbolFlags.Export | ts.SymbolFlags.Alias);
var localSet = new Set();
for (var _i = 0, locals_1 = locals; _i < locals_1.length; _i++) {
var local = locals_1[_i];
if (local.declarations &&
local.declarations.some(function (d) { return d.kind === ts.SyntaxKind.ImportSpecifier; })) {
continue;
}
localSet.add(local.name);
}
// Expand the export list, then filter it to the symbols we want to reexport.
var exports = typeChecker.getExportsOfModule(typeChecker.getSymbolAtLocation(moduleSpecifier));
var reexports = new Set();
for (var _a = 0, exports_2 = exports; _a < exports_2.length; _a++) {
var sym = exports_2[_a];
var name_4 = rewriter_1.unescapeName(sym.name);
if (localSet.has(name_4)) {
// This name is shadowed by a local definition, such as:
// - export var foo ...
// - export {foo} from ...
continue;
}
if (this.generatedExports.has(name_4)) {
// Already exported via an earlier expansion of an "export * from ...".
continue;
}
this.generatedExports.add(name_4);
reexports.add(name_4);
}
return util_1.toArray(reexports.keys());
};
/**
* Convert from implicit `import {} from 'pkg'` to `import {} from 'pkg/index'.
* TypeScript supports the shorthand, but not all ES6 module loaders do.
* Workaround for https://github.com/Microsoft/TypeScript/issues/12597
*/
Annotator.prototype.writeModuleSpecifier = function (moduleSpecifier) {
if (moduleSpecifier.kind !== ts.SyntaxKind.StringLiteral) {
throw new Error("unhandled moduleSpecifier kind: " + ts.SyntaxKind[moduleSpecifier.kind]);
}
var moduleId = moduleSpecifier.text;
if (this.options.convertIndexImportShorthand) {
if (!this.tsOpts || !this.host) {
throw new Error('option convertIndexImportShorthand requires that annotate be called with a TypeScript host and options.');
}
var resolved = ts.resolveModuleName(moduleId, this.file.fileName, this.tsOpts, this.host);
if (resolved && resolved.resolvedModule) {
var resolvedModule = resolved.resolvedModule.resolvedFileName.replace(/(\.d)?\.ts$/, '');
var requestedModule = moduleId.replace(/\.js$/, '');
// If the imported module resolves to foo/index, but the specified module was foo, then we
// append the /index.
if (resolvedModule.substr(resolvedModule.length - 6) === '/index' &&
requestedModule.substr(requestedModule.length - 6) !== '/index') {
moduleId += '/index';
}
}
}
this.emit(" '" + moduleId + "'");
};
/**
* Handles emit of an "import ..." statement.
* We need to do a bit of rewriting so that imported types show up under the
* correct name in JSDoc.
* @return true if the decl was handled, false to allow default processing.
*/
Annotator.prototype.emitImportDeclaration = function (decl) {
this.writeRange(decl.getFullStart(), decl.getStart());
this.emit('import');
var importClause = decl.importClause;
if (!importClause) {
// import './foo';
this.writeModuleSpecifier(decl.moduleSpecifier);
this.emit(';');
return true;
}
else if (importClause.name || (importClause.namedBindings &&
importClause.namedBindings.kind === ts.SyntaxKind.NamedImports)) {
this.visit(importClause);
this.emit(' from');
this.writeModuleSpecifier(decl.moduleSpecifier);
this.emit(';');
// importClause.name implies
// import a from ...;
// namedBindings being NamedImports implies
// import {a as b} from ...;
//
// Both of these forms create a local name "a", which after
// TypeScript CommonJS compilation will become some renamed
// variable like "module_1.a". But a user might still use plain
// "a" in some JSDoc comment, so gather up these local names for
// imports and make an alias for each for JSDoc purposes.
if (!this.options.untyped) {
var localNames = void 0;
if (importClause.name) {
// import a from ...;
localNames = [rewriter_1.getIdentifierText(importClause.name)];
}
else {
// import {a as b} from ...;
var namedImports = importClause.namedBindings;
localNames = namedImports.elements.map(function (imp) { return rewriter_1.getIdentifierText(imp.name); });
}
for (var _i = 0, localNames_1 = localNames; _i < localNames_1.length; _i++) {
var name_5 = localNames_1[_i];
// This may look like a self-reference but TypeScript will rename the
// right-hand side!
this.emit("\nconst " + name_5 + ": NeverTypeCheckMe = " + name_5 + "; /* local alias for Closure JSDoc */");
}
}
return true;
}
else if (importClause.namedBindings &&
importClause.namedBindings.kind === ts.SyntaxKind.NamespaceImport) {
// import * as foo from ...;
this.visit(importClause);
this.emit(' from');
this.writeModuleSpecifier(decl.moduleSpecifier);
this.emit(';');
return true;
}
else {
this.errorUnimplementedKind(decl, 'unexpected kind of import');
return false; // Use default processing.
}
};
Annotator.prototype.visitClassDeclaration = function (classDecl) {
var jsDoc = this.getJSDoc(classDecl) || [];
if (hasModifierFlag(classDecl, ts.ModifierFlags.Abstract)) {
jsDoc.push({ tagName: 'abstract' });
}
if (!this.options.untyped && classDecl.heritageClauses) {
// If the class has "extends Foo", that is preserved in the ES6 output
// and we don't need to do anything. But if it has "implements Foo",
// that is a TS-specific thing and we need to translate it to the
// the Closure "@implements {Foo}".
for (var _i = 0, _a = classDecl.heritageClauses; _i < _a.length; _i++) {
var heritage = _a[_i];
if (!heritage.types)
continue;
if (heritage.token === ts.SyntaxKind.ImplementsKeyword) {
for (var _b = 0, _c = heritage.types; _b < _c.length; _b++) {
var impl = _c[_b];
var tagName = 'implements';
// We can only @implements an interface, not a class.
// But it's fine to translate TS "implements Class" into Closure
// "@extends {Class}" because this is just a type hint.
var typeChecker = this.program.getTypeChecker();
var sym = typeChecker.getSymbolAtLocation(impl.expression);
if (sym.flags & ts.SymbolFlags.TypeAlias) {
// It's implementing a type alias. Follow the type alias back
// to the original symbol to check whether it's a type or a value.
var type = typeChecker.getDeclaredTypeOfSymbol(sym);
if (!type.symbol) {
// It's not clear when this can happen, but if it does all we
// do is fail to emit the @implements, which isn't so harmful.
continue;
}
sym = type.symbol;
}
if (sym.flags & ts.SymbolFlags.Class) {
tagName = 'extends';
}
else if (sym.flags & ts.SymbolFlags.Value) {
// If the symbol was already in the value namespace, then it will
// not be a type in the Closure output (because Closure collapses
// the type and value namespaces). Just ignore the implements.
continue;
}
jsDoc.push({ tagName: tagName, type: impl.getText() });
}
}
}
}
this.emit('\n');
if (jsDoc.length > 0)
this.emit(jsdoc.toString(jsDoc));
if (classDecl.members.length > 0) {
// We must visit all members individually, to strip out any
// /** @export */ annotations that show up in the constructor
// and to annotate methods.
this.writeRange(classDecl.getStart(), classDecl.members[0].getFullStart());
for (var _d = 0, _e = classDecl.members; _d < _e.length; _d++) {
var member = _e[_d];
this.visit(member);
}
}
else {
this.writeRange(classDecl.getStart(), classDecl.getLastToken().getFullStart());
}
this.writeNode(classDecl.getLastToken());
this.emitTypeAnnotationsHelper(classDecl);
return true;
};
Annotator.prototype.emitInterface = function (iface) {
if (this.options.untyped)
return;
// If this symbol is both a type and a value, we cannot emit both into Closure's
// single namespace.
var sym = this.program.getTypeChecker().getSymbolAtLocation(iface.name);
if (sym.flags & ts.SymbolFlags.Value)
return;
this.emit("\n/** @record */\n");
if (hasModifierFlag(iface, ts.ModifierFlags.Export))
this.emit('export ');
var name = rewriter_1.getIdentifierText(iface.name);
this.emit("function " + name + "() {}\n");
if (iface.typeParameters) {
this.emit("// TODO: type parameters.\n");
}
if (iface.heritageClauses) {
this.emit("// TODO: derived interfaces.\n");
}
var memberNamespace = [name, 'prototype'];
for (var _i = 0, _a = iface.members; _i < _a.length; _i++) {
var elem = _a[_i];
this.visitProperty(memberNamespace, elem);
}
};
// emitTypeAnnotationsHelper produces a
// _tsickle_typeAnnotationsHelper() where none existed in the
// original source. It's necessary in the case where TypeScript
// syntax specifies there are additional properties on the class,
// because to declare these in Closure you must declare these in a
// method somewhere.
Annotator.prototype.emitTypeAnnotationsHelper = function (classDecl) {
var _this = this;
// Gather parameter properties from the constructor, if it exists.
var ctors = [];
var paramProps = [];
var nonStaticProps = [];
var staticProps = [];
for (var _i = 0, _a = classDecl.members; _i < _a.length; _i++) {
var member = _a[_i];
if (member.kind === ts.SyntaxKind.Constructor) {
ctors.push(member);
}
else if (member.kind === ts.SyntaxKind.PropertyDeclaration) {
var prop = member;
var isStatic = hasModifierFlag(prop, ts.ModifierFlags.Static);
if (isStatic) {
staticProps.push(prop);
}
else {
nonStaticProps.push(prop);
}
}
}
if (ctors.length > 0) {
var ctor = ctors[0];
paramProps = ctor.parameters.filter(function (p) { return hasModifierFlag(p, VISIBILITY_FLAGS); });
}
if (nonStaticProps.length === 0 && paramProps.length === 0 && staticProps.length === 0) {
// There are no members so we don't need to emit any type
// annotations helper.
return;
}
if (!classDecl.name)
return;
var className = rewriter_1.getIdentifierText(classDecl.name);
this.emit("\n\nfunction " + className + "_tsickle_Closure_declarations() {\n");
staticProps.forEach(function (p) { return _this.visitProperty([className], p); });
var memberNamespace = [className, 'prototype'];
nonStaticProps.forEach(function (p) { return _this.visitProperty(memberNamespace, p); });
paramProps.forEach(function (p) { return _this.visitProperty(memberNamespace, p); });
this.emit("}\n");
};
Annotator.prototype.propertyName = function (prop) {
if (!prop.name)
return null;
switch (prop.name.kind) {
case ts.SyntaxKind.Identifier:
return rewriter_1.getIdentifierText(prop.name);
case ts.SyntaxKind.StringLiteral:
// E.g. interface Foo { 'bar': number; }
// If 'bar' is a name that is not valid in Closure then there's nothing we can do.
return prop.name.text;
default:
return null;
}
};
Annotator.prototype.visitProperty = function (namespace, p) {
var name = this.propertyName(p);
if (!name) {
this.emit("/* TODO: handle strange member:\n" + this.escapeForComment(p.getText()) + "\n*/\n");
return;
}
var tags = this.getJSDoc(p) || [];
tags.push({ tagName: 'type', type: this.typeToClosure(p) });
// Avoid printing annotations that can conflict with @type
// This avoids Closure's error "type annotation incompatible with other annotations"
this.emit(jsdoc.toString(tags, ['param', 'return']));
namespace = namespace.concat([name]);
this.emit(namespace.join('.') + ";\n");
};
Annotator.prototype.visitTypeAlias = function (node) {
if (this.options.untyped)
return;
// Write a Closure typedef, which involves an unused "var" declaration.
// Note: in the case of an export, we cannot emit a literal "var" because
// TypeScript drops exports that are never assigned to (and Closure
// requires us to not assign to typedef exports). Instead, emit the
// "exports.foo;" line directly in that case.
this.emit("\n/** @typedef {" + this.typeToClosure(node) + "} */\n");
if (hasModifierFlag(node, ts.ModifierFlags.Export)) {
this.emit('exports.');
}
else {
this.emit('var ');
}
this.emit(node.name.getText() + ";\n");
};
/** Processes an EnumDeclaration or returns false for ordinary processing. */
Annotator.prototype.maybeProcessEnum = function (node) {
if (hasModifierFlag(node, ts.ModifierFlags.Const)) {
// const enums disappear after TS compilation and consequently need no
// help from tsickle.
return false;
}
// Gather the members of enum, saving the constant value or
// initializer expression in the case of a non-constant value.
var members = new Map();
var i = 0;
for (var _i = 0, _a = node.members; _i < _a.length; _i++) {
var member = _a[_i];
var memberName = member.name.getText();
if (member.initializer) {
var enumConstValue = this.program.getTypeChecker().getConstantValue(member);
if (enumConstValue !== undefined) {
members.set(memberName, enumConstValue);
i = enumConstValue + 1;
}
else {
// Non-constant enum value. Save the initializer expression for
// emitting as-is.
// Note: if the member's initializer expression refers to another
// value within the enum (e.g. something like
// enum Foo {
// Field1,
// Field2 = Field1 + something(),
// }
// Then when we emit the initializer we produce invalid code because
// on the Closure side it has to be written "Foo.Field1 + something()".
// Hopefully this doesn't come up often -- if the enum instead has
// something like
// Field2 = Field1 + 3,
// then it's still a constant expression and we inline the constant
// value in the above branch of this "if" statement.
members.set(memberName, member.initializer);
}
}
else {
members.set(memberName, i);
i++;
}
}
// Emit the enum declaration, which looks like:
// type Foo = number;
// let Foo: any = {};
// We use an "any" here rather than a more specific type because
// we think TypeScript has already checked types for us, and it's
// a bit difficult to provide a type that matches all the interfaces
// expected of an enum (in particular, it is keyable both by
// string and number).
// We don't emit a specific Closure type for the enum because it's
// also difficult to make work: for example, we can't make the name
// both a typedef and an indexable object if we export it.
this.emit('\n');
var name = node.name.getText();
var isExported = hasModifierFlag(node, ts.ModifierFlags.Export);
if (isExported)
this.emit('export ');
this.emit("type " + name + " = number;\n");
if (isExported)
this.emit('export ');
this.emit("let " + name + ": any = {};\n");
// Emit foo.BAR = 0; lines.
for (var _b = 0, _c = util_1.toArray(members.keys()); _b < _c.length; _b++) {
var member = _c[_b];
if (!this.options.untyped)
this.emit("/** @type {number} */\n");
this.emit(name + "." + member + " = ");
var value = members.get(member);
if (typeof value === 'number') {
this.emit(value.toString());
}
else {
this.visit(value);
}
this.emit(';\n');
}
// Emit foo[foo.BAR] = 'BAR'; lines.
for (var _d = 0, _e = util_1.toArray(members.keys()); _d < _e.length; _d++) {
var member = _e[_d];
this.emit(name + "[" + name + "." + member + "] = \"" + member + "\";\n");
}
return true;
};
return Annotator;
}(ClosureRewriter));
/** ExternsWriter generates Closure externs from TypeScript source. */
var ExternsWriter = (function (_super) {
__extends(ExternsWriter, _super);
function ExternsWriter() {
return _super !== null && _super.apply(this, arguments) || this;
}
/** visit is the main entry point. It generates externs from a ts.Node. */
ExternsWriter.prototype.visit = function (node, namespace) {
if (namespace === void 0) { namespace = []; }
switch (node.kind) {
case ts.SyntaxKind.SourceFile:
var sourceFile = node;
for (var _i = 0, _a = sourceFile.statements; _i < _a.length; _i++) {