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

com.google.javascript.jscomp.RewriteGoogJsImports 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.

There is a newer version: v20240317
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.checkNotNull;
import static com.google.common.base.Preconditions.checkState;

import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.javascript.jscomp.NodeTraversal.AbstractPostOrderCallback;
import com.google.javascript.jscomp.NodeTraversal.AbstractPreOrderCallback;
import com.google.javascript.jscomp.deps.ModuleLoader.ModulePath;
import com.google.javascript.jscomp.modules.Binding;
import com.google.javascript.jscomp.modules.Module;
import com.google.javascript.jscomp.modules.ModuleMap;
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.Node;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import org.jspecify.nullness.Nullable;

/**
 * Looks for references to Closure's goog.js file and globalizes. The goog.js file is an ES6 module
 * that forwards some symbols on goog from Closure's base.js file, like require.
 *
 * 

This pass scans the goog.js file to find what keys are exported. When rewriting imports only * those in the goog.js file are globalized; if there is a reference to something not in the goog.js * file it is rewritten in such a way to cause an error later at type checking. * *

This is a separate pass that needs to run before Es6RewriteModules which will remove import * statements. * *

This pass enforces the following so that later compiler passes and regex based tools can * correctly recognize references to these properties of goog (like goog.require): * *

    *
  • No other file is named goog.js *
  • The file is always imported as import * as goog *
  • No other module level variable is named goog *
  • export from is never used on goog.js *
* *

Example: * *

{@code import * as goog from 'path/to/closure/goog.js'; const myNamespace = * goog.require('my.namespace'); } * *

Will be rewritten to: * *

{@code import 'path/to/closure/goog.js'; const myNamespace = goog.require('my.namespace'); } */ public class RewriteGoogJsImports implements CompilerPass { static final DiagnosticType GOOG_JS_IMPORT_MUST_BE_GOOG_STAR = DiagnosticType.error( "JSC_GOOG_JS_IMPORT_MUST_BE_GOOG_STAR", "Closure''s goog.js file must be imported as `import * as goog`."); // Since many tools scan for "goog.require" ban re-exporting. static final DiagnosticType GOOG_JS_REEXPORTED = DiagnosticType.error("JSC_GOOG_JS_REEXPORTED", "Do not re-export from goog.js."); static final DiagnosticType CANNOT_NAME_FILE_GOOG = DiagnosticType.error( "JSC_CANNOT_NAME_FILE_GOOG", "Do not name files goog.js, it is reserved for Closure Library."); static final DiagnosticType CANNOT_HAVE_MODULE_VAR_NAMED_GOOG = DiagnosticType.error( "JSC_CANNOT_HAVE_MODULE_VAR_NAMED_GOOG", "Module scoped variables named ''goog'' must come from importing Closure Library''s " + "goog.js file.."); /** * Possible traversal modes - either linting or linting+rewriting. */ public enum Mode { /* Lint only. */ LINT_ONLY, /* Lint and perform a rewrite on an entire compilation job */ LINT_AND_REWRITE } private static final String EXPECTED_BASE_PROVIDE = "goog"; private final Mode mode; private final ModuleMap moduleMap; private final AbstractCompiler compiler; private @Nullable Module googModule; private final Map moduleReplacements = new HashMap<>(); public RewriteGoogJsImports(AbstractCompiler compiler, Mode mode, ModuleMap moduleMap) { checkNotNull(moduleMap); this.compiler = compiler; this.mode = mode; this.moduleMap = moduleMap; } private void changeModules() { ImmutableMap.Builder resolvedModules = ImmutableMap.builder(); ImmutableMap.Builder closureModules = ImmutableMap.builder(); for (Map.Entry m : moduleMap.getModulesByPath().entrySet()) { Module newModule = moduleReplacements.getOrDefault(m.getValue(), m.getValue()); resolvedModules.put(m.getKey(), newModule); } for (Map.Entry m : moduleMap.getModulesByClosureNamespace().entrySet()) { Module newModule = moduleReplacements.getOrDefault(m.getValue(), m.getValue()); closureModules.put(m.getKey(), newModule); } compiler.setModuleMap( new ModuleMap(resolvedModules.buildOrThrow(), closureModules.buildOrThrow())); moduleReplacements.clear(); } /** * Determines if a script imports the goog.js file and if so ensures that property accesses are * valid and then detaches the import. */ private class ReferenceReplacer extends AbstractPostOrderCallback { private boolean hasBadExport; private final Node googImportNode; private final boolean globalizeAllReferences; ReferenceReplacer( Node script, Node googImportNode, Module module, boolean globalizeAllReferences) { this.googImportNode = googImportNode; this.globalizeAllReferences = globalizeAllReferences; NodeTraversal.traverse(compiler, script, this); if (googImportNode.getSecondChild().isImportStar()) { Map newBindings = new LinkedHashMap<>(module.boundNames()); newBindings.remove("goog"); if (hasBadExport) { // We might be able to get rid of this case now. Originally this was to cause a type error // later in compilation. But now the ModuleMapCreator should log an error for the bad // export. However if we're still running we might be in some tool that expects us to be // fault tolerant and still rewrite modules. So assume this is needed in those tools. googImportNode.getSecondChild().setOriginalName("goog"); googImportNode.getSecondChild().setString("$goog"); newBindings.put("$goog", module.boundNames().get("goog")); } else { googImportNode.getSecondChild().replaceWith(IR.empty()); } Module newModule = module.toBuilder().boundNames(ImmutableMap.copyOf(newBindings)).build(); moduleReplacements.put(module, newModule); compiler.reportChangeToEnclosingScope(googImportNode); } } private void maybeRewriteBadGoogJsImportRef(NodeTraversal t, Node nameNode, Node parent) { if (!parent.isGetProp() || !nameNode.getString().equals("goog")) { return; } Var var = t.getScope().getVar(nameNode.getString()); if (var == null || var.getNameNode() == null || var.getNameNode().getParent() != googImportNode) { return; } if (globalizeAllReferences || googModule.namespace().containsKey(parent.getString())) { return; } // Rewrite and keep the import so later type checking can report an error that this property // does not exist on the ES6 module. hasBadExport = true; nameNode.setOriginalName("goog"); nameNode.setString("$goog"); t.reportCodeChange(); return; } @Override public void visit(NodeTraversal t, Node n, Node parent) { switch (n.getToken()) { case NAME: maybeRewriteBadGoogJsImportRef(t, n, parent); break; default: break; } } } /** * Finds instances where goog is reexported. Not an exhaustive search; does not find alias, e.g. * {@code export const x = goog;}. Not to say that people should export using aliases; they * shouldn't. Just that we're catching the 99% case and that should be an indication to people not * to do bad things. */ private static class FindReexports extends AbstractPreOrderCallback { private final boolean hasGoogImport; public FindReexports(boolean hasGoogImport) { this.hasGoogImport = hasGoogImport; } private void checkIfForwardingExport(NodeTraversal t, Node export) { if (export.hasTwoChildren() && export.getLastChild().getString().endsWith("/goog.js")) { t.report(export, GOOG_JS_REEXPORTED); } } private void checkIfNameFowardedExport(NodeTraversal t, Node nameNode, Node parent) { if (hasGoogImport && nameNode.getString().equals("goog")) { if ((parent.isExportSpec() && parent.getFirstChild() == nameNode) || parent.isExport()) { t.report(nameNode, GOOG_JS_REEXPORTED); } } } @Override public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) { switch (n.getToken()) { case ROOT: case SCRIPT: case MODULE_BODY: case EXPORT_SPECS: case EXPORT_SPEC: return true; case EXPORT: checkIfForwardingExport(t, n); return true; case NAME: // Visit names in export specs and export defaults. checkIfNameFowardedExport(t, n, parent); return false; default: return false; } } } private @Nullable Node findGoogImportNode(Node scriptRoot) { // Cannot use the module map here - information is lost about the imports. The "bound names" // could be from transitive imports, but we lose the original import. boolean valid = true; Node googImportNode = null; for (Node child = scriptRoot.getFirstFirstChild(); child != null; child = child.getNext()) { if (child.isImport() && child.getLastChild().getString().endsWith("/goog.js")) { if (child.getFirstChild().isEmpty() && child.getSecondChild().isImportStar() && child.getSecondChild().getString().equals("goog")) { googImportNode = child; } else { valid = false; compiler.report( JSError.make( scriptRoot.getSourceFileName(), child.getLineno(), child.getCharno(), GOOG_JS_IMPORT_MUST_BE_GOOG_STAR)); } } } if (!valid) { return null; } else if (googImportNode != null) { return googImportNode; } Scope moduleScope = new SyntacticScopeCreator(compiler) .createScope(scriptRoot.getFirstChild(), Scope.createGlobalScope(scriptRoot)); Var googVar = moduleScope.getVar("goog"); if (googVar != null && googVar.getNameNode() != null) { compiler.report( JSError.make( scriptRoot.getSourceFileName(), googVar.getNameNode().getLineno(), googVar.getNameNode().getCharno(), CANNOT_HAVE_MODULE_VAR_NAMED_GOOG)); } return null; } private void rewriteImports(Node scriptRoot) { if (Es6RewriteModules.isEs6ModuleRoot(scriptRoot)) { Node googImportNode = findGoogImportNode(scriptRoot); NodeTraversal.traverse(compiler, scriptRoot, new FindReexports(googImportNode != null)); Module module = moduleMap.getModule(compiler.getInput(scriptRoot.getInputId()).getPath()); checkNotNull(module); if (googImportNode != null && mode == Mode.LINT_AND_REWRITE) { // If googModule is null then goog.js was not part of the input. Try to be fault tolerant // and just assume that everything exported is on the global goog. ReferenceReplacer unused = new ReferenceReplacer( scriptRoot, googImportNode, module, /* globalizeAllReferences= */ googModule == null); } } } private @Nullable Node findGoogJsScriptNode(Node root) { ModulePath expectedGoogPath = null; // Find Closure's base.js file. goog.js should be right next to it. for (Node script = root.getFirstChild(); script != null; script = script.getNext()) { ImmutableList provides = compiler.getInput(script.getInputId()).getProvides(); if (provides.contains(EXPECTED_BASE_PROVIDE)) { // Use resolveModuleAsPath as if it is not part of the input we don't want to report an // error. expectedGoogPath = compiler.getInput(script.getInputId()).getPath().resolveModuleAsPath("./goog.js"); break; } } if (expectedGoogPath != null) { Node googScriptNode = null; for (Node script = root.getFirstChild(); script != null; script = script.getNext()) { if (compiler .getInput(script.getInputId()) .getPath() .equalsIgnoreLeadingSlash(expectedGoogPath)) { googScriptNode = script; } else if (script.getSourceFileName().endsWith("/goog.js")) { // Ban the name goog.js as input except for Closure's goog.js file. This simplifies a lot // of logic if the only file that is allowed to be named goog.js is Closure's. compiler.report(JSError.make(script.getSourceFileName(), -1, -1, CANNOT_NAME_FILE_GOOG)); } } return googScriptNode; } return null; } @Override public void process(Node externs, Node root) { googModule = null; Node googJsScriptNode = findGoogJsScriptNode(root); if (googJsScriptNode == null) { // Potentially in externs if library level type checking. googJsScriptNode = findGoogJsScriptNode(externs); } if (mode == Mode.LINT_AND_REWRITE) { if (googJsScriptNode != null) { ModulePath googJsPath = compiler.getInput(googJsScriptNode.getInputId()).getPath(); googModule = moduleMap.getModule(googJsPath); checkNotNull(googModule); Predicate isFromGoog = (Binding b) -> b.originatingExport().modulePath() == googJsPath; checkState( googModule.boundNames().values().stream().allMatch(isFromGoog), "goog.js should never import anything"); checkState( !googModule.namespace().containsKey("default"), "goog.js should never have a default export."); checkState( googModule.namespace().values().stream().allMatch(isFromGoog), "goog.js should never export from anything."); } } else { checkState(mode == Mode.LINT_ONLY); } for (Node script = root.getFirstChild(); script != null; script = script.getNext()) { rewriteImports(script); } changeModules(); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy