UNPKG

tsickle

Version:

Transpile TypeScript code to JavaScript with Closure annotations.

486 lines (484 loc) 21.7 kB
/** * @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 ts = require("typescript"); function assertTypeChecked(sourceFile) { if (!('resolvedModules' in sourceFile)) { throw new Error('must provide typechecked program'); } } exports.assertTypeChecked = assertTypeChecked; /** * Determines if fileName refers to a builtin lib.d.ts file. * This is a terrible hack but it mirrors a similar thing done in Clutz. */ function isBuiltinLibDTS(fileName) { return fileName.match(/\blib\.(?:[^/]+\.)?d\.ts$/) != null; } exports.isBuiltinLibDTS = isBuiltinLibDTS; /** * @return True if the named type is considered compatible with the Closure-defined * type of the same name, e.g. "Array". Note that we don't actually enforce * that the types are actually compatible, but mostly just hope that they are due * to being derived from the same HTML specs. */ function isClosureProvidedType(symbol) { return symbol.declarations != null && symbol.declarations.some(function (n) { return isBuiltinLibDTS(n.getSourceFile().fileName); }); } function typeToDebugString(type) { var debugString = "flags:0x" + type.flags.toString(16); var basicTypes = [ ts.TypeFlags.Any, ts.TypeFlags.String, ts.TypeFlags.Number, ts.TypeFlags.Boolean, ts.TypeFlags.Enum, ts.TypeFlags.StringLiteral, ts.TypeFlags.NumberLiteral, ts.TypeFlags.BooleanLiteral, ts.TypeFlags.EnumLiteral, ts.TypeFlags.ESSymbol, ts.TypeFlags.Void, ts.TypeFlags.Undefined, ts.TypeFlags.Null, ts.TypeFlags.Never, ts.TypeFlags.TypeParameter, ts.TypeFlags.Object, ts.TypeFlags.Union, ts.TypeFlags.Intersection, ts.TypeFlags.Index, ts.TypeFlags.IndexedAccess, ]; for (var _i = 0, basicTypes_1 = basicTypes; _i < basicTypes_1.length; _i++) { var flag = basicTypes_1[_i]; if ((type.flags & flag) !== 0) { debugString += " " + ts.TypeFlags[flag]; } } if (type.flags === ts.TypeFlags.Object) { var objType = type; var objectFlags = [ ts.ObjectFlags.Class, ts.ObjectFlags.Interface, ts.ObjectFlags.Reference, ts.ObjectFlags.Tuple, ts.ObjectFlags.Anonymous, ts.ObjectFlags.Mapped, ts.ObjectFlags.Instantiated, ts.ObjectFlags.ObjectLiteral, ts.ObjectFlags.EvolvingArray, ts.ObjectFlags.ObjectLiteralPatternWithComputedProperties, ]; for (var _a = 0, objectFlags_1 = objectFlags; _a < objectFlags_1.length; _a++) { var flag = objectFlags_1[_a]; if ((objType.objectFlags & flag) !== 0) { debugString += " object:" + ts.ObjectFlags[flag]; } } } if (type.symbol && type.symbol.name !== '__type') { debugString += " symbol.name:" + JSON.stringify(type.symbol.name); } if (type.pattern) { debugString += " destructuring:true"; } return "{type " + debugString + "}"; } exports.typeToDebugString = typeToDebugString; function symbolToDebugString(sym) { var debugString = JSON.stringify(sym.name) + " flags:0x" + sym.flags.toString(16); var symbolFlags = [ ts.SymbolFlags.FunctionScopedVariable, ts.SymbolFlags.BlockScopedVariable, ts.SymbolFlags.Property, ts.SymbolFlags.EnumMember, ts.SymbolFlags.Function, ts.SymbolFlags.Class, ts.SymbolFlags.Interface, ts.SymbolFlags.ConstEnum, ts.SymbolFlags.RegularEnum, ts.SymbolFlags.ValueModule, ts.SymbolFlags.NamespaceModule, ts.SymbolFlags.TypeLiteral, ts.SymbolFlags.ObjectLiteral, ts.SymbolFlags.Method, ts.SymbolFlags.Constructor, ts.SymbolFlags.GetAccessor, ts.SymbolFlags.SetAccessor, ts.SymbolFlags.Signature, ts.SymbolFlags.TypeParameter, ts.SymbolFlags.TypeAlias, ts.SymbolFlags.ExportValue, ts.SymbolFlags.ExportType, ts.SymbolFlags.ExportNamespace, ts.SymbolFlags.Alias, ts.SymbolFlags.Instantiated, ts.SymbolFlags.Merged, ts.SymbolFlags.Transient, ts.SymbolFlags.Prototype, ts.SymbolFlags.SyntheticProperty, ts.SymbolFlags.Optional, ts.SymbolFlags.ExportStar, ]; for (var _i = 0, symbolFlags_1 = symbolFlags; _i < symbolFlags_1.length; _i++) { var flag = symbolFlags_1[_i]; if ((sym.flags & flag) !== 0) { debugString += " " + ts.SymbolFlags[flag]; } } return debugString; } exports.symbolToDebugString = symbolToDebugString; /** TypeTranslator translates TypeScript types to Closure types. */ var TypeTranslator = (function () { /** * @param node is the source AST ts.Node the type comes from. This is used * in some cases (e.g. anonymous types) for looking up field names. * @param pathBlackList is a set of paths that should never get typed; * any reference to symbols defined in these paths should by typed * as {?}. */ function TypeTranslator(typeChecker, node, pathBlackList) { this.typeChecker = typeChecker; this.node = node; this.pathBlackList = pathBlackList; /** * A list of types we've encountered while emitting; used to avoid getting stuck in recursive * types. */ this.seenTypes = []; } /** * Converts a ts.Symbol to a string. * Other approaches that don't work: * - TypeChecker.typeToString translates Array as T[]. * - TypeChecker.symbolToString emits types without their namespace, * and doesn't let you pass the flag to control that. */ TypeTranslator.prototype.symbolToString = function (sym) { // This follows getSingleLineStringWriter in the TypeScript compiler. var str = ''; var writeText = function (text) { return str += text; }; var doNothing = function () { return; }; var builder = this.typeChecker.getSymbolDisplayBuilder(); var writer = { writeKeyword: writeText, writeOperator: writeText, writePunctuation: writeText, writeSpace: writeText, writeStringLiteral: writeText, writeParameter: writeText, writeProperty: writeText, writeSymbol: writeText, writeLine: doNothing, increaseIndent: doNothing, decreaseIndent: doNothing, clear: doNothing, trackSymbol: function (symbol, enclosingDeclaration, meaning) { return; }, reportInaccessibleThisError: doNothing, }; builder.buildSymbolDisplay(sym, writer, this.node); return str; }; TypeTranslator.prototype.translate = function (type) { // NOTE: Though type.flags has the name "flags", it usually can only be one // of the enum options at a time. This switch handles all the cases in // the ts.TypeFlags enum in the order they occur. // NOTE: Some TypeFlags are marked "internal" in the d.ts but still show // up in the value of type.flags. This mask limits the flag checks to // the ones in the public API. "lastFlag" here is the last flag handled // in this switch statement, and should be kept in sync with typescript.d.ts. var lastFlag = ts.TypeFlags.IndexedAccess; var mask = (lastFlag << 1) - 1; switch (type.flags & mask) { case ts.TypeFlags.Any: return '?'; case ts.TypeFlags.String: case ts.TypeFlags.StringLiteral: return 'string'; case ts.TypeFlags.Number: case ts.TypeFlags.NumberLiteral: return 'number'; case ts.TypeFlags.Boolean: case ts.TypeFlags.BooleanLiteral: // See the note in translateUnion about booleans. return 'boolean'; case ts.TypeFlags.Enum: case ts.TypeFlags.EnumLiteral: return 'number'; case ts.TypeFlags.ESSymbol: // NOTE: currently this is just a typedef for {?}, shrug. // https://github.com/google/closure-compiler/blob/55cf43ee31e80d89d7087af65b5542aa63987874/externs/es3.js#L34 return 'symbol'; case ts.TypeFlags.Void: return 'void'; case ts.TypeFlags.Undefined: return 'undefined'; case ts.TypeFlags.Null: return 'null'; case ts.TypeFlags.Never: this.warn("should not emit a 'never' type"); return '?'; case ts.TypeFlags.TypeParameter: // This is e.g. the T in a type like Foo<T>. this.warn("unhandled type flags: " + ts.TypeFlags[type.flags]); return '?'; case ts.TypeFlags.Object: return this.translateObject(type); case ts.TypeFlags.Union: return this.translateUnion(type); case ts.TypeFlags.Intersection: case ts.TypeFlags.Index: case ts.TypeFlags.IndexedAccess: // TODO(ts2.1): handle these special types. this.warn("unhandled type flags: " + ts.TypeFlags[type.flags]); return '?'; default: // Handle cases where multiple flags are set. // Booleans are represented as // ts.TypeFlags.Union | ts.TypeFlags.Boolean // where the union is a union of true|false. // Note also that in a more complex union, e.g. boolean|number, then // it's a union of three things (true|false|number) and // ts.TypeFlags.Boolean doesn't show up at all. if (type.flags & ts.TypeFlags.Union) { return this.translateUnion(type); } // The switch statement should have been exhaustive. throw new Error("unknown type flags: " + type.flags); } }; TypeTranslator.prototype.translateUnion = function (type) { var _this = this; var parts = type.types.map(function (t) { return _this.translate(t); }); // Union types that include boolean literals and other literals can // end up repeating the same Closure type. For example: true | boolean // will be translated to boolean | boolean. Remove duplicates to produce // types that read better. parts = parts.filter(function (el, idx) { return parts.indexOf(el) === idx; }); return parts.length === 1 ? parts[0] : "(" + parts.join('|') + ")"; }; // translateObject translates a ts.ObjectType, which is the type of all // object-like things in TS, such as classes and interfaces. TypeTranslator.prototype.translateObject = function (type) { var _this = this; if (type.symbol && this.isBlackListed(type.symbol)) return '?'; // NOTE: objectFlags is an enum, but a given type can have multiple flags. // Array<string> is both ts.ObjectFlags.Reference and ts.ObjectFlags.Interface. if (type.objectFlags & ts.ObjectFlags.Class) { if (!type.symbol) { this.warn('class has no symbol'); return '?'; } return '!' + this.symbolToString(type.symbol); } else if (type.objectFlags & ts.ObjectFlags.Interface) { // Note: ts.InterfaceType has a typeParameters field, but that // specifies the parameters that the interface type *expects* // when it's used, and should not be transformed to the output. // E.g. a type like Array<number> is a TypeReference to the // InterfaceType "Array", but the "number" type parameter is // part of the outer TypeReference, not a typeParameter on // the InterfaceType. if (!type.symbol) { this.warn('interface has no symbol'); return '?'; } if (type.symbol.flags & ts.SymbolFlags.Value) { // The symbol is both a type and a value. // For user-defined types in this state, we don't have a Closure name // for the type. See the type_and_value test. if (!isClosureProvidedType(type.symbol)) { this.warn("type/symbol conflict for " + type.symbol.name + ", using {?} for now"); return '?'; } } return '!' + this.symbolToString(type.symbol); } else if (type.objectFlags & ts.ObjectFlags.Reference) { // A reference to another type, e.g. Array<number> refers to Array. // Emit the referenced type and any type arguments. var referenceType = type; // A tuple is a ReferenceType where the target is flagged Tuple and the // typeArguments are the tuple arguments. Just treat it as a mystery // array, because Closure doesn't understand tuples. if (referenceType.target.objectFlags & ts.ObjectFlags.Tuple) { return '!Array<?>'; } var typeStr = ''; if (referenceType.target === referenceType) { // We get into an infinite loop here if the inner reference is // the same as the outer; this can occur when this function // fails to translate a more specific type before getting to // this point. throw new Error("reference loop in " + typeToDebugString(referenceType) + " " + referenceType.flags); } typeStr += this.translate(referenceType.target); if (referenceType.typeArguments) { var params = referenceType.typeArguments.map(function (t) { return _this.translate(t); }); typeStr += "<" + params.join(', ') + ">"; } return typeStr; } else if (type.objectFlags & ts.ObjectFlags.Anonymous) { if (!type.symbol) { // This comes up when generating code for an arrow function as passed // to a generic function. The passed-in type is tagged as anonymous // and has no properties so it's hard to figure out what to generate. // Just avoid it for now so we don't crash. this.warn('anonymous type has no symbol'); return '?'; } if (type.symbol.flags === ts.SymbolFlags.TypeLiteral) { return this.translateTypeLiteral(type); } else if (type.symbol.flags === ts.SymbolFlags.Function || type.symbol.flags === ts.SymbolFlags.Method) { var sigs = this.typeChecker.getSignaturesOfType(type, ts.SignatureKind.Call); if (sigs.length === 1) { return this.signatureToClosure(sigs[0]); } } this.warn('unhandled anonymous type'); return '?'; } /* TODO(ts2.1): more unhandled object type flags: Tuple Mapped Instantiated ObjectLiteral EvolvingArray ObjectLiteralPatternWithComputedProperties */ this.warn("unhandled type " + typeToDebugString(type)); return '?'; }; /** * translateTypeLiteral translates a ts.SymbolFlags.TypeLiteral type, which * is the anonymous type encountered in e.g. * let x: {a: number}; */ TypeTranslator.prototype.translateTypeLiteral = function (type) { // Avoid infinite loops on recursive types. // It would be nice to just emit the name of the recursive type here, // but type.symbol doesn't seem to have the name here (perhaps something // to do with aliases?). if (this.seenTypes.indexOf(type) !== -1) return '?'; this.seenTypes.push(type); // Gather up all the named fields and whether the object is also callable. var callable = false; var indexable = false; var fields = []; if (!type.symbol || !type.symbol.members) { this.warn('type literal has no symbol'); return '?'; } // special-case construct signatures. var ctors = type.getConstructSignatures(); if (ctors.length) { // TODO(martinprobst): this does not support additional properties defined on constructors // (not expressible in Closure), nor multiple constructors (same). var params = this.convertParams(ctors[0]); var paramsStr = params.length ? (', ' + params.join(', ')) : ''; var constructedType = this.translate(ctors[0].getReturnType()); // In the specific case of the "new" in a function, it appears that // function(new: !Bar) // fails to parse, while // function(new: (!Bar)) // parses in the way you'd expect. // It appears from testing that Closure ignores the ! anyway and just // assumes the result will be non-null in either case. (To be pedantic, // it's possible to return null from a ctor it seems like a bad idea.) return "function(new: (" + constructedType + ")" + paramsStr + "): ?"; } for (var _i = 0, _a = Object.keys(type.symbol.members); _i < _a.length; _i++) { var field = _a[_i]; switch (field) { case '__call': callable = true; break; case '__index': indexable = true; break; default: var member = type.symbol.members[field]; // optional members are handled by the type including |undefined in a union type. var memberType = this.translate(this.typeChecker.getTypeOfSymbolAtLocation(member, this.node)); fields.push(field + ": " + memberType); } } // Try to special-case plain key-value objects and functions. if (fields.length === 0) { if (callable && !indexable) { // A function type. var sigs = this.typeChecker.getSignaturesOfType(type, ts.SignatureKind.Call); if (sigs.length === 1) { return this.signatureToClosure(sigs[0]); } } else if (indexable && !callable) { // A plain key-value map type. var keyType = 'string'; var valType = this.typeChecker.getIndexTypeOfType(type, ts.IndexKind.String); if (!valType) { keyType = 'number'; valType = this.typeChecker.getIndexTypeOfType(type, ts.IndexKind.Number); } if (!valType) { this.warn('unknown index key type'); return "!Object<?,?>"; } return "!Object<" + keyType + "," + this.translate(valType) + ">"; } else if (!callable && !indexable) { // Special-case the empty object {} because Closure doesn't like it. // TODO(evanm): revisit this if it is a problem. return '!Object'; } } if (!callable && !indexable) { // Not callable, not indexable; implies a plain object with fields in it. return "{" + fields.join(', ') + "}"; } this.warn('unhandled type literal'); return '?'; }; /** Converts a ts.Signature (function signature) to a Closure function type. */ TypeTranslator.prototype.signatureToClosure = function (sig) { var params = this.convertParams(sig); var typeStr = "function(" + params.join(', ') + ")"; var retType = this.translate(this.typeChecker.getReturnTypeOfSignature(sig)); if (retType) { typeStr += ": " + retType; } return typeStr; }; TypeTranslator.prototype.convertParams = function (sig) { var _this = this; return sig.parameters.map(function (param) { var paramType = _this.typeChecker.getTypeOfSymbolAtLocation(param, _this.node); return _this.translate(paramType); }); }; TypeTranslator.prototype.warn = function (msg) { // By default, warn() does nothing. The caller will overwrite this // if it wants different behavior. }; /** @return true if sym should always have type {?}. */ TypeTranslator.prototype.isBlackListed = function (symbol) { if (this.pathBlackList === undefined) return false; var pathBlackList = this.pathBlackList; if (symbol.declarations === undefined) { this.warn('symbol has no declarations'); return true; } return symbol.declarations.every(function (n) { var path = n.getSourceFile().fileName; return pathBlackList.has(path); }); }; return TypeTranslator; }()); exports.TypeTranslator = TypeTranslator; //# sourceMappingURL=type-translator.js.map