UNPKG

tsickle

Version:

Transpile TypeScript code to JavaScript with Closure annotations.

875 lines 70 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.jsdocTransformer = exports.removeTypeAssertions = exports.escapeForComment = exports.maybeAddHeritageClauses = exports.maybeAddTemplateClause = void 0; /** * @fileoverview jsdoc_transformer contains the logic to add JSDoc comments to TypeScript code. * * One of tsickle's features is to add Closure Compiler compatible JSDoc comments containing type * annotations, inheritance information, etc., onto TypeScript code. This allows Closure Compiler to * make better optimization decisions compared to an untyped code base. * * The entry point to the annotation operation is jsdocTransformer below. It adds synthetic comments * to existing TypeScript constructs, for example: * const x: number = 1; * Might get transformed to: * /.. \@type {number} ./ * const x: number = 1; * Later TypeScript phases then remove the type annotation, and the final emit is JavaScript that * only contains the JSDoc comment. * * To handle certain constructs, this transformer also performs AST transformations, e.g. by adding * CommonJS-style exports for type constructs, expanding `export *`, parenthesizing casts, etc. */ const ts = require("typescript"); const annotator_host_1 = require("./annotator_host"); const decorators_1 = require("./decorators"); const googmodule = require("./googmodule"); const jsdoc = require("./jsdoc"); const module_type_translator_1 = require("./module_type_translator"); const transformerUtil = require("./transformer_util"); const transformer_util_1 = require("./transformer_util"); const type_translator_1 = require("./type_translator"); function addCommentOn(node, tags, escapeExtraTags, hasTrailingNewLine = true) { const comment = jsdoc.toSynthesizedComment(tags, escapeExtraTags, hasTrailingNewLine); const comments = ts.getSyntheticLeadingComments(node) || []; comments.push(comment); ts.setSyntheticLeadingComments(node, comments); return comment; } /** Adds an \@template clause to docTags if decl has type parameters. */ function maybeAddTemplateClause(docTags, decl) { if (!decl.typeParameters) return; // Closure does not support template constraints (T extends X), these are ignored below. docTags.push({ tagName: 'template', text: decl.typeParameters.map(tp => transformerUtil.getIdentifierText(tp.name)).join(', ') }); } exports.maybeAddTemplateClause = maybeAddTemplateClause; /** * Adds heritage clauses (\@extends, \@implements) to the given docTags for * decl. Used by jsdoc_transformer and externs generation. */ function maybeAddHeritageClauses(docTags, mtt, decl) { if (!decl.heritageClauses) return; const isClass = decl.kind === ts.SyntaxKind.ClassDeclaration; const hasAnyExtends = decl.heritageClauses.some(c => c.token === ts.SyntaxKind.ExtendsKeyword); for (const heritage of decl.heritageClauses) { const isExtends = heritage.token === ts.SyntaxKind.ExtendsKeyword; for (const expr of heritage.types) { addHeritage(isExtends ? 'extends' : 'implements', expr); } } /** * Adds the relevant Closure JSdoc tags for an expression occurring in a * heritage clause, e.g. "implements FooBar" => "@implements {FooBar}". * * Will omit the JSDoc and add a comment saying why if the expression is * inexpressible in Closure semantics. * * Note that we don't need to consider all possible combinations of * types/values and extends/implements because our input is already verified * to be valid TypeScript. See test_files/class/ for the full cartesian * product of test cases. * * In some cases adding the Closure JSDoc tags is unnecessary, like in * "class Foo {} /** @extends {Foo} * / class Bar extends Foo {}" * but having the extra tag doesn't affect Closure Compiler typechecking * semantics. */ function addHeritage(relation, expr) { const supertype = mtt.typeChecker.getTypeAtLocation(expr); // We ultimately need to have a named type in the JSDoc, so verify that // the resolved type maps back to some specific symbol. // You cannot @implements an anonymous record type, for example. if (!supertype.symbol) { warn(`type without symbol`); return; } if (!supertype.symbol.name) { warn(`type without symbol name`); return; } if (supertype.symbol.flags & ts.SymbolFlags.TypeLiteral) { // A type literal is a type like `{foo: string}`. // These can come up as the output of a mapped type. warn(`dropped ${relation} of a type literal: ${expr.getText()}`); return; } // Translate the reference to the parent into the type expression used // in the @extends/@implements JSDoc. // Normally we'd use mtt.typeToClosure for type translation, but two // caveats: // 1) We need to avoid // https://github.com/microsoft/TypeScript/issues/38391 // 2) We can't emit a leading ! in the type reference. So // @extends {X<!Y>}, not @extends {!X<!Y>}. const typeTranslator = mtt.newTypeTranslator(expr); // Workaround for #1, see also the definition of dropFinalTypeArgument // for why we use this. typeTranslator.dropFinalTypeArgument = true; let closureType = typeTranslator.translate(supertype); if (closureType === '?') { warn(`{?} type`); return; } // Workaround for #2 closureType = closureType.replace(/^!/, ''); // Choose the @tag to use. For the (questionable) reasons described in // this block, sometimes we emit @extends even if the TS code uses // 'implements'. let tagName = relation; if (supertype.symbol.flags & ts.SymbolFlags.Class) { if (!isClass) { warn(`interface cannot extend/implement class`); return; } if (relation !== 'extends') { if (!hasAnyExtends) { // A special case: for a class that has no existing 'extends' clause // but does have an 'implements' clause that refers to another // class, we change it to instead be an 'extends'. This was a // poorly-thought-out hack that may actually cause compiler bugs: // https://github.com/google/closure-compiler/issues/3126 // but we have code that now relies on it, ugh. tagName = 'extends'; } else { warn(`cannot implements a class`); return; } } } docTags.push({ tagName, type: closureType, }); /** Records a warning, both in the source text and in the emit host. */ function warn(message) { message = `dropped ${relation}: ${message}`; docTags.push({ tagName: '', text: `tsickle: ${message}` }); mtt.debugWarn(decl, message); } } } exports.maybeAddHeritageClauses = maybeAddHeritageClauses; /** * createMemberTypeDeclaration emits the type annotations for members of a class. 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 separately from the class. * * createMemberTypeDeclaration produces an if (false) statement containing property declarations, or * null if no declarations could or needed to be generated (e.g. no members, or an unnamed type). * The if statement is used to make sure the code is not executed, otherwise property accesses could * trigger getters on a superclass. See test_files/fields/fields.ts:BaseThatThrows. */ function createMemberTypeDeclaration(mtt, typeDecl) { // Gather parameter properties from the constructor, if it exists. const ctors = []; let paramProps = []; const nonStaticProps = []; const staticProps = []; const unhandled = []; const abstractMethods = []; for (const member of typeDecl.members) { if (member.kind === ts.SyntaxKind.Constructor) { ctors.push(member); } else if (ts.isPropertyDeclaration(member) || ts.isPropertySignature(member) || (ts.isMethodDeclaration(member) && member.questionToken)) { const isStatic = transformerUtil.hasModifierFlag(member, ts.ModifierFlags.Static); if (isStatic) { staticProps.push(member); } else { nonStaticProps.push(member); } } else if (member.kind === ts.SyntaxKind.MethodDeclaration || member.kind === ts.SyntaxKind.MethodSignature || member.kind === ts.SyntaxKind.GetAccessor || member.kind === ts.SyntaxKind.SetAccessor) { if (transformerUtil.hasModifierFlag(member, ts.ModifierFlags.Abstract) || ts.isInterfaceDeclaration(typeDecl)) { abstractMethods.push(member); } // Non-abstract methods only exist on classes, and are handled in regular // emit. } else { unhandled.push(member); } } if (ctors.length > 0) { // Only the actual constructor implementation, which must be last in a potential sequence of // overloaded constructors, may contain parameter properties. const ctor = ctors[ctors.length - 1]; paramProps = ctor.parameters.filter(p => transformerUtil.hasModifierFlag(p, ts.ModifierFlags.ParameterPropertyModifier)); } if (nonStaticProps.length === 0 && paramProps.length === 0 && staticProps.length === 0 && abstractMethods.length === 0) { // There are no members so we don't need to emit any type // annotations helper. return null; } if (!typeDecl.name) { mtt.debugWarn(typeDecl, 'cannot add types on unnamed declarations'); return null; } const className = transformerUtil.getIdentifierText(typeDecl.name); const staticPropAccess = ts.factory.createIdentifier(className); const instancePropAccess = ts.factory.createPropertyAccessExpression(staticPropAccess, 'prototype'); // Closure Compiler will report conformance errors about this being unknown type when emitting // class properties as {?|undefined}, instead of just {?}. So make sure to only emit {?|undefined} // on interfaces. const isInterface = ts.isInterfaceDeclaration(typeDecl); const propertyDecls = staticProps.map(p => createClosurePropertyDeclaration(mtt, staticPropAccess, p, isInterface && !!p.questionToken)); propertyDecls.push(...[...nonStaticProps, ...paramProps].map(p => createClosurePropertyDeclaration(mtt, instancePropAccess, p, isInterface && !!p.questionToken))); propertyDecls.push(...unhandled.map(p => transformerUtil.createMultiLineComment(p, `Skipping unhandled member: ${escapeForComment(p.getText())}`))); for (const fnDecl of abstractMethods) { // If the function declaration is computed, its name is the computed expression; otherwise, its // name can be resolved to a string. const name = fnDecl.name && ts.isComputedPropertyName(fnDecl.name) ? fnDecl.name.expression : propertyName(fnDecl); if (!name) { mtt.error(fnDecl, 'anonymous abstract function'); continue; } const { tags, parameterNames } = mtt.getFunctionTypeJSDoc([fnDecl], []); if ((0, decorators_1.hasExportingDecorator)(fnDecl, mtt.typeChecker)) tags.push({ tagName: 'export' }); // Use element access instead of property access for computed names. const lhs = typeof name === 'string' ? ts.factory.createPropertyAccessExpression(instancePropAccess, name) : ts.factory.createElementAccessExpression(instancePropAccess, name); // memberNamespace because abstract methods cannot be static in TypeScript. const abstractFnDecl = ts.factory.createExpressionStatement(ts.factory.createAssignment(lhs, ts.factory.createFunctionExpression( /* modifiers */ undefined, /* asterisk */ undefined, /* name */ undefined, /* typeParameters */ undefined, parameterNames.map(n => ts.factory.createParameterDeclaration( /* decorators */ undefined, /* modifiers */ undefined, /* dotDotDot */ undefined, n)), undefined, ts.factory.createBlock([])))); ts.setSyntheticLeadingComments(abstractFnDecl, [jsdoc.toSynthesizedComment(tags)]); propertyDecls.push(ts.setSourceMapRange(abstractFnDecl, fnDecl)); } // Wrap the property declarations in an 'if (false)' block. // See test_files/fields/fields.ts:BaseThatThrows for a note on this wrapper. const ifStmt = ts.factory.createIfStatement(ts.factory.createFalse(), ts.factory.createBlock(propertyDecls, true)); // Also add a comment above the block to exclude it from coverage. ts.addSyntheticLeadingComment(ifStmt, ts.SyntaxKind.MultiLineCommentTrivia, ' istanbul ignore if ', /* trailing newline */ true); return ifStmt; } function propertyName(prop) { if (!prop.name) return null; switch (prop.name.kind) { case ts.SyntaxKind.Identifier: return transformerUtil.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. const text = prop.name.text; if (!(0, type_translator_1.isValidClosurePropertyName)(text)) return null; return text; default: return null; } } /** Removes comment metacharacters from a string, to make it safe to embed in a comment. */ function escapeForComment(str) { return str.replace(/\/\*/g, '__').replace(/\*\//g, '__'); } exports.escapeForComment = escapeForComment; function createClosurePropertyDeclaration(mtt, expr, prop, optional) { const name = propertyName(prop); if (!name) { // Skip warning for private identifiers because it is expected they are skipped in the // Closure output. // TODO(rdel): Once Closure Compiler determines how private properties should be represented, // adjust this output accordingly. if (ts.isPrivateIdentifier(prop.name)) { return transformerUtil.createMultiLineComment(prop, `Skipping private member:\n${escapeForComment(prop.getText())}`); } else { mtt.debugWarn(prop, `handle unnamed member:\n${escapeForComment(prop.getText())}`); return transformerUtil.createMultiLineComment(prop, `Skipping unnamed member:\n${escapeForComment(prop.getText())}`); } } if (name === 'prototype') { // Code that declares a property named 'prototype' typically is doing something // funny with the TS type system, and isn't actually interested in naming a // a field 'prototype', as prototype has special meaning in JS. return transformerUtil.createMultiLineComment(prop, `Skipping illegal member name:\n${escapeForComment(prop.getText())}`); } let type = mtt.typeToClosure(prop); // When a property is optional, e.g. // foo?: string; // Then the TypeScript type of the property is string|undefined, the // typeToClosure translation handles it correctly, and string|undefined is // how you write an optional property in Closure. // // But in the special case of an optional property with type any: // foo?: any; // The TypeScript type of the property is just "any" (because any includes // undefined as well) so our default translation of the type is just "?". // To mark the property as optional in Closure it must have "|undefined", // so the Closure type must be ?|undefined. if (optional && type === '?') type += '|undefined'; // Don't report warnings here to avoid duplicate warnings. We already warn // once when visiting this ts.PropertyDeclaration in jsdocTransformer.visitor const tags = mtt.getJSDoc(prop, /* reportWarnings */ false); const flags = ts.getCombinedModifierFlags(prop); const isReadonly = !!(flags & ts.ModifierFlags.Readonly); tags.push({ tagName: isReadonly ? 'const' : 'type', type }); if ((0, decorators_1.hasExportingDecorator)(prop, mtt.typeChecker)) { tags.push({ tagName: 'export' }); } else if (flags & ts.ModifierFlags.Protected) { tags.push({ tagName: 'protected' }); } else if (flags & ts.ModifierFlags.Private) { tags.push({ tagName: 'private' }); } else if (!tags.find((t) => t.tagName === 'export' || t.tagName === 'package')) { // TODO(b/202495167): remove the 'package' check above. // TS members are implicitly public if no visibility modifier was specified. // In Closure Compiler, members might inherit their superclass' visiblity. // Always explicitly emitting the visibility makes sure there is no // disagreement. // However we may only do this if there is no @export modifier, as that also // counts as a visibility modifier in Closure Compiler. tags.push({ tagName: 'public' }); } const declStmt = ts.setSourceMapRange(ts.factory.createExpressionStatement(ts.factory.createPropertyAccessExpression(expr, name)), prop); // Avoid printing annotations that can conflict with @type // This avoids Closure's error "type annotation incompatible with other annotations" addCommentOn(declStmt, tags, jsdoc.TAGS_CONFLICTING_WITH_TYPE); return declStmt; } /** * Removes any type assertions and non-null expressions from the AST before TypeScript processing. * * Ideally, the code in jsdoc_transformer below should just remove the cast expression and * replace it with the Closure equivalent. However Angular's compiler is fragile to AST * nodes being removed or changing type, so the code must retain the type assertion * expression, see: https://github.com/angular/angular/issues/24895. * * tsickle also cannot just generate and keep a `(/.. @type {SomeType} ./ (expr as SomeType))` * because TypeScript removes the parenthesized expressions in that syntax, (reasonably) believing * they were only added for the TS cast. * * The final workaround is then to keep the TypeScript type assertions, and have a post-Angular * processing step that removes the assertions before TypeScript sees them. * * TODO(martinprobst): remove once the Angular issue is fixed. */ function removeTypeAssertions() { return (context) => { return (sourceFile) => { function visitor(node) { switch (node.kind) { case ts.SyntaxKind.TypeAssertionExpression: case ts.SyntaxKind.AsExpression: return ts.visitNode(node.expression, visitor); case ts.SyntaxKind.NonNullExpression: return ts.visitNode(node.expression, visitor); default: break; } return ts.visitEachChild(node, visitor, context); } return visitor(sourceFile); }; }; } exports.removeTypeAssertions = removeTypeAssertions; /** * Returns true if node lexically (recursively) contains an 'async' function. */ function containsAsync(node) { if (ts.isFunctionLike(node) && transformerUtil.hasModifierFlag(node, ts.ModifierFlags.Async)) { return true; } return ts.forEachChild(node, containsAsync) || false; } /** * Determines if a given expression contains an optional property chain. */ function containsOptionalChainingOperator(node) { let maybePropertyAccessChain = node; // We know this is a property access chain if each member is a // PropertyAccessExpression`, a `NonNullExpression`, a `CallExpression`, or an // `ElementAccessExpression`. Once we get to an expression that isn't, we have // traversed the chain and can see if this was an optional chain. while (ts.isPropertyAccessExpression(maybePropertyAccessChain) || ts.isNonNullExpression(maybePropertyAccessChain) || ts.isCallExpression(maybePropertyAccessChain) || ts.isElementAccessExpression(maybePropertyAccessChain)) { // If we're at an access that used `?.`, we have found an optional property chain. if (!ts.isNonNullExpression(maybePropertyAccessChain) && maybePropertyAccessChain.questionDotToken != null) { return true; } maybePropertyAccessChain = maybePropertyAccessChain.expression; } return false; } /** * jsdocTransformer returns a transformer factory that converts TypeScript types into the equivalent * JSDoc annotations. */ function jsdocTransformer(host, tsOptions, typeChecker, diagnostics) { return (context) => { return (sourceFile) => { const moduleTypeTranslator = new module_type_translator_1.ModuleTypeTranslator(sourceFile, typeChecker, host, diagnostics, /*isForExterns*/ false); /** * The set of all names exported from an export * in the current module. Used to prevent * emitting duplicated exports. The first export * takes precedence in ES6. */ const expandedStarImports = new Set(); /** * While Closure compiler supports parameterized types, including parameterized `this` on * methods, it does not support constraints on them. That means that an `\@template`d type is * always considered to be `unknown` within the method, including `THIS`. * * To help Closure Compiler, we keep track of any templated this return type, and substitute * explicit casts to the templated type. * * This is an incomplete solution and works around a specific problem with warnings on unknown * this accesses. More generally, Closure also cannot infer constraints for any other * templated types, but that might require a more general solution in Closure Compiler. */ let contextThisType = null; function visitClassDeclaration(classDecl) { const contextThisTypeBackup = contextThisType; const mjsdoc = moduleTypeTranslator.getMutableJSDoc(classDecl); if (transformerUtil.hasModifierFlag(classDecl, ts.ModifierFlags.Abstract)) { mjsdoc.tags.push({ tagName: 'abstract' }); } maybeAddTemplateClause(mjsdoc.tags, classDecl); if (!host.untyped) { maybeAddHeritageClauses(mjsdoc.tags, moduleTypeTranslator, classDecl); } mjsdoc.updateComment(jsdoc.TAGS_CONFLICTING_WITH_TYPE); const decls = []; const memberDecl = createMemberTypeDeclaration(moduleTypeTranslator, classDecl); // WARNING: order is significant; we must create the member decl before transforming away // parameter property comments when visiting the constructor. decls.push(ts.visitEachChild(classDecl, visitor, context)); if (memberDecl) decls.push(memberDecl); contextThisType = contextThisTypeBackup; return decls; } /** * visitHeritageClause works around a Closure Compiler issue, where the expression in an * "extends" clause must be a simple identifier, and in particular must not be a parenthesized * expression. * * This is triggered when TS code writes "class X extends (Foo as Bar) { ... }", commonly done * to support mixins. For extends clauses in classes, the code below drops the cast and any * parentheticals, leaving just the original expression. * * This is an incomplete workaround, as Closure will still bail on other super expressions, * but retains compatibility with the previous emit that (accidentally) dropped the cast * expression. * * TODO(martinprobst): remove this once the Closure side issue has been resolved. */ function visitHeritageClause(heritageClause) { if (heritageClause.token !== ts.SyntaxKind.ExtendsKeyword || !heritageClause.parent || heritageClause.parent.kind === ts.SyntaxKind.InterfaceDeclaration) { return ts.visitEachChild(heritageClause, visitor, context); } if (heritageClause.types.length !== 1) { moduleTypeTranslator.error(heritageClause, `expected exactly one type in class extension clause`); } const type = heritageClause.types[0]; let expr = type.expression; while (ts.isParenthesizedExpression(expr) || ts.isNonNullExpression(expr) || ts.isAssertionExpression(expr)) { expr = expr.expression; } return ts.factory.updateHeritageClause(heritageClause, [ts.factory.updateExpressionWithTypeArguments(type, expr, type.typeArguments || [])]); } function visitInterfaceDeclaration(iface) { const sym = typeChecker.getSymbolAtLocation(iface.name); if (!sym) { moduleTypeTranslator.error(iface, 'interface with no symbol'); return []; } // If this symbol is both a type and a value, we cannot emit both into Closure's // single namespace. if ((0, transformer_util_1.symbolIsValue)(typeChecker, sym)) { moduleTypeTranslator.debugWarn(iface, `type/symbol conflict for ${sym.name}, using {?} for now`); return [transformerUtil.createSingleLineComment(iface, 'WARNING: interface has both a type and a value, skipping emit')]; } const tags = moduleTypeTranslator.getJSDoc(iface, /* reportWarnings */ true) || []; tags.push({ tagName: 'record' }); maybeAddTemplateClause(tags, iface); if (!host.untyped) { maybeAddHeritageClauses(tags, moduleTypeTranslator, iface); } const name = transformerUtil.getIdentifierText(iface.name); const modifiers = transformerUtil.hasModifierFlag(iface, ts.ModifierFlags.Export) ? [ts.factory.createToken(ts.SyntaxKind.ExportKeyword)] : undefined; const decl = ts.setSourceMapRange(ts.factory.createFunctionDeclaration( /* decorators */ undefined, modifiers, /* asterisk */ undefined, name, /* typeParameters */ undefined, /* parameters */ [], /* type */ undefined, /* body */ ts.factory.createBlock([])), iface); addCommentOn(decl, tags, jsdoc.TAGS_CONFLICTING_WITH_TYPE); const memberDecl = createMemberTypeDeclaration(moduleTypeTranslator, iface); return memberDecl ? [decl, memberDecl] : [decl]; } /** Function declarations are emitted as they are, with only JSDoc added. */ function visitFunctionLikeDeclaration(fnDecl) { if (!fnDecl.body) { // Two cases: abstract methods and overloaded methods/functions. // Abstract methods are handled in emitTypeAnnotationsHandler. // Overloads are union-ized into the shared type in FunctionType. return ts.visitEachChild(fnDecl, visitor, context); } const extraTags = []; if ((0, decorators_1.hasExportingDecorator)(fnDecl, typeChecker)) extraTags.push({ tagName: 'export' }); const { tags, thisReturnType } = moduleTypeTranslator.getFunctionTypeJSDoc([fnDecl], extraTags); // async functions when down-leveled access `this` to pass it to // tslib.__awaiter. Closure wants to know the type of 'this' for that. // The type is known in many contexts (e.g. methods, arrow functions) // per the normal rules (e.g. looking at parent nodes and @this tags) // but if the search bottoms out at a function scope, then Closure // warns that 'this' is unknown. // Because we have already checked the type of 'this', we are ok to just // suppress in that case. We do so by stuffing a @this on any function // where it might be needed; it's harmless to overapproximate. const isDownlevellingAsync = tsOptions.target !== undefined && tsOptions.target <= ts.ScriptTarget.ES2018; const isFunction = fnDecl.kind === ts.SyntaxKind.FunctionDeclaration; const hasExistingThisTag = tags.some(t => t.tagName === 'this'); if (isDownlevellingAsync && isFunction && !hasExistingThisTag && containsAsync(fnDecl)) { tags.push({ tagName: 'this', type: '*' }); } const mjsdoc = moduleTypeTranslator.getMutableJSDoc(fnDecl); mjsdoc.tags = tags; mjsdoc.updateComment(); const contextThisTypeBackup = contextThisType; // Arrow functions retain their context `this` type. All others reset the this type to // either none (if not specified) or the type given in a fn(this: T, ...) declaration. if (!ts.isArrowFunction(fnDecl)) contextThisType = thisReturnType; fnDecl = ts.visitEachChild(fnDecl, visitor, context); contextThisType = contextThisTypeBackup; if (!fnDecl.body) { // abstract functions do not need aliasing of their destructured // arguments. return fnDecl; } // Alias destructured function parameters for more precise types. const bindingAliases = []; const updatedParams = []; let hasUpdatedParams = false; for (const param of fnDecl.parameters) { if (!ts.isArrayBindingPattern(param.name)) { updatedParams.push(param); continue; } const updatedParamName = renameArrayBindings(param.name, bindingAliases); if (!updatedParamName) { updatedParams.push(param); continue; } hasUpdatedParams = true; updatedParams.push(ts.factory.updateParameterDeclaration(param, param.decorators, param.modifiers, param.dotDotDotToken, updatedParamName, param.questionToken, param.type, param.initializer)); } if (!hasUpdatedParams || bindingAliases.length === 0) return fnDecl; let body = fnDecl.body; const stmts = createArrayBindingAliases(ts.NodeFlags.Let, bindingAliases); if (!ts.isBlock(body)) { stmts.push(ts.factory.createReturnStatement( // Use ( parens ) to protect the return statement against // automatic semicolon insertion. ts.factory.createParenthesizedExpression(body))); body = ts.factory.createBlock(stmts, true); } else { stmts.push(...body.statements); body = ts.factory.updateBlock(body, stmts); } switch (fnDecl.kind) { case ts.SyntaxKind.FunctionDeclaration: fnDecl = ts.factory.updateFunctionDeclaration(fnDecl, fnDecl.decorators, fnDecl.modifiers, fnDecl.asteriskToken, fnDecl.name, fnDecl.typeParameters, updatedParams, fnDecl.type, body); break; case ts.SyntaxKind.MethodDeclaration: fnDecl = ts.factory.updateMethodDeclaration(fnDecl, fnDecl.decorators, fnDecl.modifiers, fnDecl.asteriskToken, fnDecl.name, fnDecl.questionToken, fnDecl.typeParameters, updatedParams, fnDecl.type, body); break; case ts.SyntaxKind.SetAccessor: fnDecl = ts.factory.updateSetAccessorDeclaration(fnDecl, fnDecl.decorators, fnDecl.modifiers, fnDecl.name, updatedParams, body); break; case ts.SyntaxKind.Constructor: fnDecl = ts.factory.updateConstructorDeclaration(fnDecl, fnDecl.decorators, fnDecl.modifiers, updatedParams, body); break; case ts.SyntaxKind.FunctionExpression: fnDecl = ts.factory.updateFunctionExpression(fnDecl, fnDecl.modifiers, fnDecl.asteriskToken, fnDecl.name, fnDecl.typeParameters, updatedParams, fnDecl.type, body); break; case ts.SyntaxKind.ArrowFunction: fnDecl = ts.factory.updateArrowFunction(fnDecl, fnDecl.modifiers, fnDecl.name, updatedParams, fnDecl.type, fnDecl.equalsGreaterThanToken, body); break; case ts.SyntaxKind.GetAccessor: moduleTypeTranslator.error(fnDecl, `get accessors cannot have parameters`); break; default: moduleTypeTranslator.error(fnDecl, `unexpected function like declaration`); break; } return fnDecl; } /** * In methods with a templated this type, adds explicit casts to accesses on this. * * @see contextThisType */ function visitThisExpression(node) { if (!contextThisType) return ts.visitEachChild(node, visitor, context); return createClosureCast(node, node, contextThisType); } /** * visitVariableStatement flattens variable declaration lists (`var a, b;` to `var a; var * b;`), and attaches JSDoc comments to each variable. JSDoc comments preceding the * original variable are attached to the first newly created one. */ function visitVariableStatement(varStmt) { const stmts = []; // "const", "let", etc are stored in node flags on the declarationList. const flags = ts.getCombinedNodeFlags(varStmt.declarationList); let tags = moduleTypeTranslator.getJSDoc(varStmt, /* reportWarnings */ true); const leading = ts.getSyntheticLeadingComments(varStmt); if (leading) { // Attach non-JSDoc comments to a not emitted statement. const commentHolder = ts.factory.createNotEmittedStatement(varStmt); ts.setSyntheticLeadingComments(commentHolder, leading.filter(c => c.text[0] !== '*')); stmts.push(commentHolder); } for (const decl of varStmt.declarationList.declarations) { const localTags = []; if (tags) { // Add any tags and docs preceding the entire statement to the first variable. localTags.push(...tags); tags = null; } // Add an @type for plain identifiers, but not for bindings patterns (i.e. object or array // destructuring - those do not have a syntax in Closure) or @defines, which already // declare their type. if (ts.isIdentifier(decl.name)) { // For variables that are initialized and use a type marked as unknown, do not emit a // type at all. Closure Compiler might be able to infer a better type from the // initializer than the `?` the code below would emit. // TODO(martinprobst): consider doing this for all types that get emitted as ?, not just // for marked ones. const initializersMarkedAsUnknown = !!decl.initializer && moduleTypeTranslator.isAlwaysUnknownSymbol(decl); if (!initializersMarkedAsUnknown) { // getOriginalNode(decl) is required because the type checker cannot type check // synthesized nodes. const typeStr = moduleTypeTranslator.typeToClosure(ts.getOriginalNode(decl)); // If @define is present then add the type to it, rather than adding a normal @type. const defineTag = localTags.find(({ tagName }) => tagName === 'define'); if (defineTag) { defineTag.type = typeStr; } else { localTags.push({ tagName: 'type', type: typeStr }); } } } else if (ts.isArrayBindingPattern(decl.name)) { const aliases = []; const updatedBinding = renameArrayBindings(decl.name, aliases); if (updatedBinding && aliases.length > 0) { const declVisited = ts.visitNode(decl, visitor); const newDecl = ts.factory.updateVariableDeclaration(declVisited, updatedBinding, declVisited.exclamationToken, declVisited.type, declVisited.initializer); const newStmt = ts.factory.createVariableStatement(varStmt.modifiers, ts.factory.createVariableDeclarationList([newDecl], flags)); if (localTags.length) { addCommentOn(newStmt, localTags, jsdoc.TAGS_CONFLICTING_WITH_TYPE); } stmts.push(newStmt); stmts.push(...createArrayBindingAliases(varStmt.declarationList.flags, aliases)); continue; } } const newDecl = ts.visitNode(decl, visitor); const newStmt = ts.factory.createVariableStatement(varStmt.modifiers, ts.factory.createVariableDeclarationList([newDecl], flags)); if (localTags.length) addCommentOn(newStmt, localTags, jsdoc.TAGS_CONFLICTING_WITH_TYPE); stmts.push(newStmt); } return stmts; } /** * shouldEmitExportsAssignments returns true if tsickle should emit `exports.Foo = ...` style * export statements. * * TypeScript modules can export types. Because types are pure design-time constructs in * TypeScript, it does not emit any actual exported symbols for these. But tsickle has to emit * an export, so that downstream Closure code (including tsickle-converted Closure code) can * import upstream types. tsickle has to pick a module format for that, because the pure ES6 * export would get stripped by TypeScript. * * tsickle uses CommonJS to emit googmodule, and code not using googmodule doesn't care about * the Closure annotations anyway, so tsickle skips emitting exports if the module target * isn't commonjs. */ function shouldEmitExportsAssignments() { return tsOptions.module === ts.ModuleKind.CommonJS; } function visitTypeAliasDeclaration(typeAlias) { const sym = moduleTypeTranslator.mustGetSymbolAtLocation(typeAlias.name); // If the type is also defined as a value, skip emitting it. Closure collapses type & value // namespaces, the two emits would conflict if tsickle emitted both. if ((0, transformer_util_1.symbolIsValue)(typeChecker, sym)) return []; if (!shouldEmitExportsAssignments()) return []; const typeName = typeAlias.name.getText(); // Set any type parameters as unknown, Closure does not support type aliases with type // parameters. moduleTypeTranslator.newTypeTranslator(typeAlias).markTypeParameterAsUnknown(moduleTypeTranslator.symbolsToAliasedNames, typeAlias.typeParameters); const typeStr = host.untyped ? '?' : moduleTypeTranslator.typeToClosure(typeAlias, undefined); // We want to emit a @typedef. They are a bit weird because they are 'var' statements // that have no value. const tags = moduleTypeTranslator.getJSDoc(typeAlias, /* reportWarnings */ true); tags.push({ tagName: 'typedef', type: typeStr }); const isExported = transformerUtil.hasModifierFlag(typeAlias, ts.ModifierFlags.Export); let decl; if (isExported) { // Given: export type T = ...; // We cannot emit `export var foo;` and let TS generate from there because TypeScript // drops exports that are never assigned values, and Closure requires us to not assign // values to typedef exports. Introducing a new local variable and exporting it can cause // bugs due to name shadowing and confusing TypeScript's logic on what symbols and types // vs values are exported. Mangling the name to avoid the conflicts would be reasonably // clean, but would require a two pass emit to first find all type alias names, mangle // them, and emit the use sites only later. // So we produce: exports.T; decl = ts.factory.createExpressionStatement(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('exports'), ts.factory.createIdentifier(typeName))); } else { // Given: type T = ...; // We produce: var T; // Note: not const, because 'const Foo;' is illegal; // not let, because we want hoisting behavior for types. decl = ts.factory.createVariableStatement( /* modifiers */ undefined, ts.factory.createVariableDeclarationList([ts.factory.createVariableDeclaration(ts.factory.createIdentifier(typeName))])); } decl = ts.setSourceMapRange(decl, typeAlias); addCommentOn(decl, tags, jsdoc.TAGS_CONFLICTING_WITH_TYPE); return [decl]; } /** Emits a parenthesized Closure cast: `(/** \@type ... * / (expr))`. */ function createClosureCast(context, expression, type) { const inner = ts.factory.createParenthesizedExpression(expression); const comment = addCommentOn(inner, [{ tagName: 'type', type: moduleTypeTranslator.typeToClosure(context, type) }]); comment.hasTrailingNewLine = false; return ts.setSourceMapRange(ts.factory.createParenthesizedExpression(inner), context); } /** Converts a TypeScript type assertion into a Closure Cast. */ function visitAssertionExpression(assertion) { const type = typeChecker.getTypeAtLocation(assertion.type); return createClosureCast(assertion, ts.visitEachChild(assertion, visitor, context), type); } /** * Converts a TypeScript non-null assertion into a Closure Cast, by stripping |null and * |undefined from a union type. */ function visitNonNullExpression(nonNull) { // If this is a NonNullExpression inside of a property chain with a `?.` // access we cannot add a cast telling Closure Compiler that this node // is non-nullable. Adding that cast requires additional parentheses, // which changes the behavior of the optional chain. Instead, we drop // the `!` and return the inner expression as-is. This works in the // context of chained property access because JSCompiler will not check // this. If this non-null expression is part of a property access chain // but comes before the ?. access (for example a!.b?.c) // `containsOptionalChainingOperator` will return false, but in that // situation we can safely add the cast because extra parens only matter // after the ?. access. if (containsOptionalChainingOperator(nonNull)) { return nonNull.expression; } const type = typeChecker.getTypeAtLocation(nonNull.expression); const nonNullType = typeChecker.getNonNullableType(type); return createClosureCast(nonNull, ts.visitEachChild(nonNull, visitor, context), nonNullType); } function visitImportDeclaration(importDecl) { // For each import, insert a goog.requireType for the module, so that if // TypeScript does not emit the module because it's only used in type // positions, the JSDoc comments still reference a valid Closure level // symbol. const resolveModuleNameOptions = { options: tsOptions, moduleResolutionHost: host.moduleResolutionHost }; // No need to requireType side effect imports. // Note that this means tsickle does not report diagnostics for // side-effect path imports of JavaScript modules with conflicting // provides. That is working as intended. if (!importDecl.importClause) return importDecl; const sym = typeChecker.getSymbolAtLocation(importDecl.moduleSpecifier); // Scripts do not have a symbol, and neither do unused modules (empty // import list). Scripts can still be imported using side effect // imports. TypeScript emits a runtime load for a side-effect imports, // which has the desired effect of executing side-effects, and can also // be used to make sure global declarations are present. Neither of // these need a `goog.requireType`. // Empty import lists (`import {} from 'x';`) intentionally create no // emit in TS and do not need a `goog.requireType` either (as there is // no symbol imported). Users that wish to force a load should use // side-effect imports. if (!sym) return importDecl; const importPath = googmodule.resolveModuleName(resolveModuleNameOptions, sourceFile.fileName, importDecl.moduleSpecifier.text); moduleTypeTranslator.requireType(importDecl.moduleSpecifier, importPath, sym, /* default import? */ !!importDecl.importClause.name); return importDecl; } /** * Parses and then re-serializes JSDoc comments, escaping or removing * illegal tags. * * Closure Compiler will fail when it finds incorrect JSDoc tags on * nodes. This function also escapes some type-syntax tags used by * JSCompiler, in case they would end up in incorrect places after * transformation. */ function escapeIllegalJSDoc(node) { if (!ts.getParseTreeNode(node)) return; // TODO(b/139687753): support escaping multiple pieces of JSDoc attached // to a single ts.Node instead of just the last JSDoc or ban them const mjsdoc = moduleTypeTranslator.getMutableJSDoc(node); mjsdoc.updateComment(jsdoc.TAGS_CONFLICTING_WITH_TYPE); } /** Returns true if a value export should be emitted for the given symbol in export *. */ function shouldEmitValueExportForSymbol(sym) { if (sym.flags & ts.SymbolFlags.Alias) { sym = typeChecker.getAliasedSymbol(sym); } if ((sym.flags & ts