content: package com.google.javascript.gents;
  
  import static com.google.javascript.rhino.TypeDeclarationsIR.anyType;
  import static com.google.javascript.rhino.TypeDeclarationsIR.arrayType;
  import static com.google.javascript.rhino.TypeDeclarationsIR.booleanType;
  import static com.google.javascript.rhino.TypeDeclarationsIR.functionType;
  import static com.google.javascript.rhino.TypeDeclarationsIR.namedType;
  import static com.google.javascript.rhino.TypeDeclarationsIR.numberType;
  import static com.google.javascript.rhino.TypeDeclarationsIR.parameterizedType;
  import static com.google.javascript.rhino.TypeDeclarationsIR.recordType;
  import static com.google.javascript.rhino.TypeDeclarationsIR.stringType;
  import static com.google.javascript.rhino.TypeDeclarationsIR.undefinedType;
  import static com.google.javascript.rhino.TypeDeclarationsIR.unionType;
  import static com.google.javascript.rhino.TypeDeclarationsIR.voidType;
  
  import com.google.common.base.Predicates;
  import com.google.common.collect.FluentIterable;
  import com.google.common.collect.HashBasedTable;
  import com.google.common.collect.ImmutableList;
  import com.google.common.collect.Iterables;
  import com.google.common.collect.LinkedHashMultimap;
  import com.google.common.collect.Multimap;
  import com.google.common.collect.Table;
  import com.google.javascript.gents.CollectModuleMetadata.FileModule;
  import com.google.javascript.jscomp.AbstractCompiler;
  import com.google.javascript.jscomp.CompilerPass;
  import com.google.javascript.jscomp.NodeTraversal;
  import com.google.javascript.jscomp.NodeTraversal.AbstractPostOrderCallback;
  import com.google.javascript.jscomp.NodeUtil;
  import com.google.javascript.rhino.IR;
  import com.google.javascript.rhino.JSDocInfo;
  import com.google.javascript.rhino.JSDocInfo.Visibility;
  import com.google.javascript.rhino.JSTypeExpression;
  import com.google.javascript.rhino.Node;
  import com.google.javascript.rhino.Node.TypeDeclarationNode;
  import com.google.javascript.rhino.Token;
  import java.util.ArrayList;
  import java.util.HashMap;
  import java.util.HashSet;
  import java.util.LinkedHashMap;
  import java.util.List;
  import java.util.Map;
  import java.util.Set;
  import java.util.regex.Pattern;
  import javax.annotation.Nullable;
  
  /**
   * Converts JavaScript code into JavaScript code annotated with TypeScript {@code
   * TypeDeclarationNode} information provided by the corresponding jsdoc.
   *
   * <p>This compiler pass is based off of the {@code JsdocToEs6TypedConverter} compiler pass.
   */
  public final class TypeAnnotationPass implements CompilerPass {
  
    private final AbstractCompiler compiler;
    private final PathUtil pathUtil;
    private final NameUtil nameUtil;
    private final NodeComments nodeComments;
  
    /** symbolName -> fileModule */
    private final Map<String, FileModule> symbolToModule;
    /** filename, namespace -> local name */
    private final Table<String, String, String> typeRewrite;
    /** filename -> extra imports needed to be added */
    private final Multimap<String, Node> importsNeeded = LinkedHashMultimap.create();
    /** extern -> typing map for when extern and TS typing names differ */
    private final Map<String, String> externsMap;
  
    TypeAnnotationPass(
        AbstractCompiler compiler,
        PathUtil pathUtil,
        NameUtil nameUtil,
        Map<String, FileModule> symbolMap,
        Table<String, String, String> typeRewrite,
        NodeComments nodeComments,
        Map<String, String> externsMap) {
      this.compiler = compiler;
      this.pathUtil = pathUtil;
      this.nameUtil = nameUtil;
      this.nodeComments = nodeComments;
  
      this.symbolToModule = new HashMap<>(symbolMap);
      this.typeRewrite = HashBasedTable.create(typeRewrite);
      this.externsMap = externsMap;
    }
  
    @Override
    public void process(Node externs, Node root) {
      NodeTraversal.traverse(compiler, root, new TypeAnnotationConverter());
      NodeTraversal.traverse(compiler, root, new AccessModifierConverter());
      for (Node script : root.children()) {
        addTypeOnlyImports(script);
      }
    }
  
    /** Annotates variables and functions with their corresponding TypeScript type. */
    private class TypeAnnotationConverter extends AbstractPostOrderCallback {
      @Override
      public void visit(NodeTraversal t, Node n, Node parent) {
        JSDocInfo bestJSDocInfo = NodeUtil.getBestJSDocInfo(n);
        switch (n.getToken()) {
            // Fields default to any type
          case MEMBER_VARIABLE_DEF:
            if (bestJSDocInfo != null && bestJSDocInfo.getType() != null) {
              setTypeExpression(n, bestJSDocInfo.getType(), false);
            } else {
              n.setDeclaredTypeExpression(anyType());
            }
            break;
            // Functions are annotated with their return type
          case FUNCTION:
            if (bestJSDocInfo != null) {
              setTypeExpression(n, bestJSDocInfo.getReturnType(), true);
            }
            break;
          case CLASS:
            if (bestJSDocInfo != null) {
              List<JSTypeExpression> interfaces = bestJSDocInfo.getImplementedInterfaces();
              if (!interfaces.isEmpty()) {
                // Convert the @implements {...} JSDoc comments to TypeNodeASTs, and add them into the
                // implements part of the class definition.
                Node impls = new Node(Token.IMPLEMENTS);
                for (JSTypeExpression type : interfaces) {
                  impls.addChildToBack(convertTypeNodeAST(type.getRoot()));
                }
                impls.useSourceInfoFrom(n);
                n.putProp(Node.IMPLEMENTS, impls);
              }
            }
            break;
          case INTERFACE_EXTENDS:
            Node newExtends = n.cloneNode();
            for (Node c : n.children()) {
              newExtends.addChildToBack(convertTypeNodeAST(c));
            }
            parent.replaceChild(n, newExtends);
            break;
          case TYPE_ALIAS:
            if (bestJSDocInfo != null && bestJSDocInfo.hasTypedefType()) {
              JSTypeExpression typeDef = bestJSDocInfo.getTypedefType();
              n.addChildToBack(convertTypeNodeAST(typeDef.getRoot()));
            }
            break;
            // Names and properties are annotated with their types
          case NAME:
          case GETPROP:
            if (parent == null) {
              break;
            }
            if (NodeUtil.isNameDeclaration(parent)) { // Variable declaration
              maybeSetInlineTypeExpression(n, n, bestJSDocInfo, false);
            } else if (parent.isParamList()) { // Function parameters
              boolean wasSetFromFunctionDoc = setParameterTypeFromFunctionDoc(n, parent);
              if (!wasSetFromFunctionDoc) {
                // If we didn't set the parameter type from the functions's JsDoc, then maybe the type
                // is inlined just before the parameter?
                maybeSetInlineTypeExpression(n, n, bestJSDocInfo, false);
              }
            } else if (parent.isFunction() && n.getJSDocInfo() == bestJSDocInfo) {
              // If this is a name of the function and the JsDoc is just before this name,
              // then it may be an inline return type of this function.
              maybeSetInlineTypeExpression(parent, n, bestJSDocInfo, true);
            }
            break;
            // If a DEVAULE_VALUE is in a PARAM_LIST we type annotate its first child which is the
            // actual parameter.
          case DEFAULT_VALUE:
            Node paramNode = n.getFirstChild();
            if (parent != null && parent.isParamList() && paramNode != null && paramNode.isName()) {
              boolean wasSetFromFunctionDoc = setParameterTypeFromFunctionDoc(paramNode, parent);
              if (!wasSetFromFunctionDoc) {
                // If we didn't set the parameter type from the functions's JsDoc, then maybe the type
                // is inlined just before the parameter?
                maybeSetInlineTypeExpression(paramNode, paramNode, bestJSDocInfo, false);
              }
            }
            break;
          case CAST:
            setTypeExpression(n, n.getJSDocInfo().getType(), false);
            break;
          case INTERFACE_MEMBERS:
            // Closure code generator expects the form:
            //
            // INTERFACE_MEMBERS
            //     MEMBER_VARIABLE_DEF property1 [jsdoc_info: JSDocInfo]
            //     MEMBER_VARIABLE_DEF property2 [jsdoc_info: JSDocInfo]
            //
            // Each MEMBER_VARIABLE_DEF has a jsdoc about its typing.
            //
            // Closure annotated interfaces are already in this format at this point so it's a no-op
            // here. typedefs inside classes are converted to top level interfaces in
            // TypeConversionPass. They are in a different format:
            //
            // INTERFACE_MEMBERS [jsdoc_info: JSDocInfo]
            //
            // There are no MEMBER_VARIABLE_DEFs yet. All the information are stored in
            // INTERFACE_MEMBERS's jsdoc. We are extracting the properties from the jsdoc and
            // creating each MEMBER_VARIABLE_DEFs so code generator works.
            if (bestJSDocInfo != null && bestJSDocInfo.hasTypedefType()) {
              Node typedefTypeRoot = bestJSDocInfo.getTypedefType().getRoot();
              if ((typedefTypeRoot.getToken() == Token.BANG)
                  || (typedefTypeRoot.getToken() == Token.QMARK)) {
                typedefTypeRoot = typedefTypeRoot.getFirstChild();
              }
              // (BANG|QMARK)
              //     LC
              //         LB
              //             COLON
              //                 STRING_KEY a
              //                 STRING number
              //             COLON
              //                 STRING_KEY b
              //                 STRING number
              for (Node colonNode : typedefTypeRoot.getFirstChild().children()) {
                Node memberVariableDefNode =
                    Node.newString(Token.MEMBER_VARIABLE_DEF, colonNode.getFirstChild().getString());
                memberVariableDefNode.setDeclaredTypeExpression(
                    convertTypeNodeAST(colonNode.getSecondChild()));
                n.addChildToBack(memberVariableDefNode);
              }
            }
            break;
          default:
            break;
        }
      }
  
      private void maybeSetInlineTypeExpression(
          Node nameNode, Node commentNode, JSDocInfo docInfo, boolean isReturnType) {
        if (docInfo == null) {
          return;
        }
        JSTypeExpression type = docInfo.getType();
        if (type == null) {
          return;
        }
        setTypeExpression(nameNode, type, isReturnType);
        // Remove the part of comment that sets the inline type.
        String toRemove = docInfo.getOriginalCommentString();
        String nodeComment = nodeComments.getComment(commentNode);
        if (nodeComment == null) {
          return;
        }
        nodeComment = nodeComment.replaceFirst("\\n?" + Pattern.quote(toRemove), "");
        nodeComments.setComment(commentNode, nodeComment);
        compiler.reportChangeToEnclosingScope(commentNode);
      }
  
      /**
       * Attempts to set the type of parameter represented by node by extracting it from @param
       * annotations in the function's jsDoc. Returns true if it succeeds (i.e. if it finds the type).
       */
      private boolean setParameterTypeFromFunctionDoc(Node node, Node parent) {
        JSDocInfo parentDocInfo = NodeUtil.getBestJSDocInfo(parent.getParent());
        if (parentDocInfo == null) {
          return false;
        }
        JSTypeExpression parameterType = parentDocInfo.getParameterType(node.getString());
        if (parameterType == null) {
          return false;
        }
        TypeDeclarationNode parameterTypeNode = convertTypeNodeAST(parameterType.getRoot());
        // Parameter is declared using verbose @param syntax before the function definition.
        Node attachTypeExpr = node;
        // Modify the primary AST to represent a function parameter as a
        // REST node, if the type indicates it is a rest parameter.
        if (parameterType.getRoot().getToken() == Token.ITER_REST) {
          attachTypeExpr = IR.iterRest(IR.name(node.getString()));
          nodeComments.replaceWithComment(node, attachTypeExpr);
        }
        // Modify the AST to represent an optional parameter
        if (parameterType.getRoot().getToken() == Token.EQUALS) {
          attachTypeExpr = IR.name(node.getString());
          if (!node.getParent().isDefaultValue()) {
            attachTypeExpr.putBooleanProp(Node.OPT_ES6_TYPED, true);
          } else if (node.getParent().getChildAtIndex(1).isName()
              && node.getParent().getChildAtIndex(1).getString().equals("undefined")) {
            // if default value is "undefined" add undefined to the type
            parameterTypeNode = flatUnionType(ImmutableList.of(parameterTypeNode, undefinedType()));
          }
          nodeComments.replaceWithComment(node, attachTypeExpr);
        }
        setTypeExpression(attachTypeExpr, parameterTypeNode);
        return true;
      }
    }
  
    /** Annotates variables with their corresponding modifiers. (ie. private, protected) */
    private static class AccessModifierConverter extends AbstractPostOrderCallback {
      @Override
      public void visit(NodeTraversal t, Node n, Node parent) {
        JSDocInfo bestJSDocInfo = NodeUtil.getBestJSDocInfo(n);
        if (bestJSDocInfo == null) {
          return;
        }
  
        // Add visibility for private and protected.
        if (Visibility.PRIVATE.equals(bestJSDocInfo.getVisibility())) {
          n.putProp(Node.ACCESS_MODIFIER, Visibility.PRIVATE);
        } else if (Visibility.PROTECTED.equals(bestJSDocInfo.getVisibility())) {
          n.putProp(Node.ACCESS_MODIFIER, Visibility.PROTECTED);
        }
  
        // Change variable declarations to constants
        if (bestJSDocInfo.isConstant() && (n.isVar() || n.isLet())) {
          n.setToken(Token.CONST);
        }
      }
    }
  
    /** Adds nodes for new imports required to resolve type declarations. */
    void addTypeOnlyImports(Node script) {
      if (!script.isScript() || !importsNeeded.containsKey(script.getSourceFileName())) {
        return;
      }
  
      Node body = script.getFirstChild().isModuleBody() ? script.getFirstChild() : script;
  
      for (Node n : body.children()) {
        if (n.isImport()) {
          // Prepends new imports to the first import
          for (Node newImport : importsNeeded.get(script.getSourceFileName())) {
            body.addChildBefore(newImport, n);
          }
          return;
        }
      }
      for (Node newImport : importsNeeded.get(script.getSourceFileName())) {
        script.addChildToFront(newImport);
      }
    }
  
    /** Sets the annotated type expression corresponding to Node {@code n}. */
    private void setTypeExpression(Node n, @Nullable JSTypeExpression type, boolean isReturnType) {
      TypeDeclarationNode node = convert(type, isReturnType);
      setTypeExpression(n, node);
    }
  
    /** Sets the annotated type expression corresponding to Node {@code n}. */
    private void setTypeExpression(Node n, @Nullable TypeDeclarationNode type) {
      if (type != null) {
        n.setDeclaredTypeExpression(type);
        compiler.reportChangeToEnclosingScope(n);
      }
    }
  
    @Nullable
    private TypeDeclarationNode convert(@Nullable JSTypeExpression typeExpr, boolean isReturnType) {
      if (typeExpr == null) {
        return null;
      }
      return convertTypeNodeAST(typeExpr.getRoot(), isReturnType);
    }
  
    /**
     * The root of a JSTypeExpression is very different from an AST node, even though we use the same
     * Java class to represent them. This function converts root nodes of JSTypeExpressions into
     * TypeDeclaration ASTs, to make them more similar to ordinary AST nodes.
     *
     * @return the root node of a TypeDeclaration AST, or null if no type is available for the node.
     */
    @Nullable
    private TypeDeclarationNode convertTypeNodeAST(Node n) {
      return convertTypeNodeAST(n, false);
    }
  
    @Nullable
    private TypeDeclarationNode convertTypeNodeAST(Node n, boolean isReturnType) {
      switch (n.getToken()) {
          // for function types that don't declare a return type
          // ex. /** @return */ var f = function() {};
        case EMPTY:
          return null;
          // TODO(renez): re-evaluate whether or not we want to convert {*} to the any type.
        case STAR:
          return anyType();
        case VOID:
          return isReturnType ? voidType() : undefinedType();
          // TypeScript types are non-nullable by default with --strictNullChecks
        case BANG:
          return convertTypeNodeAST(n.getFirstChild());
        case QMARK:
          Node child = n.getFirstChild();
          if (child == null) {
            return anyType();
          } else {
            ImmutableList<TypeDeclarationNode> types =
                ImmutableList.of(convertTypeNodeAST(child), new TypeDeclarationNode(Token.NULL));
            return flatUnionType(types);
          }
        case STRING:
          String typeName = n.getString();
          switch (typeName) {
            case "boolean":
              return booleanType();
            case "number":
              return numberType();
            case "string":
              return stringType();
            case "null":
              // TODO(renez): refactor this once Token.NULL_TYPE exists
              return new TypeDeclarationNode(Token.NULL);
              // Both undefined and void are converted to undefined for all non-return types.
              // In closure, "void" and "undefined" are type aliases and thus, equivalent types.
              // However, in TS, it is more ideomatic to emit "void" for return types.
              // Additionally, there is semantic difference to note: TS "undefined" return types require
              // a return statement, while "void" does not.
            case "undefined":
            case "void":
              return isReturnType ? voidType() : undefinedType();
            default:
              String newTypeName = convertTypeName(n.getSourceFileName(), typeName);
              newTypeName = convertExternNameToTypingName(newTypeName);
              TypeDeclarationNode root = namedType(newTypeName);
              if (n.getChildCount() > 0 && n.getFirstChild().isNormalBlock()) {
                Node block = n.getFirstChild();
                // Convert {Array<t>} to t[]
                if ("Array".equals(typeName)) {
                  return arrayType(convertTypeNodeAST(block.getFirstChild()));
                }
  
                // Convert index signature types
                if ("Object".equals(typeName)) {
                  TypeDeclarationNode indexSignatureNode =
                      indexSignatureType(
                          convertTypeNodeAST(block.getFirstChild()),
                          convertTypeNodeAST(block.getSecondChild()));
                  return indexSignatureNode;
                }
  
                // Convert generic types
                return parameterizedType(
                    root,
                    Iterables.filter(
                        Iterables.transform(block.children(), this::convertTypeNodeAST),
                        Predicates.notNull()));
              }
              return root;
          }
          // Convert records
        case LC:
          LinkedHashMap<String, TypeDeclarationNode> properties = new LinkedHashMap<>();
          for (Node field : n.getFirstChild().children()) {
            boolean isFieldTypeDeclared = field.getToken() == Token.COLON;
            Node fieldNameNode = isFieldTypeDeclared ? field.getFirstChild() : field;
            String fieldName = fieldNameNode.getString();
            if (fieldName.startsWith("'") || fieldName.startsWith("\"")) {
              fieldName = fieldName.substring(1, fieldName.length() - 1);
            }
            TypeDeclarationNode fieldType =
                isFieldTypeDeclared ? convertTypeNodeAST(field.getLastChild()) : null;
            properties.put(fieldName, fieldType);
          }
          return recordType(properties);
          // Convert unions
        case PIPE:
          ImmutableList<TypeDeclarationNode> types =
              FluentIterable.from(n.children())
                  .transform(this::convertTypeNodeAST)
                  .filter(Predicates.notNull())
                  .toList();
          switch (types.size()) {
            case 0:
              return null;
            case 1:
              return types.get(0);
            default:
              return flatUnionType(types);
          }
          // Convert function types
        case FUNCTION:
          Node returnType = anyType();
          LinkedHashMap<String, TypeDeclarationNode> requiredParams = new LinkedHashMap<>();
          LinkedHashMap<String, TypeDeclarationNode> optionalParams = new LinkedHashMap<>();
          String restName = null;
          TypeDeclarationNode restType = null;
          for (Node child2 : n.children()) {
            if (child2.isParamList()) {
              int paramIdx = 1;
              for (Node param : child2.children()) {
                String paramName = "p" + paramIdx++;
                if (param.getToken() == Token.ITER_REST) {
                  if (param.getFirstChild() != null) {
                    restType = convertTypeNodeAST(param);
                  }
                  restName = paramName;
                } else if (param.getToken() == Token.EQUALS) {
                  optionalParams.put(paramName, convertTypeNodeAST(param));
                } else {
                  requiredParams.put(paramName, convertTypeNodeAST(param));
                }
              }
            } else if (child2.isNew()) {
              // keep the constructor signatures on the tree, and emit them following
              // the syntax in TypeScript 1.8 spec, section 3.8.9 Constructor Type Literals
            } else if (child2.isThis()) {
              // Not expressible in TypeScript syntax, so we omit them from the tree.
              // They could be added as properties on the result node.
            } else {
              returnType = convertTypeNodeAST(child2, true);
              if (returnType == null) {
                returnType = anyType();
              }
            }
          }
          return functionType(returnType, requiredParams, optionalParams, restName, restType);
          // Variable function parameters are encoded as an array.
        case ITER_REST:
          Node arrType = convertTypeNodeAST(n.getFirstChild());
          if (arrType == null) {
            arrType = anyType();
          }
          return arrayType(arrType);
          // Optional parameters are entirely encoded within the parameter name while the type
          // remains the same.
        case EQUALS:
          return convertTypeNodeAST(n.getFirstChild());
        case NAME:
          return namedType(n.getString());
        default:
          throw new IllegalArgumentException("Unsupported node type:\n" + n.toStringTree());
      }
    }
  
    /** Returns a new node representing an index signature type. */
    private TypeDeclarationNode indexSignatureType(
        TypeDeclarationNode keyType, TypeDeclarationNode valueType) {
      TypeDeclarationNode node = new TypeDeclarationNode(Token.INDEX_SIGNATURE);
      TypeDeclarationNode first = null;
      first = new TypeDeclarationNode(Token.STRING_KEY, "key");
      first.setDeclaredTypeExpression(keyType);
      node.addChildToBack(first);
      node.setDeclaredTypeExpression(valueType);
      return node;
    }
  
    /** Converts the global type name to the local type name. */
    private String convertTypeName(String sourceFile, String typeName) {
      Map<String, String> rewriteMap =
          typeRewrite.containsRow(sourceFile)
              ? typeRewrite.rowMap().get(sourceFile)
              : new HashMap<>();
  
      // All type symbols declared anywhere in the compilation unit
      Set<String> allTypes = new HashSet<>();
      allTypes.addAll(rewriteMap.keySet());
      allTypes.addAll(symbolToModule.keySet());
  
      String importedNamespace = nameUtil.findLongestNamePrefix(typeName, allTypes);
      // Closure compilation can be permissive to the point where there are missing types.
      // Gents supports these usecases by syntactically emitting the type as written. The file emitted
      // file might not work with TS compiler, but at least the type is not lost.
      if (importedNamespace == null) {
        return typeName;
      }
  
      if (rewriteMap.containsKey(importedNamespace)) {
        // Rewrite already imported types
        String symbol = rewriteMap.get(importedNamespace);
        return nameUtil.replacePrefixInName(typeName, importedNamespace, symbol);
      } else {
        // Rewrite ALL types in compilation unit
        FileModule module = symbolToModule.get(importedNamespace);
        String symbol = module.importedNamespacesToSymbols.get(importedNamespace);
  
        FileModule typeModule = symbolToModule.get(typeName);
  
        // Create a new import statement if the symbol to import isn't from the same file or the type
        // is not part of the compilation unit.
        if (typeModule != null && !sourceFile.equals(typeModule.file)) {
          Node importSpec;
          Node importFile;
          if (module.shouldUseOldSyntax()) {
            importSpec = Node.newString(Token.NAME, symbol);
            importFile = Node.newString("goog:" + importedNamespace);
          } else {
            Node spec = new Node(Token.IMPORT_SPEC, IR.name(symbol));
            spec.setShorthandProperty(true);
            importSpec = new Node(Token.IMPORT_SPECS, spec);
            importFile = Node.newString(pathUtil.getImportPath(sourceFile, module.file));
          }
          Node importNode = new Node(Token.IMPORT, IR.empty(), importSpec, importFile);
          importsNeeded.put(sourceFile, importNode);
        }
        typeRewrite.put(sourceFile, importedNamespace, symbol);
        return nameUtil.replacePrefixInName(typeName, importedNamespace, symbol);
      }
    }
  
    /**
     * If an extern-to-typing map is provided, try to look up the extern type name and replace it with
     * the TypeScript version.
     */
    private String convertExternNameToTypingName(String externTypeName) {
      String typingName = this.externsMap.get(externTypeName);
      if (typingName != null) {
        return typingName;
      } else {
        return externTypeName;
      }
    }
  
    /** Helper function to recursively flatten union types. */
    private void flatten(
        Iterable<TypeDeclarationNode> types, List<TypeDeclarationNode> result, boolean hasNull) {
      for (TypeDeclarationNode t : types) {
        switch (t.getToken()) {
          case NULL:
            if (!hasNull) {
              result.add(new TypeDeclarationNode(Token.NULL));
              hasNull = true;
            }
            break;
          case UNION_TYPE:
            Iterable<TypeDeclarationNode> children =
                FluentIterable.from(t.children())
                    .transform(node -> (TypeDeclarationNode) node)
                    .toList();
            // We had to invoke .toList() as detachChildren() breaks the Iterable.
            t.detachChildren();
            flatten(children, result, hasNull);
            break;
          default:
            result.add(t);
            break;
        }
      }
    }
  
    /**
     * Deep flattens a list of types into a single union type.
     *
     * @return A flattened union type with at most 1 null.
     */
    private TypeDeclarationNode flatUnionType(Iterable<TypeDeclarationNode> types) {
      List<TypeDeclarationNode> flatTypes = new ArrayList<>();
      flatten(types, flatTypes, false);
      return unionType(flatTypes);
    }
  }
  