Skip to content

Commit

Permalink
Allow relative, path based requires in goog modules for other modules.
Browse files Browse the repository at this point in the history
Part of the interop between ES6 modules (eventually you will be able to goog require ES6 modules by path).

Note that this only adds totally compiled support and no bundled support yet. That requires some library work first.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=170130084
  • Loading branch information
johnplaisted authored and brad4d committed Sep 27, 2017
1 parent 669df53 commit c44d17c
Show file tree
Hide file tree
Showing 2 changed files with 237 additions and 16 deletions.
135 changes: 119 additions & 16 deletions src/com/google/javascript/jscomp/ClosureRewriteModule.java
Expand Up @@ -43,6 +43,7 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Stack;
import javax.annotation.Nullable;

/**
Expand Down Expand Up @@ -152,6 +153,16 @@ final class ClosureRewriteModule implements HotSwapCompilerPass {
"JSC_MISSING_MODULE_OR_PROVIDE",
"Required namespace \"{0}\" never defined.");

static final DiagnosticType MISSING_FILE_REQUIRE =
DiagnosticType.error(
"JSC_MISSING_FILE_REQUIRE",
"Required file \"{0}\" does not exist.");

static final DiagnosticType FILE_REQUIRE_FOR_NON_MODULE =
DiagnosticType.error(
"JSC_FILE_REQUIRE_FOR_NON_MODULE",
"Required file \"{0}\" is not a module.");

static final DiagnosticType LATE_PROVIDE_ERROR =
DiagnosticType.error(
"JSC_LATE_PROVIDE_ERROR",
Expand All @@ -173,6 +184,11 @@ final class ClosureRewriteModule implements HotSwapCompilerPass {
"JSC_ILLEGAL_DESTRUCTURING_NOT_EXPORTED",
"Destructuring import reference to name \"{0}\" was not exported in module {1}");

static final DiagnosticType PATH_REQUIRE_IN_PROVIDE =
DiagnosticType.error(
"JSC_PATH_REQUIRE_IN_PROVIDE",
"Cannot used path based require \"{0}\" from goog.provide'd file.");

private static final ImmutableSet<String> USE_STRICT_ONLY = ImmutableSet.of("use strict");

private static final String MODULE_EXPORTS_PREFIX = "module$exports$";
Expand Down Expand Up @@ -216,11 +232,14 @@ private static final class UnrecognizedRequire {
final Node requireNode;
final String legacyNamespace;
final boolean mustBeOrdered;
final boolean isPathRequire;

UnrecognizedRequire(Node requireNode, String legacyNamespace, boolean mustBeOrdered) {
UnrecognizedRequire(
Node requireNode, String legacyNamespace, boolean mustBeOrdered, boolean isPath) {
this.requireNode = requireNode;
this.legacyNamespace = legacyNamespace;
this.mustBeOrdered = mustBeOrdered;
this.isPathRequire = isPath;
}
}

Expand Down Expand Up @@ -612,8 +631,53 @@ public void visit(Node typeRefNode) {
static class GlobalRewriteState {
private Map<String, ScriptDescription> scriptDescriptionsByGoogModuleNamespace =
new HashMap<>();
private final Map<String, String> modulePathsToNamespaces =
new HashMap<>();
private Multimap<Node, String> legacyNamespacesByScriptNode = HashMultimap.create();
private Set<String> legacyScriptNamespaces = new HashSet<>();
private final Set<String> nonModulePaths = new HashSet<>();

public static String resolve(String fromModulePath, String relativeToModulePath) {
// Normally we'd use java.nio.file.Path here, but GWT/J2cl does not support it.
String path = fromModulePath + "/../" + relativeToModulePath;
Stack<String> stack = new Stack<>();
for (String component : Splitter.on('/').split(path)) {
if (component.equals("..") && !stack.isEmpty() && !stack.peek().equals("..")) {
stack.pop();
} else if (!component.equals(".")) {
stack.push(component);
}
}
return Joiner.on('/').join(stack);
}

String getNamespaceForModulePath(String fromModulePath, String relativeToModulePath) {
return modulePathsToNamespaces.get(resolve(fromModulePath, relativeToModulePath));
}

void recordModulePath(String modulePath, String namespace) {
modulePathsToNamespaces.put(modulePath, namespace);
}

boolean hasModuleForPath(String fromModulePath, String relativeToModulePath) {
return hasModuleForPath(resolve(fromModulePath, relativeToModulePath));
}

boolean hasModuleForPath(String fullPath) {
return modulePathsToNamespaces.containsKey(fullPath);
}

void recordNonModulePath(String path) {
nonModulePaths.add(path);
}

boolean hasNonModuleForPath(String fromModulePath, String relativeToModulePath) {
return hasNonModuleForPath(resolve(fromModulePath, relativeToModulePath));
}

boolean hasNonModuleForPath(String fullPath) {
return nonModulePaths.contains(fullPath);
}

boolean containsModule(String legacyNamespace) {
return scriptDescriptionsByGoogModuleNamespace.containsKey(legacyNamespace);
Expand Down Expand Up @@ -836,9 +900,10 @@ private void recordGoogModule(NodeTraversal t, Node call) {
t.report(call, DUPLICATE_NAMESPACE, legacyNamespace);
}

Node scriptNode = NodeUtil.getEnclosingScript(currentScript.rootNode);
rewriteState.scriptDescriptionsByGoogModuleNamespace.put(legacyNamespace, currentScript);
rewriteState.legacyNamespacesByScriptNode.put(NodeUtil.getEnclosingScript(currentScript.rootNode), legacyNamespace);

rewriteState.legacyNamespacesByScriptNode.put(scriptNode, legacyNamespace);
rewriteState.recordModulePath(scriptNode.getSourceFileName(), legacyNamespace);
}

private void recordGoogDeclareLegacyNamespace() {
Expand All @@ -860,17 +925,22 @@ private void recordGoogProvide(NodeTraversal t, Node call) {
t.report(call, DUPLICATE_NAMESPACE, legacyNamespace);
}

Node scriptNode = NodeUtil.getEnclosingScript(call);
// Log legacy namespaces and prefixes.
rewriteState.legacyScriptNamespaces.add(legacyNamespace);
rewriteState.legacyNamespacesByScriptNode.put(
NodeUtil.getEnclosingScript(call), legacyNamespace);
rewriteState.legacyNamespacesByScriptNode.put(scriptNode, legacyNamespace);
rewriteState.recordNonModulePath(scriptNode.getSourceFileName());
LinkedList<String> parts = Lists.newLinkedList(Splitter.on('.').split(legacyNamespace));
while (!parts.isEmpty()) {
legacyScriptNamespacesAndPrefixes.add(Joiner.on('.').join(parts));
parts.removeLast();
}
}

private static boolean isRelativePath(String pathOrNamespace) {
return pathOrNamespace.startsWith("./") || pathOrNamespace.startsWith("../");
}

private void recordGoogRequire(NodeTraversal t, Node call, boolean mustBeOrdered) {
maybeSplitMultiVar(call);

Expand All @@ -880,15 +950,27 @@ private void recordGoogRequire(NodeTraversal t, Node call, boolean mustBeOrdered
return;
}
String legacyNamespace = legacyNamespaceNode.getString();
boolean isPath = isRelativePath(legacyNamespace);

if (!currentScript.isModule && isPath) {
t.report(call, PATH_REQUIRE_IN_PROVIDE, legacyNamespace);
}

String sourceFilePath = NodeUtil.getEnclosingScript(currentScript.rootNode).getSourceFileName();

// Maybe report an error if there is an attempt to import something that is expected to be a
// goog.module() but no such goog.module() has been defined.
boolean targetIsAModule = rewriteState.containsModule(legacyNamespace);
boolean targetIsALegacyScript = rewriteState.legacyScriptNamespaces.contains(legacyNamespace);
if (currentScript.isModule
&& !targetIsAModule
&& !targetIsALegacyScript) {
unrecognizedRequires.add(new UnrecognizedRequire(call, legacyNamespace, mustBeOrdered));
boolean targetIsAModule = !isPath && rewriteState.containsModule(legacyNamespace);
boolean targetIsALegacyScript =
!isPath && rewriteState.legacyScriptNamespaces.contains(legacyNamespace);
boolean isRecognizedPath =
isPath && rewriteState.hasModuleForPath(sourceFilePath, legacyNamespace);
if (currentScript.isModule && !targetIsAModule && !targetIsALegacyScript && !isRecognizedPath) {
if (isPath) {
legacyNamespace = GlobalRewriteState.resolve(sourceFilePath, legacyNamespace);
}
unrecognizedRequires.add(
new UnrecognizedRequire(call, legacyNamespace, mustBeOrdered, isPath));
}
}

Expand Down Expand Up @@ -922,7 +1004,11 @@ private void recordGoogModuleGet(NodeTraversal t, Node call) {

if (!rewriteState.containsModule(legacyNamespace)) {
unrecognizedRequires.add(
new UnrecognizedRequire(call, legacyNamespace, false /** mustBeOrderd */));
new UnrecognizedRequire(
call,
legacyNamespace,
false /** mustBeOrderd */,
false /* isPath */));
}

String aliasName = null;
Expand Down Expand Up @@ -1069,6 +1155,16 @@ private void updateGoogRequire(NodeTraversal t, Node call) {
Node statementNode = NodeUtil.getEnclosingStatement(call);
String legacyNamespace = legacyNamespaceNode.getString();

if (isRelativePath(legacyNamespace)) {
legacyNamespace =
rewriteState.getNamespaceForModulePath(
NodeUtil.getEnclosingScript(currentScript.rootNode).getSourceFileName(),
legacyNamespace);
Node newLegacyNamespaceNode = IR.string(legacyNamespace);
call.replaceChild(legacyNamespaceNode, newLegacyNamespaceNode);
legacyNamespaceNode = newLegacyNamespaceNode;
}

boolean targetIsNonLegacyGoogModule =
rewriteState.containsModule(legacyNamespace)
&& !rewriteState.isLegacyModule(legacyNamespace);
Expand Down Expand Up @@ -1582,22 +1678,29 @@ private void reportUnrecognizedRequires() {
for (UnrecognizedRequire unrecognizedRequire : unrecognizedRequires) {
String legacyNamespace = unrecognizedRequire.legacyNamespace;

boolean isPath = unrecognizedRequire.isPathRequire;
Node requireNode = unrecognizedRequire.requireNode;
boolean targetGoogModuleExists = rewriteState.containsModule(legacyNamespace);
boolean targetGoogModuleExists = !isPath && rewriteState.containsModule(legacyNamespace);
boolean targetLegacyScriptExists =
rewriteState.legacyScriptNamespaces.contains(legacyNamespace);
!isPath && rewriteState.legacyScriptNamespaces.contains(legacyNamespace);
boolean targetPathExits = isPath && rewriteState.hasModuleForPath(legacyNamespace);

if (!targetGoogModuleExists && !targetLegacyScriptExists) {
if (!targetGoogModuleExists && !targetLegacyScriptExists && !targetPathExits) {
// The required thing was free to be either a goog.module() or a legacy script but neither
// flavor of file provided the required namespace, so report a vague error.
compiler.report(
JSError.make(
requireNode,
MISSING_MODULE_OR_PROVIDE,
unrecognizedRequire.isPathRequire
? rewriteState.hasNonModuleForPath(legacyNamespace)
? FILE_REQUIRE_FOR_NON_MODULE
: MISSING_FILE_REQUIRE
: MISSING_MODULE_OR_PROVIDE,
legacyNamespace));
// Remove the require node so this problem isn't reported all over again in
// ProcessClosurePrimitives.
if (!preserveSugar) {
compiler.reportChangeToEnclosingScope(requireNode);
NodeUtil.getEnclosingStatement(requireNode).detach();
}
continue;
Expand Down
118 changes: 118 additions & 0 deletions test/com/google/javascript/jscomp/ClosureRewriteModuleTest.java
Expand Up @@ -17,6 +17,7 @@

import static com.google.javascript.jscomp.ClosureRewriteModule.DUPLICATE_MODULE;
import static com.google.javascript.jscomp.ClosureRewriteModule.DUPLICATE_NAMESPACE;
import static com.google.javascript.jscomp.ClosureRewriteModule.FILE_REQUIRE_FOR_NON_MODULE;
import static com.google.javascript.jscomp.ClosureRewriteModule.ILLEGAL_DESTRUCTURING_DEFAULT_EXPORT;
import static com.google.javascript.jscomp.ClosureRewriteModule.ILLEGAL_DESTRUCTURING_NOT_EXPORTED;
import static com.google.javascript.jscomp.ClosureRewriteModule.IMPORT_INLINING_SHADOWS_VAR;
Expand All @@ -29,6 +30,8 @@
import static com.google.javascript.jscomp.ClosureRewriteModule.INVALID_PROVIDE_CALL;
import static com.google.javascript.jscomp.ClosureRewriteModule.INVALID_REQUIRE_NAMESPACE;
import static com.google.javascript.jscomp.ClosureRewriteModule.LATE_PROVIDE_ERROR;
import static com.google.javascript.jscomp.ClosureRewriteModule.MISSING_FILE_REQUIRE;
import static com.google.javascript.jscomp.ClosureRewriteModule.PATH_REQUIRE_IN_PROVIDE;

import com.google.javascript.jscomp.CompilerOptions.LanguageMode;

Expand Down Expand Up @@ -2309,4 +2312,119 @@ public void testEs6Module() {
testSame("export var x;");
testSame("import {x} from 'y';");
}

public void testRelativePathBasedRequire() {
Expected expected =
expected(
new String[] {
"/** @const */ var module$exports$mod_A = 'A';",
LINE_JOINER.join(
"/** @const */ var module$exports$mod_B = {};",
"/** @const */ module$exports$mod_B.ASDF = module$exports$mod_A;"
)
}
);

test(
srcs(
SourceFile.fromCode(
"/a/project/path/dep.js",
LINE_JOINER.join(
"goog.module('mod_A');",
"exports = 'A';")
),
SourceFile.fromCode(
"/a/project/path/file.js",
LINE_JOINER.join(
"goog.module('mod_B');",
"var A = goog.require('./dep.js')",
"exports.ASDF = A;")
)
),
expected);

test(
srcs(
SourceFile.fromCode(
"/a/project/path/in/some/path/dep.js",
LINE_JOINER.join(
"goog.module('mod_A');",
"exports = 'A';")
),
SourceFile.fromCode(
"/a/project/path/file.js",
LINE_JOINER.join(
"goog.module('mod_B');",
"var A = goog.require('./in/some/path/dep.js')",
"exports.ASDF = A;")
)
),
expected);

test(
srcs(
SourceFile.fromCode(
"/a/project/different/path/dep.js",
LINE_JOINER.join(
"goog.module('mod_A');",
"exports = 'A';")
),
SourceFile.fromCode(
"/a/project/path/file.js",
LINE_JOINER.join(
"goog.module('mod_B');",
"var A = goog.require('../different/path/dep.js')",
"exports.ASDF = A;")
)
),
expected);
}

public void testPathRequireForNonModule() {
testError(
srcs(
SourceFile.fromCode(
"/dep.js",
"goog.provide('p');"),
SourceFile.fromCode(
"/file.js",
LINE_JOINER.join(
"goog.module('mod_B');",
"var A = goog.require('./dep.js')"
)
)
),
error(FILE_REQUIRE_FOR_NON_MODULE));
}

public void testPathRequireForMissingFile() {
testError(
srcs(
SourceFile.fromCode(
"/file.js",
LINE_JOINER.join(
"goog.module('mod_B');",
"goog.require('./dep.js');"
)
)
),
error(MISSING_FILE_REQUIRE));
}

public void testPathRequireInProvide() {
testError(
srcs(
SourceFile.fromCode(
"/dep.js",
"goog.provide('p');"),
SourceFile.fromCode(
"/file.js",
LINE_JOINER.join(
"goog.provide('B');",
"goog.require('./dep.js');"
)
)
),
error(PATH_REQUIRE_IN_PROVIDE));
}
}

0 comments on commit c44d17c

Please sign in to comment.