Skip to content

Commit

Permalink
Add support for template literals in NTI.
Browse files Browse the repository at this point in the history
-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=160445621
  • Loading branch information
EatingW authored and brad4d committed Jun 29, 2017
1 parent 047f209 commit e4ba67d
Show file tree
Hide file tree
Showing 8 changed files with 362 additions and 66 deletions.
2 changes: 1 addition & 1 deletion src/com/google/javascript/jscomp/Es6ConvertSuper.java
Expand Up @@ -146,7 +146,7 @@ public boolean apply(Node n) {
visitSuperCall(node, parent, enclosingMemberDef);
} else if (parent.isGetProp() || parent.isGetElem()) {
if (parent.getFirstChild() == node) {
if (parent.getParent().isCall() && NodeUtil.isCallOrNewTarget(parent)) {
if (parent.getParent().isCall() && NodeUtil.isInvocationTarget(parent)) {
// super.something(...) or super['something'](..)
visitSuperPropertyCall(node, parent, enclosingMemberDef);
} else {
Expand Down
3 changes: 3 additions & 0 deletions src/com/google/javascript/jscomp/GlobalTypeInfo.java
Expand Up @@ -1354,6 +1354,9 @@ private void maybeRecordBuiltinType(String name, RawNominalType rawType) {
case "Iterable":
commonTypes.setIterableType(rawType);
break;
case "ITemplateArray":
commonTypes.setITemplateArrayType(rawType);
break;
default:
// No other type names are added to commonTypes.
break;
Expand Down
200 changes: 141 additions & 59 deletions src/com/google/javascript/jscomp/NewTypeInference.java

Large diffs are not rendered by default.

72 changes: 70 additions & 2 deletions src/com/google/javascript/jscomp/NodeUtil.java
Expand Up @@ -23,7 +23,10 @@
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.base.Splitter;
import com.google.common.collect.AbstractIterator;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.javascript.jscomp.CompilerOptions.LanguageMode;
import com.google.javascript.jscomp.parsing.parser.FeatureSet;
import com.google.javascript.jscomp.parsing.parser.FeatureSet.Feature;
Expand All @@ -45,6 +48,7 @@
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
Expand Down Expand Up @@ -4440,9 +4444,10 @@ static Node getArgumentForCallOrNew(Node call, int index) {
/**
* Returns whether this is a target of a call or new.
*/
static boolean isCallOrNewTarget(Node n) {
static boolean isInvocationTarget(Node n) {
Node parent = n.getParent();
return parent != null && isCallOrNew(parent) && parent.getFirstChild() == n;
return parent != null && (isCallOrNew(parent) || parent.isTaggedTemplateLit())
&& parent.getFirstChild() == n;
}

static boolean isCallOrNewArgument(Node n) {
Expand Down Expand Up @@ -4981,4 +4986,67 @@ public static List<Node> removeNestedChangeScopeNodes(List<Node> scopeNodes) {
}
return new ArrayList<>(uniqueScopeNodes);
}

static Iterable<Node> getInvocationArgsAsIterable(Node invocation){
if (invocation.isTaggedTemplateLit()) {
return new TemplateArgsIterable(invocation.getLastChild());
} else {
return getCallArgsAsIterable(invocation);
}
}

/** Returns an iterable of arguments of this call node */
private static Iterable<Node> getCallArgsAsIterable(Node call){
if (call.hasOneChild()) {
return ImmutableList.<Node>of();
}
return call.getSecondChild().siblings();
}

/**
* Returns the number of arguments in this invocation. For template literals it takes into
* account the implicit first argument of ITemplateArray
*/
static int getInvocationArgsCount(Node invocation) {
if (invocation.isTaggedTemplateLit()) {
Iterable<Node> args = new TemplateArgsIterable(invocation.getLastChild());
return Iterables.size(args) + 1;
} else {
return invocation.getChildCount() - 1;
}
}

/**
* Represents an iterable of the children of templatelit_sub nodes of a template lit node
* This iterable will skip over the String children of the template lit node.
*/
private static final class TemplateArgsIterable implements Iterable<Node>{
private final Node templateLit;

TemplateArgsIterable(Node templateLit) {
checkState(templateLit.isTemplateLit());
this.templateLit = templateLit;
}

@Override
public Iterator<Node> iterator() {
return new AbstractIterator<Node>() {
@Nullable private Node nextChild = templateLit.getFirstChild();

@Override
protected Node computeNext() {
while (nextChild != null && !nextChild.isTemplateLitSub()) {
nextChild = nextChild.getNext();
}
if (nextChild == null) {
return endOfData();
} else {
Node result = nextChild.getFirstChild();
nextChild = nextChild.getNext();
return result;
}
}
};
}
}
}
5 changes: 3 additions & 2 deletions src/com/google/javascript/jscomp/TypeInference.java
Expand Up @@ -122,12 +122,13 @@ class TypeInference
/**
* Infers all of a function's arguments if their types aren't declared.
*/
@SuppressWarnings("ReferenceEquality") // unknownType is a singleton
private void inferArguments(TypedScope functionScope) {
Node functionNode = functionScope.getRootNode();
Node astParameters = functionNode.getSecondChild();
Node iifeArgumentNode = null;

if (NodeUtil.isCallOrNewTarget(functionNode)) {
if (NodeUtil.isInvocationTarget(functionNode)) {
iifeArgumentNode = functionNode.getNext();
}

Expand Down Expand Up @@ -1548,7 +1549,7 @@ private FlowScope dereferencePointer(Node n, FlowScope scope) {
if (n.isQualifiedName()) {
JSType type = getJSType(n);
JSType narrowed = type.restrictByNotNullOrUndefined();
if (type != narrowed) {
if (!type.equals(narrowed)) {
scope = narrowScope(scope, n, narrowed);
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/com/google/javascript/jscomp/TypedScopeCreator.java
Expand Up @@ -2073,7 +2073,7 @@ private void declareArguments(Node functionNode) {
Node astParameters = functionNode.getSecondChild();
Node iifeArgumentNode = null;

if (NodeUtil.isCallOrNewTarget(functionNode)) {
if (NodeUtil.isInvocationTarget(functionNode)) {
iifeArgumentNode = functionNode.getNext();
}

Expand All @@ -2086,7 +2086,7 @@ private void declareArguments(Node functionNode) {
for (Node astParameter : astParameters.children()) {
JSType paramType = jsDocParameter == null ?
unknownType : jsDocParameter.getJSType();
boolean inferred = paramType == null || paramType == unknownType;
boolean inferred = paramType == null || paramType.equals(unknownType);

if (iifeArgumentNode != null && inferred) {
String argumentName = iifeArgumentNode.getQualifiedName();
Expand Down
9 changes: 9 additions & 0 deletions src/com/google/javascript/jscomp/newtypes/JSTypes.java
Expand Up @@ -164,6 +164,7 @@ public final class JSTypes implements Serializable {
private RawNominalType iObject;
private RawNominalType iArrayLike;
private RawNominalType iterable;
private RawNominalType iTemplateArray;

final boolean allowMethodsAsFunctions;
final boolean looseSubtypingForLooseObjects;
Expand Down Expand Up @@ -432,6 +433,10 @@ public JSType getArgumentsArrayType() {
return getArgumentsArrayType(this.UNKNOWN);
}

public JSType getITemplateArrayType() {
return iTemplateArray != null ? iTemplateArray.getInstanceAsJSType() : UNKNOWN;
}

public JSType getNativeType(JSTypeNative typeId) {
// NOTE(aravindpg): not all JSTypeNative variants are handled here; add more as-needed.
switch (typeId) {
Expand Down Expand Up @@ -526,6 +531,10 @@ public void setIterableType(RawNominalType iterable) {
this.iterable = iterable;
}

public void setITemplateArrayType(RawNominalType iTemplateArray) {
this.iTemplateArray = iTemplateArray;
}

public void setRegexpInstance(JSType regexpInstance) {
this.regexpInstance = regexpInstance;
}
Expand Down
133 changes: 133 additions & 0 deletions test/com/google/javascript/jscomp/NewTypeInferenceTest.java
Expand Up @@ -20659,4 +20659,137 @@ public void testBackwardAnalyzedLooseFunctionParametersInRightOrder() {
" Array.prototype.forEach.call(foos, f);",
"};"));
}

public void testTemplateLitBothMode() {
this.mode = InputLanguageMode.BOTH;

// Normal case
typeCheck(
LINE_JOINER.join(
"var a, b",
"var /** string */ s = `template ${a} string ${b}`;"));

// Template strings can take many types.
typeCheck(
LINE_JOINER.join(
"function f(/** * */ x){",
" var /** string */ s = `template ${x} string`;",
"}"));

// Check that we analyze inside the Template Sub
typeCheck(
"var s = `template ${1 - 'asdf'} string`;",
NewTypeInference.INVALID_OPERAND_TYPE);

// Check template string has type string
typeCheck(
"var /** number */ n = `${1}`;",
NewTypeInference.MISTYPED_ASSIGN_RHS);
}

public void testTaggedTemplateBothMode() {
this.mode = InputLanguageMode.BOTH;

// ITemplateArray as first argument
typeCheck("String.raw`one ${1} two`");

// Infers first argument of tag function is supertype of ITemplateArray.
typeCheck(
LINE_JOINER.join(
"function tag(strings, /** number */ a){",
" var str0 = strings[0];",
" return ''",
"}",
"var /** string */ s = tag`template ${1} string`;"));

// ?Array<string> works as first argument.
typeCheck(
LINE_JOINER.join(
"function tag(/** ?Array<string> */ strings){}",
"tag`template string`;"));

// Check argument count to tag function
typeCheck(
LINE_JOINER.join(
"function tag(/** !ITemplateArray */ strings, /** number */ x, /** string */ y) {}",
"tag`template ${123} string`;"),
NewTypeInference.WRONG_ARGUMENT_COUNT);

// Check argument count with no strings in template lit
typeCheck(
LINE_JOINER.join(
"function tag(/** !ITemplateArray */ strings, /** number */ x){}",
"tag`${0}${1}`;"),
NewTypeInference.WRONG_ARGUMENT_COUNT);

// Check argument count with optional arguments
typeCheck(LINE_JOINER.join(
"/** @param {number=} y */",
"function tag(strings, y){}",
"tag`str`;"));

// Simply having Object as first parameter is fine
typeCheck(
LINE_JOINER.join(
"function tag(/** Object */ strings){}",
"tag `template string`;"));

// Check argument type
typeCheck(
LINE_JOINER.join(
"function tag(/** !ITemplateArray */ strings, /** string */ y){}",
"tag`template string ${1}`;"),
NewTypeInference.INVALID_ARGUMENT_TYPE);

// Tag function does not have to return strings
typeCheck(
LINE_JOINER.join(
"function tag(strings){",
" return (function (){});",
"}",
"var g = tag`template string`;",
"g()"));
}

public void testTaggedTemplateBadTagFunction() {
// Invalid first parameter type for specific object
typeCheck(
LINE_JOINER.join(
"function tag(/** {a:number} */ strings){}",
"tag `template string`;"),
NewTypeInference.TEMPLATE_ARGUMENT_MISMATCH);

// !Array<number> does not work as first argument.
typeCheck(
LINE_JOINER.join(
"function tag(/** !Array<number> */ strings){}",
"tag`template string`;"),
NewTypeInference.TEMPLATE_ARGUMENT_MISMATCH);

// Check argument count with tag function that has no parameters
typeCheck(
LINE_JOINER.join(
"function tag(){}",
"tag``;"),
NewTypeInference.TEMPLATE_ARGUMENT_MISMATCH,
NewTypeInference.WRONG_ARGUMENT_COUNT);

// Tag function not a function
typeCheck(
LINE_JOINER.join(
"var tag = 42;",
"tag `template string`;"),
NewTypeInference.NOT_CALLABLE);

// Check backwards-infer type of template sub from type of tag function.
typeCheck(
LINE_JOINER.join(
"function tag(/** !Array<string> */ strs, /** string */ x){}",
"function h(x) {",
" tag `asdf ${x} asdf`;",
" return x - 2;",
"}"),
NewTypeInference.INVALID_OPERAND_TYPE
);
}
}

0 comments on commit e4ba67d

Please sign in to comment.