UNPKG

tsickle

Version:

Transpile TypeScript code to JavaScript with Closure annotations.

1,029 lines 49.8 kB
"use strict"; /** * @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 */ Object.defineProperty(exports, "__esModule", { value: true }); exports.restParameterType = exports.isAlwaysUnknownSymbol = exports.TypeTranslator = exports.symbolToDebugString = exports.typeToDebugString = exports.typeValueConflictHandled = exports.isDeclaredInBuiltinLibDTS = exports.isValidClosurePropertyName = void 0; const ts = require("typescript"); const annotator_host_1 = require("./annotator_host"); const path = require("./path"); const transformer_util_1 = require("./transformer_util"); /** * 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); } exports.isValidClosurePropertyName = isValidClosurePropertyName; /** * 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 isDeclaredInBuiltinLibDTS(node) { var _a; const fileName = (_a = node === null || node === void 0 ? void 0 : node.getSourceFile()) === null || _a === void 0 ? void 0 : _a.fileName; return !!fileName && fileName.match(/\blib\.(?:[^/]+\.)?d\.ts$/) != null; } exports.isDeclaredInBuiltinLibDTS = isDeclaredInBuiltinLibDTS; /** * Returns true if the given node's source file is generated by Clutz, i.e. has * the magic Clutz header. */ function isDeclaredInClutzDts(node) { const sourceFile = node === null || node === void 0 ? void 0 : node.getSourceFile(); return !!sourceFile && sourceFile.text.startsWith('//!! generated by clutz.'); } /** * typeValueConflictHandled returns true for symbols whose type/value conflict * is handled outside of tsickle. * * This covers two cases: * * - symbols provided by Clutz. Given that Closure has a merged type/value * namespace, apparent type/value conflicts on the TypeScript level are actually * fine. * - builtin lib*.d.ts symbols, such as "Array", which are considered * Closure-compatible. 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 typeValueConflictHandled(symbol) { // TODO(#1072): if the symbol comes from a tsickle-transpiled file, either .ts // or .d.ts with externs generation? then maybe we can emit it with name // mangling. return symbol.declarations != null && symbol.declarations.some(n => isDeclaredInBuiltinLibDTS(n) || isDeclaredInClutzDts(n)); } exports.typeValueConflictHandled = typeValueConflictHandled; /** Returns a string describing the type for usage in debug logs. */ function typeToDebugString(type) { let debugString = `flags:0x${type.flags.toString(16)}`; if (type.aliasSymbol) { debugString += ` alias:${symbolToDebugString(type.aliasSymbol)}`; } if (type.aliasTypeArguments) { debugString += ` aliasArgs:<${type.aliasTypeArguments.map(typeToDebugString).join(',')}>`; } // Just the unique flags (powers of two). Declared in src/compiler/types.ts. const 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.BigIntLiteral, ts.TypeFlags.ESSymbol, ts.TypeFlags.UniqueESSymbol, 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, ts.TypeFlags.Conditional, ts.TypeFlags.Substitution, ]; for (const flag of basicTypes) { if ((type.flags & flag) !== 0) { debugString += ` ${ts.TypeFlags[flag]}`; } } if (type.flags === ts.TypeFlags.Object) { const objType = type; debugString += ` objectFlags:0x${objType.objectFlags.toString(16)}`; // Just the unique flags (powers of two). Declared in src/compiler/types.ts. const 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 (const flag of objectFlags) { 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; /** Returns a string describing the symbol for usage in debug logs. */ function symbolToDebugString(sym) { let debugString = `${JSON.stringify(sym.name)} flags:0x${sym.flags.toString(16)}`; // Just the unique flags (powers of two). Declared in src/compiler/types.ts. const 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.Alias, ts.SymbolFlags.Prototype, ts.SymbolFlags.ExportStar, ts.SymbolFlags.Optional, ts.SymbolFlags.Transient, ]; for (const flag of symbolFlags) { if ((sym.flags & flag) !== 0) { debugString += ` ${ts.SymbolFlags[flag]}`; } } return debugString; } exports.symbolToDebugString = symbolToDebugString; /** * Searches for an ambient module declaration in the ancestors of declarations, * depth first, and returns the first or null if none found. */ function getContainingAmbientModuleDeclaration(declarations) { for (const declaration of declarations) { let parent = declaration.parent; while (parent) { if (ts.isModuleDeclaration(parent) && ts.isStringLiteral(parent.name)) { return parent; } parent = parent.parent; } } return null; } /** * Returns true if any of declarations is a top level declaration in an * external module. */ function isTopLevelExternal(declarations) { for (const declaration of declarations) { if (declaration.parent === undefined) continue; if (ts.isSourceFile(declaration.parent) && ts.isExternalModule(declaration.parent)) { return true; } } return false; } /** * Returns true if a and b are (or were originally before transformation) nodes * of the same source file. */ function isDeclaredInSameFile(a, b) { return ts.getOriginalNode(a).getSourceFile() === ts.getOriginalNode(b).getSourceFile(); } /** * TypeTranslator translates TypeScript types to Closure types. It keeps state per type, so each * translation operation has to create a new instance. */ class TypeTranslator { /** * @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 pathUnknownSymbolsSet is a set of paths that should never get typed; * any reference to symbols defined in these paths should by typed * as {?}. * @param symbolsToAliasedNames a mapping from symbols (`Foo`) to a name in scope they should be * emitted as (e.g. `tsickle_reqType_1.Foo`). Can be augmented during type translation, e.g. * to mark a symbol as unknown. */ constructor(host, typeChecker, node, pathUnknownSymbolsSet, symbolsToAliasedNames, symbolToNameCache, ensureSymbolDeclared = () => { }) { this.host = host; this.typeChecker = typeChecker; this.node = node; this.pathUnknownSymbolsSet = pathUnknownSymbolsSet; this.symbolsToAliasedNames = symbolsToAliasedNames; this.symbolToNameCache = symbolToNameCache; this.ensureSymbolDeclared = ensureSymbolDeclared; /** * A list of type literals we've encountered while emitting; used to avoid * getting stuck in recursive types. */ this.seenTypes = []; /** * Whether to write types suitable for an #externs file. Externs types must not refer to * non-externs types (i.e. non ambient types) and need to use fully qualified names. */ this.isForExterns = false; /** * When translating the type of an 'extends' clause, e.g. Y in * class X extends Y<T> { ... } * then TS believes there is an additional type argument always passed, as if * you had written "extends Y<T, X>". * https://github.com/microsoft/TypeScript/issues/38391 * * But we want to emit Y<T> as just Y<T>. So this flag, when set, causes us * to ignore this final generic argument when translating. */ this.dropFinalTypeArgument = false; // Normalize paths to not break checks on Windows. this.pathUnknownSymbolsSet = new Set(Array.from(this.pathUnknownSymbolsSet.values()).map(p => path.normalize(p))); } /** * Converts a ts.Symbol to a string, applying aliases and ensuring symbols are imported. * @return a string representation of the symbol as a valid Closure type name, or `undefined` if * the type cannot be expressed (e.g. for anonymous types). */ symbolToString(sym) { // symbolToEntityName can be relatively expensive (40 ms calls with symbols in large namespaces // with many declarations, i.e. Clutz). symbolToString is idempotent per symbol and file, thus // we cache the entire operation to avoid the hit. const cachedName = this.symbolToNameCache.get(sym); if (cachedName) return cachedName; // TypeScript resolves e.g. union types to their members, which can include symbols not declared // in the current scope. Ensure that all symbols found this way are actually declared. // This must happen before the alias check below, it might introduce a new alias for the symbol. if (!this.isForExterns && (sym.flags & ts.SymbolFlags.TypeParameter) === 0) { this.ensureSymbolDeclared(sym); } const name = this.typeChecker.symbolToEntityName(sym, ts.SymbolFlags.Type, this.node, ts.NodeBuilderFlags.UseFullyQualifiedType | ts.NodeBuilderFlags.UseOnlyExternalAliasing); // name might be undefined, e.g. for anonymous classes. if (!name) return undefined; let str = ''; /** Recursively visits components of entity name and writes them to `str` above. */ const writeEntityWithSymbols = (name) => { let identifier; if (ts.isQualifiedName(name)) { writeEntityWithSymbols(name.left); str += '.'; identifier = name.right; } else { identifier = name; } let symbol = identifier.symbol; // When writing a symbol, check if there is an alias for it in the current scope that should // take precedence, e.g. from a goog.requireType. if (symbol.flags & ts.SymbolFlags.Alias) { symbol = this.typeChecker.getAliasedSymbol(symbol); } const alias = this.symbolsToAliasedNames.get(symbol); if (alias) { // If so, discard the entire current text and only use the alias - otherwise if a symbol has // a local alias but appears in a dotted type path (e.g. when it's imported using import * // as foo), str would contain both the prefx *and* the full alias (foo.alias.name). str = alias; return; } let text = (0, transformer_util_1.getIdentifierText)(identifier); if (str.length === 0) { const mangledPrefix = this.maybeGetMangledNamePrefix(symbol); text = mangledPrefix + text; } str += text; }; writeEntityWithSymbols(name); str = this.stripClutzNamespace(str); this.symbolToNameCache.set(sym, str); return str; } /** * Returns the mangled name prefix for symbol, or an empty string if not applicable. * * Type names are emitted with a mangled prefix if they are top level symbols declared in an * external module (.d.ts or .ts), and are ambient declarations ("declare ..."). This is because * their declarations get moved to externs files (to make external names visible to Closure and * prevent renaming), which only use global names. This means the names must be mangled to prevent * collisions and allow referencing them uniquely. * * This method also handles the special case of symbols declared in an ambient external module * context. * * Symbols declared in a global block, e.g. "declare global { type X; }", are handled implicitly: * when referenced, they are written as just "X", which is not a top level declaration, so the * code below ignores them. */ maybeGetMangledNamePrefix(symbol) { if (!symbol.declarations) return ''; const declarations = symbol.declarations; let ambientModuleDeclaration = null; // If the symbol is neither a top level declaration in an external module nor in an ambient // block, tsickle should not emit a prefix: it's either not an external symbol, or it's an // external symbol nested in a module, so it will need to be qualified, and the mangling prefix // goes on the qualifier. if (!isTopLevelExternal(declarations)) { ambientModuleDeclaration = getContainingAmbientModuleDeclaration(declarations); if (!ambientModuleDeclaration) return ''; } // At this point, the declaration is from an external module (possibly ambient). // These declarations must be prefixed if either: // (a) tsickle is emitting an externs file, so all symbols are qualified within it // (b) or the declaration must be an exported ambient declaration from the local file. // Ambient external declarations from other files are imported, so there's a local alias for the // module and no mangling is needed. if (!this.isForExterns && !declarations.every(d => isDeclaredInSameFile(this.node, d) && (0, transformer_util_1.isAmbient)(d) && (0, transformer_util_1.hasModifierFlag)(d, ts.ModifierFlags.Export))) { return ''; } // If from an ambient declaration, use and resolve the name from that. Otherwise, use the file // name from the (arbitrary) first declaration to mangle. const fileName = ambientModuleDeclaration ? ambientModuleDeclaration.name.text : ts.getOriginalNode(declarations[0]).getSourceFile().fileName; const mangled = (0, annotator_host_1.moduleNameAsIdentifier)(this.host, fileName); return mangled + '.'; } // Clutz (https://github.com/angular/clutz) emits global type symbols hidden in a special // ಠ_ಠ.clutz namespace. While most code seen by Tsickle will only ever see local aliases, Clutz // symbols can be written by users directly in code, and they can appear by dereferencing // TypeAliases. The code below simply strips the prefix, the remaining type name then matches // Closure's type. stripClutzNamespace(name) { if (name.startsWith('ಠ_ಠ.clutz.')) return name.substring('ಠ_ಠ.clutz.'.length); return name; } translate(type) { // NOTE: Though type.flags has the name "flags", it usually can only be one // of the enum options at a time (except for unions of literal types, e.g. unions of boolean // values, string values, enum values). This switch handles all the cases in the ts.TypeFlags // enum in the order they occur. var _a, _b, _c; // 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. // NonPrimitive occurs on its own on the lower case "object" type. Special case to "!Object". if (type.flags === ts.TypeFlags.NonPrimitive) return '!Object'; // TemplateLiteral falls outside of the masked range used for the switch statement // below, so we'll check for it first. if (type.flags === ts.TypeFlags.TemplateLiteral) return 'string'; // Avoid infinite loops on recursive type literals. // It would be nice to just emit the name of the recursive type here (in type.aliasSymbol // below), but Closure Compiler does not allow recursive type definitions. if (this.seenTypes.indexOf(type) !== -1) return '?'; let isAmbient = false; let isInUnsupportedNamespace = false; let isModule = false; if (type.symbol) { for (const decl of type.symbol.declarations || []) { if (ts.isExternalModule(decl.getSourceFile())) isModule = true; if (decl.getSourceFile().isDeclarationFile) isAmbient = true; let current = decl; while (current) { if (ts.getCombinedModifierFlags(current) & ts.ModifierFlags.Ambient) isAmbient = true; if (current.kind === ts.SyntaxKind.ModuleDeclaration && !(0, transformer_util_1.isTransformedDeclMergeNs)(current)) { isInUnsupportedNamespace = true; } current = current.parent; } } } // tsickle cannot generate types for most non-ambient namespaces nor any // symbols contained in them. Only types from declaration merging namespaces // are supported. if (isInUnsupportedNamespace && !isAmbient) { return '?'; } // Types in externs cannot reference types from external modules. // However ambient types in modules get moved to externs, too, so type references work and we // can emit a precise type. if (this.isForExterns && isModule && !isAmbient) return '?'; const lastFlag = ts.TypeFlags.StringMapping; const mask = (lastFlag << 1) - 1; switch (type.flags & mask) { case ts.TypeFlags.Any: return '?'; case ts.TypeFlags.Unknown: return '*'; case ts.TypeFlags.String: case ts.TypeFlags.StringLiteral: case ts.TypeFlags.StringMapping: return 'string'; case ts.TypeFlags.Number: case ts.TypeFlags.NumberLiteral: return 'number'; case ts.TypeFlags.BigInt: case ts.TypeFlags.BigIntLiteral: return 'bigint'; case ts.TypeFlags.Boolean: case ts.TypeFlags.BooleanLiteral: // See the note in translateUnion about booleans. return 'boolean'; case ts.TypeFlags.Enum: if (!type.symbol) { this.warn(`EnumType without a symbol`); return '?'; } return this.symbolToString(type.symbol) || '?'; case ts.TypeFlags.ESSymbol: case ts.TypeFlags.UniqueESSymbol: // ESSymbol indicates something typed symbol. // UniqueESSymbol indicates a specific unique symbol, used e.g. to index into an object. // Closure does not have this distinction, so tsickle emits both as 'symbol'. 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>. if (!type.symbol) { this.warn(`TypeParameter without a symbol`); // should not happen (tm) return '?'; } // In Closure, type parameters ("<T>") are non-nullable by default, unlike references to // classes or interfaces. However this code path can be reached by bound type parameters, // where the type parameter's symbol references a plain class or interface. In this case, // add `!` to avoid emitting a nullable type. let prefix = ''; if ((type.symbol.flags & ts.SymbolFlags.TypeParameter) === 0) { prefix = '!'; } const name = this.symbolToString(type.symbol); if (!name) return '?'; return prefix + name; case ts.TypeFlags.Object: return this.translateObject(type); case ts.TypeFlags.Union: return this.translateUnion(type); case ts.TypeFlags.Conditional: case ts.TypeFlags.Substitution: if (((_a = type.aliasSymbol) === null || _a === void 0 ? void 0 : _a.escapedName) === 'NonNullable' && isDeclaredInBuiltinLibDTS((_b = type.aliasSymbol.declarations) === null || _b === void 0 ? void 0 : _b[0])) { let innerSymbol = undefined; // Pretend that NonNullable<T> is really just T, as this doesn't // tend to affect optimization. T might not be a symbol we can // represent in Closure's type-system, and in this case we fall // back to '?' (the old behavior). if ((_c = type.aliasTypeArguments) === null || _c === void 0 ? void 0 : _c[0]) { innerSymbol = this.translate(type.aliasTypeArguments[0]); } else { const srcFile = this.node.getSourceFile().fileName; const start = this.node.getStart(); const end = this.node.getEnd(); throw new Error(`NonNullable missing expected type argument: ${srcFile}(${start}-${end})`); // Fallthrough to returning '?' below } return innerSymbol !== null && innerSymbol !== void 0 ? innerSymbol : '?'; } this.warn(`emitting ? for conditional/substitution type`); return '?'; 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. // Types with literal members are represented as // ts.TypeFlags.Union | [literal member] // E.g. an enum typed value is a union type with the enum's members as its members. A // boolean type is a union type with 'true' and 'false' as its members. // 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); } if (type.flags & ts.TypeFlags.EnumLiteral) { return this.translateEnumLiteral(type); } // The switch statement should have been exhaustive. throw new Error(`unknown type flags ${type.flags} on ${typeToDebugString(type)}`); } } translateUnion(type) { return this.translateUnionMembers(type.types); } translateUnionMembers(types) { // Union types that include literals (e.g. boolean, enum) 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. const parts = new Set(types.map(t => this.translate(t))); // If it's a single element set, return the single member. if (parts.size === 1) return parts.values().next().value; return `(${Array.from(parts.values()).join('|')})`; } translateEnumLiteral(type) { // Suppose you had: // enum EnumType { MEMBER } // then the type of "EnumType.MEMBER" is an enum literal (the thing passed to this function) // and it has type flags that include // ts.TypeFlags.NumberLiteral | ts.TypeFlags.EnumLiteral // // Closure Compiler doesn't support literals in types, so this code must not emit // "EnumType.MEMBER", but rather "EnumType". const enumLiteralBaseType = this.typeChecker.getBaseTypeOfLiteralType(type); if (!enumLiteralBaseType.symbol) { this.warn(`EnumLiteralType without a symbol`); return '?'; } let symbol = enumLiteralBaseType.symbol; if (enumLiteralBaseType === type) { // TypeScript's API will return the same EnumLiteral type if the enum only has a single member // value. See https://github.com/Microsoft/TypeScript/issues/28869. // In that case, take the parent symbol of the enum member, which should be the enum // declaration. // tslint:disable-next-line:no-any working around a TS API deficiency. const parent = symbol['parent']; if (!parent) return '?'; symbol = parent; } const name = this.symbolToString(symbol); if (!name) return '?'; // In Closure, enum types are non-null by default, so we wouldn't need to emit the `!` here. // However that's confusing to users, to the point that style guides and linters require to // *always* specify the nullability modifier. To be consistent with that style, include it here // as well. return '!' + name; } // translateObject translates a ts.ObjectType, which is the type of all // object-like things in TS, such as classes and interfaces. translateObject(type) { var _a; if (type.symbol && this.isAlwaysUnknownSymbol(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 '?'; } const name = this.symbolToString(type.symbol); if (!name) { // An anonymous type. Make sure not to emit '!?', as that is a syntax error in Closure // Compiler. return '?'; } return '!' + name; } 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 may not have a Closure name // for the type. See the type_and_value test. if (!typeValueConflictHandled(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. const referenceType = type; // A tuple is a ReferenceType where the target is flagged Tuple and the // typeArguments are the tuple arguments. Closure Compiler does not // support tuple types, so tsickle emits this as `Array<?>`. // It would also be possible to emit an Array of the union of the // constituent types. In experimentation, this however does not seem to // improve optimization compatibility much, as long as destructuring // assignments are aliased. if (referenceType.target.objectFlags & ts.ObjectFlags.Tuple) { return '!Array<?>'; } let 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); // Translate can return '?' for a number of situations, e.g. type/value conflicts. // `?<?>` is illegal syntax in Closure Compiler, so just return `?` here. if (typeStr === '?') return '?'; let typeArgs = (_a = this.typeChecker.getTypeArguments(referenceType)) !== null && _a !== void 0 ? _a : []; // Nested types have references to type parameters of all enclosing types. // Those are always at the beginning of the list of type arguments. const outerTypeParameters = referenceType.target.outerTypeParameters; if (outerTypeParameters) { typeArgs = typeArgs.slice(outerTypeParameters.length); } if (this.dropFinalTypeArgument) { typeArgs = typeArgs.slice(0, typeArgs.length - 1); } if (typeArgs.length > 0) { // If a type references itself recursively, such as in `type A = B<A>`, // the type parameter will resolve to itself. In the example above B's // type parameter will be B<B<B<...>>> and just go on indefinitely. To // prevent this we mark the type as seen and if this type comes up again // `?` will be used in its place. Note this won't trigger for something // like `Node<Node<number>>` because this is comparing the types, not // the symbols. In the nested nodes case the symbols are the same, but // `Node<Node<number>> !== Node<number>`. if (t === referenceType) // return '?'; this.seenTypes.push(referenceType); const params = typeArgs.map(t => this.translate(t)); this.seenTypes.pop(); typeStr += `<${params.join(', ')}>`; } return typeStr; } else if (type.objectFlags & ts.ObjectFlags.Anonymous) { return this.translateAnonymousType(type); } /* TODO(ts2.1): more unhandled object type flags: Mapped Instantiated ObjectLiteral EvolvingArray ObjectLiteralPatternWithComputedProperties */ this.warn(`unhandled type ${typeToDebugString(type)}`); return '?'; } /** * translateAnonymousType translates a ts.TypeFlags.ObjectType that is also * ts.ObjectFlags.Anonymous. That is, this type's symbol does not have a name. This is the * anonymous type encountered in e.g. * let x: {a: number}; * But also the inferred type in: * let x = {a: 1}; // type of x is {a: number}, as above */ translateAnonymousType(type) { this.seenTypes.push(type); try { 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.Function || type.symbol.flags & ts.SymbolFlags.Method) { const sigs = this.typeChecker.getSignaturesOfType(type, ts.SignatureKind.Call); if (sigs.length === 1) { return this.signatureToClosure(sigs[0]); } this.warn('unhandled anonymous type with multiple call signatures'); return '?'; } // Gather up all the named fields and whether the object is also callable. let callable = false; let indexable = false; const fields = []; if (!type.symbol.members) { this.warn('anonymous type has no symbol'); return '?'; } // special-case construct signatures. const 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). const decl = ctors[0].declaration; if (!decl) { this.warn('unhandled anonymous type with constructor signature but no declaration'); return '?'; } if (decl.kind === ts.SyntaxKind.JSDocSignature) { this.warn('unhandled JSDoc based constructor signature'); return '?'; } // new <T>(tee: T) is not supported by Closure, always set as ?. this.markTypeParameterAsUnknown(this.symbolsToAliasedNames, decl.typeParameters); const params = this.convertParams(ctors[0], decl.parameters); const paramsStr = params.length ? (', ' + params.join(', ')) : ''; const constructedType = this.translate(ctors[0].getReturnType()); let constructedTypeStr = constructedType[0] === '!' ? constructedType.substring(1) : constructedType; // TypeScript also allows {} and unknown as return types of construct // signatures, though it will make sure that no primitive types are // returned. // // Normally Tsickle translates {}/unknown to {*}. But Closure Compiler // expects an ObjectType for constructed types, which roughly // corresponds to "a singular non-primitive type". {*} includes // primitive types, so it is not allowed here. // // There is no 100% correct type for that, so fall back to {?}. if (constructedTypeStr === '*') { constructedTypeStr = '?'; } // In the specific case of the "new" in a function, the correct Closure // type is: // // function(new:Bar, ...args) // // Including the nullability annotation can cause the Closure compiler // to no longer recognize the function as a constructor type in externs. return `function(new:${constructedTypeStr}${paramsStr})`; } // members is an ES6 map, but the .d.ts defining it defined their own map // type, so typescript doesn't believe that .keys() is iterable. for (const field of type.symbol.members.keys()) { const fieldName = ts.unescapeLeadingUnderscores(field); switch (field) { case ts.InternalSymbolName.Call: callable = true; break; case ts.InternalSymbolName.Index: indexable = true; break; default: if (!isValidClosurePropertyName(fieldName)) { this.warn(`omitting inexpressible property name: ${field}`); continue; } const member = type.symbol.members.get(field); // optional members are handled by the type including |undefined in // a union type. const memberType = this.translate(this.typeChecker.getTypeOfSymbolAtLocation(member, this.node)); fields.push(`${fieldName}: ${memberType}`); break; } } // Try to special-case plain key-value objects and functions. if (fields.length === 0) { if (callable && !indexable) { // A function type. const 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. let keyType = 'string'; let 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) { // The object has no members. This is the TS type '{}', // which means "any value other than null or undefined". // What is this in Closure's type system? // // First, {!Object} is wrong because it is not a supertype of // {string} or {number}. This would mean you cannot assign a // number to a variable of TS type {}. // // We get closer with {*}, aka the ALL type. This one better // captures the typical use of the TS {}, which users use for // "I don't care". // // {*} unfortunately does include null/undefined, so it's a closer // match for TS 3.0's 'unknown'. return '*'; } } if (!callable && !indexable) { // Not callable, not indexable; implies a plain object with fields in // it. return `{${fields.join(', ')}}`; } this.warn('unhandled anonymous type'); return '?'; } finally { this.seenTypes.pop(); } } /** Converts a ts.Signature (function signature) to a Closure function type. */ signatureToClosure(sig) { // TODO(martinprobst): Consider harmonizing some overlap with emitFunctionType in externs.ts. if (!sig.declaration) { this.warn('signature without declaration'); return 'Function'; } if (sig.declaration.kind === ts.SyntaxKind.JSDocSignature) { this.warn('signature with JSDoc declaration'); return 'Function'; } this.markTypeParameterAsUnknown(this.symbolsToAliasedNames, sig.declaration.typeParameters); let typeStr = `function(`; let paramDecls = sig.declaration.parameters || []; const maybeThisParam = paramDecls[0]; // Oddly, the this type shows up in paramDecls, but not in the type's parameters. // Handle it here and then pass paramDecls down without its first element. if (maybeThisParam && maybeThisParam.name.getText() === 'this') { if (maybeThisParam.type) { const thisType = this.typeChecker.getTypeAtLocation(maybeThisParam.type); typeStr += `this: (${this.translate(thisType)})`; if (paramDecls.length > 1) typeStr += ', '; } else { this.warn('this type without type'); } paramDecls = paramDecls.slice(1); } const params = this.convertParams(sig, paramDecls); typeStr += `${params.join(', ')})`; const retType = this.translate(this.typeChecker.getReturnTypeOfSignature(sig)); if (retType) { typeStr += `: ${retType}`; } return typeStr; } /** * Converts parameters for the given signature. Takes parameter declarations as those might not * match the signature parameters (e.g. there might be an additional this parameter). This * difference is handled by the caller, as is converting the "this" parameter. */ convertParams(sig, paramDecls) { const paramTypes = []; for (let i = 0; i < sig.parameters.length; i++) { const param = sig.parameters[i]; const paramDecl = paramDecls[i]; // Parameters are optional if either marked '?' or if have a default const optional = !!paramDecl.questionToken || !!paramDecl.initializer; const varArgs = !!paramDecl.dotDotDotToken; const paramType = this.typeChecker.getTypeOfSymbolAtLocation(param, this.node); let typeStr; if (varArgs) { // When translating (...x: number[]) into {...number}, remove the array. const argType = restParameterType(this.typeChecker, paramType); if (argType) { typeStr = '...' + this.translate(argType); } else { this.warn('unable to translate rest args type'); typeStr = '...?'; } } else { typeStr = this.translate(paramType); } if (optional) typeStr = typeStr + '='; paramTypes.push(typeStr); } return paramTypes; } warn(msg) { // By default, warn() does nothing. The caller will overwrite this // if it wants different behavior. } /** @return true if sym should always have type {?}. */ isAlwaysUnknownSymbol(symbol) { return isAlwaysUnknownSymbol(this.pathUnknownSymbolsSet, symbol); } /** * Closure doesn not support type parameters for function types, i.e. generic function types. * Mark the symbols declared by them as unknown and emit a ? for the types. * * This mutates the given map of unknown symbols. The map's scope is one file, and symbols are * unique objects, so this should neither lead to excessive memory consumption nor introduce * errors. * * @param unknownSymbolsMap a map to store the unkown symbols in, with a value of '?'. In practice, * this is always === this.symbolsToAliasedNames, but we're passing it explicitly to make it * clear that the map is mutated (in particular when used from outside the class). * @param decls the declarations whose symbols should be marked as unknown. */ markTypeParameterAsUnknown(unknownSymbolsMap, decls) { if (!decls || !decls.length) return; for (const tpd of decls) { const sym = this.typeChecker.getSymbolAtLocation(tpd.name); if (!sym) { this.warn(`type parameter with no symbol`); continue; } unknownSymbolsMap.set(sym, '?'); } } } exports.TypeTranslator = TypeTranslator; /** @return true if sym should always have type {?}. */ function isAlwaysUnknownSymbol(pathUnknownSymbolsSet, symbol) { if (pathUnknownSymbolsSet === undefined) return false; // Some builtin types, such as {}, get represented by a symbol that has no declarations. if (symbol.declarations === undefined) return false; return symbol.declarations.every(n => { const fileName = path.normalize(n.getSourceFile().fileName); return pathUnknownSymbolsSet.has(fileName); }); } exports.isAlwaysUnknownSymbol = isAlwaysUnknownSymbol; /** * Extracts the contained element type from a rest parameter. * * In TypeScript, a rest parameter is written as an array type: * function f(...xs: number[]) * while in JS, that same param would be written without the array: * @-param {...number} number * This function is used to convert the former into the latter. It may return * undefined in cases where the type is too complex; e.g. TS allows things like * function f<T extends More>(...xs: T) */ function restParameterType(typeChecker, type) { if (((type.flags & ts.TypeFlags.Object) === 0) && (type.flags & ts.TypeFlags.TypeParameter)) { // function f<T extends string[]>(...ts: T) has the Array type on the type // parameter constraint, not on the parameter itself. Resolve it. const baseConstraint = typeChecker.getBaseConstraintOfType(type); if (baseConstraint) type = baseConstraint; } if ((type.flags & ts.TypeFlags.Object) === 0) { // This can happen in cases like // function f(...args: any) return undefined; } const objType = type; if ((objType.objectFlags & ts.ObjectFlags.Reference) === 0) { return undefined; } const typeRef = objType; const typeArgs = typeChecker.getTypeArguments(typeRef); if (typeArgs.length < 1) { // length can be zero when a generic is instantiated to create a zero-arg // function; see rest_parameters_generic_empty test. // // Per https://github.com/microsoft/TypeScript/issues/38391 // it can also happen that length >1, but the first type argument is the one // that matters. return undefined; } return typeArgs[0]; } exports.restParameterType = restParameterType; //# sourceMappingURL=type_translator.js.map