content: package com.google.javascript.gents;
  
  import static com.google.common.base.MoreObjects.firstNonNull;
  
  import com.google.common.collect.ImmutableSet;
  import com.google.common.collect.Iterables;
  import com.google.javascript.jscomp.AbstractCompiler;
  import com.google.javascript.jscomp.CompilerPass;
  import com.google.javascript.jscomp.JSError;
  import com.google.javascript.jscomp.NodeTraversal;
  import com.google.javascript.jscomp.NodeUtil;
  import com.google.javascript.rhino.JSDocInfo;
  import com.google.javascript.rhino.Node;
  import java.util.HashMap;
  import java.util.HashSet;
  import java.util.LinkedHashMap;
  import java.util.LinkedHashSet;
  import java.util.Map;
  import java.util.Set;
  import javax.annotation.Nullable;
  
  /**
   * Preprocesses all source and library files to build a mapping between Closure namespaces and file
   * based modules.
   */
  public final class CollectModuleMetadata extends AbstractTopLevelCallback implements CompilerPass {
  
    private final AbstractCompiler compiler;
    private final NameUtil nameUtil;
  
    private final Set<String> filesToConvert;
    private final Map<String, FileModule> fileToModule = new LinkedHashMap<>();
    private final Map<String, FileModule> namespaceToModule = new LinkedHashMap<>();
  
    Map<String, FileModule> getFileMap() {
      return fileToModule;
    }
  
    void addFileMap(String filename) {
      if (!fileToModule.containsKey(filename)) {
        fileToModule.put(filename, new FileModule(filename, false));
      }
    }
  
    Map<String, FileModule> getNamespaceMap() {
      return namespaceToModule;
    }
  
    /** Returns a map from all symbols in the compilation unit to their respective modules */
    Map<String, FileModule> getSymbolMap() {
      Map<String, FileModule> out = new LinkedHashMap<>();
      for (FileModule module : fileToModule.values()) {
        for (String symbol : module.importedNamespacesToSymbols.keySet()) {
          out.put(symbol, module);
        }
      }
      return out;
    }
  
    CollectModuleMetadata(AbstractCompiler compiler, NameUtil nameUtil, Set<String> filesToConvert) {
      this.compiler = compiler;
      this.nameUtil = nameUtil;
      this.filesToConvert = filesToConvert;
    }
  
    @Override
    public void process(Node externs, Node root) {
      NodeTraversal.traverse(compiler, root, this);
    }
  
    @Override
    public void visit(NodeTraversal t, Node n, Node parent) {
      String filename = n.getSourceFileName();
      @Nullable FileModule module = fileToModule.get(filename);
  
      // const A = goog.require('path.to.A');
      if (module != null && (n.isConst() || n.isLet() || n.isVar())) {
        @Nullable Node rhs = n.getFirstChild().getLastChild();
        if (rhs != null && rhs.isCall() && rhs.getFirstChild().matchesQualifiedName("goog.require")) {
          module.reportImport();
        }
      }
  
      if (!n.isExprResult()) {
        return;
      }
  
      Node child = n.getFirstChild();
      switch (child.getToken()) {
        case CALL:
          // Ignore unusual call cases
          // (function() {...})()
          // nested().calls()
          if (child.getFirstChild().getQualifiedName() == null) {
            break;
          }
          switch (child.getFirstChild().getQualifiedName()) {
            case "goog.module":
              if (!parent.getFirstChild().equals(n)) { // is first statement
                compiler.report(
                    JSError.make(
                        n,
                        GentsErrorManager.GENTS_MODULE_PASS_ERROR,
                        "goog.module must be the first top level statement."));
                break;
              }
              registerGoogModule(child, filename, child.getLastChild().getString());
              break;
            case "goog.provide":
              registerProvidesModule(child, filename, child.getLastChild().getString());
              break;
            case "goog.require":
              if (module != null) {
                module.reportImport();
              }
              break;
            default:
              break;
          }
          break;
        case GETPROP:
          // Typedefs are often just on property gets, not on assignments.
          JSDocInfo jsdoc = NodeUtil.getBestJSDocInfo(n);
          if (jsdoc != null && jsdoc.containsTypeDefinition() && module != null) {
            module.maybeAddExport(child);
          }
          break;
        case ASSIGN:
          if (module == null) {
            break;
          }
          Node maybeExportNode = child.getFirstChild();
          if (maybeExportNode != null) {
            String maybeExportString = maybeExportNode.getQualifiedName();
            if (maybeExportString != null
                && (maybeExportString.equals("exports")
                    || module.jsNamespaces.contains(maybeExportString))) {
              if (module.isGoogModule) {
                module.namespaceHasDefaultExport.put(
                    Iterables.getOnlyElement(module.jsNamespaces), true);
              } else {
                module.namespaceHasDefaultExport.put(maybeExportString, true);
              }
            }
          }
          module.maybeAddExport(maybeExportNode);
          break;
        default:
          break;
      }
    }
  
    /** Registers a goog.module namespace for future lookup. */
    private void registerGoogModule(Node n, String file, String namespace) {
      if (fileToModule.containsKey(file)) {
        compiler.report(
            JSError.make(
                n,
                GentsErrorManager.GENTS_MODULE_PASS_ERROR,
                String.format(
                    "goog.module files cannot contain other goog.module or goog.provides.")));
        return;
      }
      FileModule module = new FileModule(file, true);
      module.jsNamespaces.add(namespace);
      module.registerNamespaceToGlobalScope(namespace);
    }
  
    /** Registers a goog.provide namespace for future lookup. */
    void registerProvidesModule(Node n, String file, String namespace) {
      FileModule module;
      if (fileToModule.containsKey(file)) {
        if (fileToModule.get(file).isGoogModule) {
          compiler.report(
              JSError.make(
                  n,
                  GentsErrorManager.GENTS_MODULE_PASS_ERROR,
                  String.format("goog.provide cannot be used in the same file as goog.module.")));
          return;
        }
        module = fileToModule.get(file);
      } else {
        module = new FileModule(file, false);
      }
      module.jsNamespaces.add(namespace);
      module.registerNamespaceToGlobalScope(namespace);
    }
  
    /** Encapsulates the module provided by each file. */
    class FileModule {
      final String file;
  
      /** Module is not part of the conversion process and only exists for its exported symbols */
      private final boolean isJsLibrary;
      /** Declared with goog.module rather than goog.provide */
      private final boolean isGoogModule;
  
      /** {@code True}, if the module has any imports (e.g.{@code goog.require}). */
      private boolean hasImports = false;
  
      /** namespace of the module in the original closure javascript. */
      private Set<String> jsNamespaces = new HashSet<>();
      /**
       * true if the goog.provide namespace/goog.module module's clutz generated .d.ts will have a
       * default export.
       */
      Map<String, Boolean> namespaceHasDefaultExport = new HashMap<>();
  
      /**
       * Map from each provided namespace to all exported subproperties. Note that only namespaces
       * declared with 'goog.module' or 'goog.provide' are considered provided. Their subproperties
       * are considered exported from the file, but not directly provided. This is to determine what
       * namespaces other files are allowed to reference with 'goog.require'.
       *
       * <p>For example,
       *
       * <pre>
       *   goog.module('A.B');
       *   exports = ...;
       *   exports.C = ...;
       *   exports.C.D = ...;
       * </pre>
       *
       * Would result in providesObjectChildren['A.B'] = {'C'}
       */
      final Map<String, Set<String>> providesObjectChildren = new LinkedHashMap<>();
  
      /** Map of the goog.provided namespace to the node assigned to it. */
      private final Map<String, Node> googProvideNamespaceToNode = new LinkedHashMap<>();
  
      /**
       * Map from the fully qualified name being exported to the exported symbol. For example,
       *
       * <pre>
       *   goog.module('A.B');
       *   exports = ...;
       *   exports.C = ...;
       *   exports.C.D = ...;
       * </pre>
       *
       * Would result in:
       *
       * <pre>
       *    exportedNamespacesToSymbols['exports'] = 'B'
       *    exportedNamespacesToSymbols['exports.C'] = 'C'
       *  </pre>
       */
      final Map<String, String> exportedNamespacesToSymbols = new LinkedHashMap<>();
  
      /**
       * Map from the fully qualified name that would be imported to the exported symbol. For example,
       *
       * <pre>
       *   goog.module('A.B');
       *   exports = ...;
       *   exports.C = ...;
       *   exports.C.D = ...;
       * </pre>
       *
       * Would result in:
       *
       * <pre>
       *    importedNamespacesToSymbols['A.B'] = 'B'
       *    importedNamespacesToSymbols['A.B.C'] = 'C'
       *  </pre>
       */
      final Map<String, String> importedNamespacesToSymbols = new LinkedHashMap<>();
  
      FileModule(String file, boolean isGoogModule) {
        this.file = file;
        this.isGoogModule = isGoogModule;
        this.isJsLibrary = !filesToConvert.contains(file);
      }
  
      /** Returns if the import statement for this file should use the old 'goog:' namespace syntax */
      boolean shouldUseOldSyntax() {
        return isJsLibrary;
      }
  
      /** Returns if the file actually exports any symbols. */
      boolean hasExports() {
        return !exportedNamespacesToSymbols.isEmpty();
      }
  
      /** Records that the module has at least one import. */
      void reportImport() {
        this.hasImports = true;
      }
  
      /** Returns {@code true} if the module has at least one import. */
      boolean hasImports() {
        return this.hasImports;
      }
  
      /**
       * Register namespace name to global scope so that other files can call 'goog.require' on the
       * qualified name.
       */
      void registerNamespaceToGlobalScope(String namespace) {
        providesObjectChildren.put(namespace, new LinkedHashSet<String>());
        if (isJsLibrary) {
          maybeAddExport(NodeUtil.newQName(compiler, namespace));
        }
        fileToModule.put(file, this);
        namespaceToModule.put(namespace, this);
      }
  
      /**
       * Attempts to export the name exportsName. Does nothing if exportsName is an invalid export.
       */
      void maybeAddExport(Node exportsName) {
        if (isGoogModule) {
          maybeAddGoogExport(exportsName);
        } else {
          maybeAddProvidesExport(exportsName);
        }
      }
  
      private void maybeAddGoogExport(Node exportsName) {
        String fullname = providesObjectChildren.keySet().iterator().next();
        if (exportsName.matchesName("exports")) {
          String identifier =
              firstNonNull(
                  exportsName.getNext().getQualifiedName(), nameUtil.lastStepOfName(fullname));
          addExport(exportsName.getQualifiedName(), fullname, identifier);
        } else if (exportsName.isGetProp() && exportsName.getFirstChild().matchesName("exports")) {
          String identifier = exportsName.getLastChild().getString();
          String importName = fullname + "." + identifier;
          addExport(exportsName.getQualifiedName(), importName, identifier);
  
          // Register the named export to the module namespace.
          if (!namespaceToModule.containsKey(importName)) {
            namespaceToModule.put(importName, this);
            providesObjectChildren.put(importName, ImmutableSet.<String>of());
          }
        }
      }
  
      private void maybeAddProvidesExport(Node exportsName) {
        String fullname = exportsName.getQualifiedName();
  
        if (providesObjectChildren.containsKey(fullname)) {
          googProvideNamespaceToNode.put(fullname, exportsName);
          addExport(fullname, fullname, nameUtil.lastStepOfName(exportsName));
  
        } else if (exportsName.isGetProp()
            && providesObjectChildren.containsKey(exportsName.getFirstChild().getQualifiedName())) {
          googProvideNamespaceToNode.put(fullname, exportsName);
  
          // functions declared on functions should be exported.
          // static functions on classes should not be exported.
          String parentName = exportsName.getFirstChild().getQualifiedName();
          @Nullable Node parentNode = googProvideNamespaceToNode.get(parentName);
          JSDocInfo jsDoc = parentNode != null ? NodeUtil.getBestJSDocInfo(parentNode) : null;
  
          if (providesObjectChildren.containsKey(parentName)
              && (jsDoc == null || !jsDoc.isConstructor())) {
            addExport(fullname, fullname, nameUtil.lastStepOfName(exportsName));
          }
        }
      }
  
      private void addExport(String exportName, String importName, String identifier) {
        exportedNamespacesToSymbols.put(exportName, identifier);
        importedNamespacesToSymbols.put(importName, identifier);
  
        Node namespace = NodeUtil.newQName(compiler, importName);
        if (namespace.isGetProp()) {
          String parentName = namespace.getFirstChild().getQualifiedName();
          if (providesObjectChildren.containsKey(parentName)) {
            providesObjectChildren.get(parentName).add(identifier);
          }
        }
      }
    }
  }
  