Skip to content

Commit

Permalink
ProcessClosurePrimitives: create namespace objects for typedefs
Browse files Browse the repository at this point in the history
See added test case for an example.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=184075575
  • Loading branch information
brad4d authored and lauraharker committed Feb 1, 2018
1 parent 9fcbe3e commit 62ba0f6
Show file tree
Hide file tree
Showing 3 changed files with 144 additions and 55 deletions.
144 changes: 89 additions & 55 deletions src/com/google/javascript/jscomp/ProcessClosurePrimitives.java
Expand Up @@ -24,8 +24,10 @@
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import com.google.javascript.jscomp.NodeTraversal.AbstractPostOrderCallback; import com.google.javascript.jscomp.NodeTraversal.AbstractPostOrderCallback;
import com.google.javascript.jscomp.parsing.JsDocInfoParser;
import com.google.javascript.rhino.IR; import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.JSDocInfo; import com.google.javascript.rhino.JSDocInfo;
import com.google.javascript.rhino.JSDocInfoBuilder;
import com.google.javascript.rhino.JSTypeExpression; import com.google.javascript.rhino.JSTypeExpression;
import com.google.javascript.rhino.Node; import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.Token; import com.google.javascript.rhino.Token;
Expand Down Expand Up @@ -1302,6 +1304,9 @@ private class ProvidedName {
private Node explicitNode = null; private Node explicitNode = null;
private JSModule explicitModule = null; private JSModule explicitModule = null;


// There are child namespaces of this one.
private boolean hasAChildNamespace = false;

// The candidate definition. // The candidate definition.
private Node candidateDefinition = null; private Node candidateDefinition = null;


Expand All @@ -1326,10 +1331,14 @@ private class ProvidedName {
*/ */
void addProvide(Node node, JSModule module, boolean explicit) { void addProvide(Node node, JSModule module, boolean explicit) {
if (explicit) { if (explicit) {
// goog.provide('name.space');
checkState(explicitNode == null); checkState(explicitNode == null);
checkArgument(node.isExprResult()); checkArgument(node.isExprResult());
explicitNode = node; explicitNode = node;
explicitModule = module; explicitModule = module;
} else {
// goog.provide('name.space.some.child');
hasAChildNamespace = true;
} }
updateMinimumModule(module); updateMinimumModule(module);
} }
Expand Down Expand Up @@ -1416,56 +1425,48 @@ void replace() {


// Does this need a VAR keyword? // Does this need a VAR keyword?
replacementNode = candidateDefinition; replacementNode = candidateDefinition;
if (candidateDefinition.isExprResult() if (candidateDefinition.isExprResult()) {
&& !candidateDefinition.getFirstChild().isQualifiedName()) { Node exprNode = candidateDefinition.getOnlyChild();
candidateDefinition.putBooleanProp(Node.IS_NAMESPACE, true); if (exprNode.isAssign()) {
Node assignNode = candidateDefinition.getFirstChild(); // namespace = value;
Node nameNode = assignNode.getFirstChild(); candidateDefinition.putBooleanProp(Node.IS_NAMESPACE, true);
if (nameNode.isName()) { Node nameNode = exprNode.getFirstChild();
// Need to convert this assign to a var declaration. if (nameNode.isName()) {
Node valueNode = nameNode.getNext(); // Need to convert this assign to a var declaration.
assignNode.removeChild(nameNode); Node valueNode = nameNode.getNext();
assignNode.removeChild(valueNode); exprNode.removeChild(nameNode);
nameNode.addChildToFront(valueNode); exprNode.removeChild(valueNode);
Node varNode = IR.var(nameNode); nameNode.addChildToFront(valueNode);
varNode.useSourceInfoFrom(candidateDefinition); Node varNode = IR.var(nameNode);
candidateDefinition.replaceWith(varNode); varNode.useSourceInfoFrom(candidateDefinition);
varNode.setJSDocInfo(assignNode.getJSDocInfo()); candidateDefinition.replaceWith(varNode);
compiler.reportChangeToEnclosingScope(varNode); varNode.setJSDocInfo(exprNode.getJSDocInfo());
replacementNode = varNode; compiler.reportChangeToEnclosingScope(varNode);
replacementNode = varNode;
}
} else {
// /** @typedef {something} */ name.space.Type;
checkState(exprNode.isQualifiedName(), exprNode);
// If this namespace has child namespaces, we still need to add an object to hang them
// on to avoid creating broken code.
// We must cast the type of the literal to unknown, because the type checker doesn't
// expect the namespace to have a value.
if (hasAChildNamespace) {
replaceWith(createDeclarationNode(
IR.cast(IR.objectlit(), createUnknownTypeJsDocInfo())));
}
} }
} }
} else { } else {
// Handle the case where there's not a duplicate definition. // Handle the case where there's not a duplicate definition.
replacementNode = createDeclarationNode(); replaceWith(createDeclarationNode(IR.objectlit()));
if (firstModule == minimumModule) {
firstNode.getParent().addChildBefore(replacementNode, firstNode);
} else {
// In this case, the name was implicitly provided by two independent
// modules. We need to move this code up to a common module.
int indexOfDot = namespace.lastIndexOf('.');
if (indexOfDot == -1) {
// Any old place is fine.
compiler.getNodeForCodeInsertion(minimumModule)
.addChildToBack(replacementNode);
} else {
// Add it after the parent namespace.
ProvidedName parentName =
providedNames.get(namespace.substring(0, indexOfDot));
checkNotNull(parentName);
checkNotNull(parentName.replacementNode);
parentName.replacementNode.getParent().addChildAfter(
replacementNode, parentName.replacementNode);
}
}
compiler.reportChangeToEnclosingScope(replacementNode);
} }
if (explicitNode != null) { if (explicitNode != null) {
if (preserveGoogProvidesAndRequires && explicitNode.hasChildren()) { if (preserveGoogProvidesAndRequires && explicitNode.hasChildren()) {
return; return;
} }
/** /*
* If 'explicitNode' was added earlier in this pass then don't bother to report it's removal * If 'explicitNode' was added earlier in this pass then don't bother to report its removal
* right here as a change (since the original AST state is being restored). Also remove * right here as a change (since the original AST state is being restored). Also remove
* 'explicitNode' from the list of "possibly live" nodes so that it does not get reported as * 'explicitNode' from the list of "possibly live" nodes so that it does not get reported as
* a change at the end of the pass. * a change at the end of the pass.
Expand All @@ -1477,25 +1478,50 @@ void replace() {
} }
} }


private void replaceWith(Node replacement) {
replacementNode = replacement;
if (firstModule == minimumModule) {
firstNode.getParent().addChildBefore(replacementNode, firstNode);
} else {
// In this case, the name was implicitly provided by two independent
// modules. We need to move this code up to a common module.
int indexOfDot = namespace.lastIndexOf('.');
if (indexOfDot == -1) {
// Any old place is fine.
compiler.getNodeForCodeInsertion(minimumModule)
.addChildToBack(replacementNode);
} else {
// Add it after the parent namespace.
ProvidedName parentName =
providedNames.get(namespace.substring(0, indexOfDot));
checkNotNull(parentName);
checkNotNull(parentName.replacementNode);
parentName.replacementNode.getParent().addChildAfter(
replacementNode, parentName.replacementNode);
}
}
compiler.reportChangeToEnclosingScope(replacementNode);
}

/** /**
* Create the declaration node for this name, without inserting it * Create the declaration node for this name, without inserting it
* into the AST. * into the AST.
*/ */
private Node createDeclarationNode() { private Node createDeclarationNode(Node value) {
if (namespace.indexOf('.') == -1) { if (namespace.indexOf('.') == -1) {
return makeVarDeclNode(); return makeVarDeclNode(value);
} else { } else {
return makeAssignmentExprNode(); return makeAssignmentExprNode(value);
} }
} }


/** /**
* Creates a simple namespace variable declaration * Creates a simple namespace variable declaration
* (e.g. <code>var foo = {};</code>). * (e.g. <code>var foo = {};</code>).
*/ */
private Node makeVarDeclNode() { private Node makeVarDeclNode(Node value) {
Node name = IR.name(namespace); Node name = IR.name(namespace);
name.addChildToFront(createNamespaceLiteral()); name.addChildToFront(value);


Node decl = IR.var(name); Node decl = IR.var(name);
decl.putBooleanProp(Node.IS_NAMESPACE, true); decl.putBooleanProp(Node.IS_NAMESPACE, true);
Expand All @@ -1512,22 +1538,18 @@ private Node makeVarDeclNode() {
return decl; return decl;
} }


private Node createNamespaceLiteral() {
return IR.objectlit();
}

/** /**
* Creates a dotted namespace assignment expression * Creates a dotted namespace assignment expression
* (e.g. <code>foo.bar = {};</code>). * (e.g. <code>foo.bar = {};</code>).
*/ */
private Node makeAssignmentExprNode() { private Node makeAssignmentExprNode(Node value) {
Node decl = IR.exprResult( Node decl = IR.exprResult(
IR.assign( IR.assign(
NodeUtil.newQName( NodeUtil.newQName(
compiler, namespace, compiler, namespace,
firstNode /* real source info will be filled in below */, firstNode /* real source info will be filled in below */,
namespace), namespace),
createNamespaceLiteral())); value));
decl.putBooleanProp(Node.IS_NAMESPACE, true); decl.putBooleanProp(Node.IS_NAMESPACE, true);
if (candidateDefinition == null) { if (candidateDefinition == null) {
decl.getFirstChild().setJSDocInfo(NodeUtil.createConstantJsDoc()); decl.getFirstChild().setJSDocInfo(NodeUtil.createConstantJsDoc());
Expand Down Expand Up @@ -1571,6 +1593,13 @@ private Node getProvideStringNode() {
} }
} }


private JSDocInfo createUnknownTypeJsDocInfo() {
JSDocInfoBuilder castToUnknownBuilder = new JSDocInfoBuilder(true);
castToUnknownBuilder.recordType(
new JSTypeExpression(JsDocInfoParser.parseTypeString("?"), ""));
return castToUnknownBuilder.build();
}

/** /**
* @return Whether the node is namespace placeholder. * @return Whether the node is namespace placeholder.
*/ */
Expand All @@ -1588,9 +1617,14 @@ private static boolean isNamespacePlaceholder(Node n) {
value = name.getFirstChild(); value = name.getFirstChild();
} }


return value != null if (value == null) {
&& value.isObjectLit() return false;
&& !value.hasChildren(); }
if (value.isCast()) {
// There may be a cast to unknown type wrapped around the value.
value = value.getOnlyChild();
}
return value.isObjectLit() && !value.hasChildren();
} }


/** /**
Expand Down
24 changes: 24 additions & 0 deletions test/com/google/javascript/jscomp/IntegrationTest.java
Expand Up @@ -709,6 +709,30 @@ public void testTypedefBeforeOwner2() {
"var foo$Bar = function() {};")); "var foo$Bar = function() {};"));
} }


@GwtIncompatible // b/63595345
public void testTypedefProvides() {
CompilerOptions options = createCompilerOptions();
CompilationLevel.ADVANCED_OPTIMIZATIONS.setOptionsForCompilationLevel(options);
test(
options,
lines(
"goog.provide('ns');",
"goog.provide('ns.SomeType');",
"goog.provide('ns.SomeType.EnumValue');",
"goog.provide('ns.SomeType.defaultName');",
// subnamespace assignment happens before parent.
"/** @enum {number} */",
"ns.SomeType.EnumValue = { A: 1, B: 2 };",
// parent namespace isn't ever actually assigned.
// we're relying on goog.provide to provide it.
"/** @typedef {{name: string, value: ns.SomeType.EnumValue}} */",
"ns.SomeType;",
"/** @const {string} */",
"ns.SomeType.defaultName = 'foobarbaz';"),
// the provides should be rewritten, then collapsed, then removed by RemoveUnusedCode
"");
}

public void testExportedNames() { public void testExportedNames() {
CompilerOptions options = createCompilerOptions(); CompilerOptions options = createCompilerOptions();
options.setClosurePass(true); options.setClosurePass(true);
Expand Down
Expand Up @@ -136,6 +136,37 @@ private void testModule(String[] moduleInputs, String[] expected) {
test(createModuleStar(moduleInputs), expected); test(createModuleStar(moduleInputs), expected);
} }


public void testTypedefProvides() {
test(
lines(
"goog.provide('ns');",
"goog.provide('ns.SomeType');",
"goog.provide('ns.SomeType.EnumValue');",
"goog.provide('ns.SomeType.defaultName');",
// subnamespace assignment happens before parent.
"/** @enum {number} */",
"ns.SomeType.EnumValue = { A: 1, B: 2 };",
// parent namespace isn't ever actually assigned.
// we're relying on goog.provide to provide it.
"/** @typedef {{name: string, value: ns.SomeType.EnumValue}} */",
"ns.SomeType;",
"/** @const {string} */",
"ns.SomeType.defaultName = 'foobarbaz';"),
lines(
// Created from goog.provide
"/** @const */ var ns = {};",
// Created from goog.provide.
// Cast to unknown is necessary, because the type checker does not expect a symbol
// used as a typedef to have a value.
"ns.SomeType = /** @type {?} */ ({});", // created from goog.provide
"/** @enum {number} */",
"ns.SomeType.EnumValue = {A:1, B:2};",
"/** @typedef {{name: string, value: ns.SomeType.EnumValue}} */",
"ns.SomeType;",
"/** @const {string} */",
"ns.SomeType.defaultName = 'foobarbaz';"));
}

public void testSimpleProvides() { public void testSimpleProvides() {
test("goog.provide('foo');", "/** @const */ var foo={};"); test("goog.provide('foo');", "/** @const */ var foo={};");
test("goog.provide('foo.bar');", "/** @const */ var foo={}; /** @const */ foo.bar={};"); test("goog.provide('foo.bar');", "/** @const */ var foo={}; /** @const */ foo.bar={};");
Expand Down

0 comments on commit 62ba0f6

Please sign in to comment.