diff --git a/src/com/google/javascript/jscomp/modules/Binding.java b/src/com/google/javascript/jscomp/modules/Binding.java index 20a41499a60..2d276cdc7d8 100644 --- a/src/com/google/javascript/jscomp/modules/Binding.java +++ b/src/com/google/javascript/jscomp/modules/Binding.java @@ -79,7 +79,14 @@ enum CreatedBy { * const x = goog.forwardDeclare(); * const {x} = goog.forwardDeclare(); */ - GOOG_FORWARD_DECLARE, + GOOG_FORWARD_DECLARE; + + /** Whether this is some goog.* dependency import */ + boolean isClosureImport() { + return this.equals(GOOG_REQUIRE) + || this.equals(GOOG_REQUIRE_TYPE) + || this.equals(GOOG_FORWARD_DECLARE); + } } /** Binding for an exported value that is not a module namespace object. */ @@ -97,9 +104,7 @@ static Binding from(Export boundExport, Node sourceNode) { static Binding from( ModuleMetadata metadata, Node sourceNode, String closureNamespace, CreatedBy createdBy) { Preconditions.checkArgument( - createdBy == CreatedBy.GOOG_REQUIRE - || createdBy == CreatedBy.GOOG_FORWARD_DECLARE - || createdBy == CreatedBy.GOOG_REQUIRE_TYPE, + createdBy.isClosureImport(), "Expected goog.require(Type) or goog.forwardDeclare, got %s", createdBy); return new AutoValue_Binding( diff --git a/src/com/google/javascript/jscomp/modules/ClosureModuleProcessor.java b/src/com/google/javascript/jscomp/modules/ClosureModuleProcessor.java index 0d134ea0baa..cf80c74018c 100644 --- a/src/com/google/javascript/jscomp/modules/ClosureModuleProcessor.java +++ b/src/com/google/javascript/jscomp/modules/ClosureModuleProcessor.java @@ -26,7 +26,7 @@ import com.google.javascript.jscomp.NodeTraversal; import com.google.javascript.jscomp.NodeTraversal.AbstractPreOrderCallback; import com.google.javascript.jscomp.deps.ModuleLoader.ModulePath; -import com.google.javascript.jscomp.modules.Binding.CreatedBy; +import com.google.javascript.jscomp.modules.ClosureRequireProcessor.Require; import com.google.javascript.jscomp.modules.ModuleMapCreator.ModuleProcessor; import com.google.javascript.jscomp.modules.ModuleMetadataMap.ModuleMetadata; import com.google.javascript.rhino.Node; @@ -47,16 +47,6 @@ */ final class ClosureModuleProcessor implements ModuleProcessor { - private static class Require { - private final Import i; - private final Binding.CreatedBy createdBy; - - private Require(Import i, Binding.CreatedBy createdBy) { - this.i = i; - this.createdBy = createdBy; - } - } - private static class UnresolvedGoogModule extends UnresolvedModule { private final ModuleMetadata metadata; @@ -142,41 +132,42 @@ Map getAllResolvedImports(ModuleRequestResolver moduleRequestRe ResolveExportResult resolveImport(ModuleRequestResolver moduleRequestResolver, String name) { Require require = requiresByLocalName.get(name); - Import i = require.i; + Import importRecord = require.importRecord(); - UnresolvedModule requested = moduleRequestResolver.resolve(i); + UnresolvedModule requested = moduleRequestResolver.resolve(importRecord); if (requested == null) { return ResolveExportResult.ERROR; - } else if (i.importName().equals(Export.NAMESPACE)) { + } else if (importRecord.importName().equals(Export.NAMESPACE)) { // Return a binding based on the other module's metadata. return ResolveExportResult.of( Binding.from( requested.metadata(), requested.metadata().rootNode(), - i.moduleRequest(), - require.createdBy)); + importRecord.moduleRequest(), + require.createdBy())); } else { ResolveExportResult result = requested.resolveExport( moduleRequestResolver, - i.moduleRequest(), - i.importName(), + importRecord.moduleRequest(), + importRecord.importName(), new HashSet<>(), new HashSet<>()); if (!result.found() && !result.hadError()) { compiler.report( JSError.make( srcFileName, - i.importNode().getLineno(), - i.importNode().getCharno(), + importRecord.importNode().getLineno(), + importRecord.importNode().getCharno(), DOES_NOT_HAVE_EXPORT, - i.importName())); + importRecord.importName())); return ResolveExportResult.ERROR; } - Node forSourceInfo = i.nameNode() == null ? i.importNode() : i.nameNode(); - return result.copy(forSourceInfo, require.createdBy); + Node forSourceInfo = + importRecord.nameNode() == null ? importRecord.importNode() : importRecord.nameNode(); + return result.copy(forSourceInfo, require.createdBy()); } } @@ -364,68 +355,10 @@ private void addPropertyExport(String exportedId, Node propNode) { propNode)); } - /** - * Adds a goog.require to the list of imports - * - * @param nameDeclaration a VAR, LET, or CONST - */ + /** Adds a goog.require(Type) or forwardDeclare to the list of {@code requiresByLocalName} */ private void maybeInitializeRequire(Node nameDeclaration) { - // TODO(b/123598058): These requires can also be in ES modules. Move this code to a common - // location. - Node rhs = - nameDeclaration.getFirstChild().isDestructuringLhs() - ? nameDeclaration.getFirstChild().getSecondChild() - : nameDeclaration.getFirstFirstChild(); - // this may be a require, requireType, or forwardDeclare - Binding.CreatedBy requireKind = getModuleDependencyType(rhs); - if (requireKind == null) { - return; - } - String namespace = rhs.getSecondChild().getString(); - if (nameDeclaration.getFirstChild().isName()) { - // const modA = goog.require('modA'); - Node lhs = nameDeclaration.getFirstChild(); - requiresByLocalName.putIfAbsent( - lhs.getString(), - new Require( - Import.builder() - .moduleRequest(namespace) - .localName(lhs.getString()) - .importName(Export.NAMESPACE) - .importNode(nameDeclaration) - .nameNode(lhs) - .build(), - requireKind)); - } else { - // const {x, y} = goog.require('modA'); - Node objectPattern = nameDeclaration.getFirstFirstChild(); - if (!objectPattern.isObjectPattern()) { - // bad JS, ignore - return; - } - for (Node key : objectPattern.children()) { - if (!key.isStringKey()) { - // Bad code, just ignore. We warn elsewhere. - continue; - } - Node lhs = key.getOnlyChild(); - if (!lhs.isName()) { - // Bad code ( e.g. `const {a = 0} = goog.require(...)`). We warn elsewhere. - continue; - } - - requiresByLocalName.putIfAbsent( - lhs.getString(), - new Require( - Import.builder() - .moduleRequest(namespace) - .localName(lhs.getString()) - .importName(key.getString()) - .importNode(nameDeclaration) - .nameNode(lhs) - .build(), - requireKind)); - } + for (Require require : ClosureRequireProcessor.getAllRequires(nameDeclaration)) { + requiresByLocalName.putIfAbsent(require.localName(), require); } } } @@ -453,30 +386,4 @@ private static boolean isNamedExportsLiteral(Node objLit) { } return true; } - - // TODO(b/123598058): move this code to a common location. - private static final ImmutableMap GOOG_DEPENDENCY_CALLS = - ImmutableMap.of( - "require", - CreatedBy.GOOG_REQUIRE, - "requireType", - CreatedBy.GOOG_REQUIRE_TYPE, - "forwardDeclare", - CreatedBy.GOOG_FORWARD_DECLARE); - - @Nullable - private static CreatedBy getModuleDependencyType(Node value) { - if (value == null || !value.isCall()) { - return null; - } - Node callee = value.getFirstChild(); - if (!callee.isGetProp()) { - return null; - } - Node owner = callee.getFirstChild(); - if (!owner.isName() || !owner.getString().equals("goog")) { - return null; - } - return GOOG_DEPENDENCY_CALLS.get(callee.getSecondChild().getString()); - } } diff --git a/src/com/google/javascript/jscomp/modules/ClosureRequireProcessor.java b/src/com/google/javascript/jscomp/modules/ClosureRequireProcessor.java new file mode 100644 index 00000000000..c5a48c31249 --- /dev/null +++ b/src/com/google/javascript/jscomp/modules/ClosureRequireProcessor.java @@ -0,0 +1,174 @@ +/* + * Copyright 2019 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.modules; + +import static com.google.common.base.Preconditions.checkArgument; + +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.javascript.jscomp.NodeUtil; +import com.google.javascript.jscomp.modules.Binding.CreatedBy; +import com.google.javascript.rhino.Node; +import javax.annotation.Nullable; + +/** + * Handles creating an {@link Import} from goog.require(Type) or goog.forwardDeclare. + * + *

This logic can be used by both goog.modules and ES modules + */ +final class ClosureRequireProcessor { + private final Node nameDeclaration; + private final CreatedBy requireKind; + + /** Represents a goog.require(Type) or goog.forwardDeclare */ + @AutoValue + abstract static class Require { + /** The name local to the module with the require; e.g. `b` in `const b = goog.require('a');` */ + abstract String localName(); + /** An {@link Import} containing all metadata about this require */ + abstract Import importRecord(); + /** Whether this is a goog.require, goog.requireType, or goog.forwardDeclare */ + abstract Binding.CreatedBy createdBy(); + + private static Require create( + String localName, Import importRecord, Binding.CreatedBy createdBy) { + checkArgument(createdBy.isClosureImport()); + return new AutoValue_ClosureRequireProcessor_Require(localName, importRecord, createdBy); + } + } + + private ClosureRequireProcessor(Node nameDeclaration, Binding.CreatedBy requireKind) { + checkArgument(NodeUtil.isNameDeclaration(nameDeclaration)); + this.nameDeclaration = nameDeclaration; + this.requireKind = requireKind; + } + + /** + * Returns all Require built from the given statement, or null if it is not a require + * + * @param nameDeclaration a VAR, LET, or CONST + * @return all Requires contained in this declaration + */ + static ImmutableList getAllRequires(Node nameDeclaration) { + Node rhs = + nameDeclaration.getFirstChild().isDestructuringLhs() + ? nameDeclaration.getFirstChild().getSecondChild() + : nameDeclaration.getFirstFirstChild(); + // This may be a require, requireType, or forwardDeclare. + Binding.CreatedBy requireKind = getModuleDependencyTypeFromRhs(rhs); + if (requireKind == null) { + return ImmutableList.of(); + } + + return new ClosureRequireProcessor(nameDeclaration, requireKind).getAllRequiresInDeclaration(); + } + + private static final ImmutableMap GOOG_DEPENDENCY_CALLS = + ImmutableMap.of( + "require", + CreatedBy.GOOG_REQUIRE, + "requireType", + CreatedBy.GOOG_REQUIRE_TYPE, + "forwardDeclare", + CreatedBy.GOOG_FORWARD_DECLARE); + + /** + * Checks if the given rvalue is a goog.require(Type) or goog.forwardDeclare call, and if so + * returns which one. + * + * @return A Closure require (where {@link CreatedBy#isClosureImport()} is true) or null. + */ + @Nullable + private static CreatedBy getModuleDependencyTypeFromRhs(@Nullable Node value) { + if (value == null || !value.isCall()) { + return null; + } + Node callee = value.getFirstChild(); + if (!callee.isGetProp()) { + return null; + } + Node owner = callee.getFirstChild(); + if (!owner.isName() || !owner.getString().equals("goog")) { + return null; + } + return GOOG_DEPENDENCY_CALLS.get(callee.getSecondChild().getString()); + } + + /** Returns a new list of all required names in {@link #nameDeclaration} */ + private ImmutableList getAllRequiresInDeclaration() { + Node rhs = + nameDeclaration.getFirstChild().isDestructuringLhs() + ? nameDeclaration.getFirstChild().getSecondChild() + : nameDeclaration.getFirstFirstChild(); + + String namespace = rhs.getSecondChild().getString(); + + if (nameDeclaration.getFirstChild().isName()) { + // const modA = goog.require('modA'); + Node lhs = nameDeclaration.getFirstChild(); + return ImmutableList.of( + Require.create( + lhs.getString(), + Import.builder() + .moduleRequest(namespace) + .localName(lhs.getString()) + .importName(Export.NAMESPACE) + .importNode(nameDeclaration) + .nameNode(lhs) + .build(), + requireKind)); + } else { + // const {x, y} = goog.require('modA'); + Node objectPattern = nameDeclaration.getFirstFirstChild(); + if (!objectPattern.isObjectPattern()) { + // bad JS, ignore + return ImmutableList.of(); + } + return getAllRequiresFromDestructuring(objectPattern, namespace); + } + } + + /** Returns all requires from destructruring, like `const {x, y, z} = goog.require('a');` */ + private ImmutableList getAllRequiresFromDestructuring( + Node objectPattern, String namespace) { + ImmutableList.Builder requireBuilder = ImmutableList.builder(); + for (Node key : objectPattern.children()) { + if (!key.isStringKey()) { + // Bad code, just ignore. We warn elsewhere. + continue; + } + Node lhs = key.getOnlyChild(); + if (!lhs.isName()) { + // Bad code ( e.g. `const {a = 0} = goog.require(...)`). We warn elsewhere. + continue; + } + + requireBuilder.add( + Require.create( + lhs.getString(), + Import.builder() + .moduleRequest(namespace) + .localName(lhs.getString()) + .importName(key.getString()) + .importNode(nameDeclaration) + .nameNode(lhs) + .build(), + requireKind)); + } + return requireBuilder.build(); + } +}