Java tutorial
/* * Copyright 2008 The Closure Compiler Authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.javascript.jscomp; import static com.google.javascript.rhino.jstype.JSTypeNative.ARRAY_TYPE; import static com.google.javascript.rhino.jstype.JSTypeNative.BOOLEAN_OBJECT_TYPE; import static com.google.javascript.rhino.jstype.JSTypeNative.BOOLEAN_TYPE; import static com.google.javascript.rhino.jstype.JSTypeNative.CHECKED_UNKNOWN_TYPE; import static com.google.javascript.rhino.jstype.JSTypeNative.NULL_TYPE; import static com.google.javascript.rhino.jstype.JSTypeNative.NUMBER_TYPE; import static com.google.javascript.rhino.jstype.JSTypeNative.NUMBER_VALUE_OR_OBJECT_TYPE; import static com.google.javascript.rhino.jstype.JSTypeNative.STRING_TYPE; import static com.google.javascript.rhino.jstype.JSTypeNative.UNKNOWN_TYPE; import static com.google.javascript.rhino.jstype.JSTypeNative.VOID_TYPE; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.google.javascript.jscomp.CodingConvention.AssertionFunctionSpec; import com.google.javascript.jscomp.ControlFlowGraph.Branch; import com.google.javascript.jscomp.graph.DiGraph.DiGraphEdge; import com.google.javascript.jscomp.type.FlowScope; import com.google.javascript.jscomp.type.ReverseAbstractInterpreter; import com.google.javascript.rhino.JSDocInfo; import com.google.javascript.rhino.Node; import com.google.javascript.rhino.Token; import com.google.javascript.rhino.jstype.BooleanLiteralSet; import com.google.javascript.rhino.jstype.FunctionBuilder; import com.google.javascript.rhino.jstype.FunctionType; 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.ModificationVisitor; import com.google.javascript.rhino.jstype.ObjectType; import com.google.javascript.rhino.jstype.StaticTypedSlot; import com.google.javascript.rhino.jstype.TemplateType; import com.google.javascript.rhino.jstype.TemplateTypeMap; import com.google.javascript.rhino.jstype.TemplateTypeMapReplacer; import com.google.javascript.rhino.jstype.UnionType; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; /** * Type inference within a script node or a function body, using the data-flow * analysis framework. * */ class TypeInference extends DataFlowAnalysis.BranchedForwardDataFlowAnalysis<Node, FlowScope> { // TODO(johnlenz): We no longer make this check, but we should. static final DiagnosticType FUNCTION_LITERAL_UNDEFINED_THIS = DiagnosticType.warning( "JSC_FUNCTION_LITERAL_UNDEFINED_THIS", "Function literal argument refers to undefined this argument"); private final AbstractCompiler compiler; private final JSTypeRegistry registry; private final ReverseAbstractInterpreter reverseInterpreter; private final TypedScope syntacticScope; private final FlowScope functionScope; private final FlowScope bottomScope; private final Map<String, AssertionFunctionSpec> assertionFunctionsMap; // For convenience private final ObjectType unknownType; TypeInference(AbstractCompiler compiler, ControlFlowGraph<Node> cfg, ReverseAbstractInterpreter reverseInterpreter, TypedScope functionScope, Map<String, AssertionFunctionSpec> assertionFunctionsMap) { super(cfg, new LinkedFlowScope.FlowScopeJoinOp()); this.compiler = compiler; this.registry = compiler.getTypeRegistry(); this.reverseInterpreter = reverseInterpreter; this.unknownType = registry.getNativeObjectType(UNKNOWN_TYPE); this.syntacticScope = functionScope; inferArguments(functionScope); this.functionScope = LinkedFlowScope.createEntryLattice(functionScope); this.assertionFunctionsMap = assertionFunctionsMap; // For each local variable declared with the VAR keyword, the entry // type is VOID. Iterator<TypedVar> varIt = functionScope.getDeclarativelyUnboundVarsWithoutTypes(); while (varIt.hasNext()) { TypedVar var = varIt.next(); if (isUnflowable(var)) { continue; } this.functionScope.inferSlotType(var.getName(), getNativeType(VOID_TYPE)); } this.bottomScope = LinkedFlowScope .createEntryLattice(TypedScope.createLatticeBottom(functionScope.getRootNode())); } /** * Infers all of a function's arguments if their types aren't declared. */ private void inferArguments(TypedScope functionScope) { Node functionNode = functionScope.getRootNode(); Node astParameters = functionNode.getFirstChild().getNext(); Node iifeArgumentNode = null; if (NodeUtil.isCallOrNewTarget(functionNode)) { iifeArgumentNode = functionNode.getNext(); } FunctionType functionType = JSType.toMaybeFunctionType(functionNode.getJSType()); if (functionType != null) { Node parameterTypes = functionType.getParametersNode(); if (parameterTypes != null) { Node parameterTypeNode = parameterTypes.getFirstChild(); for (Node astParameter : astParameters.children()) { TypedVar var = functionScope.getVar(astParameter.getString()); Preconditions.checkNotNull(var); if (var.isTypeInferred() && var.getType() == unknownType) { JSType newType = null; if (iifeArgumentNode != null) { newType = iifeArgumentNode.getJSType(); } else if (parameterTypeNode != null) { newType = parameterTypeNode.getJSType(); } if (newType != null) { var.setType(newType); astParameter.setJSType(newType); } } if (parameterTypeNode != null) { parameterTypeNode = parameterTypeNode.getNext(); } if (iifeArgumentNode != null) { iifeArgumentNode = iifeArgumentNode.getNext(); } } } } } @Override FlowScope createInitialEstimateLattice() { return bottomScope; } @Override FlowScope createEntryLattice() { return functionScope; } @Override FlowScope flowThrough(Node n, FlowScope input) { // If we have not walked a path from <entry> to <n>, then we don't // want to infer anything about this scope. if (input == bottomScope) { return input; } FlowScope output = input.createChildFlowScope(); output = traverse(n, output); return output; } @Override @SuppressWarnings({ "fallthrough", "incomplete-switch" }) List<FlowScope> branchedFlowThrough(Node source, FlowScope input) { // NOTE(nicksantos): Right now, we just treat ON_EX edges like UNCOND // edges. If we wanted to be perfect, we'd actually JOIN all the out // lattices of this flow with the in lattice, and then make that the out // lattice for the ON_EX edge. But it's probably too expensive to be // worthwhile. FlowScope output = flowThrough(source, input); Node condition = null; FlowScope conditionFlowScope = null; BooleanOutcomePair conditionOutcomes = null; List<DiGraphEdge<Node, Branch>> branchEdges = getCfg().getOutEdges(source); List<FlowScope> result = new ArrayList<>(branchEdges.size()); for (DiGraphEdge<Node, Branch> branchEdge : branchEdges) { Branch branch = branchEdge.getValue(); FlowScope newScope = output; switch (branch) { case ON_TRUE: if (NodeUtil.isForIn(source)) { // item is assigned a property name, so its type should be string. Node item = source.getFirstChild(); Node obj = item.getNext(); FlowScope informed = traverse(obj, output.createChildFlowScope()); if (item.isVar()) { item = item.getFirstChild(); } if (item.isName()) { JSType iterKeyType = getNativeType(STRING_TYPE); ObjectType objType = getJSType(obj).dereference(); JSType objIndexType = objType == null ? null : objType.getTemplateTypeMap().getTemplateType(registry.getObjectIndexKey()); if (objIndexType != null && !objIndexType.isUnknownType()) { JSType narrowedKeyType = iterKeyType.getGreatestSubtype(objIndexType); if (!narrowedKeyType.isEmptyType()) { iterKeyType = narrowedKeyType; } } redeclareSimpleVar(informed, item, iterKeyType); } newScope = informed; break; } // FALL THROUGH case ON_FALSE: if (condition == null) { condition = NodeUtil.getConditionExpression(source); if (condition == null && source.isCase()) { condition = source; // conditionFlowScope is cached from previous iterations // of the loop. if (conditionFlowScope == null) { conditionFlowScope = traverse(condition.getFirstChild(), output.createChildFlowScope()); } } } if (condition != null) { if (condition.isAnd() || condition.isOr()) { // When handling the short-circuiting binary operators, // the outcome scope on true can be different than the outcome // scope on false. // // TODO(nicksantos): The "right" way to do this is to // carry the known outcome all the way through the // recursive traversal, so that we can construct a // different flow scope based on the outcome. However, // this would require a bunch of code and a bunch of // extra computation for an edge case. This seems to be // a "good enough" approximation. // conditionOutcomes is cached from previous iterations // of the loop. if (conditionOutcomes == null) { conditionOutcomes = condition.isAnd() ? traverseAnd(condition, output.createChildFlowScope()) : traverseOr(condition, output.createChildFlowScope()); } newScope = reverseInterpreter.getPreciserScopeKnowingConditionOutcome(condition, conditionOutcomes.getOutcomeFlowScope(condition.getType(), branch == Branch.ON_TRUE), branch == Branch.ON_TRUE); } else { // conditionFlowScope is cached from previous iterations // of the loop. if (conditionFlowScope == null) { conditionFlowScope = traverse(condition, output.createChildFlowScope()); } newScope = reverseInterpreter.getPreciserScopeKnowingConditionOutcome(condition, conditionFlowScope, branch == Branch.ON_TRUE); } } break; } result.add(newScope.optimize()); } return result; } private FlowScope traverse(Node n, FlowScope scope) { switch (n.getType()) { case Token.ASSIGN: scope = traverseAssign(n, scope); break; case Token.NAME: scope = traverseName(n, scope); break; case Token.GETPROP: scope = traverseGetProp(n, scope); break; case Token.AND: scope = traverseAnd(n, scope).getJoinedFlowScope().createChildFlowScope(); break; case Token.OR: scope = traverseOr(n, scope).getJoinedFlowScope().createChildFlowScope(); break; case Token.HOOK: scope = traverseHook(n, scope); break; case Token.OBJECTLIT: scope = traverseObjectLiteral(n, scope); break; case Token.CALL: scope = traverseCall(n, scope); break; case Token.NEW: scope = traverseNew(n, scope); break; case Token.ASSIGN_ADD: case Token.ADD: scope = traverseAdd(n, scope); break; case Token.POS: case Token.NEG: scope = traverse(n.getFirstChild(), scope); // Find types. n.setJSType(getNativeType(NUMBER_TYPE)); break; case Token.ARRAYLIT: scope = traverseArrayLiteral(n, scope); break; case Token.THIS: n.setJSType(scope.getTypeOfThis()); break; case Token.ASSIGN_LSH: case Token.ASSIGN_RSH: case Token.LSH: case Token.RSH: case Token.ASSIGN_URSH: case Token.URSH: case Token.ASSIGN_DIV: case Token.ASSIGN_MOD: case Token.ASSIGN_BITAND: case Token.ASSIGN_BITXOR: case Token.ASSIGN_BITOR: case Token.ASSIGN_MUL: case Token.ASSIGN_SUB: case Token.DIV: case Token.MOD: case Token.BITAND: case Token.BITXOR: case Token.BITOR: case Token.MUL: case Token.SUB: case Token.DEC: case Token.INC: case Token.BITNOT: scope = traverseChildren(n, scope); n.setJSType(getNativeType(NUMBER_TYPE)); break; case Token.PARAM_LIST: scope = traverse(n.getFirstChild(), scope); n.setJSType(getJSType(n.getFirstChild())); break; case Token.COMMA: scope = traverseChildren(n, scope); n.setJSType(getJSType(n.getLastChild())); break; case Token.TYPEOF: scope = traverseChildren(n, scope); n.setJSType(getNativeType(STRING_TYPE)); break; case Token.DELPROP: case Token.LT: case Token.LE: case Token.GT: case Token.GE: case Token.NOT: case Token.EQ: case Token.NE: case Token.SHEQ: case Token.SHNE: case Token.INSTANCEOF: case Token.IN: scope = traverseChildren(n, scope); n.setJSType(getNativeType(BOOLEAN_TYPE)); break; case Token.GETELEM: scope = traverseGetElem(n, scope); break; case Token.EXPR_RESULT: scope = traverseChildren(n, scope); if (n.getFirstChild().isGetProp()) { ensurePropertyDeclared(n.getFirstChild()); } break; case Token.SWITCH: scope = traverse(n.getFirstChild(), scope); break; case Token.RETURN: scope = traverseReturn(n, scope); break; case Token.VAR: case Token.THROW: scope = traverseChildren(n, scope); break; case Token.CATCH: scope = traverseCatch(n, scope); break; case Token.CAST: scope = traverseChildren(n, scope); JSDocInfo info = n.getJSDocInfo(); if (info != null && info.hasType()) { n.setJSType(info.getType().evaluate(syntacticScope, registry)); } break; } return scope; } /** * Traverse a return value. */ private FlowScope traverseReturn(Node n, FlowScope scope) { scope = traverseChildren(n, scope); Node retValue = n.getFirstChild(); if (retValue != null) { JSType type = functionScope.getRootNode().getJSType(); if (type != null) { FunctionType fnType = type.toMaybeFunctionType(); if (fnType != null) { inferPropertyTypesToMatchConstraint(retValue.getJSType(), fnType.getReturnType()); } } } return scope; } /** * Any value can be thrown, so it's really impossible to determine the type * of a CATCH param. Treat it as the UNKNOWN type. */ private FlowScope traverseCatch(Node catchNode, FlowScope scope) { Node name = catchNode.getFirstChild(); JSType type; // If the catch expression name was declared in the catch use that type, // otherwise use "unknown". JSDocInfo info = name.getJSDocInfo(); if (info != null && info.hasType()) { type = info.getType().evaluate(syntacticScope, registry); } else { type = getNativeType(JSTypeNative.UNKNOWN_TYPE); } redeclareSimpleVar(scope, name, type); name.setJSType(type); return scope; } private FlowScope traverseAssign(Node n, FlowScope scope) { Node left = n.getFirstChild(); Node right = n.getLastChild(); scope = traverseChildren(n, scope); JSType leftType = left.getJSType(); JSType rightType = getJSType(right); n.setJSType(rightType); updateScopeForTypeChange(scope, left, leftType, rightType); return scope; } /** * Updates the scope according to the result of a type change, like * an assignment or a type cast. */ private void updateScopeForTypeChange(FlowScope scope, Node left, JSType leftType, JSType resultType) { Preconditions.checkNotNull(resultType); switch (left.getType()) { case Token.NAME: String varName = left.getString(); TypedVar var = syntacticScope.getVar(varName); JSType varType = var == null ? null : var.getType(); boolean isVarDeclaration = left.hasChildren() && varType != null && !var.isTypeInferred(); boolean isTypelessConstDecl = isVarDeclaration && NodeUtil .isConstantDeclaration(compiler.getCodingConvention(), var.getJSDocInfo(), var.getNameNode()) && !(var.getJSDocInfo() != null && var.getJSDocInfo().hasType()); // When looking at VAR initializers for declared VARs, we tend // to use the declared type over the type it's being // initialized to in the global scope. // // For example, // /** @param {number} */ var f = goog.abstractMethod; // it's obvious that the programmer wants you to use // the declared function signature, not the inferred signature. // // Or, // /** @type {Object.<string>} */ var x = {}; // the one-time anonymous object on the right side // is as narrow as it can possibly be, but we need to make // sure we back-infer the <string> element constraint on // the left hand side, so we use the left hand side. boolean isVarTypeBetter = isVarDeclaration // Makes it easier to check for NPEs. && !resultType.isNullType() && !resultType.isVoidType() // Do not use the var type if the declaration looked like // /** @const */ var x = 3; // because this type was computed from the RHS && !isTypelessConstDecl; // TODO(nicksantos): This might be a better check once we have // back-inference of object/array constraints. It will probably // introduce more type warnings. It uses the result type iff it's // strictly narrower than the declared var type. // //boolean isVarTypeBetter = isVarDeclaration && // (varType.restrictByNotNullOrUndefined().isSubtype(resultType) // || !resultType.isSubtype(varType)); if (isVarTypeBetter) { redeclareSimpleVar(scope, left, varType); } else { redeclareSimpleVar(scope, left, resultType); } left.setJSType(resultType); if (var != null && var.isTypeInferred()) { JSType oldType = var.getType(); var.setType(oldType == null ? resultType : oldType.getLeastSupertype(resultType)); } else if (isTypelessConstDecl) { // /** @const */ var x = y; // should be redeclared, so that the type of y // gets propagated to inner scopes. var.setType(resultType); } break; case Token.GETPROP: String qualifiedName = left.getQualifiedName(); if (qualifiedName != null) { boolean declaredSlotType = false; JSType rawObjType = left.getFirstChild().getJSType(); if (rawObjType != null) { ObjectType objType = ObjectType.cast(rawObjType.restrictByNotNullOrUndefined()); if (objType != null) { String propName = left.getLastChild().getString(); declaredSlotType = objType.isPropertyTypeDeclared(propName); } } JSType safeLeftType = leftType == null ? unknownType : leftType; scope.inferQualifiedSlot(left, qualifiedName, safeLeftType, resultType, declaredSlotType); } left.setJSType(resultType); ensurePropertyDefined(left, resultType); break; } } /** * Defines a property if the property has not been defined yet. */ private void ensurePropertyDefined(Node getprop, JSType rightType) { String propName = getprop.getLastChild().getString(); Node obj = getprop.getFirstChild(); JSType nodeType = getJSType(obj); ObjectType objectType = ObjectType.cast(nodeType.restrictByNotNullOrUndefined()); boolean propCreationInConstructor = obj.isThis() && getJSType(syntacticScope.getRootNode()).isConstructor(); if (objectType == null) { registry.registerPropertyOnType(propName, nodeType); } else { if (nodeType.isStruct() && !objectType.hasProperty(propName)) { // In general, we don't want to define a property on a struct object, // b/c TypeCheck will later check for improper property creation on // structs. There are two exceptions. // 1) If it's a property created inside the constructor, on the newly // created instance, allow it. // 2) If it's a prototype property, allow it. For example: // Foo.prototype.bar = baz; // where Foo.prototype is a struct and the assignment happens at the // top level and the constructor Foo is defined in the same file. boolean staticPropCreation = false; Node maybeAssignStm = getprop.getParent().getParent(); if (syntacticScope.isGlobal() && NodeUtil.isPrototypePropertyDeclaration(maybeAssignStm)) { String propCreationFilename = maybeAssignStm.getSourceFileName(); Node ctor = objectType.getOwnerFunction().getSource(); if (ctor != null && ctor.getSourceFileName().equals(propCreationFilename)) { staticPropCreation = true; } } if (!propCreationInConstructor && !staticPropCreation) { return; // Early return to avoid creating the property below. } } if (ensurePropertyDeclaredHelper(getprop, objectType)) { return; } if (!objectType.isPropertyTypeDeclared(propName)) { // We do not want a "stray" assign to define an inferred property // for every object of this type in the program. So we use a heuristic // approach to determine whether to infer the property. // // 1) If the property is already defined, join it with the previously // inferred type. // 2) If this isn't an instance object, define it. // 3) If the property of an object is being assigned in the constructor, // define it. // 4) If this is a stub, define it. // 5) Otherwise, do not define the type, but declare it in the registry // so that we can use it for missing property checks. if (objectType.hasProperty(propName) || !objectType.isInstanceType()) { if ("prototype".equals(propName)) { objectType.defineDeclaredProperty(propName, rightType, getprop); } else { objectType.defineInferredProperty(propName, rightType, getprop); } } else if (propCreationInConstructor) { objectType.defineInferredProperty(propName, rightType, getprop); } else { registry.registerPropertyOnType(propName, objectType); } } } } /** * Defines a declared property if it has not been defined yet. * * This handles the case where a property is declared on an object where * the object type is inferred, and so the object type will not * be known in {@code TypedScopeCreator}. */ private void ensurePropertyDeclared(Node getprop) { ObjectType ownerType = ObjectType.cast(getJSType(getprop.getFirstChild()).restrictByNotNullOrUndefined()); if (ownerType != null) { ensurePropertyDeclaredHelper(getprop, ownerType); } } /** * Declares a property on its owner, if necessary. * @return True if a property was declared. */ private boolean ensurePropertyDeclaredHelper(Node getprop, ObjectType objectType) { String propName = getprop.getLastChild().getString(); String qName = getprop.getQualifiedName(); if (qName != null) { TypedVar var = syntacticScope.getVar(qName); if (var != null && !var.isTypeInferred()) { // Handle normal declarations that could not be addressed earlier. if (propName.equals("prototype") || // Handle prototype declarations that could not be addressed earlier. (!objectType.hasOwnProperty(propName) && (!objectType.isInstanceType() || (var.isExtern() && !objectType.isNativeObjectType())))) { return objectType.defineDeclaredProperty(propName, var.getType(), getprop); } } } return false; } private FlowScope traverseName(Node n, FlowScope scope) { String varName = n.getString(); Node value = n.getFirstChild(); JSType type = n.getJSType(); if (value != null) { scope = traverse(value, scope); updateScopeForTypeChange(scope, n, n.getJSType() /* could be null */, getJSType(value)); return scope; } else { StaticTypedSlot<JSType> var = scope.getSlot(varName); if (var != null) { // There are two situations where we don't want to use type information // from the scope, even if we have it. // 1) The var is escaped and assigned in an inner scope, e.g., // function f() { var x = 3; function g() { x = null } (x); } boolean isInferred = var.isTypeInferred(); boolean unflowable = isInferred && isUnflowable(syntacticScope.getVar(varName)); // 2) We're reading type information from another scope for an // inferred variable. That variable is assigned more than once, // and we can't know which type we're getting. // // var t = null; function f() { (t); } doStuff(); t = {}; // // Notice that this heuristic isn't perfect. For example, you might // have: // // function f() { (t); } f(); var t = 3; // // In this case, we would infer the first reference to t as // type {number}, even though it's undefined. boolean nonLocalInferredSlot = false; if (isInferred && syntacticScope.isLocal()) { TypedVar maybeOuterVar = syntacticScope.getParent().getVar(varName); if (var == maybeOuterVar && !maybeOuterVar.isMarkedAssignedExactlyOnce()) { nonLocalInferredSlot = true; } } if (!unflowable && !nonLocalInferredSlot) { type = var.getType(); if (type == null) { type = unknownType; } } } } n.setJSType(type); return scope; } /** Traverse each element of the array. */ private FlowScope traverseArrayLiteral(Node n, FlowScope scope) { scope = traverseChildren(n, scope); n.setJSType(getNativeType(ARRAY_TYPE)); return scope; } private FlowScope traverseObjectLiteral(Node n, FlowScope scope) { JSType type = n.getJSType(); Preconditions.checkNotNull(type); for (Node name = n.getFirstChild(); name != null; name = name.getNext()) { scope = traverse(name.getFirstChild(), scope); } // Object literals can be reflected on other types. // See CodingConvention#getObjectLiteralCast and goog.reflect.object // Ignore these types of literals. ObjectType objectType = ObjectType.cast(type); if (objectType == null || n.getBooleanProp(Node.REFLECTED_OBJECT) || objectType.isEnumType()) { return scope; } String qObjName = NodeUtil.getBestLValueName(NodeUtil.getBestLValue(n)); for (Node name = n.getFirstChild(); name != null; name = name.getNext()) { String memberName = NodeUtil.getObjectLitKeyName(name); if (memberName != null) { JSType rawValueType = name.getFirstChild().getJSType(); JSType valueType = TypeCheck.getObjectLitKeyTypeFromValueType(name, rawValueType); if (valueType == null) { valueType = unknownType; } objectType.defineInferredProperty(memberName, valueType, name); // Do normal flow inference if this is a direct property assignment. if (qObjName != null && name.isStringKey()) { String qKeyName = qObjName + "." + memberName; TypedVar var = syntacticScope.getVar(qKeyName); JSType oldType = var == null ? null : var.getType(); if (var != null && var.isTypeInferred()) { var.setType(oldType == null ? valueType : oldType.getLeastSupertype(oldType)); } scope.inferQualifiedSlot(name, qKeyName, oldType == null ? unknownType : oldType, valueType, false); } } else { n.setJSType(unknownType); } } return scope; } private FlowScope traverseAdd(Node n, FlowScope scope) { Node left = n.getFirstChild(); Node right = left.getNext(); scope = traverseChildren(n, scope); JSType leftType = left.getJSType(); JSType rightType = right.getJSType(); JSType type = unknownType; if (leftType != null && rightType != null) { boolean leftIsUnknown = leftType.isUnknownType(); boolean rightIsUnknown = rightType.isUnknownType(); if (leftIsUnknown && rightIsUnknown) { type = unknownType; } else if ((!leftIsUnknown && leftType.isString()) || (!rightIsUnknown && rightType.isString())) { type = getNativeType(STRING_TYPE); } else if (leftIsUnknown || rightIsUnknown) { type = unknownType; } else if (isAddedAsNumber(leftType) && isAddedAsNumber(rightType)) { type = getNativeType(NUMBER_TYPE); } else { type = registry.createUnionType(STRING_TYPE, NUMBER_TYPE); } } n.setJSType(type); if (n.isAssignAdd()) { updateScopeForTypeChange(scope, left, leftType, type); } return scope; } private boolean isAddedAsNumber(JSType type) { return type.isSubtype(registry.createUnionType(VOID_TYPE, NULL_TYPE, NUMBER_VALUE_OR_OBJECT_TYPE, BOOLEAN_TYPE, BOOLEAN_OBJECT_TYPE)); } private FlowScope traverseHook(Node n, FlowScope scope) { Node condition = n.getFirstChild(); Node trueNode = condition.getNext(); Node falseNode = n.getLastChild(); // verify the condition scope = traverse(condition, scope); // reverse abstract interpret the condition to produce two new scopes FlowScope trueScope = reverseInterpreter.getPreciserScopeKnowingConditionOutcome(condition, scope, true); FlowScope falseScope = reverseInterpreter.getPreciserScopeKnowingConditionOutcome(condition, scope, false); // traverse the true node with the trueScope traverse(trueNode, trueScope.createChildFlowScope()); // traverse the false node with the falseScope traverse(falseNode, falseScope.createChildFlowScope()); // meet true and false nodes' types and assign JSType trueType = trueNode.getJSType(); JSType falseType = falseNode.getJSType(); if (trueType != null && falseType != null) { n.setJSType(trueType.getLeastSupertype(falseType)); } else { n.setJSType(null); } return scope.createChildFlowScope(); } private FlowScope traverseCall(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()); backwardsInferenceFromCallSite(n, fnType); } else if (functionType.isEquivalentTo(getNativeType(CHECKED_UNKNOWN_TYPE))) { n.setJSType(getNativeType(CHECKED_UNKNOWN_TYPE)); } scope = tightenTypesAfterAssertions(scope, n); return scope; } private FlowScope tightenTypesAfterAssertions(FlowScope scope, Node callNode) { Node left = callNode.getFirstChild(); Node firstParam = left.getNext(); AssertionFunctionSpec assertionFunctionSpec = assertionFunctionsMap.get(left.getQualifiedName()); if (assertionFunctionSpec == null || firstParam == null) { return scope; } Node assertedNode = assertionFunctionSpec.getAssertedParam(firstParam); if (assertedNode == null) { return scope; } JSType assertedType = assertionFunctionSpec.getAssertedOldType(callNode, registry); String assertedNodeName = assertedNode.getQualifiedName(); JSType narrowed; // Handle assertions that enforce expressions evaluate to true. if (assertedType == null) { // Handle arbitrary expressions within the assert. scope = reverseInterpreter.getPreciserScopeKnowingConditionOutcome(assertedNode, scope, true); // Build the result of the assertExpression narrowed = getJSType(assertedNode).restrictByNotNullOrUndefined(); } else { // Handle assertions that enforce expressions are of a certain type. JSType type = getJSType(assertedNode); if (assertedType.isUnknownType() || type.isUnknownType()) { narrowed = assertedType; } else { narrowed = type.getGreatestSubtype(assertedType); } if (assertedNodeName != null && type.differsFrom(narrowed)) { scope = narrowScope(scope, assertedNode, narrowed); } } callNode.setJSType(narrowed); return scope; } private FlowScope narrowScope(FlowScope scope, Node node, JSType narrowed) { if (node.isThis()) { // "this" references don't need to be modeled in the control flow graph. return scope; } scope = scope.createChildFlowScope(); if (node.isGetProp()) { scope.inferQualifiedSlot(node, node.getQualifiedName(), getJSType(node), narrowed, false); } else { redeclareSimpleVar(scope, node, narrowed); } return scope; } /** * We only do forward type inference. We do not do full backwards * type inference. * * In other words, if we have, * <code> * var x = f(); * g(x); * </code> * a forward type-inference engine would try to figure out the type * of "x" from the return type of "f". A backwards type-inference engine * would try to figure out the type of "x" from the parameter type of "g". * * However, there are a few special syntactic forms where we do some * some half-assed backwards type-inference, because programmers * expect it in this day and age. To take an example from Java, * <code> * List<String> x = Lists.newArrayList(); * </code> * The Java compiler will be able to infer the generic type of the List * returned by newArrayList(). * * In much the same way, we do some special-case backwards inference for * JS. Those cases are enumerated here. */ private void backwardsInferenceFromCallSite(Node n, FunctionType fnType) { boolean updatedFnType = inferTemplatedTypesForCall(n, fnType); if (updatedFnType) { fnType = n.getFirstChild().getJSType().toMaybeFunctionType(); } updateTypeOfParameters(n, fnType); updateBind(n); } /** * When "bind" is called on a function, we infer the type of the returned * "bound" function by looking at the number of parameters in the call site. * We also infer the "this" type of the target, if it's a function expression. */ private void updateBind(Node n) { CodingConvention.Bind bind = compiler.getCodingConvention().describeFunctionBind(n, false, true); if (bind == null) { return; } Node target = bind.target; FunctionType callTargetFn = getJSType(target).restrictByNotNullOrUndefined().toMaybeFunctionType(); if (callTargetFn == null) { return; } if (bind.thisValue != null && target.isFunction()) { JSType thisType = getJSType(bind.thisValue); if (thisType.toObjectType() != null && !thisType.isUnknownType() && callTargetFn.getTypeOfThis().isUnknownType()) { callTargetFn = new FunctionBuilder(registry).copyFromOtherFunction(callTargetFn) .withTypeOfThis(thisType.toObjectType()).build(); target.setJSType(callTargetFn); } } n.setJSType(callTargetFn.getBindReturnType( // getBindReturnType expects the 'this' argument to be included. bind.getBoundParameterCount() + 1)); } /** * For functions with function parameters, type inference will set the type of * a function literal argument from the function parameter type. */ private void updateTypeOfParameters(Node n, FunctionType fnType) { int i = 0; int childCount = n.getChildCount(); for (Node iParameter : fnType.getParameters()) { if (i + 1 >= childCount) { // TypeCheck#visitParametersList will warn so we bail. return; } JSType iParameterType = getJSType(iParameter); Node iArgument = n.getChildAtIndex(i + 1); JSType iArgumentType = getJSType(iArgument); inferPropertyTypesToMatchConstraint(iArgumentType, iParameterType); // If the parameter to the call is a function expression, propagate the // function signature from the call site to the function node. // Filter out non-function types (such as null and undefined) as // we only care about FUNCTION subtypes here. FunctionType restrictedParameter = null; if (iParameterType.isUnionType()) { UnionType union = iParameterType.toMaybeUnionType(); for (JSType alternative : union.getAlternates()) { if (alternative.isFunctionType()) { // There is only one function type per union. restrictedParameter = alternative.toMaybeFunctionType(); break; } } } else { restrictedParameter = iParameterType.toMaybeFunctionType(); } if (restrictedParameter != null && iArgument.isFunction() && iArgumentType.isFunctionType()) { FunctionType argFnType = iArgumentType.toMaybeFunctionType(); boolean declared = iArgument.getJSDocInfo() != null; iArgument.setJSType(matchFunction(restrictedParameter, argFnType, declared)); } i++; } } /** * Take the current function type, and try to match the expected function * type. This is a form of backwards-inference, like record-type constraint * matching. */ private FunctionType matchFunction(FunctionType expectedType, FunctionType currentType, boolean declared) { if (declared) { // If the function was declared but it doesn't have a known "this" // but the expected type does, back fill it. if (currentType.getTypeOfThis().isUnknownType() && !expectedType.getTypeOfThis().isUnknownType()) { FunctionType replacement = new FunctionBuilder(registry).copyFromOtherFunction(currentType) .withTypeOfThis(expectedType.getTypeOfThis()).build(); return replacement; } } else { // For now, we just make sure the current type has enough // arguments to match the expected type, and return the // expected type if it does. if (currentType.getMaxArguments() <= expectedType.getMaxArguments()) { return expectedType; } } return currentType; } private Map<TemplateType, JSType> inferTemplateTypesFromParameters(FunctionType fnType, Node call) { if (fnType.getTemplateTypeMap().getTemplateKeys().isEmpty()) { return Collections.emptyMap(); } Map<TemplateType, JSType> resolvedTypes = Maps.newIdentityHashMap(); Set<JSType> seenTypes = Sets.newIdentityHashSet(); Node callTarget = call.getFirstChild(); if (NodeUtil.isGet(callTarget)) { Node obj = callTarget.getFirstChild(); maybeResolveTemplatedType(fnType.getTypeOfThis(), getJSType(obj), resolvedTypes, seenTypes); } if (call.hasMoreThanOneChild()) { maybeResolveTemplateTypeFromNodes(fnType.getParameters(), call.getChildAtIndex(1).siblings(), resolvedTypes, seenTypes); } return resolvedTypes; } private void maybeResolveTemplatedType(JSType paramType, JSType argType, Map<TemplateType, JSType> resolvedTypes, Set<JSType> seenTypes) { if (paramType.isTemplateType()) { // @param {T} resolvedTemplateType(resolvedTypes, paramType.toMaybeTemplateType(), argType); } else if (paramType.isUnionType()) { // @param {Array.<T>|NodeList|Arguments|{length:number}} UnionType unionType = paramType.toMaybeUnionType(); for (JSType alernative : unionType.getAlternates()) { maybeResolveTemplatedType(alernative, argType, resolvedTypes, seenTypes); } } else if (paramType.isFunctionType()) { FunctionType paramFunctionType = paramType.toMaybeFunctionType(); FunctionType argFunctionType = argType.restrictByNotNullOrUndefined().collapseUnion() .toMaybeFunctionType(); if (argFunctionType != null && argFunctionType.isSubtype(paramType)) { // infer from return type of the function type maybeResolveTemplatedType(paramFunctionType.getTypeOfThis(), argFunctionType.getTypeOfThis(), resolvedTypes, seenTypes); // infer from return type of the function type maybeResolveTemplatedType(paramFunctionType.getReturnType(), argFunctionType.getReturnType(), resolvedTypes, seenTypes); // infer from parameter types of the function type maybeResolveTemplateTypeFromNodes(paramFunctionType.getParameters(), argFunctionType.getParameters(), resolvedTypes, seenTypes); } } else if (paramType.isRecordType() && !paramType.isNominalType()) { // @param {{foo:T}} if (seenTypes.add(paramType)) { ObjectType paramRecordType = paramType.toObjectType(); ObjectType argObjectType = argType.restrictByNotNullOrUndefined().toObjectType(); if (argObjectType != null && !argObjectType.isUnknownType() && !argObjectType.isEmptyType()) { Set<String> names = paramRecordType.getPropertyNames(); for (String name : names) { if (paramRecordType.hasOwnProperty(name) && argObjectType.hasProperty(name)) { maybeResolveTemplatedType(paramRecordType.getPropertyType(name), argObjectType.getPropertyType(name), resolvedTypes, seenTypes); } } } seenTypes.remove(paramType); } } else if (paramType.isTemplatizedType()) { // @param {Array.<T>} ObjectType referencedParamType = paramType.toMaybeTemplatizedType().getReferencedType(); JSType argObjectType = argType.restrictByNotNullOrUndefined().collapseUnion(); if (argObjectType.isSubtype(referencedParamType)) { // If the argument type is a subtype of the parameter type, resolve any // template types amongst their templatized types. TemplateTypeMap paramTypeMap = paramType.getTemplateTypeMap(); TemplateTypeMap argTypeMap = argObjectType.getTemplateTypeMap(); for (TemplateType key : paramTypeMap.getTemplateKeys()) { maybeResolveTemplatedType(paramTypeMap.getTemplateType(key), argTypeMap.getTemplateType(key), resolvedTypes, seenTypes); } } } } private void maybeResolveTemplateTypeFromNodes(Iterable<Node> declParams, Iterable<Node> callParams, Map<TemplateType, JSType> resolvedTypes, Set<JSType> seenTypes) { maybeResolveTemplateTypeFromNodes(declParams.iterator(), callParams.iterator(), resolvedTypes, seenTypes); } private void maybeResolveTemplateTypeFromNodes(Iterator<Node> declParams, Iterator<Node> callParams, Map<TemplateType, JSType> resolvedTypes, Set<JSType> seenTypes) { while (declParams.hasNext() && callParams.hasNext()) { Node declParam = declParams.next(); maybeResolveTemplatedType(getJSType(declParam), getJSType(callParams.next()), resolvedTypes, seenTypes); if (declParam.isVarArgs()) { while (callParams.hasNext()) { maybeResolveTemplatedType(getJSType(declParam), getJSType(callParams.next()), resolvedTypes, seenTypes); } } } } private static void resolvedTemplateType(Map<TemplateType, JSType> map, TemplateType template, JSType resolved) { JSType previous = map.get(template); if (!resolved.isUnknownType()) { if (previous == null) { map.put(template, resolved); } else { JSType join = previous.getLeastSupertype(resolved); map.put(template, join); } } } private static class TemplateTypeReplacer extends ModificationVisitor { private final Map<TemplateType, JSType> replacements; private final JSTypeRegistry registry; boolean madeChanges = false; TemplateTypeReplacer(JSTypeRegistry registry, Map<TemplateType, JSType> replacements) { super(registry, true); this.registry = registry; this.replacements = replacements; } @Override public JSType caseTemplateType(TemplateType type) { madeChanges = true; JSType replacement = replacements.get(type); return replacement != null ? replacement : registry.getNativeType(UNKNOWN_TYPE); } } /** * Build the type environment where type transformations will be evaluated. * It only considers the template type variables that do not have a type * transformation. */ private Map<String, JSType> buildTypeVariables(Map<TemplateType, JSType> inferredTypes) { Map<String, JSType> typeVars = new HashMap<>(); for (Entry<TemplateType, JSType> e : inferredTypes.entrySet()) { // Only add the template type that do not have a type transformation if (!e.getKey().isTypeTransformation()) { typeVars.put(e.getKey().getReferenceName(), e.getValue()); } } return typeVars; } /** * This function will evaluate the type transformations associated to the * template types */ private Map<TemplateType, JSType> evaluateTypeTransformations(ImmutableList<TemplateType> templateTypes, Map<TemplateType, JSType> inferredTypes) { Map<String, JSType> typeVars = null; Map<TemplateType, JSType> result = null; TypeTransformation ttlObj = null; for (TemplateType type : templateTypes) { if (type.isTypeTransformation()) { // Lazy initialization when the first type transformation is found if (ttlObj == null) { ttlObj = new TypeTransformation(compiler, syntacticScope); typeVars = buildTypeVariables(inferredTypes); result = new HashMap<>(); } // Evaluate the type transformation expression using the current // known types for the template type variables JSType transformedType = ttlObj.eval(type.getTypeTransformation(), ImmutableMap.<String, JSType>copyOf(typeVars)); result.put(type, transformedType); // Add the transformed type to the type variables typeVars.put(type.getReferenceName(), transformedType); } } return result; } /** * For functions with function(this: T, ...) and T as parameters, type * inference will set the type of this on a function literal argument to the * the actual type of T. */ private boolean inferTemplatedTypesForCall(Node n, FunctionType fnType) { final ImmutableList<TemplateType> keys = fnType.getTemplateTypeMap().getTemplateKeys(); if (keys.isEmpty()) { return false; } // Try to infer the template types Map<TemplateType, JSType> rawInferrence = inferTemplateTypesFromParameters(fnType, n); Map<TemplateType, JSType> inferred = Maps.newIdentityHashMap(); for (TemplateType key : keys) { JSType type = rawInferrence.get(key); if (type == null) { type = unknownType; } inferred.put(key, type); } // Try to infer the template types using the type transformations Map<TemplateType, JSType> typeTransformations = evaluateTypeTransformations(keys, inferred); if (typeTransformations != null) { inferred.putAll(typeTransformations); } // Replace all template types. If we couldn't find a replacement, we // replace it with UNKNOWN. TemplateTypeReplacer replacer = new TemplateTypeReplacer(registry, inferred); Node callTarget = n.getFirstChild(); FunctionType replacementFnType = fnType.visit(replacer).toMaybeFunctionType(); Preconditions.checkNotNull(replacementFnType); callTarget.setJSType(replacementFnType); n.setJSType(replacementFnType.getReturnType()); return replacer.madeChanges; } private FlowScope traverseNew(Node n, FlowScope scope) { scope = traverseChildren(n, scope); Node constructor = n.getFirstChild(); JSType constructorType = constructor.getJSType(); JSType type = null; if (constructorType != null) { constructorType = constructorType.restrictByNotNullOrUndefined(); if (constructorType.isUnknownType()) { type = unknownType; } else { FunctionType ct = constructorType.toMaybeFunctionType(); if (ct == null && constructorType instanceof FunctionType) { // If constructorType is a NoObjectType, then toMaybeFunctionType will // return null. But NoObjectType implements the FunctionType // interface, precisely because it can validly construct objects. ct = (FunctionType) constructorType; } if (ct != null && ct.isConstructor()) { backwardsInferenceFromCallSite(n, ct); // If necessary, create a TemplatizedType wrapper around the instance // type, based on the types of the constructor parameters. ObjectType instanceType = ct.getInstanceType(); Map<TemplateType, JSType> inferredTypes = inferTemplateTypesFromParameters(ct, n); if (inferredTypes.isEmpty()) { type = instanceType; } else { type = registry.createTemplatizedType(instanceType, inferredTypes); } } } } n.setJSType(type); return scope; } private BooleanOutcomePair traverseAnd(Node n, FlowScope scope) { return traverseShortCircuitingBinOp(n, scope); } private FlowScope traverseChildren(Node n, FlowScope scope) { for (Node el = n.getFirstChild(); el != null; el = el.getNext()) { scope = traverse(el, scope); } return scope; } private FlowScope traverseGetElem(Node n, FlowScope scope) { scope = traverseChildren(n, scope); JSType type = getJSType(n.getFirstChild()).restrictByNotNullOrUndefined(); TemplateTypeMap typeMap = type.getTemplateTypeMap(); // check the template value for IObject if (typeMap.hasTemplateType(registry.getIObjectValueKey())) { n.setJSType(typeMap.getConcreteTypeOfTemplateType(registry.getIObjectValueKey())); } else if (typeMap.hasTemplateType(registry.getObjectElementKey())) { n.setJSType(typeMap.getTemplateType(registry.getObjectElementKey())); } return dereferencePointer(n.getFirstChild(), scope); } private FlowScope traverseGetProp(Node n, FlowScope scope) { Node objNode = n.getFirstChild(); Node property = n.getLastChild(); scope = traverseChildren(n, scope); n.setJSType(getPropertyType(objNode.getJSType(), property.getString(), n, scope)); return dereferencePointer(n.getFirstChild(), scope); } /** * Suppose X is an object with inferred properties. * Suppose also that X is used in a way where it would only type-check * correctly if some of those properties are widened. * Then we should be polite and automatically widen X's properties for him. * * For a concrete example, consider: * param x {{prop: (number|undefined)}} * function f(x) {} * f({}); * * If we give the anonymous object an inferred property of (number|undefined), * then this code will type-check appropriately. */ private static void inferPropertyTypesToMatchConstraint(JSType type, JSType constraint) { if (type == null || constraint == null) { return; } type.matchConstraint(constraint); } /** * If we access a property of a symbol, then that symbol is not * null or undefined. */ private FlowScope dereferencePointer(Node n, FlowScope scope) { if (n.isQualifiedName()) { JSType type = getJSType(n); JSType narrowed = type.restrictByNotNullOrUndefined(); if (type != narrowed) { scope = narrowScope(scope, n, narrowed); } } return scope; } private JSType getPropertyType(JSType objType, String propName, Node n, FlowScope scope) { // We often have a couple of different types to choose from for the // property. Ordered by accuracy, we have // 1) A locally inferred qualified name (which is in the FlowScope) // 2) A globally declared qualified name (which is in the FlowScope) // 3) A property on the owner type (which is on objType) // 4) A name in the type registry (as a last resort) JSType propertyType = null; boolean isLocallyInferred = false; // Scopes sometimes contain inferred type info about qualified names. String qualifiedName = n.getQualifiedName(); StaticTypedSlot<JSType> var = scope.getSlot(qualifiedName); if (var != null) { JSType varType = var.getType(); if (varType != null) { boolean isDeclared = !var.isTypeInferred(); isLocallyInferred = (var != syntacticScope.getSlot(qualifiedName)); if (isDeclared || isLocallyInferred) { propertyType = varType; } } } if (propertyType == null && objType != null) { JSType foundType = objType.findPropertyType(propName); if (foundType != null) { propertyType = foundType; } } if (propertyType != null && objType != null) { JSType restrictedObjType = objType.restrictByNotNullOrUndefined(); if (!restrictedObjType.getTemplateTypeMap().isEmpty() && propertyType.hasAnyTemplateTypes()) { TemplateTypeMap typeMap = restrictedObjType.getTemplateTypeMap(); TemplateTypeMapReplacer replacer = new TemplateTypeMapReplacer(registry, typeMap); propertyType = propertyType.visit(replacer); } } if ((propertyType == null || propertyType.isUnknownType()) && qualifiedName != null) { // If we find this node in the registry, then we can infer its type. ObjectType regType = ObjectType.cast(registry.getType(qualifiedName)); if (regType != null) { propertyType = regType.getConstructor(); } } if (propertyType == null) { return unknownType; } else if (propertyType.isEquivalentTo(unknownType) && isLocallyInferred) { // If the type has been checked in this scope, // then use CHECKED_UNKNOWN_TYPE instead to indicate that. return getNativeType(CHECKED_UNKNOWN_TYPE); } else { return propertyType; } } private BooleanOutcomePair traverseOr(Node n, FlowScope scope) { return traverseShortCircuitingBinOp(n, scope); } private BooleanOutcomePair traverseShortCircuitingBinOp(Node n, FlowScope scope) { Preconditions.checkArgument(n.isAnd() || n.isOr()); boolean nIsAnd = n.isAnd(); Node left = n.getFirstChild(); Node right = n.getLastChild(); // type the left node BooleanOutcomePair leftOutcome = traverseWithinShortCircuitingBinOp(left, scope.createChildFlowScope()); JSType leftType = left.getJSType(); // reverse abstract interpret the left node to produce the correct // scope in which to verify the right node FlowScope rightScope = reverseInterpreter.getPreciserScopeKnowingConditionOutcome(left, leftOutcome.getOutcomeFlowScope(left.getType(), nIsAnd), nIsAnd); // type the right node BooleanOutcomePair rightOutcome = traverseWithinShortCircuitingBinOp(right, rightScope.createChildFlowScope()); JSType rightType = right.getJSType(); JSType type; BooleanOutcomePair outcome; if (leftType != null && rightType != null) { leftType = leftType.getRestrictedTypeGivenToBooleanOutcome(!nIsAnd); if (leftOutcome.toBooleanOutcomes == BooleanLiteralSet.get(!nIsAnd)) { // Either n is && and lhs is false, or n is || and lhs is true. // Use the restricted left type; the right side never gets evaluated. type = leftType; outcome = leftOutcome; } else { // Use the join of the restricted left type knowing the outcome of the // ToBoolean predicate and of the right type. type = leftType.getLeastSupertype(rightType); outcome = new BooleanOutcomePair( joinBooleanOutcomes(nIsAnd, leftOutcome.toBooleanOutcomes, rightOutcome.toBooleanOutcomes), joinBooleanOutcomes(nIsAnd, leftOutcome.booleanValues, rightOutcome.booleanValues), leftOutcome.getJoinedFlowScope(), rightOutcome.getJoinedFlowScope()); } // Exclude the boolean type if the literal set is empty because a boolean // can never actually be returned. if (outcome.booleanValues == BooleanLiteralSet.EMPTY && getNativeType(BOOLEAN_TYPE).isSubtype(type)) { // Exclusion only makes sense for a union type. if (type.isUnionType()) { type = type.toMaybeUnionType().getRestrictedUnion(getNativeType(BOOLEAN_TYPE)); } } } else { type = null; outcome = new BooleanOutcomePair(BooleanLiteralSet.BOTH, BooleanLiteralSet.BOTH, leftOutcome.getJoinedFlowScope(), rightOutcome.getJoinedFlowScope()); } n.setJSType(type); return outcome; } private BooleanOutcomePair traverseWithinShortCircuitingBinOp(Node n, FlowScope scope) { switch (n.getType()) { case Token.AND: return traverseAnd(n, scope); case Token.OR: return traverseOr(n, scope); default: scope = traverse(n, scope); return newBooleanOutcomePair(n.getJSType(), scope); } } private static BooleanLiteralSet joinBooleanOutcomes(boolean isAnd, BooleanLiteralSet left, BooleanLiteralSet right) { // A truthy value on the lhs of an {@code &&} can never make it to the // result. Same for a falsy value on the lhs of an {@code ||}. // Hence the intersection. return right.union(left.intersection(BooleanLiteralSet.get(!isAnd))); } /** * When traversing short-circuiting binary operations, we need to keep track * of two sets of boolean literals: * 1. {@code toBooleanOutcomes}: boolean literals as converted from any types, * 2. {@code booleanValues}: boolean literals from just boolean types. */ private final class BooleanOutcomePair { final BooleanLiteralSet toBooleanOutcomes; final BooleanLiteralSet booleanValues; // The scope if only half of the expression executed, when applicable. final FlowScope leftScope; // The scope when the whole expression executed. final FlowScope rightScope; // The scope when we don't know how much of the expression is executed. FlowScope joinedScope = null; BooleanOutcomePair(BooleanLiteralSet toBooleanOutcomes, BooleanLiteralSet booleanValues, FlowScope leftScope, FlowScope rightScope) { this.toBooleanOutcomes = toBooleanOutcomes; this.booleanValues = booleanValues; this.leftScope = leftScope; this.rightScope = rightScope; } /** * Gets the safe estimated scope without knowing if all of the * subexpressions will be evaluated. */ FlowScope getJoinedFlowScope() { if (joinedScope == null) { if (leftScope == rightScope) { joinedScope = rightScope; } else { joinedScope = join(leftScope, rightScope); } } return joinedScope; } /** * Gets the outcome scope if we do know the outcome of the entire * expression. */ FlowScope getOutcomeFlowScope(int nodeType, boolean outcome) { if (nodeType == Token.AND && outcome || nodeType == Token.OR && !outcome) { // We know that the whole expression must have executed. return rightScope; } else { return getJoinedFlowScope(); } } } private BooleanOutcomePair newBooleanOutcomePair(JSType jsType, FlowScope flowScope) { if (jsType == null) { return new BooleanOutcomePair(BooleanLiteralSet.BOTH, BooleanLiteralSet.BOTH, flowScope, flowScope); } return new BooleanOutcomePair(jsType.getPossibleToBooleanOutcomes(), registry.getNativeType(BOOLEAN_TYPE).isSubtype(jsType) ? BooleanLiteralSet.BOTH : BooleanLiteralSet.EMPTY, flowScope, flowScope); } private void redeclareSimpleVar(FlowScope scope, Node nameNode, JSType varType) { Preconditions.checkState(nameNode.isName()); String varName = nameNode.getString(); if (varType == null) { varType = getNativeType(JSTypeNative.UNKNOWN_TYPE); } if (isUnflowable(syntacticScope.getVar(varName))) { return; } scope.inferSlotType(varName, varType); } private boolean isUnflowable(TypedVar v) { return v != null && v.isLocal() && v.isMarkedEscaped() && // It's OK to flow a variable in the scope where it's escaped. v.getScope() == syntacticScope; } /** * This method gets the JSType from the Node argument and verifies that it is * present. */ private JSType getJSType(Node n) { JSType jsType = n.getJSType(); if (jsType == null) { // TODO(nicksantos): This branch indicates a compiler bug, not worthy of // halting the compilation but we should log this and analyze to track // down why it happens. This is not critical and will be resolved over // time as the type checker is extended. return unknownType; } else { return jsType; } } private JSType getNativeType(JSTypeNative typeId) { return registry.getNativeType(typeId); } }