com.google.javascript.jscomp.Es6RewriteModules Maven / Gradle / Ivy
Show all versions of closure-compiler-linter Show documentation
/*
* Copyright 2014 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.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.javascript.jscomp.ClosurePrimitiveErrors.INVALID_CLOSURE_CALL_SCOPE_ERROR;
import static com.google.javascript.jscomp.ClosurePrimitiveErrors.INVALID_DESTRUCTURING_FORWARD_DECLARE;
import static com.google.javascript.jscomp.ClosurePrimitiveErrors.INVALID_FORWARD_DECLARE_NAMESPACE;
import static com.google.javascript.jscomp.ClosurePrimitiveErrors.INVALID_GET_CALL_SCOPE;
import static com.google.javascript.jscomp.ClosurePrimitiveErrors.INVALID_GET_NAMESPACE;
import static com.google.javascript.jscomp.ClosurePrimitiveErrors.INVALID_REQUIRE_NAMESPACE;
import static com.google.javascript.jscomp.ClosurePrimitiveErrors.INVALID_REQUIRE_TYPE_NAMESPACE;
import static com.google.javascript.jscomp.ClosurePrimitiveErrors.MISSING_MODULE_OR_PROVIDE;
import static com.google.javascript.jscomp.ClosurePrimitiveErrors.MODULE_USES_GOOG_MODULE_GET;
import static com.google.javascript.jscomp.parsing.parser.FeatureSet.Feature.MODULES;
import com.google.common.base.Splitter;
import com.google.common.collect.HashBasedTable;
import com.google.common.collect.Iterables;
import com.google.common.collect.Table;
import com.google.javascript.jscomp.NodeTraversal.AbstractPostOrderCallback;
import com.google.javascript.jscomp.deps.ModuleLoader;
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.jscomp.modules.ModuleMetadataMap;
import com.google.javascript.jscomp.modules.ModuleMetadataMap.ModuleMetadata;
import com.google.javascript.jscomp.parsing.parser.FeatureSet.Feature;
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.JSDocInfo;
import com.google.javascript.rhino.JSDocInfoBuilder;
import com.google.javascript.rhino.JSTypeExpression;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.Token;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;
/**
* Rewrites a ES6 module into a form that can be safely concatenated. Note that we treat a file as
* an ES6 module if it has at least one import or export statement.
*
* @author [email protected] (Michael Zhou)
*/
public final class Es6RewriteModules extends AbstractPostOrderCallback
implements HotSwapCompilerPass {
static final DiagnosticType LHS_OF_GOOG_REQUIRE_MUST_BE_CONST =
DiagnosticType.error(
"JSC_LHS_OF_GOOG_REQUIRE_MUST_BE_CONST",
"The left side of a goog.require() or goog.requireType() "
+ "must use ''const'' (not ''let'' or ''var'')");
static final DiagnosticType REQUIRE_TYPE_FOR_ES6_SHOULD_BE_CONST =
DiagnosticType.error(
"JSC_REQUIRE_TYPE_FOR_ES6_SHOULD_BE_CONST",
"goog.requireType alias for ES6 module should be const.");
static final DiagnosticType FORWARD_DECLARE_FOR_ES6_SHOULD_BE_CONST =
DiagnosticType.error(
"JSC_FORWARD_DECLARE_FOR_ES6_SHOULD_BE_CONST",
"goog.forwardDeclare alias for ES6 module should be const.");
static final DiagnosticType SHOULD_IMPORT_ES6_MODULE =
DiagnosticType.warning(
"JSC_SHOULD_IMPORT_ES6_MODULE",
"ES6 modules should import other ES6 modules rather than goog.require them.");
private final AbstractCompiler compiler;
@Nullable private final PreprocessorSymbolTable preprocessorSymbolTable;
private int scriptNodeCount;
/**
* Local variable names that were goog.require'd to qualified name we need to line.
*
* We need to inline all required names since there are certain well-known Closure symbols
* (like goog.asserts) that later stages of the compiler check for and cannot handle aliases.
*
*
We use this to rewrite something like:
*
*
* import {x} from '';
* const {assert} = goog.require('goog.asserts');
* assert(x);
*
*
* To:
*
*
* import {x} from '';
* goog.asserts.assert(x);
*
*
* Because if we used an alias like below the assertion would not be recognized:
*
*
* import {x} from '';
* const {assert} = goog.asserts;
* assert(x);
*
*/
// TODO(johnplaisted): This is actually incorrect if the require'd thing is mutated. But we need
// it so that things like goog.asserts work. Mutated closure symbols are a lot rarer than needing
// to use asserts and the like. Until there's a better solution to finding aliases of well known
// symbols we have to inline anything that is require'd.
private Map namesToInlineByAlias;
private Set typedefs;
private final ModuleMetadataMap moduleMetadataMap;
private final ModuleMap moduleMap;
/**
* Creates a new Es6RewriteModules instance which can be used to rewrite ES6 modules to a
* concatenable form.
*/
public Es6RewriteModules(
AbstractCompiler compiler,
ModuleMetadataMap moduleMetadataMap,
ModuleMap moduleMap,
@Nullable PreprocessorSymbolTable preprocessorSymbolTable) {
checkNotNull(moduleMetadataMap);
this.compiler = compiler;
this.moduleMetadataMap = moduleMetadataMap;
this.moduleMap = moduleMap;
this.preprocessorSymbolTable = preprocessorSymbolTable;
}
/**
* Return whether or not the given script node represents an ES6 module file.
*/
public static boolean isEs6ModuleRoot(Node scriptNode) {
checkArgument(scriptNode.isScript(), scriptNode);
if (scriptNode.getBooleanProp(Node.GOOG_MODULE)) {
return false;
}
return scriptNode.hasChildren() && scriptNode.getFirstChild().isModuleBody();
}
@Override
public void process(Node externs, Node root) {
checkArgument(externs.isRoot(), externs);
checkArgument(root.isRoot(), root);
for (Node file : Iterables.concat(externs.children(), root.children())) {
checkState(file.isScript(), file);
hotSwapScript(file, null);
}
compiler.setFeatureSet(compiler.getFeatureSet().without(MODULES));
// This pass may add getters properties on module objects.
GatherGetterAndSetterProperties.update(compiler, externs, root);
}
@Override
public void hotSwapScript(Node scriptNode, Node originalRoot) {
new RewriteRequiresForEs6Modules().rewrite(scriptNode);
if (isEs6ModuleRoot(scriptNode)) {
processFile(scriptNode);
}
}
/**
* Rewrite a single ES6 module file to a global script version.
*/
private void processFile(Node root) {
checkArgument(isEs6ModuleRoot(root), root);
clearState();
root.putBooleanProp(Node.TRANSPILED, true);
NodeTraversal.traverse(compiler, root, this);
}
public void clearState() {
this.scriptNodeCount = 0;
this.typedefs = new HashSet<>();
this.namesToInlineByAlias = new HashMap<>();
}
/**
* Checks for goog.require, goog.requireType, goog.module.get and goog.forwardDeclare calls that
* are meant to import ES6 modules and rewrites them.
*/
private class RewriteRequiresForEs6Modules extends AbstractPostOrderCallback {
private boolean transpiled = false;
// An (s, old, new) entry indicates that occurrences of `old` in scope `s` should be rewritten
// as `new`. This is used to rewrite namespaces that appear in calls to goog.requireType and
// goog.forwardDeclare.
private Table renameTable;
void rewrite(Node scriptNode) {
transpiled = false;
renameTable = HashBasedTable.create();
NodeTraversal.traverse(compiler, scriptNode, this);
if (transpiled) {
scriptNode.putBooleanProp(Node.TRANSPILED, true);
}
if (!renameTable.isEmpty()) {
NodeTraversal.traverse(
compiler, scriptNode, new Es6RenameReferences(renameTable, /* typesOnly= */ true));
}
}
@Override
public void visit(NodeTraversal t, Node n, Node parent) {
if (!n.isCall()) {
return;
}
boolean isRequire = n.getFirstChild().matchesQualifiedName("goog.require");
boolean isRequireType = n.getFirstChild().matchesQualifiedName("goog.requireType");
boolean isGet = n.getFirstChild().matchesQualifiedName("goog.module.get");
boolean isForwardDeclare = n.getFirstChild().matchesQualifiedName("goog.forwardDeclare");
if (!isRequire && !isRequireType && !isGet && !isForwardDeclare) {
return;
}
if (!n.hasTwoChildren() || !n.getLastChild().isString()) {
if (isRequire) {
t.report(n, INVALID_REQUIRE_NAMESPACE);
} else if (isRequireType) {
t.report(n, INVALID_REQUIRE_TYPE_NAMESPACE);
} else if (isGet) {
t.report(n, INVALID_GET_NAMESPACE);
} else {
t.report(n, INVALID_FORWARD_DECLARE_NAMESPACE);
}
return;
}
String name = n.getLastChild().getString();
ModuleMetadata moduleMetadata = moduleMetadataMap.getModulesByGoogNamespace().get(name);
if (moduleMetadata == null || !moduleMetadata.isEs6Module()) {
return;
}
// TODO(johnplaisted): Once we have an alternative to forwardDeclare / requireType that
// doesn't require Closure Library warn about those too.
// TODO(johnplaisted): Once we have import() support warn about goog.module.get.
if (isRequire) {
ModuleMetadata currentModuleMetadata =
moduleMetadataMap.getModulesByPath().get(t.getInput().getPath().toString());
if (currentModuleMetadata != null && currentModuleMetadata.isEs6Module()) {
t.report(n, SHOULD_IMPORT_ES6_MODULE);
}
}
if (isGet && t.inGlobalHoistScope()) {
t.report(n, INVALID_GET_CALL_SCOPE);
return;
}
Node statementNode = NodeUtil.getEnclosingStatement(n);
boolean importHasAlias = NodeUtil.isNameDeclaration(statementNode);
if (importHasAlias) {
if (statementNode.getFirstChild().isDestructuringLhs()) {
if (isForwardDeclare) {
// const {a, c:b} = goog.forwardDeclare('an.es6.namespace');
t.report(n, INVALID_DESTRUCTURING_FORWARD_DECLARE);
return;
}
if (isRequireType) {
if (!statementNode.isConst()) {
t.report(statementNode, REQUIRE_TYPE_FOR_ES6_SHOULD_BE_CONST);
return;
}
// const {a, c:b} = goog.requireType('an.es6.namespace');
for (Node child : statementNode.getFirstFirstChild().children()) {
checkState(child.isStringKey());
checkState(child.getFirstChild().isName());
renameTable.put(
t.getScopeRoot(),
child.getFirstChild().getString(),
ModuleRenaming.getGlobalName(moduleMetadata, name) + "." + child.getString());
}
} else {
// Work around a bug in the type checker where destructing can create
// too many layers of aliases and confuse the type checker. b/112061124.
// const {a, c:b} = goog.require('an.es6.namespace');
// const a = module$es6.a;
// const b = module$es6.c;
for (Node child : statementNode.getFirstFirstChild().children()) {
checkState(child.isStringKey());
checkState(child.getFirstChild().isName());
Node constNode =
IR.constNode(
IR.name(child.getFirstChild().getString()),
IR.getprop(
IR.name(ModuleRenaming.getGlobalName(moduleMetadata, name)),
IR.string(child.getString())));
constNode.useSourceInfoFromForTree(child);
statementNode.getParent().addChildBefore(constNode, statementNode);
}
}
statementNode.detach();
t.reportCodeChange();
} else {
if (isForwardDeclare || isRequireType) {
if (!statementNode.isConst()) {
DiagnosticType diagnostic =
isForwardDeclare
? FORWARD_DECLARE_FOR_ES6_SHOULD_BE_CONST
: REQUIRE_TYPE_FOR_ES6_SHOULD_BE_CONST;
t.report(statementNode, diagnostic);
return;
}
// const namespace = goog.forwardDeclare('an.es6.namespace');
// const namespace = goog.requireType('an.es6.namespace');
renameTable.put(
t.getScopeRoot(),
statementNode.getFirstChild().getString(),
ModuleRenaming.getGlobalName(moduleMetadata, name));
statementNode.detach();
t.reportCodeChange();
} else {
// const module = goog.require('an.es6.namespace');
// const module = module$es6;
n.replaceWith(
IR.name(ModuleRenaming.getGlobalName(moduleMetadata, name))
.useSourceInfoFromForTree(n));
t.reportCodeChange();
}
}
} else {
if (isForwardDeclare || isRequireType) {
// goog.forwardDeclare('an.es6.namespace')
// goog.requireType('an.es6.namespace')
renameTable.put(
t.getScopeRoot(), name, ModuleRenaming.getGlobalName(moduleMetadata, name));
statementNode.detach();
} else {
// goog.require('an.es6.namespace')
if (statementNode.isExprResult() && statementNode.getFirstChild() == n) {
statementNode.detach();
} else {
n.replaceWith(
IR.name(ModuleRenaming.getGlobalName(moduleMetadata, name))
.useSourceInfoFromForTree(n));
}
}
t.reportCodeChange();
}
transpiled = true;
}
}
@Override
public void visit(NodeTraversal t, Node n, Node parent) {
if (n.isImport()) {
maybeWarnExternModule(t, n, parent);
visitImport(t, n, parent);
} else if (n.isExport()) {
maybeWarnExternModule(t, n, parent);
visitExport(t, n, parent);
} else if (n.isScript()) {
scriptNodeCount++;
visitScript(t, n);
} else if (n.isCall()) {
// TODO(johnplaisted): Consolidate on declareModuleId.
if (n.getFirstChild().matchesQualifiedName("goog.declareModuleId")) {
n.getParent().detach();
}
}
}
private void maybeWarnExternModule(NodeTraversal t, Node n, Node parent) {
checkState(parent.isModuleBody());
if (parent.isFromExterns() && !NodeUtil.isFromTypeSummary(parent.getParent())) {
t.report(n, Es6ToEs3Util.CANNOT_CONVERT_YET, "ES6 modules in externs");
}
}
private void visitImport(NodeTraversal t, Node importDecl, Node parent) {
checkArgument(parent.isModuleBody(), parent);
String importName = importDecl.getLastChild().getString();
boolean isNamespaceImport = importName.startsWith("goog:");
if (isNamespaceImport) {
// Allow importing Closure namespace objects (e.g. from goog.provide or goog.module) as
// import ... from 'goog:my.ns.Object'.
String namespace = importName.substring("goog:".length());
ModuleMetadata m = moduleMetadataMap.getModulesByGoogNamespace().get(namespace);
if (m == null) {
t.report(importDecl, MISSING_MODULE_OR_PROVIDE, namespace);
} else {
checkState(m.isEs6Module() || m.isGoogModule() || m.isGoogProvide());
}
} else {
ModuleLoader.ModulePath modulePath =
t.getInput()
.getPath()
.resolveJsModule(
importName,
importDecl.getSourceFileName(),
importDecl.getLineno(),
importDecl.getCharno());
if (modulePath == null) {
// The module loader issues an error
// Fall back to assuming the module is a file path
modulePath = t.getInput().getPath().resolveModuleAsPath(importName);
}
maybeAddImportedFileReferenceToSymbolTable(importDecl.getLastChild(), modulePath.toString());
// TODO(johnplaisted): Use ModuleMetadata to ensure the path required is CommonJs or ES6 and
// if not give a better error.
}
for (Node child : importDecl.children()) {
if (child.isImportSpecs()) {
for (Node grandChild : child.children()) {
maybeAddAliasToSymbolTable(grandChild.getFirstChild(), t.getSourceName());
checkState(grandChild.hasTwoChildren());
}
} else if (child.isImportStar()) {
// import * as ns from "mod"
maybeAddAliasToSymbolTable(child, t.getSourceName());
}
}
parent.removeChild(importDecl);
t.reportCodeChange();
}
private void visitExport(NodeTraversal t, Node export, Node parent) {
checkArgument(parent.isModuleBody(), parent);
if (export.getBooleanProp(Node.EXPORT_DEFAULT)) {
// export default
// If the thing being exported is a class or function that has a name,
// extract it from the export statement, so that it can be referenced
// from within the module.
//
// export default class X {} -> class X {}; ... moduleName.default = X;
// export default function X() {} -> function X() {}; ... moduleName.default = X;
//
// Otherwise, create a local variable for it and export that.
//
// export default 'someExpression'
// ->
// var $jscompDefaultExport = 'someExpression';
// ...
// moduleName.default = $jscompDefaultExport;
Node child = export.getFirstChild();
String name = null;
if (child.isFunction() || child.isClass()) {
name = NodeUtil.getName(child);
}
if (name != null) {
Node decl = child.detach();
parent.replaceChild(export, decl);
} else {
Node var =
IR.var(IR.name(ModuleRenaming.DEFAULT_EXPORT_VAR_PREFIX), export.removeFirstChild());
var.setJSDocInfo(child.getJSDocInfo());
child.setJSDocInfo(null);
var.useSourceInfoIfMissingFromForTree(export);
parent.replaceChild(export, var);
}
t.reportCodeChange();
} else if (export.getBooleanProp(Node.EXPORT_ALL_FROM)
|| export.hasTwoChildren()
|| export.getFirstChild().getToken() == Token.EXPORT_SPECS) {
// export * from 'moduleIdentifier';
// export {x, y as z} from 'moduleIdentifier';
// export {Foo};
parent.removeChild(export);
t.reportCodeChange();
} else {
visitExportDeclaration(t, export, parent);
}
}
private void visitExportNameDeclaration(Node declaration) {
// export var Foo;
// export let {a, b:[c,d]} = {};
List lhsNodes = NodeUtil.findLhsNodesInNode(declaration);
for (Node lhs : lhsNodes) {
checkState(lhs.isName());
String name = lhs.getString();
if (declaration.getJSDocInfo() != null && declaration.getJSDocInfo().hasTypedefType()) {
typedefs.add(name);
}
}
}
private void visitExportDeclaration(NodeTraversal t, Node export, Node parent) {
// export var Foo;
// export function Foo() {}
// etc.
Node declaration = export.getFirstChild();
if (NodeUtil.isNameDeclaration(declaration)) {
visitExportNameDeclaration(declaration);
}
parent.replaceChild(export, declaration.detach());
t.reportCodeChange();
}
private void inlineModuleToGlobalScope(Node moduleNode) {
checkState(moduleNode.isModuleBody());
Node scriptNode = moduleNode.getParent();
moduleNode.detach();
scriptNode.addChildrenToFront(moduleNode.removeChildren());
}
private void visitScript(NodeTraversal t, Node script) {
inlineModuleToGlobalScope(script.getFirstChild());
ClosureRewriteModule.checkAndSetStrictModeDirective(t, script);
checkArgument(
scriptNodeCount == 1,
"Es6RewriteModules supports only one invocation per CompilerInput / script node");
Module thisModule = moduleMap.getModule(t.getInput().getPath());
String moduleName =
ModuleRenaming.getGlobalName(thisModule.metadata(), /* googNamespace= */ null);
Node moduleVar = createExportsObject(moduleName, t, script);
// Rename vars to not conflict in global scope.
NodeTraversal.traverse(compiler, script, new RenameGlobalVars(thisModule));
// rewriteRequires is here (rather than being part of the main visit() method, because we only
// want to rewrite the requires if this is an ES6 module. Note that we also want to do this
// AFTER renaming all module scoped vars in the event that something that is goog.require'd is
// a global, unqualified name (e.g. if "goog.provide('foo')" exists, we don't want to rewrite
// "const foo = goog.require('foo')" to "const foo = foo". If we rewrite our module scoped names
// first then we'll rewrite to "const foo$module$fudge = goog.require('foo')", then to
// "const foo$module$fudge = foo".
rewriteRequires(script);
// Rename the exports object to something we can reference later.
moduleVar.getFirstChild().setString(moduleName);
moduleVar.makeNonIndexableRecursive();
t.reportCodeChange();
}
private Node createExportsObject(String moduleName, NodeTraversal t, Node script) {
Node objLit = IR.objectlit();
// Going to get renamed by RenameGlobalVars, so the name we choose here doesn't matter (i.e. we
// can't use "moduleName" since it will get renamed to something else). We'll fix the name in
// visitScript after the global renaming to ensure it has a name that is deterministic from the
// path.
//
// So after this method we'll have:
// var exports = {};
// module$name.exportName = localName;
//
// After RenameGlobalVars:
// var exports$globalized = {};
// module$name.exportName = localName$globalized;
//
// After visitScript:
// var module$name = {};
// module$name.exportName = localName$globalized;
Node moduleVar = IR.var(IR.name("exports"), objLit);
moduleVar.getFirstChild().putBooleanProp(Node.MODULE_EXPORT, true);
JSDocInfoBuilder infoBuilder = new JSDocInfoBuilder(false);
infoBuilder.recordConstancy();
moduleVar.setJSDocInfo(infoBuilder.build());
script.addChildToBack(moduleVar.useSourceInfoIfMissingFromForTree(script));
Module thisModule = moduleMap.getModule(t.getInput().getPath());
for (Map.Entry entry : thisModule.namespace().entrySet()) {
String exportedName = entry.getKey();
Binding binding = entry.getValue();
Node nodeForSourceInfo = binding.sourceNode();
boolean mutated = binding.isMutated();
String boundVariableName = ModuleRenaming.getGlobalName(binding);
Node getProp = IR.getprop(IR.name(moduleName), IR.string(exportedName));
getProp.putBooleanProp(Node.MODULE_EXPORT, true);
if (typedefs.contains(exportedName)) {
// /** @typedef {foo} */
// moduleName.foo;
JSDocInfoBuilder builder = new JSDocInfoBuilder(true);
JSTypeExpression typeExpr = new JSTypeExpression(
IR.string(exportedName), script.getSourceFileName());
builder.recordTypedef(typeExpr);
JSDocInfo info = builder.build();
getProp.setJSDocInfo(info);
Node exprResult = IR.exprResult(getProp)
.useSourceInfoIfMissingFromForTree(nodeForSourceInfo);
script.addChildToBack(exprResult);
} else if (mutated) {
addGetterExport(script, nodeForSourceInfo, objLit, exportedName, boundVariableName);
NodeUtil.addFeatureToScript(t.getCurrentScript(), Feature.GETTER);
} else {
// This step is done before type checking and the type checker doesn't understand getters.
// However it does understand aliases. So if an export isn't mutated use an alias to make it
// actually type checkable.
// exports.foo = foo;
Node assign = IR.assign(getProp, NodeUtil.newQName(compiler, boundVariableName));
JSDocInfoBuilder builder = new JSDocInfoBuilder(true);
builder.recordConstancy();
JSDocInfo info = builder.build();
assign.setJSDocInfo(info);
script.addChildToBack(
IR.exprResult(assign).useSourceInfoIfMissingFromForTree(nodeForSourceInfo));
}
}
return moduleVar;
}
private void addGetterExport(
Node script, Node forSourceInfo, Node objLit, String exportedName, String localName) {
// Type checker doesn't infer getters so mark the return as unknown.
// { /** @return {?} */ get foo() { return foo; } }
Node getter = Node.newString(Token.GETTER_DEF, exportedName);
getter.putBooleanProp(Node.MODULE_EXPORT, true);
objLit.addChildToBack(getter);
Node name = NodeUtil.newQName(compiler, localName);
Node function = IR.function(IR.name(""), IR.paramList(), IR.block(IR.returnNode(name)));
getter.addChildToFront(function);
JSDocInfoBuilder builder = new JSDocInfoBuilder(true);
builder.recordReturnType(
new JSTypeExpression(new Node(Token.QMARK), script.getSourceFileName()));
getter.setJSDocInfo(builder.build());
getter.useSourceInfoIfMissingFromForTree(forSourceInfo);
compiler.reportChangeToEnclosingScope(getter.getFirstChild().getLastChild());
compiler.reportChangeToEnclosingScope(getter);
}
private void rewriteRequires(Node script) {
NodeTraversal.traversePostOrder(
compiler,
script,
(NodeTraversal t, Node n, Node parent) -> {
if (n.isCall()) {
Node fn = n.getFirstChild();
if (fn.matchesQualifiedName("goog.require")
|| fn.matchesQualifiedName("goog.requireType")) {
// TODO(tjgq): This will rewrite both type references and code references. For
// goog.requireType, the latter are potentially broken because the symbols aren't
// guaranteed to be available at run time. A separate pass needs to be added to
// detect these incorrect uses of goog.requireType.
visitRequireOrGet(t, n, parent, /* isRequire= */ true);
} else if (fn.matchesQualifiedName("goog.module.get")) {
visitGoogModuleGet(t, n, parent);
}
}
});
NodeTraversal.traversePostOrder(
compiler,
script,
(NodeTraversal t, Node n, Node parent) -> {
JSDocInfo info = n.getJSDocInfo();
if (info != null) {
for (Node typeNode : info.getTypeNodes()) {
inlineAliasedTypes(t, typeNode);
}
}
if (n.isName() && namesToInlineByAlias.containsKey(n.getString())) {
Var v = t.getScope().getVar(n.getString());
if (v == null || v.getNameNode() != n) {
Node replacement =
NodeUtil.newQName(compiler, namesToInlineByAlias.get(n.getString()));
replacement.useSourceInfoFromForTree(n);
n.replaceWith(replacement);
}
}
});
}
private void inlineAliasedTypes(NodeTraversal t, Node typeNode) {
if (typeNode.isString()) {
String name = typeNode.getString();
List split = Splitter.on('.').limit(2).splitToList(name);
// We've already removed the alias.
if (t.getScope().getVar(split.get(0)) == null) {
String replacement = namesToInlineByAlias.get(split.get(0));
if (replacement != null) {
String rest = "";
if (split.size() == 2) {
rest = "." + split.get(1);
}
typeNode.setOriginalName(name);
typeNode.setString(replacement + rest);
t.reportCodeChange();
}
}
}
for (Node child : typeNode.children()) {
inlineAliasedTypes(t, child);
}
}
private void visitGoogModuleGet(NodeTraversal t, Node getCall, Node parent) {
if (!getCall.hasTwoChildren() || !getCall.getLastChild().isString()) {
t.report(getCall, INVALID_GET_NAMESPACE);
return;
}
// Module has already been turned into a script at this point.
if (t.inGlobalHoistScope()) {
t.report(getCall, MODULE_USES_GOOG_MODULE_GET);
return;
}
visitRequireOrGet(t, getCall, parent, /* isRequire= */ false);
}
/**
* 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 module (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 visitRequireOrGet(
NodeTraversal t, Node requireCall, Node parent, boolean isRequire) {
if (!requireCall.hasTwoChildren() || !requireCall.getLastChild().isString()) {
t.report(requireCall, INVALID_REQUIRE_NAMESPACE);
return;
}
// Module has already been turned into a script at this point.
if (isRequire && !t.getScope().isGlobal()) {
t.report(requireCall, INVALID_CLOSURE_CALL_SCOPE_ERROR);
return;
}
String namespace = requireCall.getLastChild().getString();
boolean isStoredInDeclaration = NodeUtil.isDeclaration(parent.getParent());
if (isStoredInDeclaration && !parent.getParent().isConst()) {
compiler.report(JSError.make(parent.getParent(), LHS_OF_GOOG_REQUIRE_MUST_BE_CONST));
}
ModuleMetadata m = moduleMetadataMap.getModulesByGoogNamespace().get(namespace);
if (m == null) {
t.report(requireCall, MISSING_MODULE_OR_PROVIDE, namespace);
m = getFallbackMetadataForNamespace(namespace);
}
if (isStoredInDeclaration) {
if (isRequire) {
Node toDetach;
if (parent.isDestructuringLhs()) {
checkState(parent.getFirstChild().isObjectPattern());
toDetach = parent.getParent();
for (Node child : parent.getFirstChild().children()) {
if (child.isStringKey()) {
checkState(child.getFirstChild().isName());
namesToInlineByAlias.put(
child.getFirstChild().getString(),
ModuleRenaming.getGlobalName(m, namespace) + "." + child.getString());
} else {
checkState(child.isName());
namesToInlineByAlias.put(
child.getString(),
ModuleRenaming.getGlobalName(m, namespace) + "." + child.getString());
}
}
} else if (parent.isName()) {
namesToInlineByAlias.put(parent.getString(), ModuleRenaming.getGlobalName(m, namespace));
toDetach = parent.getParent();
} else {
checkState(parent.isExprResult());
toDetach = parent;
}
toDetach.detach();
} else {
Node replacement =
NodeUtil.newQName(compiler, ModuleRenaming.getGlobalName(m, namespace))
.srcrefTree(requireCall);
parent.replaceChild(requireCall, replacement);
}
} else {
checkState(requireCall.getParent().isExprResult());
requireCall.getParent().detach();
}
}
/**
* Traverses a node tree and
*
*
* - Appends a suffix to all global variable names defined in this module.
*
- Changes references to imported values to access the exported variable.
*
*/
private class RenameGlobalVars extends AbstractPostOrderCallback {
private final Module thisModule;
RenameGlobalVars(Module thisModule) {
this.thisModule = thisModule;
}
@Override
public void visit(NodeTraversal t, Node n, Node parent) {
JSDocInfo info = n.getJSDocInfo();
if (info != null) {
for (Node typeNode : info.getTypeNodes()) {
fixTypeNode(t, typeNode);
}
}
if (n.isName()) {
String name = n.getString();
Var var = t.getScope().getVar(name);
if (var != null && var.isGlobal()) {
// Avoid polluting the global namespace.
String newName =
ModuleRenaming.getGlobalNameOfEsModuleLocalVariable(thisModule.metadata(), name);
n.setString(newName);
n.setOriginalName(name);
t.reportCodeChange(n);
} else if (var == null && thisModule.boundNames().containsKey(name)) {
// Imports have been detached, so they won't show up in scope. Thus if we have a variable
// not in scope that shares the name of an import it is the import.
maybeAddAliasToSymbolTable(n, t.getSourceName());
Binding binding = thisModule.boundNames().get(name);
Node replacement = ModuleRenaming.replace(compiler, moduleMap, binding, n);
// `n.x()` may become `foo()`
if (replacement.isName()
&& parent.isCall()
&& parent.getFirstChild() == n
&& parent.getBooleanProp(Node.FREE_CALL)) {
parent.putBooleanProp(Node.FREE_CALL, true);
}
t.reportCodeChange();
}
}
}
/**
* Replace type name references. Change short names to fully qualified names
* with namespace prefixes. Eg: {Foo} becomes {module$test.Foo}.
*/
private void fixTypeNode(NodeTraversal t, Node typeNode) {
if (typeNode.isString()) {
Module thisModule = moduleMap.getModule(t.getInput().getPath());
String name = typeNode.getString();
List splitted = Splitter.on('.').splitToList(name);
String baseName = splitted.get(0);
String rest = "";
if (splitted.size() > 1) {
rest = name.substring(baseName.length());
}
Var var = t.getScope().getVar(baseName);
if (var != null && var.isGlobal()) {
maybeSetNewName(
t,
typeNode,
name,
ModuleRenaming.getGlobalNameOfEsModuleLocalVariable(thisModule.metadata(), baseName)
+ rest);
} else if (var == null && thisModule.boundNames().containsKey(baseName)) {
// Imports have been detached, so they won't show up in scope. Thus if we have a variable
// not in scope that shares the name of an import it is the import.
Binding binding = thisModule.boundNames().get(baseName);
String globalName =
ModuleRenaming.getGlobalNameForJsDoc(
moduleMap, binding, splitted.subList(1, splitted.size()));
maybeSetNewName(t, typeNode, name, globalName);
if (preprocessorSymbolTable != null) {
// Jsdoc type node is a single STRING node that spans the whole type. For example
// STRING node "bar.Foo". ES6 import rewrite replaces only "module"
// part of the type: "bar.Foo" => "module$full$path$bar$Foo". We have to record
// "bar" as alias.
Node onlyBaseName = Node.newString(baseName).useSourceInfoFrom(typeNode);
onlyBaseName.setLength(baseName.length());
maybeAddAliasToSymbolTable(onlyBaseName, t.getSourceName());
}
}
typeNode.setOriginalName(name);
}
for (Node child = typeNode.getFirstChild(); child != null; child = child.getNext()) {
fixTypeNode(t, child);
}
}
private void maybeSetNewName(NodeTraversal t, Node node, String name, String newName) {
if (!name.equals(newName)) {
node.setString(newName);
node.setOriginalName(name);
t.reportCodeChange();
}
}
}
/**
* Add alias nodes to the symbol table as they going to be removed by rewriter. Example aliases:
*
*
* import * as foo from './foo';
* import {doBar} from './bar';
*
* console.log(doBar);
*
*
* @param n Alias node. In the example above alias nodes are foo, doBar, and doBar.
* @param module Name of the module currently being processed.
*/
private void maybeAddAliasToSymbolTable(Node n, String module) {
if (preprocessorSymbolTable == null) {
return;
}
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.isImportStar()
? 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 name = "alias_" + module + "_" + nodeName;
preprocessorSymbolTable.addReference(n, name);
}
/**
* Add reference to a file that current module imports. Example:
*
*
* import * as qux from '../some/file.js';
*
*
* Will add a reference to file.js on the string node `'../some/file.js'`.
*
* @param importStringNode String node from the import statement that references imported file. In
* the example above it is the '../some/file.js' STRING node.
* @param importedFilePath Absolute path to the imported file. In the example above it can be
* myproject/folder/some/file.js
*/
private void maybeAddImportedFileReferenceToSymbolTable(
Node importStringNode, String importedFilePath) {
if (preprocessorSymbolTable == null) {
return;
}
// If this if the first import that mentions importedFilePath then we need to create a SCRIPT
// node for the imported file.
if (preprocessorSymbolTable.getSlot(importedFilePath) == null) {
Node scriptNode = compiler.getScriptNode(importedFilePath);
if (scriptNode != null) {
preprocessorSymbolTable.addReference(scriptNode, importedFilePath);
}
}
preprocessorSymbolTable.addReference(importStringNode, importedFilePath);
}
}