Skip to content

Commit

Permalink
Add support for tagged template literals in the typechecker
Browse files Browse the repository at this point in the history
This does some type inference of tagged template literals, but is missing a few features it has for regular function calls. The type inference doesn't support back inference for tagged template literals 'calls' (like it does in transpiled code).

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=196841118
  • Loading branch information
lauraharker authored and blickly committed May 17, 2018
1 parent e5eabe8 commit c2455bb
Show file tree
Hide file tree
Showing 9 changed files with 346 additions and 18 deletions.
18 changes: 15 additions & 3 deletions src/com/google/javascript/jscomp/FunctionTypeBuilder.java
Expand Up @@ -799,15 +799,21 @@ private FunctionType getOrCreateConstructor() {
returnType, returnType,
classTemplateTypeNames, classTemplateTypeNames,
isAbstract); isAbstract);
// We use "getTypeForScope" to specifically check if this was defined for getScopeDeclaredIn()
// so we don't pick up types that are going to be shadowed.
JSType existingType = typeRegistry.getTypeForScope(getScopeDeclaredIn(), fnName);


if (makesStructs) { if (makesStructs) {
fnType.setStruct(); fnType.setStruct();
} else if (makesDicts) { } else if (makesDicts) {
fnType.setDict(); fnType.setDict();
} }

// There are two cases where this type already exists in the current scope:
// 1. The type is a built-in that we initalized in JSTypeRegistry and is also defined in
// externs.
// 2. Cases like "class C {} C = class {}"
// See https://github.com/google/closure-compiler/issues/2928 for some related bugs.
// We use "getTypeForScope" to specifically check if this was defined for getScopeDeclaredIn()
// so we don't pick up types that are going to be shadowed.
JSType existingType = typeRegistry.getTypeForScope(getScopeDeclaredIn(), fnName);
if (existingType != null) { if (existingType != null) {
boolean isInstanceObject = existingType.isInstanceType(); boolean isInstanceObject = existingType.isInstanceType();
if (isInstanceObject || fnName.equals("Function")) { if (isInstanceObject || fnName.equals("Function")) {
Expand All @@ -825,6 +831,12 @@ private FunctionType getOrCreateConstructor() {
fnType.toString(), existingFn.toString()); fnType.toString(), existingFn.toString());
} }


// If the existing function is a built-in type, set its base type in case it @extends
// another function (since we don't set its prototype in JSTypeRegistry)
if (existingFn.isNativeObjectType()) {
maybeSetBaseType(existingFn);
}

return existingFn; return existingFn;
} else { } else {
// We fall through and return the created type, even though it will fail // We fall through and return the created type, even though it will fail
Expand Down
86 changes: 76 additions & 10 deletions src/com/google/javascript/jscomp/TypeCheck.java
Expand Up @@ -640,10 +640,15 @@ public void visit(NodeTraversal t, Node n, Node parent) {
ensureTyped(t, n, STRING_TYPE); ensureTyped(t, n, STRING_TYPE);
break; break;


case TEMPLATELIT: case TEMPLATELIT:
ensureTyped(t, n, STRING_TYPE); ensureTyped(t, n, STRING_TYPE);
break; break;


case TAGGED_TEMPLATELIT:
visitTaggedTemplateLit(t, n);
ensureTyped(t, n);
break;

case BITNOT: case BITNOT:
childType = getJSType(n.getFirstChild()); childType = getJSType(n.getFirstChild());
if (!childType.matchesNumberContext()) { if (!childType.matchesNumberContext()) {
Expand Down Expand Up @@ -1846,7 +1851,7 @@ private void visitNew(NodeTraversal t, Node n) {
if (ctorType != null && ctorType.isAbstract()) { if (ctorType != null && ctorType.isAbstract()) {
report(t, n, INSTANTIATE_ABSTRACT_CLASS); report(t, n, INSTANTIATE_ABSTRACT_CLASS);
} }
visitParameterList(t, n, fnType); visitArgumentList(t, n, fnType);
ensureTyped(t, n, fnType.getInstanceType()); ensureTyped(t, n, fnType.getInstanceType());
} else { } else {
ensureTyped(t, n); ensureTyped(t, n);
Expand Down Expand Up @@ -2073,7 +2078,7 @@ private void visitCall(NodeTraversal t, Node n) {
} }


checkAbstractMethodCall(t, n); checkAbstractMethodCall(t, n);
visitParameterList(t, n, functionType); visitArgumentList(t, n, functionType);
ensureTyped(t, n, functionType.getReturnType()); ensureTyped(t, n, functionType.getReturnType());
} else { } else {
ensureTyped(t, n); ensureTyped(t, n);
Expand Down Expand Up @@ -2118,17 +2123,32 @@ private void checkAbstractMethodCall(NodeTraversal t, Node call) {
} }
} }


/** Visits the parameters of a CALL or a NEW node. */
private void visitArgumentList(NodeTraversal t, Node call, FunctionType functionType) {
Iterator<Node> parameters = functionType.getParameters().iterator();
Iterator<Node> arguments = NodeUtil.getInvocationArgsAsIterable(call).iterator();
checkArgumentsMatchParameters(t, call, functionType, arguments, parameters, 0);
}

/** /**
* Visits the parameters of a CALL or a NEW node. * Checks that a list of arguments match a list of formal parameters
*
* <p>If given a TAGGED_TEMPLATE_LIT, the given Iterator should only contain the parameters
* corresponding to the actual template lit sub arguments, skipping over the first parameter.
*
* @param firstParameterIndex The index of the first parameter in the given Iterator in the
* function type's parameter list.
*/ */
private void visitParameterList(NodeTraversal t, Node call, private void checkArgumentsMatchParameters(
FunctionType functionType) { NodeTraversal t,
Iterator<Node> arguments = call.children().iterator(); Node call,
arguments.next(); // skip the function name FunctionType functionType,
Iterator<Node> arguments,
Iterator<Node> parameters,
int firstParameterIndex) {


Iterator<Node> parameters = functionType.getParameters().iterator();
int spreadArgumentCount = 0; int spreadArgumentCount = 0;
int normalArgumentCount = 0; int normalArgumentCount = firstParameterIndex;
boolean checkArgumentTypeAgainstParameter = true; boolean checkArgumentTypeAgainstParameter = true;
Node parameter = null; Node parameter = null;
Node argument = null; Node argument = null;
Expand Down Expand Up @@ -2332,6 +2352,52 @@ static JSType getTemplateTypeOfGenerator(JSTypeRegistry typeRegistry, JSType gen
return typeRegistry.getNativeType(UNKNOWN_TYPE); return typeRegistry.getNativeType(UNKNOWN_TYPE);
} }


private void visitTaggedTemplateLit(NodeTraversal t, Node n) {
Node tag = n.getFirstChild();
JSType tagType = tag.getJSType().restrictByNotNullOrUndefined();

if (!tagType.canBeCalled()) {
report(t, n, NOT_CALLABLE, tagType.toString());
return;
} else if (!tagType.isFunctionType()) {
// A few types, like the unknown, regexp, and bottom types, can be called as if they are
// functions. Return if we have one of those types that is not actually a known function.
return;
}

FunctionType tagFnType = tagType.toMaybeFunctionType();
Iterator<Node> parameters = tagFnType.getParameters().iterator();

// The tag function gets an array of all the template lit substitutions as its first argument,
// but there's no actual AST node representing that array so we typecheck it separately from
// the other tag arguments.

// Validate that the tag function takes at least one parameter
if (!parameters.hasNext()) {
report(
t,
n,
WRONG_ARGUMENT_COUNT,
typeRegistry.getReadableTypeNameNoDeref(tag),
String.valueOf(NodeUtil.getInvocationArgsCount(n)),
"0",
" and no more than 0 argument(s)");
return;
}

// Validate that the first parameter is a supertype of ITemplateArray
Node firstParameter = parameters.next();
JSType parameterType = firstParameter.getJSType().restrictByNotNullOrUndefined();
if (parameterType != null) {
validator.expectITemplateArraySupertype(
t, firstParameter, parameterType, "Invalid type for the first parameter of tag function");
}

// Validate the remaining parameters (the template literal substitutions)
checkArgumentsMatchParameters(
t, n, tagFnType, NodeUtil.getInvocationArgsAsIterable(n).iterator(), parameters, 1);
}

/** /**
* This function unifies the type checking involved in the core binary * This function unifies the type checking involved in the core binary
* operators and the corresponding assignment operators. The representation * operators and the corresponding assignment operators. The representation
Expand Down
22 changes: 22 additions & 0 deletions src/com/google/javascript/jscomp/TypeInference.java
Expand Up @@ -445,6 +445,10 @@ private FlowScope traverse(Node n, FlowScope scope) {
scope = traverseChildren(n, scope); scope = traverseChildren(n, scope);
break; break;


case TAGGED_TEMPLATELIT:
scope = traverseTaggedTemplateLiteral(n, scope);
break;

case DELPROP: case DELPROP:
case LT: case LT:
case LE: case LE:
Expand Down Expand Up @@ -1084,6 +1088,24 @@ private FlowScope traverseCall(Node n, FlowScope scope) {
return scope; return scope;
} }


private FlowScope traverseTaggedTemplateLiteral(Node n, FlowScope scope) {
scope = traverseChildren(n, scope);

Node left = n.getFirstChild();
JSType functionType = getJSType(left).restrictByNotNullOrUndefined();
if (functionType.isFunctionType()) {
FunctionType fnType = functionType.toMaybeFunctionType();
n.setJSType(fnType.getReturnType());
// TODO(b/78891530): make "backwardsInferenceFromCallSite" work for tagged template literals
} else if (functionType.isEquivalentTo(getNativeType(CHECKED_UNKNOWN_TYPE))) {
n.setJSType(getNativeType(CHECKED_UNKNOWN_TYPE));
} else {
n.setJSType(getNativeType(UNKNOWN_TYPE));
}

return scope;
}

private FlowScope tightenTypesAfterAssertions(FlowScope scope, Node callNode) { private FlowScope tightenTypesAfterAssertions(FlowScope scope, Node callNode) {
Node left = callNode.getFirstChild(); Node left = callNode.getFirstChild();
Node firstParam = left.getNext(); Node firstParam = left.getNext();
Expand Down
8 changes: 8 additions & 0 deletions src/com/google/javascript/jscomp/TypeValidator.java
Expand Up @@ -24,6 +24,7 @@
import static com.google.javascript.rhino.jstype.JSTypeNative.BOOLEAN_TYPE; import static com.google.javascript.rhino.jstype.JSTypeNative.BOOLEAN_TYPE;
import static com.google.javascript.rhino.jstype.JSTypeNative.GENERATOR_TYPE; import static com.google.javascript.rhino.jstype.JSTypeNative.GENERATOR_TYPE;
import static com.google.javascript.rhino.jstype.JSTypeNative.ITERABLE_TYPE; import static com.google.javascript.rhino.jstype.JSTypeNative.ITERABLE_TYPE;
import static com.google.javascript.rhino.jstype.JSTypeNative.I_TEMPLATE_ARRAY_TYPE;
import static com.google.javascript.rhino.jstype.JSTypeNative.NO_OBJECT_TYPE; import static com.google.javascript.rhino.jstype.JSTypeNative.NO_OBJECT_TYPE;
import static com.google.javascript.rhino.jstype.JSTypeNative.NULL_TYPE; import static com.google.javascript.rhino.jstype.JSTypeNative.NULL_TYPE;
import static com.google.javascript.rhino.jstype.JSTypeNative.NUMBER_STRING; import static com.google.javascript.rhino.jstype.JSTypeNative.NUMBER_STRING;
Expand Down Expand Up @@ -304,6 +305,13 @@ void expectGeneratorSupertype(NodeTraversal t, Node n, JSType type, String msg)
} }
} }


/** Expect the type to be an ITemplateArray or supertype of ITemplateArray. */
void expectITemplateArraySupertype(NodeTraversal t, Node n, JSType type, String msg) {
if (!getNativeType(I_TEMPLATE_ARRAY_TYPE).isSubtypeOf(type)) {
mismatch(t, n, msg, type, I_TEMPLATE_ARRAY_TYPE);
}
}

/** /**
* Expect the type to be a string, or a type convertible to string. If the * Expect the type to be a string, or a type convertible to string. If the
* expectation is not met, issue a warning at the provided node's source code * expectation is not met, issue a warning at the provided node's source code
Expand Down
16 changes: 16 additions & 0 deletions src/com/google/javascript/rhino/jstype/JSTypeRegistry.java
Expand Up @@ -442,6 +442,21 @@ private void initializeBuiltInTypes() {
ObjectType ARRAY_TYPE = ARRAY_FUNCTION_TYPE.getInstanceType(); ObjectType ARRAY_TYPE = ARRAY_FUNCTION_TYPE.getInstanceType();
registerNativeType(JSTypeNative.ARRAY_TYPE, ARRAY_TYPE); registerNativeType(JSTypeNative.ARRAY_TYPE, ARRAY_TYPE);


// ITemplateArray extends !Array<string>
FunctionType iTemplateArrayFunctionType =
new FunctionType(
this,
"ITemplateArray",
/* source= */ null,
createArrowType(),
/* typeOfThis= */ null,
/* templateTypeMap= */ null,
Kind.CONSTRUCTOR,
/* nativeType= */ true,
/* isAbstract= */ false);
registerNativeType(
JSTypeNative.I_TEMPLATE_ARRAY_TYPE, iTemplateArrayFunctionType.getInstanceType());

FunctionType iterableFunctionType = FunctionType iterableFunctionType =
new FunctionType( new FunctionType(
this, this,
Expand Down Expand Up @@ -852,6 +867,7 @@ private void initializeRegistry() {
registerGlobalType(getNativeType(JSTypeNative.GENERATOR_TYPE)); registerGlobalType(getNativeType(JSTypeNative.GENERATOR_TYPE));
registerGlobalType(getNativeType(JSTypeNative.DATE_TYPE)); registerGlobalType(getNativeType(JSTypeNative.DATE_TYPE));
registerGlobalType(getNativeType(JSTypeNative.I_OBJECT_TYPE)); registerGlobalType(getNativeType(JSTypeNative.I_OBJECT_TYPE));
registerGlobalType(getNativeType(JSTypeNative.I_TEMPLATE_ARRAY_TYPE));
registerGlobalType(getNativeType(JSTypeNative.I_THENABLE_TYPE)); registerGlobalType(getNativeType(JSTypeNative.I_THENABLE_TYPE));
registerGlobalType(getNativeType(JSTypeNative.NULL_TYPE)); registerGlobalType(getNativeType(JSTypeNative.NULL_TYPE));
registerGlobalType(getNativeType(JSTypeNative.NULL_TYPE), "Null"); registerGlobalType(getNativeType(JSTypeNative.NULL_TYPE), "Null");
Expand Down
5 changes: 5 additions & 0 deletions test/com/google/javascript/jscomp/CompilerTestCase.java
Expand Up @@ -541,6 +541,11 @@ public abstract class CompilerTestCase extends TestCase {
" * @template RESULT", " * @template RESULT",
" */", " */",
"Promise.prototype.catch = function(onRejected) {};", "Promise.prototype.catch = function(onRejected) {};",
"/**",
" * @constructor",
" * @extends {Array<string>}",
" */",
"function ITemplateArray() {}",
ACTIVE_X_OBJECT_DEF); ACTIVE_X_OBJECT_DEF);


/** /**
Expand Down

0 comments on commit c2455bb

Please sign in to comment.