Skip to content

Commit

Permalink
Improve CommonJS UMD pattern detection.
Browse files Browse the repository at this point in the history
Checks to see if the enclosing "if" test references the "module" or "define" names.
Prevents exports nested in IF clauses from being always detected as a UMD pattern when they may just be performing a conditional export.

Closes #2963

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=199227525
  • Loading branch information
ChadKillingsworth authored and tjgq committed Jun 5, 2018
1 parent dbc0888 commit 9009ecb
Show file tree
Hide file tree
Showing 2 changed files with 77 additions and 24 deletions.
80 changes: 59 additions & 21 deletions src/com/google/javascript/jscomp/ProcessCommonJSModules.java
Expand Up @@ -334,10 +334,16 @@ static class UmdPattern {
static class ExportInfo {
final Node node;
final Scope scope;
final boolean isInSupportedScope;

ExportInfo(Node node, Scope scope) {
this.node = node;
this.scope = scope;

Node disqualifyingParent =
NodeUtil.getEnclosingNode(
node, (n) -> n.isIf() || n.isHook() || n.isFunction() || n.isArrowFunction());
this.isInSupportedScope = disqualifyingParent == null;
}
}

Expand Down Expand Up @@ -575,10 +581,9 @@ public void visit(NodeTraversal t, Node n, Node parent) {
moduleExports.add(new ExportInfo(n, t.getScope()));

// If the module.exports statement is nested in the then branch of an if statement,
// and the test of the if checks for "module" or "define,
// assume the if statement is an UMD pattern with a common js export in the then branch
// This seems fragile but has worked well for a long time.
// TODO(ChadKillingsworth): Discover if there is a better way to detect these.
Node ifAncestor = getOutermostIfAncestor(parent);
Node ifAncestor = getOutermostUmdTest(parent);
if (ifAncestor != null && (NodeUtil.isLValue(n) || isInIfTest(n))) {
UmdPattern existingPattern = findUmdPattern(umdPatterns, ifAncestor);
if (existingPattern != null) {
Expand All @@ -598,11 +603,10 @@ public boolean apply(Node node) {
}
} else if (n.matchesQualifiedName("define.amd")) {
// If a define.amd statement is nested in the then branch of an if statement,
// and the test of the if checks for "module" or "define,
// assume the if statement is an UMD pattern with a common js export
// in the else branch
// This seems fragile but has worked well for a long time.
// TODO(ChadKillingsworth): Discover if there is a better way to detect these.
Node ifAncestor = getOutermostIfAncestor(parent);
Node ifAncestor = getOutermostUmdTest(parent);
if (ifAncestor != null
&& findUmdPattern(umdPatterns, ifAncestor) == null
&& (NodeUtil.isLValue(n) || isInIfTest(n))) {
Expand Down Expand Up @@ -635,10 +639,9 @@ && findUmdPattern(umdPatterns, ifAncestor) == null
exports.add(new ExportInfo(n, t.getScope()));

// If the exports statement is nested in the then branch of an if statement,
// and the test of the if checks for "module" or "define,
// assume the if statement is an UMD pattern with a common js export in the then branch
// This seems fragile but has worked well for a long time.
// TODO(ChadKillingsworth): Discover if there is a better way to detect these.
Node ifAncestor = getOutermostIfAncestor(parent);
Node ifAncestor = getOutermostUmdTest(parent);
if (ifAncestor != null
&& findUmdPattern(umdPatterns, ifAncestor) == null
&& (NodeUtil.isLValue(n) || isInIfTest(n))) {
Expand Down Expand Up @@ -848,8 +851,11 @@ boolean initializeModule() {
return directAssignments < 2;
}

/** Find the outermost if node ancestor for a node without leaving the function scope */
private Node getOutermostIfAncestor(Node n) {
/**
* Find the outermost if node ancestor for a node without leaving the function scope. To match,
* the test class of the "if" statement must reference "module" or "define" names.
*/
private Node getOutermostUmdTest(Node n) {
if (n == null || NodeUtil.isTopLevel(n) || n.isFunction()) {
return null;
}
Expand All @@ -861,15 +867,37 @@ private Node getOutermostIfAncestor(Node n) {
// When walking up ternary operations (hook), don't check if parent is the condition,
// because one ternary operation can be then/else branch of another.
if (parent.isIf() || parent.isHook()) {
Node outerIf = getOutermostIfAncestor(parent);
Node outerIf = getOutermostUmdTest(parent);
if (outerIf != null) {
return outerIf;
}

return parent;
final List<Node> umdTests = new ArrayList<>();
NodeUtil.visitPreOrder(
parent.getFirstChild(),
new NodeUtil.Visitor() {
@Override
public void visit(Node node) {
if (node.isName()
&& (node.getString().equals(MODULE) || node.getString().equals("define"))) {
umdTests.add(node);
}
}
});

// Webpack replaces tests of `typeof module !== 'undefined'` with `true`
if (umdTests.isEmpty() && parent.getFirstChild().isTrue()) {
umdTests.add(parent.getFirstChild());
}

if (!umdTests.isEmpty()) {
return parent;
}

return null;
}

return getOutermostIfAncestor(parent);
return getOutermostUmdTest(parent);
}

/** Return whether the node is within the test portion of an if statement */
Expand Down Expand Up @@ -1350,7 +1378,8 @@ private void visitExport(NodeTraversal t, ExportInfo export) {
&& root.getParent().getParent().isExprResult()
&& rValueVar != null
&& (NodeUtil.getEnclosingScript(rValueVar.nameNode) == null
|| (rValueVar.nameNode.getParent() != null && !rValueVar.isParam()))) {
|| (rValueVar.nameNode.getParent() != null && !rValueVar.isParam()))
&& export.isInSupportedScope) {
root.getParent().getParent().detach();
t.reportCodeChange();
return;
Expand Down Expand Up @@ -1388,7 +1417,8 @@ private void visitExport(NodeTraversal t, ExportInfo export) {
} else if (root.getNext() != null
&& root.getNext().isName()
&& rValueVar != null
&& rValueVar.isGlobal()) {
&& rValueVar.isGlobal()
&& export.isInSupportedScope) {
// This is a where a module export assignment is used in a complex expression.
// Before: `SOME_VALUE !== undefined && module.exports = SOME_VALUE`
// After: `SOME_VALUE !== undefined && module$name`
Expand Down Expand Up @@ -1808,10 +1838,11 @@ private String getExportedName(NodeTraversal t, Node n, Var var) {

Node exportedName = getExportedNameNode(export);
// We don't want to handle the export itself
if (exportRValue == n
|| ((NodeUtil.isClassExpression(exportRValue)
|| NodeUtil.isFunctionExpression(exportRValue))
&& exportedName == n)) {
if (export.isInSupportedScope
&& (exportRValue == n
|| ((NodeUtil.isClassExpression(exportRValue)
|| NodeUtil.isFunctionExpression(exportRValue))
&& exportedName == n))) {
return null;
}

Expand Down Expand Up @@ -1858,9 +1889,16 @@ private String getExportedName(NodeTraversal t, Node n, Var var) {
key = key.getNext();
}
if (key != null && keyIsExport) {
return baseExportName + "." + key.getString();
if (export.isInSupportedScope) {
return baseExportName + "." + key.getString();
} else {
return n.getQualifiedName();
}
}
} else {
if (!export.isInSupportedScope) {
return n.getQualifiedName();
}
if (var.getNameNode() == exportedName) {
String exportPrefix;
if (exportBaseQName.startsWith(MODULE)) {
Expand Down
21 changes: 18 additions & 3 deletions test/com/google/javascript/jscomp/ProcessCommonJSModulesTest.java
Expand Up @@ -694,7 +694,8 @@ public void testUMDRemoveIIFE() {
lines(
"/** @const */ var module$test = {};",
"(function(){",
" /** @const */ module$test.default={foo:'bar'};",
" var foobar = {foo: 'bar'};",
" /** @const */ module$test.default=foobar;",
"}).call(window);"));

// Can't remove IIFEs when there are sibling statements
Expand All @@ -714,7 +715,8 @@ public void testUMDRemoveIIFE() {
lines(
"/** @const */ var module$test = {};",
"(function(){",
" /** @const */ module$test.default={foo:\"bar\"};",
" var foobar = {foo: 'bar'};",
" /** @const */ module$test.default = foobar;",
"})();",
"alert('foo');"));

Expand All @@ -736,7 +738,8 @@ public void testUMDRemoveIIFE() {
"/** @const */ var module$test = {};",
"alert('foo');",
"(function(){",
" /** @const */ module$test.default={foo:\"bar\"};",
" var foobar={foo:\"bar\"};",
" /** @const */ module$test.default=foobar;",
"})();"));

// Annotations for local names should be preserved
Expand Down Expand Up @@ -1208,4 +1211,16 @@ public void testWebpackAMDModuleShim() {
" function(module) { return module; };")));
test(inputs, expecteds);
}

public void testUMDRequiresIfTest() {
testModules(
"test.js",
lines("var foobar = {foo: 'bar'}; if (foobar) { module.exports = foobar; }"),
lines(
"/** @const */ var module$test = {};",
"var foobar$$module$test={foo:\"bar\"};",
"if(foobar$$module$test) {",
" /** @const */ module$test.default = foobar$$module$test;",
"}"));
}
}

0 comments on commit 9009ecb

Please sign in to comment.