Skip to content

Commit

Permalink
Updates RuntimeTypeCheck to support ES6 features.
Browse files Browse the repository at this point in the history
The additional features supported include:
  - destructuring/rest params
  - class expressions
  - async/generator functions

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=229785599
  • Loading branch information
nreid260 authored and lauraharker committed Jan 18, 2019
1 parent 8fa0cd3 commit f77d7c1
Show file tree
Hide file tree
Showing 3 changed files with 592 additions and 118 deletions.
11 changes: 11 additions & 0 deletions src/com/google/javascript/jscomp/JsIterables.java
Expand Up @@ -18,6 +18,7 @@
import static com.google.javascript.rhino.jstype.JSTypeNative.UNKNOWN_TYPE; import static com.google.javascript.rhino.jstype.JSTypeNative.UNKNOWN_TYPE;


import com.google.javascript.rhino.jstype.JSType; import com.google.javascript.rhino.jstype.JSType;
import com.google.javascript.rhino.jstype.JSTypeNative;
import com.google.javascript.rhino.jstype.JSTypeRegistry; import com.google.javascript.rhino.jstype.JSTypeRegistry;
import com.google.javascript.rhino.jstype.TemplateTypeMap; import com.google.javascript.rhino.jstype.TemplateTypeMap;


Expand Down Expand Up @@ -58,5 +59,15 @@ static final JSType getElementType(JSType iterableOrIterator, JSTypeRegistry typ
return typeRegistry.getNativeType(UNKNOWN_TYPE); return typeRegistry.getNativeType(UNKNOWN_TYPE);
} }


/**
* Returns an `Iterable` type templated on {@code elementType}.
*
* <p>Example: `number' => `Iterable<number>`.
*/
static final JSType createIterableTypeOf(JSType elementType, JSTypeRegistry typeRegistry) {
return typeRegistry.createTemplatizedType(
typeRegistry.getNativeObjectType(JSTypeNative.ITERABLE_TYPE), elementType);
}

private JsIterables() {} private JsIterables() {}
} }
230 changes: 174 additions & 56 deletions src/com/google/javascript/jscomp/RuntimeTypeCheck.java
Expand Up @@ -16,17 +16,20 @@


package com.google.javascript.jscomp; 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.common.base.Preconditions.checkState;


import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables; import com.google.javascript.jscomp.parsing.parser.FeatureSet.Feature;
import com.google.javascript.rhino.IR; import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.Node; import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.StaticSourceFile; import com.google.javascript.rhino.StaticSourceFile;
import com.google.javascript.rhino.jstype.FunctionType; import com.google.javascript.rhino.jstype.FunctionType;
import com.google.javascript.rhino.jstype.JSType; import com.google.javascript.rhino.jstype.JSType;
import com.google.javascript.rhino.jstype.JSTypeRegistry;
import com.google.javascript.rhino.jstype.ObjectType; import com.google.javascript.rhino.jstype.ObjectType;
import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Comparator; import java.util.Comparator;
import java.util.TreeSet; import java.util.TreeSet;
Expand All @@ -41,7 +44,7 @@
* <p>For each function, we insert a run-time type assertion for each parameter and return value for * <p>For each function, we insert a run-time type assertion for each parameter and return value for
* which the compiler has a type. * which the compiler has a type.
* *
* <p>The JavaScript code which implements the type assertions is in js/runtime-type-check.js. * <p>The JavaScript code which implements the type assertions is in js/runtime_type_check.js.
* *
*/ */
class RuntimeTypeCheck implements CompilerPass { class RuntimeTypeCheck implements CompilerPass {
Expand Down Expand Up @@ -70,10 +73,12 @@ private String getName(JSType type) {
}; };


private final AbstractCompiler compiler; private final AbstractCompiler compiler;
private final JSTypeRegistry typeRegistry;
private final String logFunction; private final String logFunction;


RuntimeTypeCheck(AbstractCompiler compiler, @Nullable String logFunction) { RuntimeTypeCheck(AbstractCompiler compiler, @Nullable String logFunction) {
this.compiler = compiler; this.compiler = compiler;
this.typeRegistry = compiler.getTypeRegistry();
this.logFunction = logFunction; this.logFunction = logFunction;
} }


Expand All @@ -99,68 +104,117 @@ public void process(Node externs, Node root) {
private static class AddMarkers extends NodeTraversal.AbstractPostOrderCallback { private static class AddMarkers extends NodeTraversal.AbstractPostOrderCallback {


private final AbstractCompiler compiler; private final AbstractCompiler compiler;
private NodeTraversal traversal;


private AddMarkers(AbstractCompiler compiler) { private AddMarkers(AbstractCompiler compiler) {
this.compiler = compiler; this.compiler = compiler;
} }


@Override @Override
public void visit(NodeTraversal t, Node n, Node parent) { public void visit(NodeTraversal t, Node node, Node unused) {
if (n.isFunction()) { this.traversal = t;
visitFunction(n);
@Nullable FunctionType funType = JSType.toMaybeFunctionType(node.getJSType());

switch (node.getToken()) {
case FUNCTION:
Node parent = node.getParent();
if (parent.isMemberFunctionDef() && parent.getString().equals("constructor")) {
break; // "constructor" members are not constructors.
}

visitPossibleClassDeclaration(
funType, findNodeToInsertAfter(node), this::addMarkerToFunction);
break;

case CLASS:
visitPossibleClassDeclaration(funType, node.getChildAtIndex(2), this::addMarkerToClass);
break;

default:
break;
} }
} }


private void visitFunction(Node n) { private interface MarkerInserter {
FunctionType funType = n.getJSType().toMaybeFunctionType(); Node insert(String markerName, @Nullable String className, Node insertionPoint);
if (funType == null || !funType.isConstructor()) { }

private void visitPossibleClassDeclaration(
@Nullable FunctionType funType, Node insertionPoint, MarkerInserter inserter) {
// Validate the class type.
if (funType == null || funType.getSource() == null || !funType.isConstructor()) {
return; return;
} }


Node nodeToInsertAfter = findNodeToInsertAfter(n); @Nullable String className = NodeUtil.getName(funType.getSource());

nodeToInsertAfter = addMarker(funType, nodeToInsertAfter, null);


TreeSet<ObjectType> stuff = new TreeSet<>(ALPHA); // Assemble the marker names. Class marker first, then interfaces sorted aphabetically.
Iterables.addAll(stuff, funType.getAllImplementedInterfaces()); ArrayList<String> markerNames = new ArrayList<>();
for (ObjectType interfaceType : stuff) { for (ObjectType interfaceType : funType.getAllImplementedInterfaces()) {
nodeToInsertAfter = addMarker(funType, nodeToInsertAfter, interfaceType); markerNames.add("implements__" + interfaceType.getReferenceName());
}
markerNames.sort(Comparator.naturalOrder()); // Sort to ensure deterministic output.
if (className != null) {
// We can't generate markers for anonymous classes, but there's also no way to specify them
// as a parameter type, so there will never be any checks for them either.
markerNames.add(0, "instance_of__" + className);
} }
}

private Node addMarker(
FunctionType funType, Node nodeToInsertAfter, @Nullable ObjectType interfaceType) {


if (funType.getSource() == null) { // Insert the markers.
return nodeToInsertAfter; for (String markerName : markerNames) {
insertionPoint = inserter.insert(markerName, className, insertionPoint);
} }
}


String className = NodeUtil.getName(funType.getSource()); /**
* Adds a computed property method, with the name of the marker, to the class.
*
* <pre>{@code
* class C {
* ['instance_of__C']() {}
* }
* }</pre>
*/
private Node addMarkerToClass(String markerName, @Nullable String unused, Node classMembers) {
Node function = IR.function(IR.name(""), IR.paramList(), IR.block());
Node member = IR.computedProp(IR.string(markerName), function);
member.putBooleanProp(Node.COMPUTED_PROP_METHOD, true);
classMembers.addChildToBack(member);

compiler.reportChangeToEnclosingScope(member);
compiler.reportChangeToChangeScope(function);
NodeUtil.addFeatureToScript(traversal.getCurrentFile(), Feature.COMPUTED_PROPERTIES);
return classMembers;
}


// This can happen with anonymous classes declared with the type /**
// {@code Function}. * Assigns a {@code true} prop, with the name of the marker. to the prototype of the class.
*
* <pre>{@code
* /** @constructor *\/
* function C() { }
*
* C.prototype['instance_of__C'] = true;
* }</pre>
*/
private Node addMarkerToFunction(
String markerName, @Nullable String className, Node nodeToInsertAfter) {
if (className == null) { if (className == null) {
// This can happen with anonymous classes declared with the type `Function`.
return nodeToInsertAfter; return nodeToInsertAfter;
} }


Node classNode = NodeUtil.newQName(compiler, className); Node classNode = NodeUtil.newQName(compiler, className);

Node marker =
IR.string(
interfaceType == null
? "instance_of__" + className
: "implements__" + interfaceType.getReferenceName());

Node assign = Node assign =
IR.exprResult( IR.exprResult(
IR.assign( IR.assign(
IR.getelem(IR.getprop(classNode, IR.string("prototype")), marker), IR.getelem(IR.getprop(classNode, IR.string("prototype")), IR.string(markerName)),
IR.trueNode())); IR.trueNode()));


nodeToInsertAfter.getParent().addChildAfter(assign, nodeToInsertAfter); nodeToInsertAfter.getParent().addChildAfter(assign, nodeToInsertAfter);
compiler.reportChangeToEnclosingScope(assign); compiler.reportChangeToEnclosingScope(assign);
nodeToInsertAfter = assign; return assign;
return nodeToInsertAfter;
} }


/** /**
Expand Down Expand Up @@ -211,21 +265,24 @@ public void visit(NodeTraversal t, Node n, Node parent) {
return; return;
} }


if (n.isFunction()) { switch (n.getToken()) {
visitFunction(n); case FUNCTION:
} else if (n.isReturn()) { visitFunction(n);
visitReturn(t, n); break;

case RETURN:
case YIELD:
visitTerminal(t, n);
break;

default:
break;
} }
} }


/** Insert checks for the parameters of the function. */ /** Insert checks for the parameters of the function. */
private void visitFunction(Node n) { private void visitFunction(Node n) {
FunctionType funType = JSType.toMaybeFunctionType(n.getJSType());
if (funType == null) {
return;
}
Node block = n.getLastChild(); Node block = n.getLastChild();
Node paramName = NodeUtil.getFunctionParameters(n).getFirstChild();
Node insertionPoint = null; Node insertionPoint = null;


// To satisfy normalization constraints, the type checking must be // To satisfy normalization constraints, the type checking must be
Expand All @@ -236,13 +293,10 @@ private void visitFunction(Node n) {
insertionPoint = next; insertionPoint = next;
} }


for (Node paramType : funType.getParameters()) { for (Node paramName : paramNamesOf(NodeUtil.getFunctionParameters(n))) {
// Can this ever happen? checkState(paramName.isName(), paramName);
if (paramName == null) {
return;
}


Node checkNode = createCheckTypeCallNode(paramType.getJSType(), paramName.cloneTree()); Node checkNode = createCheckTypeCallNode(paramName.getJSType(), paramName.cloneTree());


if (checkNode == null) { if (checkNode == null) {
// We don't know how to check this parameter type. // We don't know how to check this parameter type.
Expand All @@ -258,15 +312,14 @@ private void visitFunction(Node n) {
} }


compiler.reportChangeToEnclosingScope(block); compiler.reportChangeToEnclosingScope(block);
paramName = paramName.getNext();
insertionPoint = checkNode; insertionPoint = checkNode;
} }
} }


private void visitReturn(NodeTraversal t, Node n) { private void visitTerminal(NodeTraversal t, Node n) {
Node function = t.getEnclosingFunction(); Node function = t.getEnclosingFunction();
FunctionType funType = function.getJSType().toMaybeFunctionType();


FunctionType funType = JSType.toMaybeFunctionType(function.getJSType());
if (funType == null) { if (funType == null) {
return; return;
} }
Expand All @@ -276,8 +329,21 @@ private void visitReturn(NodeTraversal t, Node n) {
return; return;
} }


Node checkNode = createCheckTypeCallNode(funType.getReturnType(), retValue.cloneTree()); // Transform the documented return type of the function into the appropriate terminal type
// based on any function or terminator modifiers. (e.g. `async`, `yield*`)
JSType expectedTerminalType = funType.getReturnType();
if (function.isGeneratorFunction()) {
expectedTerminalType = JsIterables.getElementType(expectedTerminalType, typeRegistry);
}
if (function.isAsyncFunction()) {
expectedTerminalType =
Promises.createAsyncReturnableType(typeRegistry, expectedTerminalType);
}
if (n.isYieldAll()) {
expectedTerminalType = JsIterables.createIterableTypeOf(expectedTerminalType, typeRegistry);
}


Node checkNode = createCheckTypeCallNode(expectedTerminalType, retValue.cloneTree());
if (checkNode == null) { if (checkNode == null) {
return; return;
} }
Expand All @@ -295,14 +361,15 @@ private void visitReturn(NodeTraversal t, Node n) {
* @return the function call node or {@code null} if the type is not checked * @return the function call node or {@code null} if the type is not checked
*/ */
private Node createCheckTypeCallNode(JSType type, Node expr) { private Node createCheckTypeCallNode(JSType type, Node expr) {
Node arrayNode = IR.arraylit(); final Collection<JSType> alternates;
Collection<JSType> alternates;
if (type.isUnionType()) { if (type.isUnionType()) {
alternates = new TreeSet<>(ALPHA); alternates = new TreeSet<>(ALPHA); // Sorted to ensure deterministic output
alternates.addAll(type.toMaybeUnionType().getAlternates()); alternates.addAll(type.toMaybeUnionType().getAlternates());
} else { } else {
alternates = ImmutableList.of(type); alternates = ImmutableList.of(type);
} }

Node arrayNode = IR.arraylit();
for (JSType alternate : alternates) { for (JSType alternate : alternates) {
Node checkerNode = createCheckerNode(alternate); Node checkerNode = createCheckerNode(alternate);
if (checkerNode == null) { if (checkerNode == null) {
Expand Down Expand Up @@ -354,6 +421,57 @@ private Node createCheckerNode(JSType type) {
} }
} }


/**
* Returns the NAME parameter nodes of a FUNCTION.
*
* <p>This lookup abstracts over the other legal node types in a PARAM_LIST. It includes only
* those nodes that declare bindings within the function body.
*/
private static ImmutableList<Node> paramNamesOf(Node paramList) {
checkArgument(paramList.isParamList(), paramList);

ImmutableList.Builder<Node> builder = ImmutableList.builder();
paramNamesOf(paramList, builder);
return builder.build();
}

/**
* Recursively collects the NAME parameter nodes of a FUNCTION.
*
* <p>This lookup abstracts over the other legal node types in a PARAM_LIST. It includes only
* those nodes that declare bindings within the function body.
*/
private static void paramNamesOf(Node node, ImmutableList.Builder<Node> names) {
switch (node.getToken()) {
case NAME:
names.add(node);
break;

case REST:
case DEFAULT_VALUE:
paramNamesOf(node.getFirstChild(), names);
break;

case PARAM_LIST:
case ARRAY_PATTERN:
for (Node child = node.getFirstChild(); child != null; child = child.getNext()) {
paramNamesOf(child, names);
}
break;

case OBJECT_PATTERN:
for (Node child = node.getFirstChild(); child != null; child = child.getNext()) {
checkArgument(child.isStringKey() || child.isComputedProp(), child);
paramNamesOf(child.getLastChild(), names);
}
break;

default:
checkArgument(false, node);
break;
}
}

private void addBoilerplateCode() { private void addBoilerplateCode() {
Node newNode = compiler.ensureLibraryInjected("runtime_type_check", false); Node newNode = compiler.ensureLibraryInjected("runtime_type_check", false);
if (newNode != null) { if (newNode != null) {
Expand Down

0 comments on commit f77d7c1

Please sign in to comment.