diff --git a/src/com/google/javascript/jscomp/Es6ConvertSuper.java b/src/com/google/javascript/jscomp/Es6ConvertSuper.java index 4fe600d406b..a1d925729cd 100644 --- a/src/com/google/javascript/jscomp/Es6ConvertSuper.java +++ b/src/com/google/javascript/jscomp/Es6ConvertSuper.java @@ -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 { diff --git a/src/com/google/javascript/jscomp/GlobalTypeInfo.java b/src/com/google/javascript/jscomp/GlobalTypeInfo.java index 346de81988c..915bc0f18b8 100644 --- a/src/com/google/javascript/jscomp/GlobalTypeInfo.java +++ b/src/com/google/javascript/jscomp/GlobalTypeInfo.java @@ -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; diff --git a/src/com/google/javascript/jscomp/NewTypeInference.java b/src/com/google/javascript/jscomp/NewTypeInference.java index bddd602bfef..8ccd3f84d37 100644 --- a/src/com/google/javascript/jscomp/NewTypeInference.java +++ b/src/com/google/javascript/jscomp/NewTypeInference.java @@ -23,6 +23,7 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import com.google.common.collect.LinkedHashMultimap; @@ -99,6 +100,11 @@ final class NewTypeInference implements CompilerPass { "Invalid type for parameter {0} of function {1}.\n" + "{2}"); + static final DiagnosticType TEMPLATE_ARGUMENT_MISMATCH = DiagnosticType.warning( + "JSC_NTI_TEMPLATE_ARGUMENT_MISMATCH", + "Invalid type for the first parameter of tag function {0}.\n" + + "{1}"); + static final DiagnosticType CROSS_SCOPE_GOTCHA = DiagnosticType.warning( "JSC_NTI_CROSS_SCOPE_GOTCHA", "Variable {0} typed inconsistently across scopes.\n" + @@ -374,6 +380,7 @@ final class NewTypeInference implements CompilerPass { INVALID_INFERRED_RETURN_TYPE, INVALID_OPERAND_TYPE, INVALID_THIS_TYPE_IN_BIND, + TEMPLATE_ARGUMENT_MISMATCH, NOT_UNIQUE_INSTANTIATION, PROPERTY_ACCESS_ON_NONOBJECT, UNKNOWN_NAMESPACE_PROPERTY); @@ -1571,7 +1578,8 @@ private EnvTypePair analyzeExprFwd( break; case CALL: case NEW: - resultPair = analyzeCallNewFwd(expr, inEnv, requiredType, specializedType); + case TAGGED_TEMPLATELIT: + resultPair = analyzeInvocationFwd(expr, inEnv, requiredType, specializedType); break; case COMMA: resultPair = analyzeExprFwd( @@ -1622,6 +1630,12 @@ private EnvTypePair analyzeExprFwd( expr.getParent().getFirstChild(), expr.getFirstChild(), inEnv, specializedType); break; + case TEMPLATELIT: + resultPair = analyzeTemplateLitFwd(expr, inEnv); + break; + case TEMPLATELIT_SUB: + resultPair = analyzeExprFwd(expr.getFirstChild(), inEnv, requiredType); + break; default: throw new RuntimeException("Unhandled expression type: " + expr.getToken()); } @@ -2049,7 +2063,7 @@ private EnvTypePair analyzeObjLitCastFwd(ObjectLiteralCast cast, Node call, Type return new EnvTypePair(pair.env, TOP_OBJECT); } - private EnvTypePair analyzeCallNewFwd( + private EnvTypePair analyzeInvocationFwd( Node expr, TypeEnv inEnv, JSType requiredType, JSType specializedType) { if (isPropertyTestCall(expr)) { return analyzePropertyTestCallFwd(expr, inEnv, specializedType); @@ -2077,7 +2091,7 @@ private EnvTypePair analyzeCallNewFwd( FunctionType funType = calleeType.getFunTypeIfSingletonObj(); if (funType == null || funType.isTopFunction() || funType.isQmarkFunction()) { - return analyzeCallNodeArgsFwdWhenError(expr, envAfterCallee); + return analyzeInvocationArgsFwdWhenError(expr, envAfterCallee); } else if (funType.isLoose()) { return analyzeLooseCallNodeFwd(expr, envAfterCallee, requiredType); } else if (!isConstructorCall(expr) @@ -2085,7 +2099,7 @@ private EnvTypePair analyzeCallNewFwd( && (funType.getReturnType().isUnknown() || funType.getReturnType().isUndefined())) { warnings.add(JSError.make(expr, CONSTRUCTOR_NOT_CALLABLE, funType.toString())); - return analyzeCallNodeArgsFwdWhenError(expr, envAfterCallee); + return analyzeInvocationArgsFwdWhenError(expr, envAfterCallee); } else if (expr.isNew()) { if (!funType.isSomeConstructorOrInterface() || funType.isInterfaceDefinition()) { // When Foo is an interface type, we don't want to warn when someone passes around @@ -2100,23 +2114,19 @@ private EnvTypePair analyzeCallNewFwd( warnings.add(JSError.make(expr, NOT_A_CONSTRUCTOR, funType.toString())); } } - return analyzeCallNodeArgsFwdWhenError(expr, envAfterCallee); + return analyzeInvocationArgsFwdWhenError(expr, envAfterCallee); } else if (funType.isConstructorOfAbstractClass()) { warnings.add(JSError.make(expr, CANNOT_INSTANTIATE_ABSTRACT_CLASS, funType.toString())); - return analyzeCallNodeArgsFwdWhenError(expr, envAfterCallee); + return analyzeInvocationArgsFwdWhenError(expr, envAfterCallee); } + } else if (expr.isTaggedTemplateLit()) { + checkTaggedFunctionFirstParam(expr.getLastChild(), expr.getFirstChild(), funType); } - int maxArity = funType.getMaxArity(); - int minArity = funType.getMinArity(); - int numArgs = expr.getChildCount() - 1; - if (numArgs < minArity || numArgs > maxArity) { - warnings.add(JSError.make( - expr, WRONG_ARGUMENT_COUNT, - getReadableCalleeName(callee), - Integer.toString(numArgs), Integer.toString(minArity), - " and at most " + maxArity)); - return analyzeCallNodeArgsFwdWhenError(expr, envAfterCallee); + + if (!isInvocationArgCountCorrectAndWarn(funType, expr, callee)) { + return analyzeInvocationArgsFwdWhenError(expr, envAfterCallee); } + FunctionType origFunType = funType; // save for later if (funType.isGeneric()) { Map typeMap = calcTypeInstantiationFwd( @@ -2128,8 +2138,10 @@ private EnvTypePair analyzeCallNewFwd( } // argTypes collects types of actuals for deferred checks. List argTypes = new ArrayList<>(); - TypeEnv tmpEnv = analyzeCallNodeArgumentsFwd( - expr, expr.getSecondChild(), funType, argTypes, envAfterCallee); + Node invocationNode = expr.isTaggedTemplateLit() ? expr.getLastChild() : expr; + Iterable argIterable = NodeUtil.getInvocationArgsAsIterable(expr); + TypeEnv tmpEnv = analyzeInvocationArgumentsFwd( + invocationNode, argIterable, funType, argTypes, envAfterCallee); if (callee.isName()) { String calleeName = callee.getString(); if (this.currentScope.isKnownFunction(calleeName) @@ -2174,6 +2186,34 @@ private EnvTypePair analyzeCallNewFwd( return new EnvTypePair(tmpEnv, retType); } + /** Check first argument of tagged function is a ITemplateArray */ + private void checkTaggedFunctionFirstParam(Node taggedLit, Node funcName, FunctionType funType) { + JSType firstArgType = funType.getFormalType(0); + JSType templateArray = this.commonTypes.getITemplateArrayType(); + firstArgType = firstArgType == null ? BOTTOM : firstArgType; + if (firstArgType.isBottom() || !templateArray.isSubtypeOf(firstArgType)) { + JSError error = JSError.make(taggedLit, TEMPLATE_ARGUMENT_MISMATCH, + getReadableCalleeName(funcName), errorMsgWithTypeDiff(templateArray, firstArgType)); + registerMismatchAndWarn(error, firstArgType, templateArray); + } + } + + private boolean isInvocationArgCountCorrectAndWarn( + FunctionType funType, Node expr, Node funcName) { + int numArgs = NodeUtil.getInvocationArgsCount(expr); + int maxArity = funType.getMaxArity(); + int minArity = funType.getMinArity(); + if (numArgs < minArity || numArgs > maxArity) { + warnings.add(JSError.make( + expr, WRONG_ARGUMENT_COUNT, + getReadableCalleeName(funcName), + Integer.toString(numArgs), Integer.toString(minArity), + " and at most " + maxArity)); + return false; + } + return true; + } + private boolean isConstructorCall(Node expr) { return expr.isNew() || (expr.isCall() && this.currentScope.isConstructor() && expr.getFirstChild().isSuper()); @@ -2194,7 +2234,7 @@ private EnvTypePair analyzeFunctionBindFwd(Node call, TypeEnv inEnv) { || boundFunType.isTopFunction() || boundFunType.isQmarkFunction() || boundFunType.isLoose()) { - return analyzeCallNodeArgsFwdWhenError(call, env); + return analyzeInvocationArgsFwdWhenError(call, env); } if (boundFunType.isSomeConstructorOrInterface()) { warnings.add(JSError.make(call, CANNOT_BIND_CTOR)); @@ -2219,7 +2259,7 @@ private EnvTypePair analyzeFunctionBindFwd(Node call, TypeEnv inEnv) { getReadableCalleeName(call.getFirstChild()), Integer.toString(numArgs), "0", " and at most " + maxArity)); - return analyzeCallNodeArgsFwdWhenError(call, inEnv); + return analyzeInvocationArgsFwdWhenError(call, inEnv); } // If the bound function is polymorphic, we only support the case where we @@ -2246,9 +2286,13 @@ private EnvTypePair analyzeFunctionBindFwd(Node call, TypeEnv inEnv) { } } + Iterable parametersIterable = + bindComponents.parameters == null + ? ImmutableList.of() + : bindComponents.parameters.siblings(); // We're passing an arraylist but don't do deferred checks for bind. - env = analyzeCallNodeArgumentsFwd(call, bindComponents.parameters, - boundFunType, new ArrayList(), env); + env = analyzeInvocationArgumentsFwd( + call, parametersIterable, boundFunType, new ArrayList(), env); // For any formal not bound here, add it to the resulting function type. for (int j = numArgs; j < boundFunType.getMaxArityWithoutRestFormals(); j++) { JSType formalType = boundFunType.getFormalType(j); @@ -2265,13 +2309,12 @@ private EnvTypePair analyzeFunctionBindFwd(Node call, TypeEnv inEnv) { builder.addRetType(boundFunType.getReturnType()).buildFunction())); } - private TypeEnv analyzeCallNodeArgumentsFwd(Node call, Node firstArg, - FunctionType funType, List argTypesForDeferredCheck, - TypeEnv inEnv) { + private TypeEnv analyzeInvocationArgumentsFwd(Node node, Iterable args, + FunctionType funType, List argTypesForDeferredCheck, TypeEnv inEnv) { + checkState(NodeUtil.isCallOrNew(node) || node.isTemplateLit()); TypeEnv env = inEnv; - Node arg = firstArg; - int i = 0; - while (arg != null) { + int i = node.isTemplateLit() ? 1 : 0; + for (Node arg : args) { JSType formalType = funType.getFormalType(i); checkState(!formalType.isBottom()); EnvTypePair pair = analyzeExprFwd(arg, env, formalType); @@ -2280,9 +2323,9 @@ private TypeEnv analyzeCallNodeArgumentsFwd(Node call, Node firstArg, if (funType.isOptionalArg(i) && pair.type.equals(UNDEFINED)) { argTypeForDeferredCheck = null; // No deferred check needed. } else if (!pair.type.isSubtypeOf(formalType)) { - String fnName = getReadableCalleeName(call.getFirstChild()); - JSError error = JSError.make(arg, INVALID_ARGUMENT_TYPE, - Integer.toString(i + 1), fnName, errorMsgWithTypeDiff(formalType, pair.type)); + String fnName = getReadableCalleeName(node.getFirstChild()); + JSError error = JSError.make(arg, INVALID_ARGUMENT_TYPE, Integer.toString(i + 1), fnName, + errorMsgWithTypeDiff(formalType, pair.type)); registerMismatchAndWarn(error, pair.type, formalType); argTypeForDeferredCheck = null; // No deferred check needed. } else { @@ -2290,7 +2333,6 @@ private TypeEnv analyzeCallNodeArgumentsFwd(Node call, Node firstArg, } argTypesForDeferredCheck.add(argTypeForDeferredCheck); env = pair.env; - arg = arg.getNext(); i++; } return env; @@ -2447,15 +2489,28 @@ private EnvTypePair analyzeCastFwd(Node expr, TypeEnv inEnv, JSType specializedT return pair; } - private EnvTypePair analyzeCallNodeArgsFwdWhenError( - Node callNode, TypeEnv inEnv) { + private EnvTypePair analyzeInvocationArgsFwdWhenError(Node call, TypeEnv env) { + return analyzeInvocationArgsFwdWhenError(NodeUtil.getInvocationArgsAsIterable(call), env); + } + + private EnvTypePair analyzeInvocationArgsFwdWhenError(Iterable args, TypeEnv inEnv) { TypeEnv env = inEnv; - for (Node arg = callNode.getSecondChild(); arg != null; arg = arg.getNext()) { + for (Node arg : args) { env = analyzeExprFwd(arg, env).env; } return new EnvTypePair(env, UNKNOWN); } + private EnvTypePair analyzeTemplateLitFwd(Node expr, TypeEnv inEnv) { + TypeEnv env = inEnv; + for (Node child = expr.getFirstChild(); child != null; + child = child.getNext()) { + EnvTypePair pair = analyzeExprFwd(child, env); + env = pair.env; + } + return new EnvTypePair(env, STRING); + } + private EnvTypePair analyzeStrictComparisonFwd(Token comparisonOp, Node lhs, Node rhs, TypeEnv inEnv, JSType specializedType) { if (specializedType.isTrueOrTruthy() || specializedType.isFalseOrFalsy()) { @@ -2934,7 +2989,7 @@ private EnvTypePair analyzeTypePredicate( warnings.add(JSError.make(call, WRONG_ARGUMENT_COUNT, call.getFirstChild().getQualifiedName(), Integer.toString(numArgs), "1", "1")); - return analyzeCallNodeArgsFwdWhenError(call, inEnv); + return analyzeInvocationArgsFwdWhenError(call, inEnv); } EnvTypePair pair = analyzeExprFwd(call.getLastChild(), inEnv); if (specializedType.isTrueOrTruthy() || specializedType.isFalseOrFalsy()) { @@ -3450,7 +3505,7 @@ private TypeEnv updateLvalueTypeInEnv( } private TypeEnv collectTypesForFreeVarsFwd(Node n, TypeEnv env) { - checkArgument(n.isFunction() || (n.isName() && NodeUtil.isCallOrNewTarget(n))); + checkArgument(n.isFunction() || (n.isName() && NodeUtil.isInvocationTarget(n))); String fnName = n.isFunction() ? symbolTable.getFunInternalName(n) : n.getString(); NTIScope innerScope = this.currentScope.getScope(fnName); for (String freeVar : innerScope.getOuterVars()) { @@ -3652,7 +3707,8 @@ private EnvTypePair analyzeExprBwd( return analyzeHookBwd(expr, outEnv, requiredType); case CALL: case NEW: - return analyzeCallNewBwd(expr, outEnv, requiredType); + case TAGGED_TEMPLATELIT: + return analyzeInvocationBwd(expr, outEnv, requiredType); case COMMA: { EnvTypePair pair = analyzeExprBwd( expr.getLastChild(), outEnv, requiredType); @@ -3693,6 +3749,10 @@ private EnvTypePair analyzeExprBwd( EnvTypePair pair = analyzeExprBwd(expr.getFirstChild(), outEnv); pair.type = symbolTable.getCastType(expr); return pair; + case TEMPLATELIT: + return analyzeTemplateLitBwd(expr, outEnv); + case TEMPLATELIT_SUB: + return analyzeExprBwd(expr.getFirstChild(), outEnv, requiredType); default: throw new RuntimeException( "BWD: Unhandled expression type: " @@ -3850,29 +3910,33 @@ private EnvTypePair analyzeHookBwd( return analyzeExprBwd(cond, TypeEnv.join(thenPair.env, elsePair.env)); } - private EnvTypePair analyzeCallNewBwd( + private EnvTypePair analyzeInvocationBwd( Node expr, TypeEnv outEnv, JSType requiredType) { - checkArgument(expr.isNew() || expr.isCall()); + checkArgument(expr.isNew() || expr.isCall() || expr.isTaggedTemplateLit()); Node callee = expr.getFirstChild(); EnvTypePair pair = analyzeExprBwd(callee, outEnv, commonTypes.topFunction()); TypeEnv envAfterCallee = pair.env; FunctionType funType = pair.type.getFunType(); if (funType == null) { - return analyzeCallNodeArgumentsBwd(expr, envAfterCallee); + return analyzeInvocationArgumentsBwd(expr, expr.getFirstChild(), envAfterCallee); } else if (funType.isLoose()) { return analyzeLooseCallNodeBwd(expr, envAfterCallee, requiredType); } else if ((expr.isCall() && funType.isSomeConstructorOrInterface()) || (expr.isNew() && !funType.isSomeConstructorOrInterface())) { - return analyzeCallNodeArgumentsBwd(expr, envAfterCallee); + return analyzeInvocationArgumentsBwd(expr, expr.getFirstChild(), envAfterCallee); } else if (funType.isTopFunction()) { - return analyzeCallNodeArgumentsBwd(expr, envAfterCallee); + return analyzeInvocationArgumentsBwd(expr, expr.getFirstChild(), envAfterCallee); } - if (callee.isName() && !funType.isGeneric() && expr.isCall()) { + if (callee.isName() && !funType.isGeneric() && (expr.isCall() || expr.isTaggedTemplateLit())) { createDeferredCheckBwd(expr, requiredType); } - int numArgs = expr.getChildCount() - 1; + int numArgs = NodeUtil.getInvocationArgsCount(expr); if (numArgs < funType.getMinArity() || numArgs > funType.getMaxArity()) { - return analyzeCallNodeArgumentsBwd(expr, envAfterCallee); + if (expr.isTaggedTemplateLit()) { + return analyzeInvocationArgumentsBwd(expr.getLastChild(), null, envAfterCallee); + } else { + return analyzeInvocationArgumentsBwd(expr, expr.getFirstChild(), envAfterCallee); + } } if (funType.isGeneric()) { Map typeMap = @@ -3881,9 +3945,17 @@ private EnvTypePair analyzeCallNewBwd( } TypeEnv tmpEnv = envAfterCallee; // In bwd direction, analyze arguments in reverse - Node target = expr.getFirstChild(); - int i = expr.getChildCount() - 1; - for (Node arg = expr.getLastChild(); arg != target; arg = arg.getPrevious()) { + Node target = expr.isTaggedTemplateLit() ? null : expr.getFirstChild(); + Node start = expr.isTaggedTemplateLit() + ? expr.getLastChild().getLastChild() + : expr.getLastChild(); + int i = numArgs; + for (Node arg = start; arg != target; arg = arg.getPrevious()) { + if (expr.isTaggedTemplateLit() && !arg.isTemplateLitSub()) { + // To correctly match the non-string parts of the template literal + // with the formal types of the tag function, i needs to stay unchanged here. + continue; + } i--; JSType formalType = funType.getFormalType(i); // The type of a formal can be BOTTOM as the result of a join. @@ -3941,18 +4013,26 @@ private EnvTypePair analyzeArrayLitBwd(Node expr, TypeEnv outEnv) { return new EnvTypePair(env, commonTypes.getArrayInstance(elementType)); } - private EnvTypePair analyzeCallNodeArgumentsBwd( - Node callNode, TypeEnv outEnv) { + private EnvTypePair analyzeInvocationArgumentsBwd( + Node callNode, Node target, TypeEnv outEnv) { TypeEnv env = outEnv; - Node target = callNode.getFirstChild(); for (Node arg = callNode.getLastChild(); arg != target; arg = arg.getPrevious()) { env = analyzeExprBwd(arg, env).env; } return new EnvTypePair(env, UNKNOWN); } + private EnvTypePair analyzeTemplateLitBwd(Node expr, TypeEnv outEnv) { + TypeEnv env = outEnv; + for (Node elm = expr.getLastChild(); elm != null; elm = elm.getPrevious()) { + EnvTypePair pair = analyzeExprBwd(elm, env); + env = pair.env; + } + return new EnvTypePair(env, STRING); + } + private void createDeferredCheckBwd(Node expr, JSType requiredType) { - checkArgument(expr.isCall()); + checkArgument(expr.isCall() || expr.isTaggedTemplateLit()); checkArgument(expr.getFirstChild().isName()); String calleeName = expr.getFirstChild().getString(); // Local function definitions will be type-checked more @@ -4612,14 +4692,16 @@ private void runCheck( this.expectedRetType, fnSummary.getReturnType()))); } int i = 0; - Node argNode = callSite.getSecondChild(); + Iterable args = NodeUtil.getInvocationArgsAsIterable(callSite); // this.argTypes can be null if in the fwd direction the analysis of the // call return prematurely, eg, because of a WRONG_ARGUMENT_COUNT. if (this.argTypes == null) { return; } - for (JSType argType : this.argTypes) { - JSType formalType = fnSummary.getFormalType(i); + int offset = this.callSite.isTaggedTemplateLit() ? 1 : 0; + for (Node argNode : args) { + JSType argType = this.argTypes.get(i); + JSType formalType = fnSummary.getFormalType(i + offset); if (argNode.isName() && callerScope.isKnownFunction(argNode.getString())) { argType = summaries.get(callerScope.getScope(argNode.getString())); } @@ -4627,13 +4709,13 @@ private void runCheck( if (argType.isSubtypeOf(formalType)) { registerImplicitUses(argNode, argType, formalType); } else { - JSError error = JSError.make(argNode, INVALID_ARGUMENT_TYPE, Integer.toString(i + 1), - calleeScope.getReadableName(), errorMsgWithTypeDiff(formalType, argType)); + JSError error = JSError.make(argNode, INVALID_ARGUMENT_TYPE, + Integer.toString(i + offset + 1), calleeScope.getReadableName(), + errorMsgWithTypeDiff(formalType, argType)); registerMismatchAndWarn(error, argType, formalType); } } i++; - argNode = argNode.getNext(); } } diff --git a/src/com/google/javascript/jscomp/NodeUtil.java b/src/com/google/javascript/jscomp/NodeUtil.java index ab0364ccba0..1c63ba1ef3e 100644 --- a/src/com/google/javascript/jscomp/NodeUtil.java +++ b/src/com/google/javascript/jscomp/NodeUtil.java @@ -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; @@ -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; @@ -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) { @@ -4981,4 +4986,67 @@ public static List removeNestedChangeScopeNodes(List scopeNodes) { } return new ArrayList<>(uniqueScopeNodes); } + + static Iterable 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 getCallArgsAsIterable(Node call){ + if (call.hasOneChild()) { + return ImmutableList.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 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{ + private final Node templateLit; + + TemplateArgsIterable(Node templateLit) { + checkState(templateLit.isTemplateLit()); + this.templateLit = templateLit; + } + + @Override + public Iterator iterator() { + return new AbstractIterator() { + @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; + } + } + }; + } + } } diff --git a/src/com/google/javascript/jscomp/TypeInference.java b/src/com/google/javascript/jscomp/TypeInference.java index 0999321beb5..13b0b298066 100644 --- a/src/com/google/javascript/jscomp/TypeInference.java +++ b/src/com/google/javascript/jscomp/TypeInference.java @@ -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(); } @@ -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); } } diff --git a/src/com/google/javascript/jscomp/TypedScopeCreator.java b/src/com/google/javascript/jscomp/TypedScopeCreator.java index 4d952043e77..3c4cb5a78f9 100644 --- a/src/com/google/javascript/jscomp/TypedScopeCreator.java +++ b/src/com/google/javascript/jscomp/TypedScopeCreator.java @@ -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(); } @@ -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(); diff --git a/src/com/google/javascript/jscomp/newtypes/JSTypes.java b/src/com/google/javascript/jscomp/newtypes/JSTypes.java index 68e13bad0f5..98e920d96af 100644 --- a/src/com/google/javascript/jscomp/newtypes/JSTypes.java +++ b/src/com/google/javascript/jscomp/newtypes/JSTypes.java @@ -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; @@ -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) { @@ -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; } diff --git a/test/com/google/javascript/jscomp/NewTypeInferenceTest.java b/test/com/google/javascript/jscomp/NewTypeInferenceTest.java index da1cb2b4a70..7b65969dc6a 100644 --- a/test/com/google/javascript/jscomp/NewTypeInferenceTest.java +++ b/test/com/google/javascript/jscomp/NewTypeInferenceTest.java @@ -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 works as first argument. + typeCheck( + LINE_JOINER.join( + "function tag(/** ?Array */ 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 does not work as first argument. + typeCheck( + LINE_JOINER.join( + "function tag(/** !Array */ 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 */ strs, /** string */ x){}", + "function h(x) {", + " tag `asdf ${x} asdf`;", + " return x - 2;", + "}"), + NewTypeInference.INVALID_OPERAND_TYPE + ); + } }