/*
* 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.Lists;
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.Scope.Var;
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.StaticSlot;
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.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 Scope 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,
Scope 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<Var> varIt =
functionScope.getDeclarativelyUnboundVarsWithoutTypes();
while (varIt.hasNext()) {
Var var = varIt.next();
if (isUnflowable(var)) {
continue;
}
this.functionScope.inferSlotType(
var.getName(), getNativeType(VOID_TYPE));
}
this.bottomScope = LinkedFlowScope.createEntryLattice(
Scope.createLatticeBottom(functionScope.getRootNode()));
}
/**
* Infers all of a function's arguments if their types aren't declared.
*/
private void inferArguments(Scope 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()) {
Var 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 = Lists.newArrayListWithCapacity(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();
Var 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) {
scope.inferQualifiedSlot(left, qualifiedName,
leftType == null ? unknownType : leftType,
resultType);
}
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) {
Var 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 {
StaticSlot<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()) {
Var 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 = NodeUtil.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;
Var 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);
}
} 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.getAssertedType(
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);
} 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.
*/
private void updateBind(Node n) {
CodingConvention.Bind bind =
compiler.getCodingConvention().describeFunctionBind(n, true);
if (bind == null) {
return;
}
FunctionType callTargetFn = getJSType(bind.target)
.restrictByNotNullOrUndefined().toMaybeFunctionType();
if (callTargetFn == null) {
return;
}
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.contains(paramType)) {
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<String, JSType>();
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<TemplateType, JSType>();
}
// 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();
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();
StaticSlot<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(Var 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);
}
}