All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.javascript.jscomp.RewriteClosureImports Maven / Gradle / Ivy

Go to download

Closure Compiler is a JavaScript optimizing compiler. It parses your JavaScript, analyzes it, removes dead code and rewrites and minimizes what's left. It also checks syntax, variable references, and types, and warns about common JavaScript pitfalls. It is used in many of Google's JavaScript apps, including Gmail, Google Web Search, Google Maps, and Google Docs. This binary checks for style issues such as incorrect or missing JSDoc usage, and missing goog.require() statements. It does not do more advanced checks such as typechecking.

There is a newer version: v20200830
Show newest version
/*
 * Copyright 2018 The Closure Compiler Authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.javascript.jscomp;

import static com.google.common.base.Preconditions.checkState;
import static com.google.javascript.jscomp.ProcessClosurePrimitives.INVALID_FORWARD_DECLARE;

import com.google.common.collect.HashBasedTable;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Table;
import com.google.javascript.jscomp.NodeTraversal.AbstractModuleCallback;
import com.google.javascript.jscomp.modules.ModuleMetadataMap;
import com.google.javascript.jscomp.modules.ModuleMetadataMap.ModuleMetadata;
import com.google.javascript.jscomp.modules.ModuleMetadataMap.ModuleType;
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.JSDocInfo;
import com.google.javascript.rhino.Node;
import java.util.HashMap;
import java.util.Map;
import java.util.NoSuchElementException;
import javax.annotation.Nullable;

/**
 * Rewrites all goog.require, goog.module.get, goog.forwardDeclare, and goog.requireType calls for
 * goog.provide, goog.module, and ES6 modules (that call goog.declareModuleId).
 */
final class RewriteClosureImports implements HotSwapCompilerPass {

  private static final Node GOOG_REQUIRE = IR.getprop(IR.name("goog"), IR.string("require"));
  private static final Node GOOG_MODULE_GET =
      IR.getprop(IR.getprop(IR.name("goog"), IR.string("module")), IR.string("get"));
  private static final Node GOOG_FORWARD_DECLARE =
      IR.getprop(IR.name("goog"), IR.string("forwardDeclare"));
  private static final Node GOOG_REQUIRE_TYPE =
      IR.getprop(IR.name("goog"), IR.string("requireType"));

  private final AbstractCompiler compiler;
  @Nullable private final PreprocessorSymbolTable preprocessorSymbolTable;
  private final ImmutableSet typesToRewriteIn;
  private final Rewriter rewriter;

  /**
   * @param typesToRewriteIn A temporary set of module types to rewrite in. As this pass replaces
   *     the logic inside ES6RewriteModules, ClosureRewriteModules, and ProcessClosurePrimitives,
   *     this set should be expanded. Once it covers all module types it should be removed. This is
   *     how we will slowly and safely consolidate all logic to this pass. e.g. if the set is empty
   *     then this pass does nothing.
   */
  RewriteClosureImports(
      AbstractCompiler compiler,
      ModuleMetadataMap moduleMetadataMap,
      @Nullable PreprocessorSymbolTable preprocessorSymbolTable,
      ImmutableSet typesToRewriteIn) {
    this.compiler = compiler;
    this.preprocessorSymbolTable = preprocessorSymbolTable;
    this.typesToRewriteIn = typesToRewriteIn;
    this.rewriter = new Rewriter(compiler, moduleMetadataMap);
  }

  @Override
  public void hotSwapScript(Node scriptRoot, Node originalRoot) {
    if (typesToRewriteIn.isEmpty()) {
      return;
    }

    NodeTraversal.traverse(compiler, scriptRoot, rewriter);
  }

  @Override
  public void process(Node externs, Node root) {
    if (typesToRewriteIn.isEmpty()) {
      return;
    }

    NodeTraversal.traverse(compiler, externs, rewriter);
    NodeTraversal.traverse(compiler, root, rewriter);
  }

  private class Rewriter extends AbstractModuleCallback {
    // Requires for Closure files need to be inlined if aliased. This behavior isn't really correct,
    // but is required as some Closure symbols need to be recognized by the compiler, like
    // goog.asserts. e.g.:
    //
    // const {assert} = goog.require('goog.asserts');
    // assert(false);
    //
    // Needs to be written to:
    //
    // goog.asserts.assert(false);
    //
    // As other stages of the compiler look for "goog.asserts.assert".
    // However we don't do this for ES modules due to mutable exports.
    // Note that doing this for types also allows type references to non-imported non-legacy
    // goog.module files - which we really shouldn't have allowed, but people are now relying on it.
    //
    // These tables are [old name, alias node, new name].
    private final Table variablesToInline = HashBasedTable.create();
    private final Table typesToInline = HashBasedTable.create();

    // Map from Closure ID to global name for imports that have no alias.
    private final Map nonAliasedNamespaces = new HashMap<>();

    private final ModuleMetadataMap moduleMetadataMap;

    Rewriter(AbstractCompiler compiler, ModuleMetadataMap moduleMetadataMap) {
      super(compiler, moduleMetadataMap);
      this.moduleMetadataMap = moduleMetadataMap;
    }

    @Override
    protected void exitModule(ModuleMetadata oldModule, Node moduleScopeRoot) {
      variablesToInline.clear();
      typesToInline.clear();
      nonAliasedNamespaces.clear();
    }

    @Override
    public void visit(
        NodeTraversal t,
        Node n,
        @Nullable ModuleMetadata currentModule,
        @Nullable Node moduleScopeRoot) {
      // currentModule is null on ROOT nodes.
      if (currentModule == null || !typesToRewriteIn.contains(currentModule.moduleType())) {
        return;
      }

      Node parent = n.getParent();

      switch (n.getToken()) {
        case CALL:
          if (n.getFirstChild().matchesQualifiedName(GOOG_REQUIRE)
              || n.getFirstChild().matchesQualifiedName(GOOG_REQUIRE_TYPE)) {
            visitRequire(t, n, parent, currentModule);
          } else if (n.getFirstChild().matchesQualifiedName(GOOG_FORWARD_DECLARE)) {
            visitForwardDeclare(t, n, parent, currentModule);
          } else if (n.getFirstChild().matchesQualifiedName(GOOG_MODULE_GET)) {
            visitGoogModuleGet(t, n);
          }
          break;
        case NAME:
          maybeInlineName(t, n);
          break;
        default:
          break;
      }

      if (n.getJSDocInfo() != null) {
        rewriteJsdoc(t, n.getJSDocInfo(), currentModule);
      }
    }

    /**
     * Gets some made-up metadata for the given Closure namespace.
     *
     * 

This is used when the namespace is not part of the input so that this pass can be fault * tolerant and still rewrite to something. Some tools don't care about rewriting correctly and * just want the type information of this modules (e.g. clutz). */ private ModuleMetadata getFallbackMetadataForNamespace(String namespace) { // Assume a provide'd file to be consistent with goog.module rewriting. ModuleMetadata.Builder builder = ModuleMetadata.builder() .moduleType(ModuleMetadataMap.ModuleType.GOOG_PROVIDE) .usesClosure(true) .isTestOnly(false); builder.googNamespacesBuilder().add(namespace); return builder.build(); } private void rewriteImport( NodeTraversal t, ModuleMetadata currentModule, @Nullable ModuleMetadata requiredModule, String requiredNamespace, Node callNode, Node parentNode) { Node callee = callNode.getFirstChild(); maybeAddToSymbolTable(callee); Node statementNode = NodeUtil.getEnclosingStatement(callNode); Node changeScope = NodeUtil.getEnclosingChangeScopeRoot(statementNode); String globalName = ModuleRenaming.getGlobalName(requiredModule, requiredNamespace); if (parentNode.isExprResult()) { // goog.require('something'); if (requiredModule.isNonLegacyGoogModule() || requiredModule.isEs6Module()) { // Fully qualified module namespaces can be referenced in type annotations. // Legacy modules and provides don't need rewriting since their references will be the // same after rewriting. nonAliasedNamespaces.put(requiredNamespace, globalName); } statementNode.detach(); maybeAddToSymbolTable(callNode.getLastChild(), globalName); } else if (requiredModule.isEs6Module()) { // const alias = goog.require('es6'); // const {alias} = goog.require('es6'); // ES6 stored in a const or destructured - keep the variables and just replace the call. // const alias = module$es6; // const {alias} = module$es6; checkState(NodeUtil.isNameDeclaration(parentNode.getParent())); callNode.replaceWith(NodeUtil.newQName(compiler, globalName).srcrefTree(callNode)); maybeAddToSymbolTable(callNode.getLastChild(), globalName); } else if (parentNode.isName()) { // const alias = goog.require('closure'); // use(alias); // Closure file stored in a var or const without destructuring. Inline the variable. // use(closure-global-name); String name = parentNode.getString(); // Get the variable from the scope. Scope creation is lazy, and we want to snapshot the // scope BEFORE we detach any nodes. Node fromScope = t.getScope().getVar(parentNode.getString()).getNameNode(); checkState(fromScope == parentNode); variablesToInline.put(name, fromScope, globalName); typesToInline.put(parentNode.getString(), parentNode, globalName); statementNode.detach(); maybeAddAliasToSymbolTable(statementNode.getFirstChild(), currentModule); } else { // const {alias} = goog.require('closure'); // use(alias); // Closure file stored in a var or const with destructuring. Inline the variables. // use(closure-global-name.alias); checkState(parentNode.isDestructuringLhs()); for (Node importSpec : parentNode.getFirstChild().children()) { checkState(importSpec.hasChildren(), importSpec); String importedProperty = importSpec.getString(); Node aliasNode = importSpec.getFirstChild(); String aliasName = aliasNode.getString(); String fullName = globalName + "." + importedProperty; // Get the variable from the scope. Scope creation is lazy, and we want to snapshot the // scope BEFORE we detach any nodes. Node fromScope = t.getScope().getVar(aliasNode.getString()).getNameNode(); checkState(fromScope == aliasNode); variablesToInline.put(aliasName, aliasNode, fullName); typesToInline.put(aliasName, aliasNode, fullName); maybeAddAliasToSymbolTable(aliasNode, currentModule); } statementNode.detach(); } compiler.reportChangeToChangeScope(changeScope); } private void visitRequire( NodeTraversal t, Node callNode, Node parentNode, ModuleMetadata currentModule) { String requiredNamespace = callNode.getLastChild().getString(); ModuleMetadata requiredModule = moduleMetadataMap.getModulesByGoogNamespace().get(requiredNamespace); if (requiredModule == null) { requiredModule = getFallbackMetadataForNamespace(requiredNamespace); } rewriteImport(t, currentModule, requiredModule, requiredNamespace, callNode, parentNode); } private void rewriteGoogModuleGet( ModuleMetadata requiredModule, String requiredNamespace, NodeTraversal t, Node callNode) { Node maybeAssign = callNode.getParent(); boolean isFillingAnAlias = maybeAssign.isAssign() && maybeAssign.getParent().isExprResult(); if (isFillingAnAlias) { // Only valid to assign if the original declaration was a forward declare. Verified in the // CheckClosureImports pass. We can just detach this node as we will replace the // goog.forwardDeclare. // let x = goog.forwardDeclare('x'); // x = goog.module.get('x'); maybeAssign.getParent().detach(); } else { callNode.replaceWith( NodeUtil.newQName( compiler, ModuleRenaming.getGlobalName(requiredModule, requiredNamespace)) .srcrefTree(callNode)); } t.reportCodeChange(); } private void visitGoogModuleGet(NodeTraversal t, Node callNode) { String requiredNamespace = callNode.getLastChild().getString(); ModuleMetadata requiredModule = moduleMetadataMap.getModulesByGoogNamespace().get(requiredNamespace); if (requiredModule == null) { // Be fault tolerant. An error should've been reported in the checks. compiler.reportChangeToChangeScope(NodeUtil.getEnclosingChangeScopeRoot(callNode)); NodeUtil.getEnclosingStatement(callNode).detach(); return; } rewriteGoogModuleGet(requiredModule, requiredNamespace, t, callNode); } /** Process a goog.forwardDeclare() call and record the specified forward declaration. */ private void visitForwardDeclare( NodeTraversal t, Node n, Node parent, ModuleMetadata currentModule) { CodingConvention convention = compiler.getCodingConvention(); String typeDeclaration; try { typeDeclaration = Iterables.getOnlyElement(convention.identifyTypeDeclarationCall(n)); } catch (NullPointerException | NoSuchElementException | IllegalArgumentException e) { compiler.report( JSError.make( n, INVALID_FORWARD_DECLARE, "A single type could not identified for the goog.forwardDeclare statement")); return; } if (typeDeclaration != null) { compiler.forwardDeclareType(typeDeclaration); } String requiredNamespace = n.getLastChild().getString(); ModuleMetadata requiredModule = moduleMetadataMap.getModulesByGoogNamespace().get(requiredNamespace); // Assume anything not provided is a global, and that this isn't a module (we've checked this // in the checks pass). if (requiredModule == null) { checkState(parent.isExprResult()); parent.detach(); t.reportCodeChange(); } else { rewriteImport(t, currentModule, requiredModule, requiredNamespace, n, parent); } } private void maybeInlineName(NodeTraversal t, Node n) { if (variablesToInline.isEmpty()) { return; } Var var = t.getScope().getVar(n.getString()); // The scope hasn't updated, all aliases should be in scope still. if (var == null) { return; } String newName = variablesToInline.get(n.getString(), var.getNameNode()); if (newName == null) { return; } if (n.getParent().isExportSpec() && n.getParent().getFirstChild() == n) { // We can't inline a name in an export spec. So split it out into its own export statement // first. Note that it should always be okay to make this new export a const export, which // differs from export specs, because we force the goog.require alias to be const. // const a = goog.require('some.provide'); // let qux; // export {qux, a as b}; // ======================================= // const a = goog.require('some.provide'); // let qux; // export {qux}; // export const b = a; // After this rewriting we can then replace the new RHS `a` with `some.provide`. n = splitExportSpec(t, n, n.getParent()); } if (!newName.contains(".")) { safeSetString(n, newName); } else { Node changeScope = NodeUtil.getEnclosingChangeScopeRoot(n); n.replaceWith(NodeUtil.newQName(compiler, newName).srcrefTree(n)); if (changeScope != null) { compiler.reportChangeToChangeScope(changeScope); } } } /** * Splits the given name of an export spec into its own constant export statement. * * @return the right hand side of the new const node */ private Node splitExportSpec(NodeTraversal t, Node nameNode, Node exportSpec) { Node oldExport = exportSpec.getGrandparent(); // export spec -> specs -> export Node newRHSNameNode = IR.name(nameNode.getString()); Node newExport = IR.export(IR.constNode(IR.name(exportSpec.getSecondChild().getString()), newRHSNameNode)) .srcrefTree(exportSpec); oldExport.getParent().addChildAfter(newExport, oldExport); exportSpec.detach(); t.reportCodeChange(); return newRHSNameNode; } private void safeSetString(Node n, String newString) { if (n.getString().equals(newString)) { return; } String originalName = n.getString(); n.setString(newString); if (n.getOriginalName() == null) { n.setOriginalName(originalName); } // TODO(blickly): It would be better not to be renaming detached nodes Node changeScope = NodeUtil.getEnclosingChangeScopeRoot(n); if (changeScope != null) { compiler.reportChangeToChangeScope(changeScope); } } private void rewriteJsdoc(NodeTraversal t, JSDocInfo info, ModuleMetadata currentModule) { if (typesToInline.isEmpty() && nonAliasedNamespaces.isEmpty()) { return; } JsDocRefReplacer replacer = new JsDocRefReplacer(currentModule, t.getScope()); for (Node typeNode : info.getTypeNodes()) { NodeUtil.visitPreOrder(typeNode, replacer); } } private final class JsDocRefReplacer implements NodeUtil.Visitor { private final ModuleMetadata currentModule; private final Scope scope; JsDocRefReplacer(ModuleMetadata currentModule, Scope scope) { this.currentModule = currentModule; this.scope = scope; } @Override public void visit(Node typeRefNode) { if (!typeRefNode.isString()) { return; } // A type name that might be simple like "Foo" or qualified like "foo.Bar". String typeName = typeRefNode.getString(); // Tries to rename progressively shorter type prefixes like "foo.Bar.Baz", "foo.Bar", // "foo". String prefixTypeName = typeName; String suffix = ""; do { // If the name is an alias for an imported namespace rewrite from // "{Foo}" to // "{module$exports$bar$Foo}" or // "{bar.Foo}" String globalName = null; // First see if this is an aliased variable created by an import. if (!prefixTypeName.contains(".")) { Var var = scope.getVar(prefixTypeName); if (var != null) { globalName = typesToInline.get(prefixTypeName, var.getNameNode()); } } // If null it is not an aliased, local variable. See if there was a non-aliased import // for this namespace. if (globalName == null) { globalName = nonAliasedNamespaces.get(prefixTypeName); } if (globalName != null) { if (preprocessorSymbolTable != null) { // Jsdoc type node is a single STRING node that spans the whole type. For example // STRING node "bar.Foo". When rewriting modules potentially replace only "module" // part of the type: "bar.Foo" => "module$exports$bar$Foo". So we need to remember // that "bar" as alias. To do that we clone type node and make "bar" node from it. Node moduleOnlyNode = typeRefNode.cloneNode(); safeSetString(moduleOnlyNode, prefixTypeName); moduleOnlyNode.setLength(prefixTypeName.length()); maybeAddAliasToSymbolTable(moduleOnlyNode, currentModule); } safeSetString(typeRefNode, globalName + suffix); return; } if (prefixTypeName.contains(".")) { prefixTypeName = prefixTypeName.substring(0, prefixTypeName.lastIndexOf('.')); suffix = typeName.substring(prefixTypeName.length()); } else { return; } } while (true); } } /** Add the given qualified name node to the symbol table. */ private void maybeAddToSymbolTable(Node n) { if (preprocessorSymbolTable != null) { preprocessorSymbolTable.addReference(n); } } private void maybeAddToSymbolTable(Node n, String name) { if (preprocessorSymbolTable != null) { preprocessorSymbolTable.addReference(n, name); } } /** * Add alias nodes to the symbol table as they going to be removed by rewriter. Example aliases: * *

     * const Foo = goog.require('my.project.Foo');
     * const bar = goog.require('my.project.baz');
     * const {baz} = goog.require('my.project.utils');
     * 
*/ private void maybeAddAliasToSymbolTable(Node n, ModuleMetadata currentModule) { if (preprocessorSymbolTable != null) { n.putBooleanProp(Node.MODULE_ALIAS, true); // Alias can be used in js types. Types have node type STRING and not NAME so we have to // use their name as string. String nodeName = n.isString() ? n.getString() : preprocessorSymbolTable.getQualifiedName(n); // We need to include module as part of the name because aliases are local to current // module. Aliases with the same name from different module should be completely different // entities. String module = ModuleRenaming.getGlobalName( currentModule, Iterables.getFirst(currentModule.googNamespaces(), null)); String name = "alias_" + module + "_" + nodeName; maybeAddToSymbolTable(n, name); } } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy