Skip to content

Commit

Permalink
Preserve type information in Es6ConvertSuper.
Browse files Browse the repository at this point in the history
This is a step toward moving the transpilation after type checking.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=212070340
  • Loading branch information
brad4d authored and blickly committed Sep 11, 2018
1 parent 7dd17cc commit 5ece03d
Show file tree
Hide file tree
Showing 7 changed files with 1,065 additions and 56 deletions.
66 changes: 66 additions & 0 deletions src/com/google/javascript/jscomp/AstFactory.java
Expand Up @@ -15,6 +15,7 @@
*/
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;

Expand Down Expand Up @@ -105,6 +106,22 @@ Node createThis(JSType thisType) {
return result;
}

/**
* Creates a reference to "arguments" with the type specified in externs, or unknown if the
* externs for it weren't included.
*/
Node createArgumentsReference() {
Node result = IR.name("arguments");
if (isAddingTypes()) {
JSType argumentsType = registry.getGlobalType("Arguments");
if (argumentsType == null) {
argumentsType = getNativeType(JSTypeNative.UNKNOWN_TYPE);
}
result.setJSType(argumentsType);
}
return result;
}

Node createName(String name, JSType type) {
Node result = IR.name(name);
if (isAddingTypes()) {
Expand Down Expand Up @@ -294,6 +311,23 @@ Node createCall(Node callee, Node... args) {
return result;
}

/**
* Create a call that returns an instance of the given class type.
*
* <p>This method is intended for use in special cases, such as calling `super()` in a
* constructor.
*/
Node createConstructorCall(@Nullable JSType classType, Node callee, Node... args) {
Node result = NodeUtil.newCallNode(callee, args);
if (isAddingTypes()) {
checkNotNull(classType);
FunctionType constructorType = checkNotNull(classType.toMaybeFunctionType());
ObjectType instanceType = checkNotNull(constructorType.getInstanceType());
result.setJSType(instanceType);
}
return result;
}

Node createAssign(Node lhs, Node rhs) {
Node result = IR.assign(lhs, rhs);
if (isAddingTypes()) {
Expand All @@ -302,6 +336,38 @@ Node createAssign(Node lhs, Node rhs) {
return result;
}

Node createEmptyFunction(JSType type) {
Node result = NodeUtil.emptyFunction();
if (isAddingTypes()) {
checkNotNull(type);
checkArgument(type.isFunctionType(), type);
result.setJSType(checkNotNull(type));
}
return result;
}

Node createFunction(String name, Node paramList, Node body, JSType type) {
Node nameNode = createName(name, type);
Node result = IR.function(nameNode, paramList, body);
if (isAddingTypes()) {
checkArgument(type.isFunctionType(), type);
result.setJSType(type);
}
return result;
}

Node createMemberFunctionDef(String name, Node function) {
// A function used for a member function definition must have an empty name,
// because the name string goes on the MEMBER_FUNCTION_DEF node.
checkArgument(function.getFirstChild().getString().isEmpty(), function);
Node result = IR.memberFunctionDef(name, function);
if (isAddingTypes()) {
// member function definition must share the type of the function that implements it
result.setJSType(function.getJSType());
}
return result;
}

private JSType getNativeType(JSTypeNative nativeType) {
checkNotNull(registry, "registry is null");
return checkNotNull(
Expand Down
96 changes: 77 additions & 19 deletions src/com/google/javascript/jscomp/Es6ConvertSuper.java
Expand Up @@ -15,6 +15,7 @@
*/
package com.google.javascript.jscomp;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import static com.google.javascript.jscomp.Es6ToEs3Util.CANNOT_CONVERT_YET;

Expand All @@ -28,6 +29,8 @@
import com.google.javascript.rhino.JSTypeExpression;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.Token;
import com.google.javascript.rhino.jstype.JSType;
import com.google.javascript.rhino.jstype.JSTypeNative;

/**
* Converts {@code super.method()} calls and adds constructors to any classes that lack them.
Expand All @@ -38,10 +41,12 @@
public final class Es6ConvertSuper extends NodeTraversal.AbstractPostOrderCallback
implements HotSwapCompilerPass {
private final AbstractCompiler compiler;
private final AstFactory astFactory;
private static final FeatureSet transpiledFeatures = FeatureSet.BARE_MINIMUM.with(Feature.SUPER);

public Es6ConvertSuper(AbstractCompiler compiler) {
this.compiler = compiler;
this.astFactory = compiler.createAstFactory();
}

@Override
Expand Down Expand Up @@ -69,9 +74,9 @@ private void addSyntheticConstructor(NodeTraversal t, Node classNode) {
Node classMembers = classNode.getLastChild();
Node memberDef;
if (superClass.isEmpty()) {
Node function = NodeUtil.emptyFunction();
Node function = astFactory.createEmptyFunction(classNode.getJSType());
compiler.reportChangeToChangeScope(function);
memberDef = IR.memberFunctionDef("constructor", function);
memberDef = astFactory.createMemberFunctionDef("constructor", function);
} else {
if (!superClass.isQualifiedName()) {
// This will be reported as an error in Es6ToEs3Converter.
Expand All @@ -88,14 +93,23 @@ private void addSyntheticConstructor(NodeTraversal t, Node classNode) {
// `super(...arguments);`
// Note that transpilation of spread args must occur after this pass for this to work.
Node exprResult =
IR.exprResult(NodeUtil.newCallNode(IR.superNode(), IR.spread(IR.name("arguments"))));
IR.exprResult(
astFactory.createConstructorCall(
classNode.getJSType(), // returned type is the subclass
IR.superNode().setJSType(superClass.getJSType()),
IR.spread(astFactory.createArgumentsReference())));
body.addChildToFront(exprResult);
NodeUtil.addFeatureToScript(t.getCurrentFile(), Feature.SUPER);
NodeUtil.addFeatureToScript(t.getCurrentFile(), Feature.SPREAD_EXPRESSIONS);
}
Node constructor = IR.function(IR.name(""), IR.paramList(IR.name("var_args")), body);
compiler.reportChangeToChangeScope(constructor);
memberDef = IR.memberFunctionDef("constructor", constructor);
Node constructor =
astFactory.createFunction(
"",
IR.paramList(astFactory.createName("var_args", JSTypeNative.UNKNOWN_TYPE)),
body,
classNode.getJSType());
memberDef = astFactory.createMemberFunctionDef("constructor", constructor);
// TODO(bradfordcsmith): Drop creation of JSDoc once transpilation moves after all checks.
JSDocInfoBuilder info = new JSDocInfoBuilder(false);
info.recordParameter(
"var_args",
Expand All @@ -107,6 +121,9 @@ private void addSyntheticConstructor(NodeTraversal t, Node classNode) {
memberDef.makeNonIndexableRecursive();
classMembers.addChildToFront(memberDef);
NodeUtil.addFeatureToScript(t.getCurrentFile(), Feature.MEMBER_DECLARATIONS);
// report newly created constructor
compiler.reportChangeToChangeScope(memberDef.getOnlyChild());
// report change to scope containing the class
compiler.reportChangeToEnclosingScope(memberDef);
}

Expand Down Expand Up @@ -220,29 +237,54 @@ private void visitSuperPropertyCall(Node node, Node parent, Node enclosingMember
}

Node callTarget = parent;
Node thisNode = IR.thisNode();
thisNode.makeNonIndexable();
Node callNode = IR.string("call");
callNode.makeNonIndexable();
if (enclosingMemberDef.isStaticMember()) {
callTarget.replaceChild(node, superName.cloneTree().useSourceInfoFromForTree(node));
callTarget = IR.getprop(callTarget.detach(), callNode);
Node expandedSuper = superName.cloneTree().useSourceInfoFromForTree(node);
expandedSuper.setOriginalName("super");
callTarget.replaceChild(node, expandedSuper);
callTarget = astFactory.createGetProp(callTarget.detach(), "call");
grandparent.addChildToFront(callTarget);
Node thisNode = astFactory.createThis(clazz.getJSType());
thisNode.makeNonIndexable(); // no direct correlation with original source
grandparent.addChildAfter(thisNode, callTarget);
grandparent.useSourceInfoIfMissingFromForTree(parent);
} else {
String newPropName = Joiner.on('.').join(superName.getQualifiedName(), "prototype");
Node newProp = NodeUtil.newQName(compiler, newPropName).useSourceInfoFromForTree(node);
node.replaceWith(newProp);
callTarget = IR.getprop(callTarget.detach(), callNode);
// Replace super node to give
// super.method(...) -> SuperClass.prototype.method(...)
Node expandedSuper =
astFactory
.createGetProp(superName.cloneTree(), "prototype")
.useSourceInfoFromForTree(node);
expandedSuper.setOriginalName("super");
node.replaceWith(expandedSuper);
// Set the 'this' object correctly for the call
// SuperClass.prototype.method(...) -> SuperClass.prototype.method.call(this, ...)
callTarget = astFactory.createGetProp(callTarget.detach(), "call");
grandparent.addChildToFront(callTarget);
JSType thisType = getInstanceTypeForClassNode(clazz);
Node thisNode = astFactory.createThis(thisType);
thisNode.makeNonIndexable(); // no direct correlation with original source
grandparent.addChildAfter(thisNode, callTarget);
grandparent.putBooleanProp(Node.FREE_CALL, false);
grandparent.useSourceInfoIfMissingFromForTree(parent);
}
compiler.reportChangeToEnclosingScope(grandparent);
}

private JSType getInstanceTypeForClassNode(Node classNode) {
checkArgument(classNode.isClass(), classNode);
final JSType constructorType = classNode.getJSType();
final JSType result;
if (constructorType != null) {
checkArgument(constructorType.isConstructor(), classNode);
result = JSType.toMaybeFunctionType(constructorType).getInstanceType();
} else {
result = null;
}
return result;
}

private void visitSuperPropertyAccess(Node node, Node parent, Node enclosingMemberDef) {
checkState(parent.isGetProp() || parent.isGetElem(), parent);
checkState(node.isSuper(), node);
Expand All @@ -262,12 +304,28 @@ private void visitSuperPropertyAccess(Node node, Node parent, Node enclosingMemb
}

if (enclosingMemberDef.isStaticMember()) {
node.replaceWith(superName.cloneTree().useSourceInfoFromForTree(node));
// super.prop -> SuperClass.prop
Node expandedSuper = superName.cloneTree().useSourceInfoFromForTree(node);
expandedSuper.setOriginalName("super");
node.replaceWith(expandedSuper);
} else {
String newPropName = Joiner.on('.').join(superName.getQualifiedName(), "prototype");
Node newprop =
NodeUtil.newQName(compiler, newPropName, node, "super").useSourceInfoFromForTree(node);
node.replaceWith(newprop);
if (astFactory.isAddingTypes()) {
// super.prop -> SuperClass.prototype.prop
Node newprop =
astFactory
.createGetProp(superName.cloneTree(), "prototype")
.useSourceInfoFromForTree(node);
newprop.setOriginalName("super");
node.replaceWith(newprop);
} else {
String newPropName = Joiner.on('.').join(superName.getQualifiedName(), "prototype");
// TODO(bradfordcsmith): This is required for Kythe, which doesn't work correctly with
// Node#useSourceInfoIfMissingFromForTree. Fortunately, we only care about Kythe
// if we're not adding types.
// Once this pass is always run after type checking, we can eliminate this branch.
node.replaceWith(
NodeUtil.newQName(compiler, newPropName, node, "super").useSourceInfoFromForTree(node));
}
}

compiler.reportChangeToEnclosingScope(grandparent);
Expand Down
56 changes: 30 additions & 26 deletions src/com/google/javascript/jscomp/TypeCheck.java
Expand Up @@ -272,9 +272,10 @@ public final class TypeCheck implements NodeTraversal.Callback, CompilerPass {
+ " (If you already declared the property, make sure to give it a type.)");

static final DiagnosticType ILLEGAL_OBJLIT_KEY =
DiagnosticType.warning(
"JSC_ILLEGAL_OBJLIT_KEY",
"Illegal key, the object literal is a {0}");
DiagnosticType.warning("JSC_ILLEGAL_OBJLIT_KEY", "Illegal key, the object literal is a {0}");

static final DiagnosticType ILLEGAL_CLASS_KEY =
DiagnosticType.warning("JSC_ILLEGAL_CLASS_KEY", "Illegal key, the class is a {0}");

static final DiagnosticType NON_STRINGIFIABLE_OBJECT_KEY =
DiagnosticType.warning(
Expand Down Expand Up @@ -320,6 +321,7 @@ public final class TypeCheck implements NodeTraversal.Callback, CompilerPass {
INCOMPATIBLE_EXTENDED_PROPERTY_TYPE,
EXPECTED_THIS_TYPE,
IN_USED_WITH_STRUCT,
ILLEGAL_CLASS_KEY,
ILLEGAL_PROPERTY_CREATION,
ILLEGAL_OBJLIT_KEY,
NON_STRINGIFIABLE_OBJECT_KEY,
Expand Down Expand Up @@ -1296,18 +1298,16 @@ private void checkPropertyInheritance(

/**
* Visits an object literal field definition <code>key : value</code>, or a class member
* definition <code>key() { ... }</code>
*
* If the <code>lvalue</code> is a prototype modification, we change the
* schema of the object type it is referring to.
* definition <code>key() { ... }</code> If the <code>lvalue</code> is a prototype modification,
* we change the schema of the object type it is referring to.
*
* @param t the traversal
* @param key the ASSIGN, STRING_KEY, MEMBER_FUNCTION_DEF, or COMPUTED_PROPERTY node
* @param owner the parent node, either OBJECTLIT or CLASS_MEMBERS
* @param litType the instance type of the enclosing object/class
* @param ownerType the instance type of the enclosing object/class
*/
private void visitObjectOrClassLiteralKey(
NodeTraversal t, Node key, Node owner, JSType litType) {
NodeTraversal t, Node key, Node owner, JSType ownerType) {
// Semicolons in a CLASS_MEMBERS body will produce EMPTY nodes: skip them.
if (key.isEmpty()) {
return;
Expand All @@ -1320,27 +1320,32 @@ private void visitObjectOrClassLiteralKey(
return;
}

// Structs must have unquoted keys and dicts must have quoted keys
// (we check computed properties in structs below)
if (litType.isStruct() && key.isQuotedString()) {
report(t, key, ILLEGAL_OBJLIT_KEY, "struct");
} else if (litType.isDict() && !(key.isQuotedString() || key.isComputedProp())) {
// Allow the definition of "constructor" as an unquoted key in class bodies. They will
// never be renamed so quoting is unimportant for the ALL_UNQUOTED renaming policy
// and the property is not accessed directly.
if (!key.isMemberFunctionDef()
|| !key.getParent().isClassMembers()
|| !key.getString().equals("constructor")) {
report(t, key, ILLEGAL_OBJLIT_KEY, "dict");
}
}

// Validate computed properties similarly to how we validate GETELEMs.
if (key.isComputedProp()) {
validator.expectIndexMatch(t, key, litType, getJSType(key.getFirstChild()));
validator.expectIndexMatch(t, key, ownerType, getJSType(key.getFirstChild()));
return;
}

if (key.isQuotedString()) {
// We must have an object literal, because
// class { 'foo'() {} }
// is stored in the AST as a computed property
// class { ['foo']() {} }
checkState(owner.isObjectLit(), owner);
if (ownerType.isStruct()) {
report(t, key, ILLEGAL_OBJLIT_KEY, "struct");
}
} else {
// we have either a non-quoted string or a member function def
// Neither is allowed for an @dict type except for "constructor" as a special case.
if (ownerType.isDict()
&& !(key.isMemberFunctionDef() && "constructor".equals(key.getString()))) {
// Object literals annotated as @dict may only have
// If you annotate a class with @dict, only the constructor can be a non-computed property.
report(t, key, owner.isClass() ? ILLEGAL_CLASS_KEY : ILLEGAL_OBJLIT_KEY, "dict");
}
}

// TODO(johnlenz): Validate get and set function declarations are valid
// as is the functions can have "extraneous" bits.

Expand Down Expand Up @@ -1396,7 +1401,6 @@ private void visitObjectOrClassLiteralKey(
t, key, keyType,
type.getPropertyType(property), owner, property);
}
return;
}
}

Expand Down

0 comments on commit 5ece03d

Please sign in to comment.