/*
* Copyright 2013 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 com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.Multimap;
import com.google.javascript.jscomp.CodingConvention.AssertionFunctionSpec;
import com.google.javascript.jscomp.CodingConvention.Bind;
import com.google.javascript.jscomp.CodingConvention.ObjectLiteralCast;
import com.google.javascript.jscomp.graph.DiGraph.DiGraphEdge;
import com.google.javascript.jscomp.graph.DiGraph.DiGraphNode;
import com.google.javascript.jscomp.newtypes.DeclaredFunctionType;
import com.google.javascript.jscomp.newtypes.FunctionType;
import com.google.javascript.jscomp.newtypes.FunctionTypeBuilder;
import com.google.javascript.jscomp.newtypes.JSType;
import com.google.javascript.jscomp.newtypes.JSTypes;
import com.google.javascript.jscomp.newtypes.MismatchInfo;
import com.google.javascript.jscomp.newtypes.NominalType;
import com.google.javascript.jscomp.newtypes.QualifiedName;
import com.google.javascript.jscomp.newtypes.TypeEnv;
import com.google.javascript.jscomp.newtypes.UniqueNameGenerator;
import com.google.javascript.rhino.JSDocInfo;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.Token;
import com.google.javascript.rhino.TypeI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
/**
* New type inference algorithm.
*
* Main differences from the old type checker:
* - Infers types for unannotated functions
* - Stricter with missing-property warnings
* - Stricter when checking the operands of primitive operators
* - Requires constants to be typed
* - Tries to warn about misplaced annotations, rather than silently ignore them
*
* @author blickly@google.com (Ben Lickly)
* @author dimvar@google.com (Dimitris Vardoulakis)
*
*/
final class NewTypeInference implements CompilerPass {
static final DiagnosticType MISTYPED_ASSIGN_RHS = DiagnosticType.warning(
"JSC_NTI_MISTYPED_ASSIGN_RHS",
"The right side in the assignment is not a subtype of the left side.\n"
+ "{0}");
static final DiagnosticType INVALID_OPERAND_TYPE = DiagnosticType.warning(
"JSC_NTI_INVALID_OPERAND_TYPE",
"Invalid type(s) for operator {0}.\n"
+ "{1}");
static final DiagnosticType RETURN_NONDECLARED_TYPE = DiagnosticType.warning(
"JSC_NTI_RETURN_NONDECLARED_TYPE",
"Returned type does not match declared return type.\n"
+ "{0}");
static final DiagnosticType INVALID_INFERRED_RETURN_TYPE =
DiagnosticType.warning(
"JSC_NTI_INVALID_INFERRED_RETURN_TYPE",
"Function called in context that expects incompatible type.\n"
+ "{0}");
static final DiagnosticType INVALID_ARGUMENT_TYPE = DiagnosticType.warning(
"JSC_NTI_INVALID_ARGUMENT_TYPE",
"Invalid type for parameter {0} of function {1}.\n"
+ "{2}");
static final DiagnosticType CROSS_SCOPE_GOTCHA = DiagnosticType.warning(
"JSC_NTI_CROSS_SCOPE_GOTCHA",
"Variable {0} typed inconsistently across scopes.\n" +
"In outer scope : {1}\n" +
"In inner scope : {2}\n");
static final DiagnosticType POSSIBLY_INEXISTENT_PROPERTY =
DiagnosticType.warning(
"JSC_NTI_POSSIBLY_INEXISTENT_PROPERTY",
"Property {0} may not be present on {1}.");
static final DiagnosticType PROPERTY_ACCESS_ON_NONOBJECT =
DiagnosticType.warning(
"JSC_NTI_PROPERTY_ACCESS_ON_NONOBJECT",
"Cannot access property {0} of non-object type {1}.");
static final DiagnosticType NOT_UNIQUE_INSTANTIATION =
DiagnosticType.warning(
"JSC_NTI_NOT_UNIQUE_INSTANTIATION",
"When instantiating a polymorphic function,"
+ " you can only specify one type for each type variable.\n"
+ " Found {0} types for type variable {1}: {2},\n"
+ " when instantiating type: {3}");
static final DiagnosticType FAILED_TO_UNIFY =
DiagnosticType.warning(
"JSC_NTI_FAILED_TO_UNIFY",
"Could not instantiate type {0} with {1}.");
static final DiagnosticType INVALID_INDEX_TYPE =
DiagnosticType.warning(
"JSC_NTI_INVALID_INDEX_TYPE",
"Invalid type for index.\n"
+ "{0}");
static final DiagnosticType BOTTOM_INDEX_TYPE =
DiagnosticType.warning(
"JSC_NTI_BOTTOM_INDEX_TYPE",
"This IObject {0} cannot be accessed with a valid type.\n"
+ " Usually the result of a bad union type.\n");
static final DiagnosticType INVALID_OBJLIT_PROPERTY_TYPE =
DiagnosticType.warning(
"JSC_NTI_INVALID_OBJLIT_PROPERTY_TYPE",
"Invalid type for object-literal property.\n"
+ "{0}");
static final DiagnosticType FORIN_EXPECTS_OBJECT =
DiagnosticType.warning(
"JSC_NTI_FORIN_EXPECTS_OBJECT",
"For/in expects an object, found type {0}.");
static final DiagnosticType FORIN_EXPECTS_STRING_KEY =
DiagnosticType.warning(
"JSC_NTI_FORIN_EXPECTS_STRING_KEY",
"For/in creates string keys, but variable has declared type {1}.");
static final DiagnosticType CONST_REASSIGNED =
DiagnosticType.warning(
"JSC_NTI_CONST_REASSIGNED",
"Cannot change the value of a constant.");
static final DiagnosticType CONST_PROPERTY_REASSIGNED =
DiagnosticType.warning(
"JSC_NTI_CONST_PROPERTY_REASSIGNED",
"Cannot change the value of a constant property.");
static final DiagnosticType CONST_PROPERTY_DELETED =
DiagnosticType.warning(
"JSC_NTI_CONSTANT_PROPERTY_DELETED",
"Constant property {0} cannot be deleted");
static final DiagnosticType NOT_A_CONSTRUCTOR =
DiagnosticType.warning(
"JSC_NTI_NOT_A_CONSTRUCTOR",
"Expected a constructor but found type {0}.");
static final DiagnosticType CANNOT_INSTANTIATE_ABSTRACT_CLASS =
DiagnosticType.warning(
"JSC_NTI_CANNOT_INSTANTIATE_ABSTRACT_CLASS",
"Cannot instantiate abstract class {0}.");
static final DiagnosticType UNDEFINED_SUPER_CLASS =
DiagnosticType.warning(
"JSC_UNDEFINED_SUPER_CLASS",
"Undefined super class for {0}.");
static final DiagnosticType ASSERT_FALSE =
DiagnosticType.warning(
"JSC_NTI_ASSERT_FALSE",
"Assertion is always false. Please use a throw or fail() instead.");
static final DiagnosticType UNKNOWN_ASSERTION_TYPE =
DiagnosticType.warning(
"JSC_NTI_UNKNOWN_ASSERTION_TYPE",
"Assert with unknown asserted type.");
static final DiagnosticType INVALID_THIS_TYPE_IN_BIND =
DiagnosticType.warning(
"JSC_NTI_INVALID_THIS_TYPE_IN_BIND",
"Invalid type for the first argument to bind.\n"
+ "{0}");
static final DiagnosticType CANNOT_BIND_CTOR =
DiagnosticType.warning(
"JSC_NTI_CANNOT_BIND_CTOR",
"We do not support using .bind on constructor functions.");
static final DiagnosticType GOOG_BIND_EXPECTS_FUNCTION =
DiagnosticType.warning(
"JSC_NTI_GOOG_BIND_EXPECTS_FUNCTION",
"The first argument to goog.bind/goog.partial must be a function,"
+ " found: {0}");
static final DiagnosticType BOTTOM_PROP =
DiagnosticType.warning(
"JSC_NTI_BOTTOM_PROP",
"Property {0} of {1} cannot have a valid type."
+ "Maybe the result of a union of incompatible types?");
static final DiagnosticType INVALID_CAST =
DiagnosticType.warning("JSC_NTI_INVALID_CAST",
"invalid cast - the types do not have a common subtype\n" +
"from: {0}\n" +
"to : {1}");
static final DiagnosticType GLOBAL_THIS = DiagnosticType.warning(
"JSC_NTI_USED_GLOBAL_THIS",
"Dangerous use of the global THIS object");
static final DiagnosticType MISSING_RETURN_STATEMENT =
DiagnosticType.warning(
"JSC_NTI_MISSING_RETURN_STATEMENT",
"Missing return statement. Function expected to return {0}.");
static final DiagnosticType CONSTRUCTOR_NOT_CALLABLE =
DiagnosticType.warning(
"JSC_NTI_CONSTRUCTOR_NOT_CALLABLE",
"Constructor {0} should be called with the \"new\" keyword");
static final DiagnosticType ILLEGAL_OBJLIT_KEY =
DiagnosticType.warning(
"JSC_NTI_ILLEGAL_OBJLIT_KEY",
"Illegal key, the object literal is a {0}");
static final DiagnosticType ILLEGAL_PROPERTY_CREATION =
DiagnosticType.warning(
"JSC_NTI_ILLEGAL_PROPERTY_CREATION",
"Cannot add property {0} to a struct instance after it is constructed.");
static final DiagnosticType IN_USED_WITH_STRUCT =
DiagnosticType.warning(
"JSC_NTI_IN_USED_WITH_STRUCT",
"Cannot use the IN operator with structs");
static final DiagnosticType ADDING_PROPERTY_TO_NON_OBJECT =
DiagnosticType.warning(
"JSC_NTI_ADDING_PROPERTY_TO_NON_OBJECT",
"Cannot create property {0} on non-object type {1}.");
public static final DiagnosticType INEXISTENT_PROPERTY =
DiagnosticType.warning(
"JSC_NTI_INEXISTENT_PROPERTY",
"Property {0} never defined on {1}");
static final DiagnosticType NOT_CALLABLE =
DiagnosticType.warning(
"JSC_NTI_NOT_FUNCTION_TYPE",
"Cannot call non-function type {0}");
static final DiagnosticType WRONG_ARGUMENT_COUNT =
DiagnosticType.warning(
"JSC_NTI_WRONG_ARGUMENT_COUNT",
"Function {0}: called with {1} argument(s). " +
"Function requires at least {2} argument(s){3}.");
static final DiagnosticType ILLEGAL_PROPERTY_ACCESS =
DiagnosticType.warning(
"JSC_NTI_ILLEGAL_PROPERTY_ACCESS",
"Cannot do {0} access on a {1}");
static final DiagnosticType UNKNOWN_TYPEOF_VALUE =
DiagnosticType.warning(
"JSC_NTI_UNKNOWN_TYPEOF_VALUE",
"unknown type: {0}");
static final DiagnosticType UNKNOWN_NAMESPACE_PROPERTY =
DiagnosticType.warning(
"JSC_NTI_UNKNOWN_NAMESPACE_PROPERTY",
"Cannot determine the type of namespace property {0}. "
+ "Maybe a prefix of the property name has been redefined?");
static final DiagnosticType INCOMPATIBLE_STRICT_COMPARISON =
DiagnosticType.warning(
"JSC_INCOMPATIBLE_STRICT_COMPARISON",
"Cannot perform strict equality / inequality comparisons on incompatible types:\n"
+ "left : {0}\n"
+ "right: {1}");
static final DiagnosticType ABSTRACT_SUPER_METHOD_NOT_CALLABLE =
DiagnosticType.warning(
"JSC_NTI_ABSTRACT_SUPER_METHOD_NOT_CALLABLE",
"Abstract super method {0} cannot be called");
static final DiagnosticType REFLECT_CONSTRUCTOR_EXPECTED =
DiagnosticType.warning(
"JSC_NTI_REFLECT_CONSTRUCTOR_EXPECTED",
"Constructor expected as first argument");
// Not part of ALL_DIAGNOSTICS because it should not be enabled with
// --jscomp_error=newCheckTypes. It should only be enabled explicitly.
static final DiagnosticType NULLABLE_DEREFERENCE =
DiagnosticType.disabled(
"JSC_NTI_NULLABLE_DEREFERENCE",
"Attempt to use nullable type {0}.");
static final DiagnosticType UNKNOWN_EXPR_TYPE =
DiagnosticType.disabled(
"JSC_NTI_UNKNOWN_EXPR_TYPE",
"This {0} expression has the unknown type.");
static final DiagnosticGroup COMPATIBLE_DIAGNOSTICS = new DiagnosticGroup(
ABSTRACT_SUPER_METHOD_NOT_CALLABLE,
CANNOT_BIND_CTOR,
CANNOT_INSTANTIATE_ABSTRACT_CLASS,
CONST_PROPERTY_DELETED,
CONST_PROPERTY_REASSIGNED,
CONST_REASSIGNED,
REFLECT_CONSTRUCTOR_EXPECTED,
CONSTRUCTOR_NOT_CALLABLE,
FAILED_TO_UNIFY,
FORIN_EXPECTS_STRING_KEY,
GLOBAL_THIS,
GOOG_BIND_EXPECTS_FUNCTION,
ILLEGAL_OBJLIT_KEY,
ILLEGAL_PROPERTY_ACCESS,
ILLEGAL_PROPERTY_CREATION,
IN_USED_WITH_STRUCT,
INEXISTENT_PROPERTY,
INVALID_ARGUMENT_TYPE,
INVALID_CAST,
INVALID_INDEX_TYPE,
INVALID_OBJLIT_PROPERTY_TYPE,
MISSING_RETURN_STATEMENT,
MISTYPED_ASSIGN_RHS,
NOT_A_CONSTRUCTOR,
NOT_CALLABLE,
POSSIBLY_INEXISTENT_PROPERTY,
RETURN_NONDECLARED_TYPE,
UNKNOWN_ASSERTION_TYPE,
UNKNOWN_TYPEOF_VALUE,
WRONG_ARGUMENT_COUNT);
// TODO(dimvar): Check for which of these warnings it makes sense to keep
// going after warning, eg, for NOT_UNIQUE_INSTANTIATION, we must instantiate
// to the join of the types.
static final DiagnosticGroup NEW_DIAGNOSTICS = new DiagnosticGroup(
ADDING_PROPERTY_TO_NON_OBJECT,
ASSERT_FALSE,
BOTTOM_INDEX_TYPE,
BOTTOM_PROP,
CROSS_SCOPE_GOTCHA,
FORIN_EXPECTS_OBJECT,
INCOMPATIBLE_STRICT_COMPARISON,
INVALID_INFERRED_RETURN_TYPE,
INVALID_OPERAND_TYPE,
INVALID_THIS_TYPE_IN_BIND,
NOT_UNIQUE_INSTANTIATION,
PROPERTY_ACCESS_ON_NONOBJECT,
UNKNOWN_NAMESPACE_PROPERTY);
public static class WarningReporter {
AbstractCompiler compiler;
WarningReporter(AbstractCompiler compiler) { this.compiler = compiler; }
void add(JSError warning) {
String filename = warning.node.getSourceFileName();
// Avoid some warnings in code generated by the ES6 transpilation.
// TODO(dimvar): typecheck that code properly and remove this.
if (filename != null && filename.startsWith(" [synthetic")
|| JSType.mockToString) {
return;
}
compiler.report(warning);
}
}
private WarningReporter warnings;
private List<TypeMismatch> mismatches;
private List<TypeMismatch> implicitInterfaceUses;
private final AbstractCompiler compiler;
private final CodingConvention convention;
private Map<DiGraphEdge<Node, ControlFlowGraph.Branch>, TypeEnv> envs;
private Map<NTIScope, JSType> summaries;
private Map<Node, DeferredCheck> deferredChecks;
private ControlFlowGraph<Node> cfg;
private NTIScope currentScope;
// This TypeEnv should be computed once per scope
private TypeEnv typeEnvFromDeclaredTypes = null;
private GlobalTypeInfo symbolTable;
private JSTypes commonTypes;
// RETVAL_ID is used when we calculate the summary type of a function
private static final String RETVAL_ID = "%return";
private static final String THIS_ID = "this";
private final String ABSTRACT_METHOD_NAME;
private final Map<String, AssertionFunctionSpec> assertionFunctionsMap;
// To avoid creating warning objects for disabled warnings
private final boolean reportUnknownTypes;
private final boolean reportNullDeref;
// Fields used in the compatibility mode
private final boolean joinTypesWhenInstantiatingGenerics;
private final boolean allowPropertyOnSubtypes;
private final boolean areTypeVariablesUnknown;
// Used only for development
private static boolean showDebuggingPrints = false;
static boolean measureMem = false;
private static long peakMem = 0;
// Used to avoid typing this.commonTypes.TYPENAME everywhere.
private JSType BOOLEAN;
private JSType BOTTOM;
private JSType FALSE_TYPE;
private JSType FALSY;
private JSType NULL;
private JSType NULL_OR_UNDEFINED;
private JSType NUMBER;
private JSType NUMBER_OR_STRING;
private JSType STRING;
private JSType TOP;
private JSType TOP_OBJECT;
private JSType TRUE_TYPE;
private JSType TRUTHY;
private JSType UNDEFINED;
private JSType UNKNOWN;
NewTypeInference(AbstractCompiler compiler) {
this.warnings = new WarningReporter(compiler);
this.compiler = compiler;
this.convention = compiler.getCodingConvention();
this.envs = new LinkedHashMap<>();
this.summaries = new LinkedHashMap<>();
this.deferredChecks = new LinkedHashMap<>();
this.ABSTRACT_METHOD_NAME = convention.getAbstractMethodName();
this.reportUnknownTypes =
compiler.getOptions().enables(DiagnosticGroups.REPORT_UNKNOWN_TYPES);
this.reportNullDeref = compiler.getOptions()
.enables(DiagnosticGroups.NEW_CHECK_TYPES_ALL_CHECKS);
assertionFunctionsMap = new LinkedHashMap<>();
for (AssertionFunctionSpec assertionFunction : convention.getAssertionFunctions()) {
assertionFunctionsMap.put(
assertionFunction.getFunctionName(),
assertionFunction);
}
boolean inCompatibilityMode =
compiler.getOptions().disables(DiagnosticGroups.NEW_CHECK_TYPES_EXTRA_CHECKS);
this.joinTypesWhenInstantiatingGenerics = inCompatibilityMode;
this.allowPropertyOnSubtypes = inCompatibilityMode;
this.areTypeVariablesUnknown = inCompatibilityMode;
}
@VisibleForTesting // Only used from tests
public NTIScope processForTesting(Node externs, Node root) {
process(externs, root);
return symbolTable.getGlobalScope();
}
@Override
public void process(Node externs, Node root) {
try {
this.symbolTable = (GlobalTypeInfo) compiler.getSymbolTable();
this.commonTypes = symbolTable.getCommonTypes();
this.mismatches = symbolTable.getMismatches();
this.implicitInterfaceUses = symbolTable.getImplicitInterfaceUses();
this.BOOLEAN = this.commonTypes.BOOLEAN;
this.BOTTOM = this.commonTypes.BOTTOM;
this.FALSE_TYPE = this.commonTypes.FALSE_TYPE;
this.FALSY = this.commonTypes.FALSY;
this.NULL = this.commonTypes.NULL;
this.NULL_OR_UNDEFINED = this.commonTypes.NULL_OR_UNDEFINED;
this.NUMBER = this.commonTypes.NUMBER;
this.NUMBER_OR_STRING = this.commonTypes.NUMBER_OR_STRING;
this.STRING = this.commonTypes.STRING;
this.TOP = this.commonTypes.TOP;
this.TOP_OBJECT = this.commonTypes.getTopObject();
this.TRUE_TYPE = this.commonTypes.TRUE_TYPE;
this.TRUTHY = this.commonTypes.TRUTHY;
this.UNDEFINED = this.commonTypes.UNDEFINED;
this.UNKNOWN = this.commonTypes.UNKNOWN;
for (NTIScope scope : symbolTable.getScopes()) {
analyzeFunction(scope);
envs.clear();
}
for (DeferredCheck check : deferredChecks.values()) {
check.runCheck(summaries, warnings);
}
if (measureMem) {
System.out.println("Peak mem: " + peakMem + "MB");
}
} catch (Exception unexpectedException) {
String message = unexpectedException.getMessage();
if (currentScope != null) {
message += "\nIn scope: " + currentScope;
}
this.compiler.throwInternalError(message, unexpectedException);
}
}
static void updatePeakMem() {
Runtime rt = Runtime.getRuntime();
long currentUsedMem = (rt.totalMemory() - rt.freeMemory()) / (1024 * 1024);
if (currentUsedMem > peakMem) {
peakMem = currentUsedMem;
}
}
private static void println(Object ... objs) {
if (showDebuggingPrints) {
StringBuilder b = new StringBuilder();
for (Object obj : objs) {
b.append(obj);
}
System.out.println(b);
}
}
private void registerMismatchAndWarn(JSError error, JSType found, JSType required) {
TypeMismatch.registerMismatch(
this.mismatches, this.implicitInterfaceUses, found, required, error);
warnings.add(error);
}
private void registerImplicitUses(Node src, JSType from, JSType to) {
TypeMismatch.recordImplicitInterfaceUses(this.implicitInterfaceUses, src, from, to);
TypeMismatch.recordImplicitUseOfNativeObject(this.mismatches, src, from, to);
}
private TypeEnv getInEnv(DiGraphNode<Node, ControlFlowGraph.Branch> dn) {
List<DiGraphEdge<Node, ControlFlowGraph.Branch>> inEdges = dn.getInEdges();
// True for code considered dead in the CFG
if (inEdges.isEmpty()) {
return getEntryTypeEnv();
}
if (inEdges.size() == 1) {
return envs.get(inEdges.get(0));
}
Set<TypeEnv> envSet = new LinkedHashSet<>();
for (DiGraphEdge<Node, ControlFlowGraph.Branch> de : inEdges) {
TypeEnv env = envs.get(de);
if (env != null) {
envSet.add(env);
}
}
if (envSet.isEmpty()) {
return null;
}
return TypeEnv.join(envSet);
}
private TypeEnv getOutEnv(DiGraphNode<Node, ControlFlowGraph.Branch> dn) {
List<DiGraphEdge<Node, ControlFlowGraph.Branch>> outEdges = dn.getOutEdges();
if (outEdges.isEmpty()) {
// This occurs when visiting a throw in the backward direction.
Preconditions.checkArgument(dn.getValue().isThrow());
return this.typeEnvFromDeclaredTypes;
}
if (outEdges.size() == 1) {
return envs.get(outEdges.get(0));
}
Set<TypeEnv> envSet = new LinkedHashSet<>();
for (DiGraphEdge<Node, ControlFlowGraph.Branch> de : outEdges) {
TypeEnv env = envs.get(de);
if (env != null) {
envSet.add(env);
}
}
Preconditions.checkState(!envSet.isEmpty());
return TypeEnv.join(envSet);
}
private TypeEnv setOutEnv(
DiGraphNode<Node, ControlFlowGraph.Branch> dn, TypeEnv e) {
for (DiGraphEdge<Node, ControlFlowGraph.Branch> de : dn.getOutEdges()) {
envs.put(de, e);
}
return e;
}
// Initialize the type environments on the CFG edges before the FWD analysis.
private void initEdgeEnvsFwd(TypeEnv entryEnv) {
envs.clear();
// For function scopes, add the formal parameters and the free variables
// from outer scopes to the environment.
Set<String> nonLocals = new LinkedHashSet<>();
if (this.currentScope.hasThis()) {
nonLocals.add(THIS_ID);
}
if (this.currentScope.isFunction()) {
if (this.currentScope.getName() != null) {
nonLocals.add(this.currentScope.getName());
}
nonLocals.addAll(this.currentScope.getOuterVars());
nonLocals.addAll(this.currentScope.getFormals());
entryEnv = envPutType(entryEnv, RETVAL_ID, UNDEFINED);
} else {
nonLocals.addAll(this.currentScope.getExterns());
}
for (String name : nonLocals) {
JSType declType = this.currentScope.getDeclaredTypeOf(name);
JSType initType = declType;
if (initType == null) {
initType = envGetType(entryEnv, name);
} else if (this.areTypeVariablesUnknown) {
initType = initType.substituteGenericsWithUnknown();
}
println("Adding non-local ", name,
" with decltype: ", declType,
" and inittype: ", initType);
entryEnv = envPutType(entryEnv, name, initType);
}
// For all scopes, add local variables and (local) function definitions
// to the environment.
for (String local : this.currentScope.getLocals()) {
if (!this.currentScope.isFunctionNamespace(local)) {
entryEnv = envPutType(entryEnv, local, UNDEFINED);
}
}
for (String fnName : this.currentScope.getLocalFunDefs()) {
entryEnv = envPutType(entryEnv, fnName, getSummaryOfLocalFunDef(fnName));
}
println("Keeping env: ", entryEnv);
setOutEnv(this.cfg.getEntry(), entryEnv);
}
private TypeEnv getTypeEnvFromDeclaredTypes() {
TypeEnv env = new TypeEnv();
Set<String> varNames = this.currentScope.getOuterVars();
Set<String> locals = this.currentScope.getLocals();
varNames.addAll(locals);
varNames.addAll(this.currentScope.getExterns());
if (this.currentScope.hasThis()) {
varNames.add(THIS_ID);
}
if (this.currentScope.isFunction()) {
Node fn = this.currentScope.getRoot();
if (!this.currentScope.hasThis()
// Can't use NodeUtil.referencesSuper here because that function is correct only
// on valid ASTs, but here we may have an invalid AST that contains super inside
// a function.
&& NodeUtil.containsType(fn.getLastChild(), Token.SUPER, NodeUtil.MATCH_NOT_FUNCTION)) {
// This function is a static method on some class. To do lookups of the
// class name, we add the root of the qualified name to the environment.
Node funNameNode = NodeUtil.getBestLValue(fn);
Node qnameRoot = NodeUtil.getRootOfQualifiedName(funNameNode);
Preconditions.checkState(qnameRoot.isName());
varNames.add(qnameRoot.getString());
}
if (this.currentScope.getName() != null) {
varNames.add(this.currentScope.getName());
}
varNames.addAll(this.currentScope.getFormals());
// In the rare case when there is a local variable named "arguments",
// this entry will be overwritten in the foreach loop below.
JSType argumentsType;
DeclaredFunctionType dft = this.currentScope.getDeclaredFunctionType();
if (dft.getOptionalArity() == 0 && dft.hasRestFormals()) {
argumentsType = dft.getRestFormalsType();
} else {
argumentsType = UNKNOWN;
}
env = envPutType(env, "arguments",
commonTypes.getArgumentsArrayType(argumentsType));
}
for (String varName : varNames) {
if (!this.currentScope.isLocalFunDef(varName)) {
JSType declType = this.currentScope.getDeclaredTypeOf(varName);
if (declType == null) {
declType = UNKNOWN;
} else if (areTypeVariablesUnknown) {
declType = declType.substituteGenericsWithUnknown();
}
env = envPutType(env, varName, declType);
}
}
for (String fnName : this.currentScope.getLocalFunDefs()) {
env = envPutType(env, fnName, getSummaryOfLocalFunDef(fnName));
}
return env;
}
private JSType getSummaryOfLocalFunDef(String name) {
NTIScope fnScope = this.currentScope.getScope(name);
JSType fnType = summaries.get(fnScope);
if (fnType != null) {
return fnType;
}
// Functions defined in externs have no summary, so use the declared type
fnType = this.currentScope.getDeclaredTypeOf(name);
if (fnType.getFunType() == null) {
// Can happen when a function defined in externs clashes with a variable
// defined by a catch block.
// TODO(dimvar): once we fix scoping for catch blocks, uncomment the
// precondition below.
Preconditions.checkState(fnType.isUnknown());
return this.commonTypes.qmarkFunction();
// Preconditions.checkState(fnType.getFunType() != null,
// "Needed function but found %s", fnType);
}
return changeTypeIfFunctionNamespace(fnScope, fnType);
}
private void buildWorkset(
DiGraphNode<Node, ControlFlowGraph.Branch> dn,
List<DiGraphNode<Node, ControlFlowGraph.Branch>> workset) {
buildWorksetHelper(dn, workset,
new LinkedHashSet<DiGraphNode<Node, ControlFlowGraph.Branch>>());
}
private void buildWorksetHelper(
DiGraphNode<Node, ControlFlowGraph.Branch> dn,
List<DiGraphNode<Node, ControlFlowGraph.Branch>> workset,
Set<DiGraphNode<Node, ControlFlowGraph.Branch>> seen) {
if (seen.contains(dn) || dn == this.cfg.getImplicitReturn()) {
return;
}
switch (dn.getValue().getToken()) {
case DO:
case WHILE:
case FOR:
case FOR_IN:
// Do the loop body first, then the loop follow.
// For DO loops, we do BODY-CONDT-CONDF-FOLLOW
// Since CONDT is currently unused, this could be optimized.
List<DiGraphEdge<Node, ControlFlowGraph.Branch>> outEdges = dn.getOutEdges();
seen.add(dn);
workset.add(dn);
for (DiGraphEdge<Node, ControlFlowGraph.Branch> outEdge : outEdges) {
if (outEdge.getValue() == ControlFlowGraph.Branch.ON_TRUE) {
buildWorksetHelper(outEdge.getDestination(), workset, seen);
}
}
workset.add(dn);
for (DiGraphEdge<Node, ControlFlowGraph.Branch> outEdge : outEdges) {
if (outEdge.getValue() == ControlFlowGraph.Branch.ON_FALSE) {
buildWorksetHelper(outEdge.getDestination(), workset, seen);
}
}
break;
default: {
// Wait for all other incoming edges at join nodes.
for (DiGraphEdge<Node, ControlFlowGraph.Branch> inEdge :
dn.getInEdges()) {
if (!seen.contains(inEdge.getSource())
&& !inEdge.getSource().getValue().isDo()) {
return;
}
}
seen.add(dn);
if (this.cfg.getEntry() != dn) {
workset.add(dn);
}
// Don't recur for straight-line code
while (true) {
Node n = dn.getValue();
if (n.isTry()) {
maybeAddDeadCode(workset, seen, n.getSecondChild());
} else if (n.isBreak() || n.isContinue() || n.isThrow()) {
maybeAddDeadCode(workset, seen, n.getNext());
}
List<DiGraphNode<Node, ControlFlowGraph.Branch>> succs =
this.cfg.getDirectedSuccNodes(dn);
if (succs.size() != 1) {
break;
}
DiGraphNode<Node, ControlFlowGraph.Branch> succ = succs.get(0);
if (succ == this.cfg.getImplicitReturn()) {
if (n.getNext() != null) {
maybeAddDeadCode(workset, seen, n.getNext());
}
return;
}
// Make sure that succ isn't a join node
if (this.cfg.getDirectedPredNodes(succ).size() > 1) {
break;
}
workset.add(succ);
seen.add(succ);
dn = succ;
}
for (DiGraphNode<Node, ControlFlowGraph.Branch> succ :
this.cfg.getDirectedSuccNodes(dn)) {
buildWorksetHelper(succ, workset, seen);
}
break;
}
}
}
// Analyze dead code, such as a catch that is never executed or a statement
// following a return/break/continue. This code can be a predecessor of live
// code in the cfg. We wait on incoming edges before adding nodes to the
// workset, and don't want dead code to block live code from being analyzed.
private void maybeAddDeadCode(
List<DiGraphNode<Node, ControlFlowGraph.Branch>> workset,
Set<DiGraphNode<Node, ControlFlowGraph.Branch>> seen,
Node maybeDeadNode) {
if (maybeDeadNode == null) {
return;
}
DiGraphNode<Node, ControlFlowGraph.Branch> cfgNode =
this.cfg.getDirectedGraphNode(maybeDeadNode);
if (cfgNode == null) {
return;
}
if (this.cfg.getDirectedPredNodes(cfgNode).isEmpty()) {
buildWorksetHelper(cfgNode, workset, seen);
}
}
private void analyzeFunction(NTIScope scope) {
println("=== Analyzing function: ", scope.getReadableName(), " ===");
currentScope = scope;
ControlFlowAnalysis cfa = new ControlFlowAnalysis(compiler, false, false);
cfa.process(null, scope.getRoot());
this.cfg = cfa.getCfg();
println(this.cfg);
// The size is > 1 when multiple files are compiled
// Preconditions.checkState(cfg.getEntry().getOutEdges().size() == 1);
List<DiGraphNode<Node, ControlFlowGraph.Branch>> workset =
new LinkedList<>();
buildWorkset(this.cfg.getEntry(), workset);
/* println("Workset: ", workset); */
this.typeEnvFromDeclaredTypes = getTypeEnvFromDeclaredTypes();
if (scope.isFunction() && scope.hasUndeclaredFormalsOrOuters()) {
Collections.reverse(workset);
// Ideally, we would like to only set the in-edges of the implicit return
// rather than all edges. However, we cannot do that because of a bug in
// workset construction. (The test testBadWorksetConstruction would fail.)
// In buildWorksetHelper, if a loop contains break, we add the FOLLOW node
// of the loop before adding the loop header twice. So, the 2nd addition
// of the loop header has no effect. We should fix workset creation
// (eg, by putting edges instead of nodes in seen, or some other way that
// correctly waits for all incoming edges).
for (DiGraphEdge<Node, ControlFlowGraph.Branch> e : this.cfg.getEdges()) {
envs.put(e, this.typeEnvFromDeclaredTypes);
}
analyzeFunctionBwd(workset);
Collections.reverse(workset);
// TODO(dimvar): Revisit what we throw away after the bwd analysis
TypeEnv entryEnv = getEntryTypeEnv();
initEdgeEnvsFwd(entryEnv);
if (measureMem) {
updatePeakMem();
}
} else {
TypeEnv entryEnv = this.typeEnvFromDeclaredTypes;
initEdgeEnvsFwd(entryEnv);
}
this.typeEnvFromDeclaredTypes = null;
analyzeFunctionFwd(workset);
if (scope.isFunction()) {
createSummary(scope);
}
if (measureMem) {
updatePeakMem();
}
}
private void analyzeFunctionBwd(
List<DiGraphNode<Node, ControlFlowGraph.Branch>> workset) {
for (DiGraphNode<Node, ControlFlowGraph.Branch> dn : workset) {
Node n = dn.getValue();
TypeEnv outEnv = Preconditions.checkNotNull(getOutEnv(dn));
TypeEnv inEnv;
println("\tBWD Statment: ", n);
println("\t\toutEnv: ", outEnv);
switch (n.getToken()) {
case EXPR_RESULT:
inEnv = analyzeExprBwd(n.getFirstChild(), outEnv, UNKNOWN).env;
break;
case RETURN: {
Node retExp = n.getFirstChild();
if (retExp == null) {
inEnv = outEnv;
} else {
JSType declRetType = this.currentScope.getDeclaredFunctionType().getReturnType();
declRetType = declRetType == null ? UNKNOWN : declRetType;
inEnv = analyzeExprBwd(retExp, outEnv, declRetType).env;
}
break;
}
case VAR: {
if (NodeUtil.isTypedefDecl(n)) {
inEnv = outEnv;
break;
}
inEnv = outEnv;
for (Node nameNode = n.getFirstChild(); nameNode != null;
nameNode = nameNode.getNext()) {
String varName = nameNode.getString();
Node rhs = nameNode.getFirstChild();
JSType declType = this.currentScope.getDeclaredTypeOf(varName);
inEnv = envPutType(inEnv, varName, UNKNOWN);
if (rhs == null || this.currentScope.isLocalFunDef(varName)) {
continue;
}
JSType inferredType = envGetType(outEnv, varName);
JSType requiredType;
if (declType == null) {
requiredType = inferredType;
} else {
// TODO(dimvar): look if the meet is needed
requiredType = JSType.meet(declType, inferredType);
if (requiredType.isBottom()) {
requiredType = UNKNOWN;
}
}
inEnv = analyzeExprBwd(rhs, inEnv, requiredType).env;
}
break;
}
case BLOCK:
case ROOT:
case BREAK:
case CATCH:
case CONTINUE:
case DEFAULT_CASE:
case DEBUGGER:
case EMPTY:
case SCRIPT:
case TRY:
case WITH:
inEnv = outEnv;
break;
case DO:
case FOR:
case FOR_IN:
case IF:
case WHILE:
Node expr = n.isForIn() ? n.getFirstChild() : NodeUtil.getConditionExpression(n);
inEnv = analyzeExprBwd(expr, outEnv).env;
break;
case THROW:
case CASE:
case SWITCH:
inEnv = analyzeExprBwd(n.getFirstChild(), outEnv).env;
break;
default:
if (NodeUtil.isStatement(n)) {
throw new RuntimeException("Unhandled statement type: " + n.getToken());
} else {
inEnv = analyzeExprBwd(n, outEnv).env;
break;
}
}
println("\t\tinEnv: ", inEnv);
for (DiGraphEdge<Node, ControlFlowGraph.Branch> de : dn.getInEdges()) {
envs.put(de, inEnv);
}
}
}
private void analyzeFunctionFwd(
List<DiGraphNode<Node, ControlFlowGraph.Branch>> workset) {
for (DiGraphNode<Node, ControlFlowGraph.Branch> dn : workset) {
Node n = dn.getValue();
Node parent = n.getParent();
Preconditions.checkState(n != null,
"Implicit return should not be in workset.");
TypeEnv inEnv = getInEnv(dn);
TypeEnv outEnv = null;
if (parent.isScript()
|| (parent.isNormalBlock() && parent.getParent().isFunction())) {
// All joins have merged; forget changes
inEnv = inEnv.clearChangeLog();
}
println("\tFWD Statment: ", n);
println("\t\tinEnv: ", inEnv);
boolean conditional = false;
switch (n.getToken()) {
case BLOCK:
case ROOT:
case BREAK:
case CONTINUE:
case DEFAULT_CASE:
case DEBUGGER:
case EMPTY:
case FUNCTION:
case SCRIPT:
case TRY:
case WITH: // We don't typecheck WITH, we just avoid crashing.
outEnv = inEnv;
break;
case CATCH:
String catchVarname = n.getFirstChild().getString();
outEnv = envPutType(inEnv, catchVarname, UNKNOWN);
break;
case EXPR_RESULT:
println("\tsemi ", n.getFirstChild().getToken());
if (n.getBooleanProp(Node.ANALYZED_DURING_GTI)) {
n.removeProp(Node.ANALYZED_DURING_GTI);
outEnv = inEnv;
} else {
outEnv = analyzeExprFwd(n.getFirstChild(), inEnv, UNKNOWN).env;
}
break;
case RETURN: {
Node retExp = n.getFirstChild();
JSType declRetType = this.currentScope.getDeclaredFunctionType().getReturnType();
if (declRetType == null) {
declRetType = UNKNOWN;
} else if (this.areTypeVariablesUnknown) {
declRetType = declRetType.substituteGenericsWithUnknown();
}
JSType actualRetType;
if (retExp == null) {
actualRetType = UNDEFINED;
outEnv = envPutType(inEnv, RETVAL_ID, actualRetType);
} else {
EnvTypePair retPair = analyzeExprFwd(retExp, inEnv, declRetType);
actualRetType = retPair.type;
outEnv = envPutType(retPair.env, RETVAL_ID, actualRetType);
}
if (!actualRetType.isSubtypeOf(declRetType)) {
registerMismatchAndWarn(JSError.make(
n, RETURN_NONDECLARED_TYPE, errorMsgWithTypeDiff(declRetType, actualRetType)),
actualRetType, declRetType);
}
break;
}
case DO:
case IF:
case FOR:
case FOR_IN:
case WHILE:
if (n.isForIn()) {
Node obj = n.getSecondChild();
EnvTypePair pair = analyzeExprFwd(obj, inEnv, pickReqObjType(n));
pair = mayWarnAboutNullableReferenceAndTighten(n, pair.type, null, inEnv);
JSType objType = pair.type;
if (!objType.isSubtypeOf(TOP_OBJECT)) {
warnings.add(JSError.make(obj, FORIN_EXPECTS_OBJECT, objType.toString()));
} else if (objType.isStruct()) {
warnings.add(JSError.make(obj, IN_USED_WITH_STRUCT));
}
Node lhs = n.getFirstChild();
LValueResultFwd lval = analyzeLValueFwd(lhs, inEnv, STRING);
if (lval.declType != null && !commonTypes.isStringScalarOrObj(lval.declType)) {
warnings.add(JSError.make(lhs, FORIN_EXPECTS_STRING_KEY, lval.declType.toString()));
outEnv = lval.env;
} else {
outEnv = updateLvalueTypeInEnv(lval.env, lhs, lval.ptr, STRING);
}
break;
}
conditional = true;
analyzeConditionalStmFwd(dn, NodeUtil.getConditionExpression(n), inEnv);
break;
case CASE: {
conditional = true;
// See analyzeExprFwd#Token.CASE for how to handle this precisely
analyzeConditionalStmFwd(dn, n, inEnv);
break;
}
case VAR:
outEnv = inEnv;
if (NodeUtil.isTypedefDecl(n)) {
break;
}
for (Node nameNode = n.getFirstChild(); nameNode != null;
nameNode = nameNode.getNext()) {
outEnv = processVarDeclaration(nameNode, outEnv);
}
break;
case SWITCH:
case THROW:
outEnv = analyzeExprFwd(n.getFirstChild(), inEnv).env;
break;
default:
if (NodeUtil.isStatement(n)) {
throw new RuntimeException("Unhandled statement type: " + n.getToken());
} else {
outEnv = analyzeExprFwd(n, inEnv, UNKNOWN).env;
break;
}
}
if (!conditional) {
println("\t\toutEnv: ", outEnv);
setOutEnv(dn, outEnv);
}
}
}
private void analyzeConditionalStmFwd(
DiGraphNode<Node, ControlFlowGraph.Branch> stm, Node cond, TypeEnv inEnv) {
for (DiGraphEdge<Node, ControlFlowGraph.Branch> outEdge :
stm.getOutEdges()) {
JSType specializedType;
switch (outEdge.getValue()) {
case ON_TRUE:
specializedType = TRUTHY;
break;
case ON_FALSE:
specializedType = FALSY;
break;
case ON_EX:
specializedType = UNKNOWN;
break;
default:
throw new RuntimeException(
"Condition with an unexpected edge type: " + outEdge.getValue());
}
envs.put(outEdge,
analyzeExprFwd(cond, inEnv, UNKNOWN, specializedType).env);
}
}
private void createSummary(NTIScope fn) {
Node fnRoot = fn.getRoot();
Preconditions.checkArgument(!fnRoot.isFromExterns());
FunctionTypeBuilder builder = new FunctionTypeBuilder(this.commonTypes);
TypeEnv entryEnv = getEntryTypeEnv();
TypeEnv exitEnv = getInEnv(this.cfg.getImplicitReturn());
if (exitEnv == null) {
// This function only exits with THROWs
exitEnv = envPutType(new TypeEnv(), RETVAL_ID, BOTTOM);
}
DeclaredFunctionType declType = fn.getDeclaredFunctionType();
int reqArity = declType.getRequiredArity();
int optArity = declType.getOptionalArity();
if (declType.isGeneric()) {
builder.addTypeParameters(declType.getTypeParameters());
}
// Every trailing undeclared formal whose inferred type is ?
// or contains undefined can be marked as optional.
List<String> formals = fn.getFormals();
for (int i = reqArity - 1; i >= 0; i--) {
JSType formalType = fn.getDeclaredFunctionType().getFormalType(i);
if (formalType != null) {
break;
}
String formalName = formals.get(i);
formalType = getTypeAfterFwd(formalName, entryEnv, exitEnv);
if (formalType.isUnknown() || UNDEFINED.isSubtypeOf(formalType)) {
reqArity--;
} else {
break;
}
}
// Collect types of formals in the builder
int formalIndex = 0;
for (String formal : formals) {
JSType formalType = fn.getDeclaredTypeOf(formal);
if (formalType == null) {
formalType = getTypeAfterFwd(formal, entryEnv, exitEnv);
}
if (formalIndex < reqArity) {
builder.addReqFormal(formalType);
} else if (formalIndex < optArity) {
builder.addOptFormal(formalType);
}
formalIndex++;
}
if (declType.hasRestFormals()) {
builder.addRestFormals(declType.getFormalType(formalIndex));
}
for (String outer : fn.getOuterVars()) {
println("Free var ", outer, " going in summary");
builder.addOuterVarPrecondition(outer, envGetType(entryEnv, outer));
}
builder.addNominalType(declType.getNominalType());
builder.addReceiverType(declType.getReceiverType());
builder.addAbstract(declType.isAbstract());
JSType declRetType = declType.getReturnType();
JSType actualRetType = Preconditions.checkNotNull(envGetType(exitEnv, RETVAL_ID));
if (declRetType != null) {
builder.addRetType(declRetType);
if (!isAllowedToNotReturn(fn)
&& !UNDEFINED.isSubtypeOf(declRetType)
&& hasPathWithNoReturn(this.cfg)) {
warnings.add(JSError.make(
fnRoot, MISSING_RETURN_STATEMENT, declRetType.toString()));
}
} else if (declType.getNominalType() == null) {
// If someone uses the result of a function that doesn't return, they get a warning.
builder.addRetType(actualRetType.isBottom() ? TOP : actualRetType);
} else {
// Don't infer a return type for constructors. We want to warn for
// constructors called without new who don't explicitly declare @return.
builder.addRetType(UNDEFINED);
}
JSType summary = commonTypes.fromFunctionType(builder.buildFunction());
println("Function summary for ", fn.getReadableName());
println("\t", summary);
summary = changeTypeIfFunctionNamespace(fn, summary);
summaries.put(fn, summary);
maybeSetTypeI(fnRoot, summary);
Node fnNameNode = NodeUtil.getNameNode(fnRoot);
if (fnNameNode != null) {
maybeSetTypeI(fnNameNode, summary);
}
}
private JSType changeTypeIfFunctionNamespace(NTIScope fnScope, JSType fnType) {
NTIScope enclosingScope = fnScope.getParent();
Node fnNameNode = NodeUtil.getNameNode(fnScope.getRoot());
JSType namespaceType = null;
if (fnNameNode == null) {
return fnType;
}
if (fnNameNode.isName()) {
String fnName = fnNameNode.getString();
if (enclosingScope.isFunctionNamespace(fnName)) {
namespaceType = enclosingScope.getDeclaredTypeOf(fnName);
}
} else if (fnNameNode.isQualifiedName()) {
QualifiedName qname = QualifiedName.fromNode(fnNameNode);
JSType rootNs = enclosingScope.getDeclaredTypeOf(qname.getLeftmostName());
if (rootNs != null && rootNs.isSubtypeOf(TOP_OBJECT)) {
namespaceType = rootNs.getProp(qname.getAllButLeftmost());
}
}
if (namespaceType != null && namespaceType.isNamespace()) {
// Replace the less-precise declared function type
// with the new function summary.
return namespaceType.withFunction(
fnType.getFunTypeIfSingletonObj(), commonTypes.getFunctionType());
}
return fnType;
}
// TODO(dimvar): To get the adjusted end-of-fwd type for objs, we must be
// able to know whether a property was added during the evaluation of the
// function, or was on the object already.
private JSType getTypeAfterFwd(
String varName, TypeEnv entryEnv, TypeEnv exitEnv) {
JSType typeAfterBwd = envGetType(entryEnv, varName);
if (!typeAfterBwd.hasNonScalar() || typeAfterBwd.getFunType() != null) {
// The type of a formal after fwd is more precise than the type after bwd,
// so we use typeAfterFwd in the summary.
// Trade-off: If the formal is assigned in the body of a function, and the
// new value has a different type, we compute a wrong summary.
// Since this is rare, we prefer typeAfterFwd to typeAfterBwd.
JSType typeAfterFwd = envGetType(exitEnv, varName);
if (typeAfterFwd != null) {
return typeAfterFwd;
}
}
return typeAfterBwd;
}
private static boolean isAllowedToNotReturn(NTIScope methodScope) {
Node fn = methodScope.getRoot();
if (fn.isFromExterns()) {
return true;
}
if (!NodeUtil.isPrototypeMethod(fn)) {
return false;
}
if (methodScope.getDeclaredFunctionType().isAbstract()) {
return true;
}
JSType maybeInterface;
Node ntQnameNode = NodeUtil.getPrototypeClassName(fn.getParent().getFirstChild());
if (ntQnameNode.isName()) {
maybeInterface = methodScope.getDeclaredTypeOf(ntQnameNode.getString());
} else {
QualifiedName ntQname = QualifiedName.fromNode(ntQnameNode);
JSType rootNamespace = methodScope.getDeclaredTypeOf(ntQname.getLeftmostName());
maybeInterface = rootNamespace == null
? null : rootNamespace.getProp(ntQname.getAllButLeftmost());
}
return maybeInterface != null && maybeInterface.isInterfaceDefinition();
}
private static boolean hasPathWithNoReturn(ControlFlowGraph<Node> cfg) {
for (DiGraphNode<Node, ControlFlowGraph.Branch> dn :
cfg.getDirectedPredNodes(cfg.getImplicitReturn())) {
Node stm = dn.getValue();
if (NodeUtil.isLoopStructure(stm)) {
Node cond = NodeUtil.getConditionExpression(stm);
if (!(cond != null && NodeUtil.isImpureTrue(cond))) {
return true;
}
} else if (stm.isBreak()) {
// Allow break after return in switches.
if (!cfg.getDirectedPredNodes(dn).isEmpty()) {
return true;
}
} else if (!stm.isReturn()) {
return true;
}
}
return false;
}
/** Processes a single variable declaration in a VAR statement. */
private TypeEnv processVarDeclaration(Node nameNode, TypeEnv inEnv) {
String varName = nameNode.getString();
JSType declType = this.currentScope.getDeclaredTypeOf(varName);
if (this.currentScope.isLocalFunDef(varName)) {
return inEnv;
}
Node rhs = nameNode.getFirstChild();
if (NodeUtil.isNamespaceDecl(nameNode)
|| (GlobalTypeInfo.isCtorDefinedByCall(nameNode)
&& !isFunctionBind(rhs.getFirstChild(), inEnv, true))
|| nameNode.getParent().getBooleanProp(Node.ANALYZED_DURING_GTI)) {
Preconditions.checkNotNull(declType,
"Can't skip var declaration with undeclared type at: %s", nameNode);
maybeSetTypeI(nameNode, declType);
return envPutType(inEnv, varName, declType);
}
TypeEnv outEnv = inEnv;
JSType rhsType;
if (rhs == null) {
rhsType = UNDEFINED;
} else {
EnvTypePair pair = analyzeExprFwd(rhs, inEnv, declType != null ? declType : UNKNOWN);
outEnv = pair.env;
rhsType = pair.type;
}
if (declType != null && rhs != null) {
if (!rhsType.isSubtypeOf(declType)) {
registerMismatchAndWarn(
JSError.make(rhs, MISTYPED_ASSIGN_RHS, errorMsgWithTypeDiff(declType, rhsType)),
rhsType, declType);
// Don't flow the wrong initialization
rhsType = declType;
} else {
registerImplicitUses(rhs, rhsType, declType);
// We do this to preserve type attributes like declaredness of
// a property
rhsType = declType.specialize(rhsType);
}
}
maybeSetTypeI(nameNode, rhsType);
return envPutType(outEnv, varName, rhsType);
}
private EnvTypePair analyzeExprFwd(Node expr, TypeEnv inEnv) {
return analyzeExprFwd(expr, inEnv, UNKNOWN, UNKNOWN);
}
private EnvTypePair analyzeExprFwd(
Node expr, TypeEnv inEnv, JSType requiredType) {
return analyzeExprFwd(expr, inEnv, requiredType, requiredType);
}
/**
* @param requiredType The context requires this type; warn if the expression
* doesn't have this type.
* @param specializedType Used in boolean contexts to infer types of names.
*
* Invariant: specializedType is a subtype of requiredType.
*/
private EnvTypePair analyzeExprFwd(
Node expr, TypeEnv inEnv, JSType requiredType, JSType specializedType) {
Preconditions.checkArgument(requiredType != null && !requiredType.isBottom());
EnvTypePair resultPair = null;
switch (expr.getToken()) {
case EMPTY: // can be created by a FOR with empty condition
resultPair = new EnvTypePair(inEnv, UNKNOWN);
break;
case FUNCTION: {
String fnName = symbolTable.getFunInternalName(expr);
JSType fnType = envGetType(inEnv, fnName);
Preconditions.checkState(fnType != null, "Could not find type for %s", fnName);
TypeEnv outEnv = collectTypesForFreeVarsFwd(expr, inEnv);
resultPair = new EnvTypePair(outEnv, fnType);
break;
}
case FALSE:
case NULL:
case NUMBER:
case STRING:
case TRUE:
resultPair = new EnvTypePair(inEnv, scalarValueToType(expr.getToken()));
break;
case OBJECTLIT:
resultPair = analyzeObjLitFwd(expr, inEnv, requiredType, specializedType);
break;
case THIS: {
resultPair = analyzeThisFwd(expr, inEnv, requiredType, specializedType);
break;
}
case SUPER: {
resultPair = analyzeSuperFwd(expr, inEnv);
break;
}
case NAME:
resultPair = analyzeNameFwd(expr, inEnv, requiredType, specializedType);
break;
case AND:
case OR:
resultPair = analyzeLogicalOpFwd(expr, inEnv, requiredType, specializedType);
break;
case INC:
case DEC:
resultPair = analyzeIncDecFwd(expr, inEnv, requiredType);
break;
case BITNOT:
case NEG:
resultPair = analyzeUnaryNumFwd(expr, inEnv);
break;
case POS: {
// We are more permissive with +, because it is used to coerce to number
resultPair = analyzeExprFwd(expr.getFirstChild(), inEnv);
resultPair.type = NUMBER;
break;
}
case TYPEOF: {
resultPair = analyzeExprFwd(expr.getFirstChild(), inEnv);
resultPair.type = STRING;
break;
}
case INSTANCEOF:
resultPair = analyzeInstanceofFwd(expr, inEnv, specializedType);
break;
case ADD:
resultPair = analyzeAddFwd(expr, inEnv, requiredType);
break;
case BITOR:
case BITAND:
case BITXOR:
case DIV:
case LSH:
case MOD:
case MUL:
case RSH:
case SUB:
case URSH:
resultPair = analyzeBinaryNumericOpFwd(expr, inEnv);
break;
case ASSIGN:
resultPair = analyzeAssignFwd(expr, inEnv, requiredType, specializedType);
break;
case ASSIGN_ADD:
resultPair = analyzeAssignAddFwd(expr, inEnv, requiredType);
break;
case ASSIGN_BITOR:
case ASSIGN_BITXOR:
case ASSIGN_BITAND:
case ASSIGN_LSH:
case ASSIGN_RSH:
case ASSIGN_URSH:
case ASSIGN_SUB:
case ASSIGN_MUL:
case ASSIGN_DIV:
case ASSIGN_MOD:
resultPair = analyzeAssignNumericOpFwd(expr, inEnv);
break;
case SHEQ:
case SHNE:
resultPair =
analyzeStrictComparisonFwd(
expr.getToken(), expr.getFirstChild(), expr.getLastChild(), inEnv, specializedType);
break;
case EQ:
case NE:
resultPair = analyzeNonStrictComparisonFwd(expr, inEnv, specializedType);
break;
case LT:
case GT:
case LE:
case GE:
resultPair = analyzeLtGtFwd(expr, inEnv);
break;
case GETPROP:
Preconditions.checkState(
!NodeUtil.isAssignmentOp(expr.getParent()) ||
!NodeUtil.isLValue(expr));
if (expr.getBooleanProp(Node.ANALYZED_DURING_GTI)) {
if (expr.isQualifiedName() && !NodeUtil.isTypedefDecl(expr)) {
markAndGetTypeOfPreanalyzedNode(expr, inEnv, true);
}
expr.removeProp(Node.ANALYZED_DURING_GTI);
resultPair = new EnvTypePair(inEnv, requiredType);
} else {
resultPair = analyzePropAccessFwd(
expr.getFirstChild(), expr.getLastChild().getString(),
inEnv, requiredType, specializedType);
}
break;
case HOOK:
resultPair = analyzeHookFwd(expr, inEnv, requiredType, specializedType);
break;
case CALL:
case NEW:
resultPair = analyzeCallNewFwd(expr, inEnv, requiredType, specializedType);
break;
case COMMA:
resultPair = analyzeExprFwd(
expr.getLastChild(),
analyzeExprFwd(expr.getFirstChild(), inEnv).env,
requiredType,
specializedType);
break;
case NOT: {
resultPair = analyzeExprFwd(expr.getFirstChild(),
inEnv, UNKNOWN, specializedType.negate());
resultPair.type = resultPair.type.negate().toBoolean();
break;
}
case GETELEM:
resultPair = analyzeGetElemFwd(expr, inEnv, requiredType, specializedType);
break;
case VOID: {
resultPair = analyzeExprFwd(expr.getFirstChild(), inEnv);
resultPair.type = UNDEFINED;
break;
}
case IN:
resultPair = analyzeInFwd(expr, inEnv, specializedType);
break;
case DELPROP: {
// IRFactory checks that the operand is a name, getprop or getelem.
// analyzePropAccessFwd warns if we delete a constant property.
resultPair = analyzeExprFwd(expr.getFirstChild(), inEnv);
resultPair.type = BOOLEAN;
break;
}
case REGEXP:
resultPair = new EnvTypePair(inEnv, commonTypes.getRegexpType());
break;
case ARRAYLIT:
resultPair = analyzeArrayLitFwd(expr, inEnv);
break;
case CAST:
resultPair = analyzeCastFwd(expr, inEnv, specializedType);
break;
case CASE:
// For a statement of the form: switch (exp1) { ... case exp2: ... }
// we analyze the case as if it were (exp1 === exp2).
// We analyze the body of the case when the test is true and the stm
// following the body when the test is false.
resultPair = analyzeStrictComparisonFwd(Token.SHEQ,
expr.getParent().getFirstChild(), expr.getFirstChild(),
inEnv, specializedType);
break;
default:
throw new RuntimeException("Unhandled expression type: " + expr.getToken());
}
mayWarnAboutUnknownType(expr, resultPair.type);
// TODO(dimvar): We attach the type to every expression because that's also
// what the old type inference does.
// But maybe most of these types are never looked at.
// See if the passes that use types only need types on a small subset of the
// nodes, and only store these types to save mem.
maybeSetTypeI(expr, resultPair.type);
if (this.currentScope.isFunction()) {
// In global scope, the env is too big and produces too much output
println("AnalyzeExprFWD: ", expr,
" ::reqtype: ", requiredType, " ::spectype: ", specializedType,
" ::resulttype: ", resultPair.type);
}
return resultPair;
}
private void mayWarnAboutUnknownType(Node expr, JSType t) {
boolean isKnownGetElem = expr.isGetElem() && expr.getLastChild().isString();
if (t.isUnknown()
&& this.reportUnknownTypes
// Don't warn for expressions whose value isn't used
&& !expr.getParent().isExprResult()
// The old type checker doesn't warn about unknown getelems.
// Maybe because we can't do anything about them, so why warn?
// We mimic that behavior here.
&& (!expr.isGetElem() || isKnownGetElem)) {
warnings.add(JSError.make(expr, UNKNOWN_EXPR_TYPE, expr.getToken().toString()));
}
}
private EnvTypePair analyzeNameFwd(
Node expr, TypeEnv inEnv, JSType requiredType, JSType specializedType) {
String varName = expr.getString();
if (varName.equals("undefined")) {
return new EnvTypePair(inEnv, UNDEFINED);
}
JSType inferredType = envGetType(inEnv, varName);
if (inferredType == null) {
println("Found global variable ", varName);
// For now, we don't warn for global variables
return new EnvTypePair(inEnv, UNKNOWN);
}
println(varName, "'s inferredType: ", inferredType,
" requiredType: ", requiredType,
" specializedType: ", specializedType);
if (!inferredType.isSubtypeOf(requiredType)) {
// The inferred type of a variable is always an upper bound, but
// sometimes it's also a lower bound, eg, if x was the lhs of an =
// where we know the type of the rhs.
// We don't track whether the inferred type is a lower bound, so we
// conservatively assume that it always is.
// This is why we warn when !inferredType.isSubtypeOf(requiredType).
// In some rare cases, the inferred type is only an upper bound,
// and we would falsely warn.
// (These usually include the polymorphic operators += and <.)
// We have a heuristic check to avoid the spurious warnings,
// but we also miss some true warnings.
JSType declType = this.currentScope.getDeclaredTypeOf(varName);
if (tightenNameTypeAndDontWarn(varName, expr, declType, inferredType, requiredType)) {
inferredType = inferredType.specialize(requiredType);
} else {
// Propagate incorrect type so that the context catches
// the mismatch
return new EnvTypePair(inEnv, inferredType);
}
}
// If preciseType is bottom, there is a condition that can't be true,
// but that's not necessarily a type error.
JSType preciseType = inferredType.specialize(specializedType);
if (preciseType.isBottom()) {
preciseType = pickFallbackTypeAfterBottom(varName, inferredType, specializedType);
}
println(varName, "'s preciseType: ", preciseType);
if ((this.currentScope.isUndeclaredFormal(varName)
|| this.currentScope.isUndeclaredOuterVar(varName))
&& preciseType.hasNonScalar()) {
// In the bwd direction, we may infer a loose type and then join w/
// top and forget it. That's why we also loosen types going fwd.
preciseType = preciseType.withLoose();
}
return EnvTypePair.addBinding(inEnv, varName, preciseType);
}
/**
* Specialization of a name or of THIS may go to bottom, e.g., as a result of an IF test that NTI
* thinks is always false. When this happens, we pick a different type to flow around,
* because bottom may cause unintuitive warnings down the line.
*/
private JSType pickFallbackTypeAfterBottom(
String name, JSType inferredType, JSType specializedType) {
JSType declType = this.currentScope.getDeclaredTypeOf(name);
if (declType == null) {
return inferredType;
}
JSType preciseType = declType.specialize(specializedType);
return preciseType.isBottom() ? declType : preciseType;
}
private EnvTypePair analyzeLogicalOpFwd(
Node expr, TypeEnv inEnv, JSType requiredType, JSType specializedType) {
Token exprKind = expr.getToken();
Node lhs = expr.getFirstChild();
Node rhs = expr.getLastChild();
if ((specializedType.isTrueOrTruthy() && exprKind == Token.AND)
|| (specializedType.isFalseOrFalsy() && exprKind == Token.OR)) {
EnvTypePair lhsPair =
analyzeExprFwd(lhs, inEnv, UNKNOWN, specializedType);
EnvTypePair rhsPair =
analyzeExprFwd(rhs, lhsPair.env, UNKNOWN, specializedType);
return rhsPair;
} else if ((specializedType.isFalseOrFalsy() && exprKind == Token.AND)
|| (specializedType.isTrueOrTruthy() && exprKind == Token.OR)) {
EnvTypePair shortCircuitPair =
analyzeExprFwd(lhs, inEnv, UNKNOWN, specializedType);
EnvTypePair lhsPair = analyzeExprFwd(
lhs, inEnv, UNKNOWN, specializedType.negate());
EnvTypePair rhsPair =
analyzeExprFwd(rhs, lhsPair.env, UNKNOWN, specializedType);
JSType lhsUnspecializedType = JSType.join(shortCircuitPair.type, lhsPair.type);
return combineLhsAndRhsForLogicalOps(
exprKind, lhsUnspecializedType, shortCircuitPair, rhsPair);
} else {
// Independently of the specializedType, && rhs is only analyzed when
// lhs is truthy, and || rhs is only analyzed when lhs is falsy.
JSType stopAfterLhsType = exprKind == Token.AND ? FALSY : TRUTHY;
EnvTypePair shortCircuitPair =
analyzeExprFwd(lhs, inEnv, UNKNOWN, stopAfterLhsType);
EnvTypePair lhsPair = analyzeExprFwd(
lhs, inEnv, UNKNOWN, stopAfterLhsType.negate());
EnvTypePair rhsPair =
analyzeExprFwd(rhs, lhsPair.env, requiredType, specializedType);
JSType lhsUnspecializedType = JSType.join(shortCircuitPair.type, lhsPair.type);
return combineLhsAndRhsForLogicalOps(
exprKind, lhsUnspecializedType, shortCircuitPair, rhsPair);
}
}
private EnvTypePair combineLhsAndRhsForLogicalOps(Token logicalOp,
JSType lhsUnspecializedType, EnvTypePair lhsPair, EnvTypePair rhsPair) {
if (logicalOp == Token.OR) {
if (lhsUnspecializedType.isAnyTruthyType()) {
return lhsPair;
}
if (lhsUnspecializedType.isAnyFalsyType()) {
return rhsPair;
}
lhsPair.type = lhsPair.type.specialize(TRUTHY);
return EnvTypePair.join(lhsPair, rhsPair);
}
Preconditions.checkState(logicalOp == Token.AND);
if (lhsUnspecializedType.isAnyFalsyType()) {
return lhsPair;
}
if (lhsUnspecializedType.isAnyTruthyType()) {
return rhsPair;
}
lhsPair.type = lhsPair.type.specialize(FALSY);
return EnvTypePair.join(lhsPair, rhsPair);
}
private EnvTypePair analyzeIncDecFwd(
Node expr, TypeEnv inEnv, JSType requiredType) {
mayWarnAboutConst(expr);
Node ch = expr.getFirstChild();
if (ch.isGetProp() || ch.isGetElem() && ch.getLastChild().isString()) {
// We prefer to analyze the child of INC/DEC one extra time here,
// to putting the @const prop check in analyzePropAccessFwd.
Node recv = ch.getFirstChild();
String pname = ch.getLastChild().getString();
EnvTypePair pair = analyzeExprFwd(recv, inEnv);
JSType recvType = pair.type;
if (mayWarnAboutConstProp(ch, recvType, new QualifiedName(pname))) {
pair.type = requiredType;
return pair;
}
}
return analyzeUnaryNumFwd(expr, inEnv);
}
private EnvTypePair analyzeUnaryNumFwd(Node expr, TypeEnv inEnv) {
// For inc and dec on a getprop, we don't want to create a property on
// a struct by accident.
// But we will get an inexistent-property warning, so we don't check
// for structness separately here.
Node child = expr.getFirstChild();
EnvTypePair pair = analyzeExprFwd(child, inEnv, NUMBER);
if (!commonTypes.isNumberScalarOrObj(pair.type)) {
warnInvalidOperand(child, expr.getToken(), NUMBER, pair.type);
}
pair.type = NUMBER;
return pair;
}
private EnvTypePair analyzeInstanceofFwd(
Node expr, TypeEnv inEnv, JSType specializedType) {
Node obj = expr.getFirstChild();
Node ctor = expr.getLastChild();
EnvTypePair objPair, ctorPair;
// First, evaluate ignoring the specialized context
objPair = analyzeExprFwd(obj, inEnv);
JSType objType = objPair.type;
if (!objType.isTop()
&& !objType.isUnknown()
&& !objType.isTrueOrTruthy()
&& !objType.hasNonScalar()
&& !objType.hasTypeVariable()) {
warnInvalidOperand(
obj, Token.INSTANCEOF,
"an object or a union type that includes an object",
objPair.type);
}
ctorPair = analyzeExprFwd(ctor, objPair.env, commonTypes.topFunction());
JSType ctorType = ctorPair.type;
FunctionType ctorFunType = ctorType.getFunType();
boolean mayBeConstructorFunction = ctorFunType != null
&& (ctorFunType.isLoose()
|| ctorFunType.isQmarkFunction()
|| ctorFunType.isSomeConstructorOrInterface());
if (!(ctorType.isUnknown() || mayBeConstructorFunction)) {
warnInvalidOperand(
ctor, Token.INSTANCEOF, "a constructor function", ctorType);
}
if (ctorFunType == null
|| !ctorFunType.isUniqueConstructor()
|| (!specializedType.isTrueOrTruthy()
&& !specializedType.isFalseOrFalsy())) {
ctorPair.type = BOOLEAN;
return ctorPair;
}
// We are in a specialized context *and* we know the constructor type
JSType instanceType = ctorFunType.getInstanceTypeOfCtor();
JSType instanceSpecType;
if (specializedType.isTrueOrTruthy()) {
instanceSpecType = objType.specialize(instanceType);
} else if (objType.isTop()) {
instanceSpecType = objType;
} else {
instanceSpecType = objType.removeType(instanceType);
}
if (!instanceSpecType.isBottom()) {
objPair = analyzeExprFwd(obj, inEnv, UNKNOWN, instanceSpecType);
ctorPair = analyzeExprFwd(ctor, objPair.env, commonTypes.topFunction());
}
ctorPair.type = BOOLEAN;
return ctorPair;
}
private EnvTypePair analyzeAddFwd(Node expr, TypeEnv inEnv, JSType requiredType) {
Node lhs = expr.getFirstChild();
Node rhs = expr.getLastChild();
JSType operandType = requiredType.isNumber() ? NUMBER : UNKNOWN;
EnvTypePair lhsPair = analyzeExprFwd(lhs, inEnv, operandType);
EnvTypePair rhsPair = analyzeExprFwd(rhs, lhsPair.env, operandType);
JSType lhsType = lhsPair.type;
JSType rhsType = rhsPair.type;
if (lhsType.isString() || rhsType.isString()) {
// Return early and don't warn, since '' + expr is used for type coercions
rhsPair.type = STRING;
return rhsPair;
}
if (!commonTypes.isNumStrScalarOrObj(lhsType)) {
warnInvalidOperand(lhs, expr.getToken(), NUMBER_OR_STRING, lhsType);
}
if (!commonTypes.isNumStrScalarOrObj(rhsType)) {
warnInvalidOperand(rhs, expr.getToken(), NUMBER_OR_STRING, rhsType);
}
return new EnvTypePair(rhsPair.env, JSType.plus(lhsType, rhsType));
}
private EnvTypePair analyzeBinaryNumericOpFwd(Node expr, TypeEnv inEnv) {
Node lhs = expr.getFirstChild();
Node rhs = expr.getLastChild();
EnvTypePair lhsPair = analyzeExprFwd(lhs, inEnv, NUMBER);
EnvTypePair rhsPair = analyzeExprFwd(rhs, lhsPair.env, NUMBER);
if (!commonTypes.isNumberScalarOrObj(lhsPair.type)) {
warnInvalidOperand(lhs, expr.getToken(), NUMBER, lhsPair.type);
}
if (!commonTypes.isNumberScalarOrObj(rhsPair.type)) {
warnInvalidOperand(rhs, expr.getToken(), NUMBER, rhsPair.type);
}
rhsPair.type = NUMBER;
return rhsPair;
}
private EnvTypePair analyzeAssignFwd(
Node expr, TypeEnv inEnv, JSType requiredType, JSType specializedType) {
if (expr.getBooleanProp(Node.ANALYZED_DURING_GTI)) {
expr.removeProp(Node.ANALYZED_DURING_GTI);
markAndGetTypeOfPreanalyzedNode(expr.getFirstChild(), inEnv, true);
return new EnvTypePair(inEnv, requiredType);
}
mayWarnAboutConst(expr);
Node lhs = expr.getFirstChild();
Node rhs = expr.getLastChild();
if (lhs.getBooleanProp(Node.ANALYZED_DURING_GTI)) {
lhs.removeProp(Node.ANALYZED_DURING_GTI);
JSType declType = markAndGetTypeOfPreanalyzedNode(lhs, inEnv, true);
if (rhs.matchesQualifiedName(ABSTRACT_METHOD_NAME)
|| (GlobalTypeInfo.isCtorDefinedByCall(lhs)
&& !isFunctionBind(rhs.getFirstChild(), inEnv, true))) {
return new EnvTypePair(inEnv, requiredType);
}
EnvTypePair rhsPair = analyzeExprFwd(rhs, inEnv, declType);
if (rhsPair.type.isSubtypeOf(declType)) {
registerImplicitUses(expr, rhsPair.type, declType);
} else if (!NodeUtil.isPrototypeAssignment(lhs)) {
registerMismatchAndWarn(
JSError.make(expr, MISTYPED_ASSIGN_RHS, errorMsgWithTypeDiff(declType, rhsPair.type)),
rhsPair.type, declType);
}
return rhsPair;
}
LValueResultFwd lvalue = analyzeLValueFwd(lhs, inEnv, requiredType);
JSType declType = lvalue.declType;
EnvTypePair rhsPair = analyzeExprFwd(rhs, lvalue.env, requiredType, specializedType);
if (declType == null) {
rhsPair.env = updateLvalueTypeInEnv(rhsPair.env, lhs, lvalue.ptr, rhsPair.type);
} else if (rhsPair.type.isSubtypeOf(declType)) {
registerImplicitUses(expr, rhsPair.type, declType);
rhsPair.env = updateLvalueTypeInEnv(rhsPair.env, lhs, lvalue.ptr, rhsPair.type);
} else {
registerMismatchAndWarn(
JSError.make(expr, MISTYPED_ASSIGN_RHS, errorMsgWithTypeDiff(declType, rhsPair.type)),
rhsPair.type, declType);
}
return rhsPair;
}
private EnvTypePair analyzeAssignAddFwd(
Node expr, TypeEnv inEnv, JSType requiredType) {
mayWarnAboutConst(expr);
Node lhs = expr.getFirstChild();
Node rhs = expr.getLastChild();
JSType lhsReqType =
specializeKeep2ndWhenBottom(requiredType, NUMBER_OR_STRING);
LValueResultFwd lvalue = analyzeLValueFwd(lhs, inEnv, lhsReqType);
JSType lhsType = lvalue.type;
if (!lhsType.isSubtypeOf(NUMBER_OR_STRING)) {
warnInvalidOperand(lhs, Token.ASSIGN_ADD, NUMBER_OR_STRING, lhsType);
}
// if lhs is a string, rhs can still be a number
JSType rhsReqType = lhsType.isNumber() ? NUMBER : NUMBER_OR_STRING;
EnvTypePair pair = analyzeExprFwd(rhs, lvalue.env, rhsReqType);
if (!pair.type.isSubtypeOf(rhsReqType)) {
warnInvalidOperand(rhs, Token.ASSIGN_ADD, rhsReqType, pair.type);
}
return pair;
}
private EnvTypePair analyzeAssignNumericOpFwd(Node expr, TypeEnv inEnv) {
mayWarnAboutConst(expr);
Node lhs = expr.getFirstChild();
Node rhs = expr.getLastChild();
LValueResultFwd lvalue = analyzeLValueFwd(lhs, inEnv, NUMBER);
JSType lhsType = lvalue.type;
boolean lhsWarned = false;
if (!commonTypes.isNumberScalarOrObj(lhsType)) {
warnInvalidOperand(lhs, expr.getToken(), NUMBER, lhsType);
lhsWarned = true;
}
EnvTypePair pair = analyzeExprFwd(rhs, lvalue.env, NUMBER);
if (!commonTypes.isNumberScalarOrObj(pair.type)) {
warnInvalidOperand(rhs, expr.getToken(), NUMBER, pair.type);
}
if (!lhsWarned) {
pair.env = updateLvalueTypeInEnv(pair.env, lhs, lvalue.ptr, NUMBER);
}
pair.type = NUMBER;
return pair;
}
private EnvTypePair analyzeLtGtFwd(Node expr, TypeEnv inEnv) {
Node lhs = expr.getFirstChild();
Node rhs = expr.getLastChild();
EnvTypePair lhsPair = analyzeExprFwd(lhs, inEnv);
EnvTypePair rhsPair = analyzeExprFwd(rhs, lhsPair.env);
// The type of either side can be specialized based on the other side
if (lhsPair.type.isScalar() && !rhsPair.type.isScalar()) {
rhsPair = analyzeExprFwd(rhs, lhsPair.env, lhsPair.type);
} else if (rhsPair.type.isScalar()) {
lhsPair = analyzeExprFwd(lhs, inEnv, rhsPair.type);
rhsPair = analyzeExprFwd(rhs, lhsPair.env, rhsPair.type);
}
JSType lhsType = lhsPair.type;
JSType rhsType = rhsPair.type;
if (!lhsType.isSubtypeOf(rhsType) && !rhsType.isSubtypeOf(lhsType)
&& !(lhsType.isBoolean() && rhsType.isBoolean())) {
warnInvalidOperand(expr, expr.getToken(), "matching types", lhsType + ", " + rhsType);
}
rhsPair.type = BOOLEAN;
return rhsPair;
}
private EnvTypePair analyzeHookFwd(
Node expr, TypeEnv inEnv, JSType requiredType, JSType specializedType) {
Node cond = expr.getFirstChild();
Node thenBranch = cond.getNext();
Node elseBranch = thenBranch.getNext();
TypeEnv trueEnv =
analyzeExprFwd(cond, inEnv, UNKNOWN, TRUTHY).env;
TypeEnv falseEnv =
analyzeExprFwd(cond, inEnv, UNKNOWN, FALSY).env;
EnvTypePair thenPair =
analyzeExprFwd(thenBranch, trueEnv, requiredType, specializedType);
EnvTypePair elsePair =
analyzeExprFwd(elseBranch, falseEnv, requiredType, specializedType);
return EnvTypePair.join(thenPair, elsePair);
}
private EnvTypePair analyzeObjLitCastFwd(ObjectLiteralCast cast, Node call, TypeEnv inEnv) {
if (cast.objectNode == null) {
warnings.add(JSError.make(call, ClosureCodingConvention.OBJECTLIT_EXPECTED));
return new EnvTypePair(inEnv, TOP_OBJECT);
}
EnvTypePair pair = analyzeExprFwd(cast.objectNode, inEnv);
if (!pair.type.isPrototypeObject()) {
warnings.add(JSError.make(call, REFLECT_CONSTRUCTOR_EXPECTED));
}
return new EnvTypePair(pair.env, TOP_OBJECT);
}
private EnvTypePair analyzeCallNewFwd(
Node expr, TypeEnv inEnv, JSType requiredType, JSType specializedType) {
if (isPropertyTestCall(expr)) {
return analyzePropertyTestCallFwd(expr, inEnv, specializedType);
}
if (expr.isCall() && this.convention.getObjectLiteralCast(expr) != null) {
return analyzeObjLitCastFwd(this.convention.getObjectLiteralCast(expr), expr, inEnv);
}
Node callee = expr.getFirstChild();
if (isFunctionBind(callee, inEnv, true)) {
return analyzeFunctionBindFwd(expr, inEnv);
}
AssertionFunctionSpec assertionFunctionSpec =
assertionFunctionsMap.get(callee.getQualifiedName());
if (assertionFunctionSpec != null) {
return analyzeAssertionCall(expr, inEnv, assertionFunctionSpec);
}
EnvTypePair calleePair = analyzeExprFwd(callee, inEnv, commonTypes.topFunction());
TypeEnv envAfterCallee = calleePair.env;
calleePair = mayWarnAboutNullableReferenceAndTighten(
callee, calleePair.type, null, envAfterCallee);
JSType calleeType = calleePair.type;
if (calleeType.isBottom() || !calleeType.isSubtypeOf(commonTypes.topFunction())) {
warnings.add(JSError.make(expr, NOT_CALLABLE, calleeType.toString()));
}
FunctionType funType = calleeType.getFunTypeIfSingletonObj();
if (funType == null
|| funType.isTopFunction() || funType.isQmarkFunction()) {
return analyzeCallNodeArgsFwdWhenError(expr, envAfterCallee);
} else if (funType.isLoose()) {
return analyzeLooseCallNodeFwd(expr, envAfterCallee, requiredType);
} else if (!isConstructorCall(expr)
&& funType.isSomeConstructorOrInterface()
&& (funType.getReturnType().isUnknown()
|| funType.getReturnType().isUndefined())) {
warnings.add(JSError.make(expr, CONSTRUCTOR_NOT_CALLABLE, funType.toString()));
return analyzeCallNodeArgsFwdWhenError(expr, envAfterCallee);
} else if (expr.isNew()) {
if (!funType.isSomeConstructorOrInterface() || funType.isInterfaceDefinition()) {
// When Foo is an interface type, we don't want to warn when someone passes around
// a first-class function of type function(new:Foo); we only want to warn when someone
// directly calls: new Foo.
// But there is no distinction between these two types in the type system, so we use
// a heuristic here: if the function was passed as a formal parameter (either directly
// or as an arbitrarily nested property), don't warn.
if (callee.isQualifiedName()) {
String qnameRoot = QualifiedName.fromNode(callee).getLeftmostName();
if (!this.currentScope.isFormalParamInAnyAncestorScope(qnameRoot)) {
warnings.add(JSError.make(expr, NOT_A_CONSTRUCTOR, funType.toString()));
}
}
return analyzeCallNodeArgsFwdWhenError(expr, envAfterCallee);
} else if (funType.isConstructorOfAbstractClass()) {
warnings.add(JSError.make(expr, CANNOT_INSTANTIATE_ABSTRACT_CLASS, funType.toString()));
return analyzeCallNodeArgsFwdWhenError(expr, envAfterCallee);
}
}
int maxArity = funType.getMaxArity();
int minArity = funType.getMinArity();
int numArgs = expr.getChildCount() - 1;
if (numArgs < minArity || numArgs > maxArity) {
warnings.add(JSError.make(
expr, WRONG_ARGUMENT_COUNT,
getReadableCalleeName(callee),
Integer.toString(numArgs), Integer.toString(minArity),
" and at most " + maxArity));
return analyzeCallNodeArgsFwdWhenError(expr, envAfterCallee);
}
FunctionType origFunType = funType; // save for later
if (funType.isGeneric()) {
Map<String, JSType> typeMap = calcTypeInstantiationFwd(
expr,
callee.isGetProp() ? callee.getFirstChild() : null,
expr.getSecondChild(), funType, envAfterCallee);
funType = funType.instantiateGenerics(typeMap);
println("Instantiated function type: ", funType);
}
// argTypes collects types of actuals for deferred checks.
List<JSType> argTypes = new ArrayList<>();
TypeEnv tmpEnv = analyzeCallNodeArgumentsFwd(
expr, expr.getSecondChild(), funType, argTypes, envAfterCallee);
if (callee.isName()) {
String calleeName = callee.getString();
if (this.currentScope.isKnownFunction(calleeName)
&& !this.currentScope.isExternalFunction(calleeName)) {
// Local function definitions will be type-checked more
// exactly using their summaries, and don't need deferred checks
if (this.currentScope.isLocalFunDef(calleeName)) {
tmpEnv = collectTypesForFreeVarsFwd(callee, tmpEnv);
} else if (!origFunType.isGeneric()) {
JSType expectedRetType = requiredType;
println("Updating deferred check with ret: ", expectedRetType,
" and args: ", argTypes);
DeferredCheck dc;
if (isConstructorCall(expr)) {
dc = new DeferredCheck(expr, null,
this.currentScope, this.currentScope.getScope(calleeName));
deferredChecks.put(expr, dc);
} else {
dc = deferredChecks.get(expr);
if (dc != null) {
dc.updateReturn(expectedRetType);
} else {
// The backward analysis of a function is skipped when all
// variables, including outer vars, are declared.
// So, we check that dc is null iff bwd was skipped.
Preconditions.checkState(
!this.currentScope.hasUndeclaredFormalsOrOuters(),
"No deferred check created in backward direction for %s",
expr);
}
}
if (dc != null) {
dc.updateArgTypes(argTypes);
}
}
}
}
JSType retType = expr.isNew() ? funType.getThisType() : funType.getReturnType();
if (retType.isSubtypeOf(requiredType)) {
retType = retType.specialize(specializedType);
}
return new EnvTypePair(tmpEnv, retType);
}
private boolean isConstructorCall(Node expr) {
return expr.isNew()
|| (expr.isCall() && this.currentScope.isConstructor() && expr.getFirstChild().isSuper());
}
private EnvTypePair analyzeFunctionBindFwd(Node call, TypeEnv inEnv) {
Preconditions.checkArgument(call.isCall());
Bind bindComponents = this.convention.describeFunctionBind(call, true, false);
Node boundFunNode = bindComponents.target;
EnvTypePair pair = analyzeExprFwd(boundFunNode, inEnv);
TypeEnv env = pair.env;
FunctionType boundFunType = pair.type.getFunTypeIfSingletonObj();
if (!pair.type.isSubtypeOf(commonTypes.topFunction())) {
warnings.add(JSError.make(boundFunNode, GOOG_BIND_EXPECTS_FUNCTION, pair.type.toString()));
}
// For some function types, we don't know enough to handle .bind specially.
if (boundFunType == null
|| boundFunType.isTopFunction()
|| boundFunType.isQmarkFunction()
|| boundFunType.isLoose()) {
return analyzeCallNodeArgsFwdWhenError(call, env);
}
if (boundFunType.isSomeConstructorOrInterface()) {
warnings.add(JSError.make(call, CANNOT_BIND_CTOR));
return new EnvTypePair(env, UNKNOWN);
}
// Check if the receiver argument is there
int callChildCount = call.getChildCount();
if (NodeUtil.isGoogBind(call.getFirstChild()) && callChildCount <= 2
|| !NodeUtil.isGoogPartial(call.getFirstChild()) && callChildCount == 1) {
warnings.add(JSError.make(
call, WRONG_ARGUMENT_COUNT,
getReadableCalleeName(call.getFirstChild()),
"0", "1", ""));
}
// Check that there are not too many of the other arguments
int maxArity = boundFunType.hasRestFormals()
? Integer.MAX_VALUE : boundFunType.getMaxArity();
int numArgs = bindComponents.getBoundParameterCount();
if (numArgs > maxArity) {
warnings.add(JSError.make(
call, WRONG_ARGUMENT_COUNT,
getReadableCalleeName(call.getFirstChild()),
Integer.toString(numArgs), "0",
" and at most " + maxArity));
return analyzeCallNodeArgsFwdWhenError(call, inEnv);
}
// If the bound function is polymorphic, we only support the case where we
// can completely calculate the type instantiation at the .bind call site.
// We don't support splitting the instantiation between call sites.
//
Node receiver = bindComponents.thisValue;
if (boundFunType.isGeneric()) {
Map<String, JSType> typeMap = calcTypeInstantiationFwd(
call, receiver, bindComponents.parameters, boundFunType, env);
boundFunType = boundFunType.instantiateGenerics(typeMap);
}
FunctionTypeBuilder builder = new FunctionTypeBuilder(this.commonTypes);
if (receiver != null) {// receiver is null for goog.partial
JSType reqThisType = boundFunType.getThisType();
if (reqThisType == null || boundFunType.isSomeConstructorOrInterface()) {
reqThisType = JSType.join(NULL, TOP_OBJECT);
}
pair = analyzeExprFwd(receiver, env, reqThisType);
env = pair.env;
if (!pair.type.isSubtypeOf(reqThisType)) {
warnings.add(JSError.make(call, INVALID_THIS_TYPE_IN_BIND,
errorMsgWithTypeDiff(reqThisType, pair.type)));
}
}
// We're passing an arraylist but don't do deferred checks for bind.
env = analyzeCallNodeArgumentsFwd(call, bindComponents.parameters,
boundFunType, new ArrayList<JSType>(), env);
// For any formal not bound here, add it to the resulting function type.
for (int j = numArgs; j < boundFunType.getMaxArityWithoutRestFormals(); j++) {
JSType formalType = boundFunType.getFormalType(j);
if (boundFunType.isRequiredArg(j)) {
builder.addReqFormal(formalType);
} else {
builder.addOptFormal(formalType);
}
}
if (boundFunType.hasRestFormals()) {
builder.addRestFormals(boundFunType.getRestFormalsType());
}
return new EnvTypePair(env, commonTypes.fromFunctionType(
builder.addRetType(boundFunType.getReturnType()).buildFunction()));
}
private TypeEnv analyzeCallNodeArgumentsFwd(Node call, Node firstArg,
FunctionType funType, List<JSType> argTypesForDeferredCheck,
TypeEnv inEnv) {
TypeEnv env = inEnv;
Node arg = firstArg;
int i = 0;
while (arg != null) {
JSType formalType = funType.getFormalType(i);
Preconditions.checkState(!formalType.isBottom());
EnvTypePair pair = analyzeExprFwd(arg, env, formalType);
JSType argTypeForDeferredCheck = pair.type;
// Allow passing undefined for an optional argument.
if (funType.isOptionalArg(i) && pair.type.equals(UNDEFINED)) {
argTypeForDeferredCheck = null; // No deferred check needed.
} else if (!pair.type.isSubtypeOf(formalType)) {
String fnName = getReadableCalleeName(call.getFirstChild());
JSError error = JSError.make(arg, INVALID_ARGUMENT_TYPE,
Integer.toString(i + 1), fnName, errorMsgWithTypeDiff(formalType, pair.type));
registerMismatchAndWarn(error, pair.type, formalType);
argTypeForDeferredCheck = null; // No deferred check needed.
} else {
registerImplicitUses(arg, pair.type, formalType);
}
argTypesForDeferredCheck.add(argTypeForDeferredCheck);
env = pair.env;
arg = arg.getNext();
i++;
}
return env;
}
private EnvTypePair analyzeAssertionCall(
Node callNode, TypeEnv env, AssertionFunctionSpec assertionFunctionSpec) {
Node firstParam = callNode.getSecondChild();
if (firstParam == null) {
return new EnvTypePair(env, UNKNOWN);
}
Node assertedNode = assertionFunctionSpec.getAssertedParam(firstParam);
if (assertedNode == null) {
return new EnvTypePair(env, UNKNOWN);
}
JSType assertedType = assertionFunctionSpec.getAssertedNewType(callNode, currentScope);
if (assertedType.isUnknown()) {
warnings.add(JSError.make(callNode, UNKNOWN_ASSERTION_TYPE));
}
EnvTypePair pair = analyzeExprFwd(assertedNode, env, UNKNOWN, assertedType);
boolean haveCommonSubtype = JSType.haveCommonSubtype(assertedType, pair.type);
if (!pair.type.isSubtypeOf(assertedType) && haveCommonSubtype) {
// We do this because the assertion needs to return a subtype of the
// asserted type to its context, but sometimes the asserted expression
// can't be specialized.
pair.type = assertedType;
}
if (!haveCommonSubtype) {
JSType t = analyzeExprFwd(assertedNode, env).type.substituteGenericsWithUnknown();
if (t.isSubtypeOf(assertedType)) {
pair.type = t;
} else {
if (!firstParam.isFalse()) { // Don't warn for an explicit: assert(false);
warnings.add(JSError.make(assertedNode, ASSERT_FALSE));
}
pair.type = UNKNOWN;
pair.env = env;
}
}
return pair;
}
private EnvTypePair analyzeGetElemFwd(
Node expr, TypeEnv inEnv, JSType requiredType, JSType specializedType) {
Node receiver = expr.getFirstChild();
Node index = expr.getLastChild();
JSType reqObjType = pickReqObjType(expr);
EnvTypePair pair = analyzeExprFwd(receiver, inEnv, reqObjType);
pair = mayWarnAboutNullableReferenceAndTighten(receiver, pair.type, null, pair.env);
JSType recvType = pair.type.autobox();
if (!mayWarnAboutNonObject(receiver, recvType, specializedType)
&& !mayWarnAboutStructPropAccess(receiver, recvType)) {
JSType indexType = recvType.getIndexType();
if (indexType != null) {
pair = analyzeExprFwd(
index, pair.env, indexType.isBottom() ? UNKNOWN : indexType);
mayWarnAboutBadIObjectIndex(index, recvType, pair.type, indexType);
pair.type = getIndexedTypeOrUnknown(recvType);
return pair;
} else if (index.isString()) {
return analyzePropAccessFwd(receiver, index.getString(), inEnv,
requiredType, specializedType);
}
}
pair = analyzeExprFwd(index, pair.env);
pair.type = UNKNOWN;
return pair;
}
private EnvTypePair analyzeInFwd(
Node expr, TypeEnv inEnv, JSType specializedType) {
Node lhs = expr.getFirstChild();
Node rhs = expr.getLastChild();
JSType reqObjType = pickReqObjType(expr);
EnvTypePair pair;
pair = analyzeExprFwd(lhs, inEnv, NUMBER_OR_STRING);
if (!pair.type.isSubtypeOf(NUMBER_OR_STRING)) {
warnInvalidOperand(lhs, Token.IN, NUMBER_OR_STRING, pair.type);
}
pair = analyzeExprFwd(rhs, pair.env, reqObjType);
if (!pair.type.isSubtypeOf(TOP_OBJECT)) {
warnInvalidOperand(rhs, Token.IN, "Object", pair.type);
pair.type = BOOLEAN;
return pair;
}
if (pair.type.isStruct()) {
warnings.add(JSError.make(rhs, IN_USED_WITH_STRUCT));
pair.type = BOOLEAN;
return pair;
}
JSType resultType = BOOLEAN;
if (lhs.isString()) {
QualifiedName pname = new QualifiedName(lhs.getString());
if (specializedType.isTrueOrTruthy()) {
pair = analyzeExprFwd(rhs, inEnv, reqObjType,
reqObjType.withPropertyRequired(pname.getLeftmostName()));
resultType = TRUE_TYPE;
} else if (specializedType.isFalseOrFalsy()) {
pair = analyzeExprFwd(rhs, inEnv, reqObjType);
// If the rhs is a loose object, we won't warn about missing
// properties, despite removing the type here.
// The only way to have that warning would be to keep track of props
// that a loose object *cannot* have; but the implementation cost
// is probably not worth it.
pair = analyzeExprFwd(
rhs, inEnv, reqObjType, pair.type.withoutProperty(pname));
resultType = FALSE_TYPE;
}
}
pair.type = resultType;
return pair;
}
private EnvTypePair analyzeArrayLitFwd(Node expr, TypeEnv inEnv) {
TypeEnv env = inEnv;
JSType elementType = BOTTOM;
for (Node arrayElm = expr.getFirstChild(); arrayElm != null;
arrayElm = arrayElm.getNext()) {
EnvTypePair pair = analyzeExprFwd(arrayElm, env);
env = pair.env;
elementType = JSType.join(elementType, pair.type);
}
if (elementType.isBottom()) {
elementType = UNKNOWN;
}
return new EnvTypePair(env, commonTypes.getArrayInstance(elementType));
}
// Because of the cast, expr doesn't need to have the required type of the context.
// However, we still pass along the specialized type, to specialize types when using
// logical operators.
private EnvTypePair analyzeCastFwd(Node expr, TypeEnv inEnv, JSType specializedType) {
Node parent = expr.getParent();
JSType newSpecType = this.commonTypes.UNKNOWN;
if ((parent.isOr() || parent.isAnd()) && expr == parent.getFirstChild()) {
newSpecType = specializedType;
}
Node insideCast = expr.getFirstChild();
EnvTypePair pair = analyzeExprFwd(insideCast, inEnv, this.commonTypes.UNKNOWN, newSpecType);
JSType fromType = pair.type;
JSType toType = symbolTable.getCastType(expr);
if (!fromType.isInterfaceInstance()
&& !toType.isInterfaceInstance()
&& !JSType.haveCommonSubtype(fromType, toType)
&& !fromType.hasTypeVariable()) {
JSError error = JSError.make(expr, INVALID_CAST, fromType.toString(), toType.toString());
registerMismatchAndWarn(error, fromType, toType);
} else {
registerImplicitUses(expr, fromType, toType);
}
insideCast.putProp(Node.TYPE_BEFORE_CAST, fromType);
insideCast.setTypeI(toType);
pair.type = toType;
return pair;
}
private EnvTypePair analyzeCallNodeArgsFwdWhenError(
Node callNode, TypeEnv inEnv) {
TypeEnv env = inEnv;
for (Node arg = callNode.getSecondChild(); arg != null; arg = arg.getNext()) {
env = analyzeExprFwd(arg, env).env;
}
return new EnvTypePair(env, UNKNOWN);
}
private EnvTypePair analyzeStrictComparisonFwd(Token comparisonOp,
Node lhs, Node rhs, TypeEnv inEnv, JSType specializedType) {
if (specializedType.isTrueOrTruthy() || specializedType.isFalseOrFalsy()) {
if (lhs.isTypeOf()) {
return analyzeSpecializedTypeof(
lhs, rhs, comparisonOp, inEnv, specializedType);
} else if (rhs.isTypeOf()) {
return analyzeSpecializedTypeof(
rhs, lhs, comparisonOp, inEnv, specializedType);
} else if (isGoogTypeof(lhs)) {
return analyzeGoogTypeof(lhs, rhs, inEnv, specializedType);
} else if (isGoogTypeof(rhs)) {
return analyzeGoogTypeof(rhs, lhs, inEnv, specializedType);
}
}
EnvTypePair lhsPair = analyzeExprFwd(lhs, inEnv);
EnvTypePair rhsPair = analyzeExprFwd(rhs, lhsPair.env);
JSType rhstype = rhsPair.type;
JSType lhstype = lhsPair.type;
if (!rhstype.isNullOrUndef()) {
if (JSType.haveCommonSubtype(lhstype, rhstype)) {
registerImplicitUses(lhs, lhstype, rhstype);
} else {
JSError error = JSError.make(
lhs, INCOMPATIBLE_STRICT_COMPARISON, lhstype.toString(), rhstype.toString());
registerMismatchAndWarn(error, lhstype, rhstype);
}
}
// This env may contain types that have been tightened after nullable deref.
TypeEnv preciseEnv = rhsPair.env;
if ((comparisonOp == Token.SHEQ && specializedType.isTrueOrTruthy())
|| (comparisonOp == Token.SHNE && specializedType.isFalseOrFalsy())) {
lhsPair = analyzeExprFwd(lhs, preciseEnv,
UNKNOWN, lhsPair.type.specialize(rhsPair.type));
rhsPair = analyzeExprFwd(rhs, lhsPair.env,
UNKNOWN, rhsPair.type.specialize(lhsPair.type));
} else if ((comparisonOp == Token.SHEQ && specializedType.isFalseOrFalsy()) ||
(comparisonOp == Token.SHNE && specializedType.isTrueOrTruthy())) {
JSType lhsType = lhsPair.type;
JSType rhsType = rhsPair.type;
if (lhsType.isNullOrUndef()) {
rhsType = rhsType.removeType(lhsType);
} else if (rhsType.isNullOrUndef()) {
lhsType = lhsType.removeType(rhsType);
}
lhsPair = analyzeExprFwd(lhs, preciseEnv, UNKNOWN, lhsType);
rhsPair = analyzeExprFwd(rhs, lhsPair.env, UNKNOWN, rhsType);
}
rhsPair.type = BOOLEAN;
return rhsPair;
}
private EnvTypePair analyzeSpecializedTypeof(Node typeof, Node typeString,
Token comparisonOp, TypeEnv inEnv, JSType specializedType) {
EnvTypePair pair;
Node typeofRand = typeof.getFirstChild();
JSType comparedType = getTypeFromString(typeString);
checkInvalidTypename(typeString);
if (comparedType.isUnknown()) {
pair = analyzeExprFwd(typeofRand, inEnv);
pair = analyzeExprFwd(typeString, pair.env);
} else if ((specializedType.isTrueOrTruthy()
&& (comparisonOp == Token.SHEQ || comparisonOp == Token.EQ))
|| (specializedType.isFalseOrFalsy()
&& (comparisonOp == Token.SHNE || comparisonOp == Token.NE))) {
pair = analyzeExprFwd(typeofRand, inEnv, UNKNOWN, comparedType);
} else {
pair = analyzeExprFwd(typeofRand, inEnv);
JSType rmType = pair.type.removeType(comparedType);
if (!rmType.isBottom()) {
pair = analyzeExprFwd(typeofRand, inEnv, UNKNOWN, rmType);
}
}
pair.type = specializedType.toBoolean();
return pair;
}
private EnvTypePair analyzeThisFwd(
Node expr, TypeEnv inEnv, JSType requiredType, JSType specializedType) {
mayWarnAboutGlobalThis(expr, currentScope);
if (!this.currentScope.hasThis()) {
return new EnvTypePair(inEnv, UNKNOWN);
}
// A trimmed-down version of analyzeNameFwd.
JSType inferredType = envGetType(inEnv, THIS_ID);
if (!inferredType.isSubtypeOf(requiredType)) {
return new EnvTypePair(inEnv, inferredType);
}
JSType preciseType = inferredType.specialize(specializedType);
if (preciseType.isBottom()) {
preciseType = pickFallbackTypeAfterBottom(THIS_ID, inferredType, specializedType);
}
return EnvTypePair.addBinding(inEnv, THIS_ID, preciseType);
}
private EnvTypePair analyzeSuperFwd(Node expr, TypeEnv inEnv) {
Preconditions.checkArgument(expr.isSuper());
if (this.currentScope.hasThis()) {
NominalType thisClass = Preconditions.checkNotNull(
envGetType(inEnv, THIS_ID).getNominalTypeIfSingletonObj());
NominalType superClass = thisClass.getInstantiatedSuperclass();
if (superClass == null) {
// This indicates bad code and there will probably be other errors reported.
// In particular JSC_NTI_INHERITANCE_CYCLE for `class Foo extends Foo ...`.
warnings.add(JSError.make(expr, UNDEFINED_SUPER_CLASS, thisClass.toString()));
return new EnvTypePair(inEnv, UNKNOWN);
}
if (this.currentScope.isConstructor()) {
JSType superCtor = commonTypes.fromFunctionType(superClass.getConstructorFunction());
return new EnvTypePair(inEnv, superCtor);
}
return new EnvTypePair(inEnv, superClass.getInstanceAsJSType());
}
// Use of super in a static method.
Node funName = NodeUtil.getBestLValue(this.currentScope.getRoot());
Node classNameNode = funName.getFirstChild();
JSType thisClassAsJstype = analyzeExprFwd(classNameNode, inEnv).type;
FunctionType thisCtor = thisClassAsJstype.getFunTypeIfSingletonObj();
NominalType thisClass = thisCtor.getThisType().getNominalTypeIfSingletonObj();
NominalType superClass = thisClass.getInstantiatedSuperclass();
if (superClass == null) {
// This indicates bad code and there will probably be other errors reported.
// In particular JSC_NTI_INHERITANCE_CYCLE for `class Foo extends Foo ...`.
warnings.add(JSError.make(expr, UNDEFINED_SUPER_CLASS, funName.toString()));
return new EnvTypePair(inEnv, UNKNOWN);
}
return new EnvTypePair(inEnv, superClass.getNamespaceType());
}
private JSType getTypeFromString(Node typeString) {
if (!typeString.isString()) {
return UNKNOWN;
}
switch (typeString.getString()) {
case "number":
return NUMBER;
case "string":
return STRING;
case "boolean":
return BOOLEAN;
case "undefined":
return UNDEFINED;
case "function":
return commonTypes.looseTopFunction();
case "object":
return JSType.join(NULL, TOP_OBJECT);
default:
return UNKNOWN;
}
}
private void checkInvalidTypename(Node typeString) {
if (!typeString.isString()) {
return;
}
String typeName = typeString.getString();
switch (typeName) {
case "number":
case "string":
case "boolean":
case "undefined":
case "function":
case "object":
case "symbol":
case "unknown": // IE-specific type name
break;
default:
warnings.add(JSError.make(typeString, UNKNOWN_TYPEOF_VALUE, typeName));
}
}
private Map<String, JSType> calcTypeInstantiationFwd(
Node callNode, Node receiver, Node firstArg, FunctionType funType, TypeEnv typeEnv) {
return calcTypeInstantiation(callNode, receiver, firstArg, funType, typeEnv, true);
}
private Map<String, JSType> calcTypeInstantiationBwd(
Node callNode, FunctionType funType, TypeEnv typeEnv) {
return calcTypeInstantiation(
callNode, null, callNode.getSecondChild(), funType, typeEnv, false);
}
/**
* We don't use the requiredType of the context to unify with the return
* type. There are several difficulties:
* 1) A polymorphic function is allowed to return ANY subtype of the
* requiredType, so we would need to use a heuristic to determine the type
* to unify with.
* 2) It's hard to give good error messages in cases like: id('str') - 5
* We want an invalid-operand-type, not a not-unique-instantiation.
*
*
* We don't take the arg evaluation order into account during instantiation.
*
*
* When calculating the instantiation, when do we use the receiver type?
* See the following snippet:
* /**
* * @constructor
* * @template T
* * @param {T} x
* * /
* function Foo(x) {}
* /**
* * @template T
* * @param {T} x
* * /
* Foo.prototype.f = function(x) {};
* Foo.prototype.f.bind(new Foo(123), 'asdf');
*
* Here, the receiver type of f is Foo<T>, but the T is the class's T,
* not the T of f's template declaration.
* OTOH, if f had a @this annotation that contained T, T would refer to
* f's T. There is no way of knowing what's the scope of the type variables
* in the receiver of the function type.
* But when THIS comes from the class, it is always a singleton object. So,
* we use a heuristic: if THIS is not a singleton obj, we know it comes from
* @this, and we use it for the instantiation.
*/
private ImmutableMap<String, JSType> calcTypeInstantiation(
Node callNode, Node receiver, Node firstArg,
FunctionType funType, TypeEnv typeEnv, boolean isFwd) {
Preconditions.checkState(receiver == null || isFwd);
List<String> typeParameters = funType.getTypeParameters();
Multimap<String, JSType> typeMultimap = LinkedHashMultimap.create();
JSType funRecvType = funType.getThisType();
if (receiver != null && funRecvType != null && !funRecvType.isSingletonObj()) {
JSType recvType = (JSType) receiver.getTypeI();
if (recvType == null) {
EnvTypePair pair = analyzeExprFwd(receiver, typeEnv);
recvType = pair.type;
typeEnv = pair.env;
}
unifyWithSubtypeWarnIfFail(
funRecvType, recvType, typeParameters, typeMultimap, receiver, isFwd);
}
Node arg = firstArg;
int i = 0;
while (arg != null) {
EnvTypePair pair = isFwd ? analyzeExprFwd(arg, typeEnv) : analyzeExprBwd(arg, typeEnv);
unifyWithSubtypeWarnIfFail(funType.getFormalType(i), pair.type,
typeParameters, typeMultimap, arg, isFwd);
arg = arg.getNext();
typeEnv = pair.env;
i++;
}
ImmutableMap.Builder<String, JSType> builder = ImmutableMap.builder();
for (String typeParam : typeParameters) {
Collection<JSType> types = typeMultimap.get(typeParam);
if (types.size() > 1) {
if (isFwd) {
warnings.add(JSError.make(
callNode, NOT_UNIQUE_INSTANTIATION,
Integer.toString(types.size()),
UniqueNameGenerator.getOriginalName(typeParam),
types.toString(),
funType.toString()));
}
if (joinTypesWhenInstantiatingGenerics) {
JSType joinedType = BOTTOM;
for (JSType t : types) {
joinedType = JSType.join(joinedType, t);
}
builder.put(typeParam, joinedType);
} else {
builder.put(typeParam, UNKNOWN);
}
} else if (types.size() == 1) {
JSType t = Iterables.getOnlyElement(types);
builder.put(typeParam, t.isBottom() ? UNKNOWN : t);
} else {
// Put ? for any uninstantiated type variables
builder.put(typeParam, UNKNOWN);
}
}
return builder.build();
}
private void unifyWithSubtypeWarnIfFail(JSType genericType, JSType concreteType,
List<String> typeParameters, Multimap<String, JSType> typeMultimap,
Node toWarnOn, boolean isFwd) {
if (!genericType.unifyWith(concreteType, typeParameters, typeMultimap) && isFwd) {
// Unification may fail b/c of types irrelevant to generics, eg,
// number vs string.
// In this case, don't warn here; we'll show invalid-arg-type later.
JSType afterInstantiation = genericType.substituteGenericsWithUnknown();
if (!concreteType.isLoose()
&& !genericType.equals(afterInstantiation)
&& concreteType.isSubtypeOf(afterInstantiation)) {
warnings.add(JSError.make(toWarnOn, FAILED_TO_UNIFY,
genericType.toString(), concreteType.toString()));
}
}
}
private EnvTypePair analyzeNonStrictComparisonFwd(
Node expr, TypeEnv inEnv, JSType specializedType) {
Token tokenType = expr.getToken();
Node lhs = expr.getFirstChild();
Node rhs = expr.getLastChild();
if (specializedType.isTrueOrTruthy() || specializedType.isFalseOrFalsy()) {
if (lhs.isTypeOf()) {
return analyzeSpecializedTypeof(
lhs, rhs, tokenType, inEnv, specializedType);
} else if (rhs.isTypeOf()) {
return analyzeSpecializedTypeof(
rhs, lhs, tokenType, inEnv, specializedType);
} else if (isGoogTypeof(lhs)) {
return analyzeGoogTypeof(lhs, rhs, inEnv, specializedType);
} else if (isGoogTypeof(rhs)) {
return analyzeGoogTypeof(rhs, lhs, inEnv, specializedType);
}
}
EnvTypePair lhsPair = analyzeExprFwd(lhs, inEnv);
EnvTypePair rhsPair = analyzeExprFwd(rhs, lhsPair.env);
// This env may contain types that have been tightened after nullable deref.
TypeEnv preciseEnv = rhsPair.env;
JSType lhsType = lhsPair.type;
JSType rhsType = rhsPair.type;
if (tokenType == Token.EQ && specializedType.isTrueOrTruthy()
|| tokenType == Token.NE && specializedType.isFalseOrFalsy()) {
if (lhsType.isNullOrUndef()) {
rhsPair = analyzeExprFwd(
rhs, preciseEnv, UNKNOWN, NULL_OR_UNDEFINED);
} else if (rhsType.isNullOrUndef()) {
lhsPair = analyzeExprFwd(
lhs, preciseEnv, UNKNOWN, NULL_OR_UNDEFINED);
rhsPair = analyzeExprFwd(rhs, lhsPair.env);
} else if (!NULL.isSubtypeOf(lhsType) && !UNDEFINED.isSubtypeOf(lhsType)) {
rhsType = rhsType.removeType(NULL_OR_UNDEFINED);
rhsPair = analyzeExprFwd(rhs, preciseEnv, UNKNOWN, rhsType);
} else if (!NULL.isSubtypeOf(rhsType) && !UNDEFINED.isSubtypeOf(rhsType)) {
lhsType = lhsType.removeType(NULL_OR_UNDEFINED);
lhsPair = analyzeExprFwd(lhs, preciseEnv, UNKNOWN, lhsType);
rhsPair = analyzeExprFwd(rhs, lhsPair.env);
}
} else if (tokenType == Token.EQ && specializedType.isFalseOrFalsy()
|| tokenType == Token.NE && specializedType.isTrueOrTruthy()) {
if (lhsType.isNullOrUndef()) {
rhsType = rhsType.removeType(NULL_OR_UNDEFINED);
rhsPair = analyzeExprFwd(rhs, preciseEnv, UNKNOWN, rhsType);
} else if (rhsType.isNullOrUndef()) {
lhsType = lhsType.removeType(NULL_OR_UNDEFINED);
lhsPair = analyzeExprFwd(lhs, preciseEnv, UNKNOWN, lhsType);
rhsPair = analyzeExprFwd(rhs, lhsPair.env);
}
}
rhsPair.type = BOOLEAN;
return rhsPair;
}
private EnvTypePair analyzeObjLitFwd(
Node objLit, TypeEnv inEnv, JSType requiredType, JSType specializedType) {
if (NodeUtil.isEnumDecl(objLit.getParent())) {
return analyzeEnumObjLitFwd(objLit, inEnv, requiredType);
}
JSDocInfo jsdoc = objLit.getJSDocInfo();
boolean isStruct = jsdoc != null && jsdoc.makesStructs();
boolean isDict = jsdoc != null && jsdoc.makesDicts();
TypeEnv env = inEnv;
JSType result = pickReqObjType(objLit);
for (Node prop : objLit.children()) {
if (isStruct && prop.isQuotedString()) {
warnings.add(JSError.make(prop, ILLEGAL_OBJLIT_KEY, "struct"));
} else if (isDict && !prop.isQuotedString()) {
warnings.add(JSError.make(prop, ILLEGAL_OBJLIT_KEY, "dict"));
}
String pname = NodeUtil.getObjectLitKeyName(prop);
// We can't assign to a getter to change its value.
// We can't do a prop access on a setter.
// So, we don't associate pname with a getter/setter.
// We add a property with a name that's weird enough to hopefully avoid
// an accidental clash.
if (prop.isGetterDef() || prop.isSetterDef()) {
EnvTypePair pair = analyzeExprFwd(prop.getFirstChild(), env);
FunctionType funType = pair.type.getFunType();
Preconditions.checkNotNull(funType);
String specialPropName;
JSType propType;
if (prop.isGetterDef()) {
specialPropName = JSType.createGetterPropName(pname);
propType = funType.getReturnType();
} else {
specialPropName = JSType.createSetterPropName(pname);
propType = pair.type;
}
result = result.withProperty(new QualifiedName(specialPropName), propType);
env = pair.env;
} else {
QualifiedName qname = new QualifiedName(pname);
JSType jsdocType = symbolTable.getPropDeclaredType(prop);
JSType reqPtype, specPtype;
if (jsdocType != null) {
reqPtype = specPtype = jsdocType;
} else if (requiredType.mayHaveProp(qname)) {
reqPtype = specPtype = requiredType.getProp(qname);
if (specializedType.mayHaveProp(qname)) {
specPtype = specializedType.getProp(qname);
}
} else {
reqPtype = specPtype = UNKNOWN;
}
EnvTypePair pair = analyzeExprFwd(prop.getFirstChild(), env, reqPtype, specPtype);
if (jsdocType != null) {
// First declare it; then set the maybe more precise inferred type
result = result.withDeclaredProperty(qname, jsdocType, false);
if (!pair.type.isSubtypeOf(jsdocType)) {
warnings.add(JSError.make(
prop, INVALID_OBJLIT_PROPERTY_TYPE,
errorMsgWithTypeDiff(jsdocType, pair.type)));
pair.type = jsdocType;
}
}
result = result.withProperty(qname, pair.type);
env = pair.env;
}
}
result = mayAdjustObjLitType(objLit, jsdoc, inEnv, result);
return new EnvTypePair(env, result);
}
/**
* If the object literal is lended, or assigned to a prototype, find a better
* type for it than the object-literal type.
*/
private JSType mayAdjustObjLitType(
Node objLit, JSDocInfo jsdoc, TypeEnv env, JSType originalType) {
Node parent = objLit.getParent();
QualifiedName classqname = null;
if (parent.isAssign() && NodeUtil.isPrototypeAssignment(parent.getFirstChild())) {
classqname = QualifiedName.fromNode(parent.getFirstFirstChild());
} else if (jsdoc != null && jsdoc.getLendsName() != null) {
QualifiedName lendsQname = QualifiedName.fromQualifiedString(jsdoc.getLendsName());
if (lendsQname.getRightmostName().equals("prototype")) {
classqname = lendsQname.getAllButRightmost();
}
} else if (parent.isCall() && this.convention.getObjectLiteralCast(parent) != null) {
ObjectLiteralCast cast = this.convention.getObjectLiteralCast(parent);
if (cast.typeName != null) {
classqname = QualifiedName.fromQualifiedString(cast.typeName);
}
}
if (classqname != null) {
JSType classType = envGetTypeOfQname(env, classqname);
if (classType != null) {
FunctionType clazz = classType.getFunTypeIfSingletonObj();
JSType instance = clazz == null ? null : clazz.getInstanceTypeOfCtor();
if (instance != null) {
return instance.getPrototypeObject();
}
}
}
return originalType;
}
private EnvTypePair analyzeEnumObjLitFwd(
Node objLit, TypeEnv inEnv, JSType requiredType) {
// We warn about malformed enum declarations in GlobalTypeInfo,
// so we ignore them here.
if (objLit.getFirstChild() == null) {
return new EnvTypePair(inEnv, requiredType);
}
String pname = NodeUtil.getObjectLitKeyName(objLit.getFirstChild());
JSType enumeratedType =
requiredType.getProp(new QualifiedName(pname)).getEnumeratedTypeOfEnumElement();
if (enumeratedType == null) {
// enumeratedType is null only if there is some other type error
return new EnvTypePair(inEnv, requiredType);
}
TypeEnv env = inEnv;
for (Node prop : objLit.children()) {
EnvTypePair pair =
analyzeExprFwd(prop.getFirstChild(), env, enumeratedType);
if (!pair.type.isSubtypeOf(enumeratedType)) {
warnings.add(JSError.make(
prop, INVALID_OBJLIT_PROPERTY_TYPE,
errorMsgWithTypeDiff(enumeratedType, pair.type)));
}
env = pair.env;
}
return new EnvTypePair(env, requiredType);
}
private EnvTypePair analyzeTypePredicate(
Node call, String typeHint, TypeEnv inEnv, JSType specializedType) {
int numArgs = call.getChildCount() - 1;
if (numArgs != 1) {
warnings.add(JSError.make(call, WRONG_ARGUMENT_COUNT,
call.getFirstChild().getQualifiedName(),
Integer.toString(numArgs), "1", "1"));
return analyzeCallNodeArgsFwdWhenError(call, inEnv);
}
EnvTypePair pair = analyzeExprFwd(call.getLastChild(), inEnv);
if (specializedType.isTrueOrTruthy() || specializedType.isFalseOrFalsy()) {
pair = analyzeExprFwd(call.getLastChild(), inEnv, UNKNOWN,
predicateTransformType(typeHint, specializedType, pair.type));
}
pair.type = BOOLEAN;
return pair;
}
private EnvTypePair analyzeGoogTypeof(
Node typeof, Node typeString, TypeEnv inEnv, JSType specializedType) {
return analyzeTypePredicate(typeof,
typeString.isString() ? typeString.getString() : "",
inEnv, specializedType);
}
private EnvTypePair analyzePropertyTestCallFwd(
Node call, TypeEnv inEnv, JSType specializedType) {
return analyzeTypePredicate(call,
call.getFirstChild().getLastChild().getString(),
inEnv, specializedType);
}
// typeHint can come from goog.typeOf or from any function
// in CodingConvention's isPropertyTestFunction.
private JSType predicateTransformType(
String typeHint, JSType booleanContext, JSType beforeType) {
switch (typeHint) {
case "array":
case "isArray": {
JSType arrayType = commonTypes.getArrayInstance();
if (arrayType.isUnknown()) {
return UNKNOWN;
}
return booleanContext.isTrueOrTruthy()
? arrayType : beforeType.removeType(arrayType);
}
case "isArrayLike":
return TOP_OBJECT.withProperty(new QualifiedName("length"), NUMBER);
case "boolean":
case "isBoolean":
return booleanContext.isTrueOrTruthy()
? BOOLEAN : beforeType.removeType(BOOLEAN);
case "function":
case "isFunction":
return booleanContext.isTrueOrTruthy()
? commonTypes.looseTopFunction()
: beforeType.removeType(commonTypes.topFunction());
case "null":
case "isNull":
return booleanContext.isTrueOrTruthy()
? NULL : beforeType.removeType(NULL);
case "number":
case "isNumber":
return booleanContext.isTrueOrTruthy()
? NUMBER : beforeType.removeType(NUMBER);
case "string":
case "isString":
return booleanContext.isTrueOrTruthy()
? STRING : beforeType.removeType(STRING);
case "isDef":
return booleanContext.isTrueOrTruthy()
? beforeType.removeType(UNDEFINED) : UNDEFINED;
case "isDefAndNotNull":
return booleanContext.isTrueOrTruthy()
? beforeType.removeType(NULL_OR_UNDEFINED) : NULL_OR_UNDEFINED;
case "isObject":
// typeof(null) === 'object', but goog.isObject(null) is false
return booleanContext.isTrueOrTruthy()
? TOP_OBJECT : beforeType.removeType(TOP_OBJECT);
case "object":
// goog.typeOf(expr) === 'object' is true only for non-function objects.
// Just do sth simple here.
return UNKNOWN;
case "undefined":
return booleanContext.isTrueOrTruthy()
? UNDEFINED : beforeType.removeType(UNDEFINED);
default:
// For when we can't figure out the type name used with goog.typeOf.
return UNKNOWN;
}
}
/**
* In certain cases, tightens the type of a variable to avoid warning.
*
* The intent is to be looser about warnings in the case when a value is passed to a function
* (as opposed to being created locally), because then we have less information about the value.
* The accurate way to do this is with taint tracking: mark types so that we can track where a
* type comes from (local or not).
* Without tainting (e.g., we used to have locations on types), we approximate this by checking
* the variable name.
*/
private boolean tightenNameTypeAndDontWarn(
String varName, Node n, JSType declared, JSType inferred, JSType required) {
boolean isSpecializableTop = declared != null && declared.isTop()
&& (!inferred.isTop() || NodeUtil.isPropertyTest(compiler, n.getParent()));
boolean fuzzyDeclaration = declared == null || declared.isUnknown();
return (fuzzyDeclaration || isSpecializableTop)
&& (varName == null || this.currentScope.isFormalParam(varName)
|| this.currentScope.isOuterVar(varName))
// If required is loose, it's easier for it to be a subtype of inferred.
// We only tighten the type if the non-loose required is also a subtype.
&& required.isNonLooseSubtypeOf(inferred);
}
/**
* In certain cases, tightens the type of a property to avoid warning.
*
* Don't do it if the receiver is a namespace or a prototype object: these are more "static"
* than some instance type, so we can be stricter.
* The heuristics for tightening the type are similar to @see #tightenNameTypeAndDontWarn.
*/
private boolean tightenPropertyTypeAndDontWarn(String recvName, Node propAccessNode,
JSType recvType, JSType propDeclType, JSType propInferredType, JSType propRequiredType) {
if (recvType != null && (recvType.isNamespace() || recvType.isPrototypeObject())) {
return false;
}
boolean isSpecializableTop = propDeclType != null && propDeclType.isTop()
&& (!propInferredType.isTop() || NodeUtil.isPropertyTest(compiler, propAccessNode));
boolean fuzzyDeclaration = propDeclType == null || propDeclType.isUnknown();
return (fuzzyDeclaration || isSpecializableTop)
&& (recvName == null || currentScope.isFormalParam(recvName)
|| currentScope.isOuterVar(recvName))
// If required is loose, it's easier for it to be a subtype of inferred.
// We only tighten the type if the non-loose required is also a subtype.
// Otherwise, we would be skipping warnings too often.
// This is important b/c analyzePropAccess & analyzePropLvalue introduce
// loose objects, even if there are no undeclared formals.
&& propRequiredType.isNonLooseSubtypeOf(propInferredType);
}
private static String errorMsgWithTypeDiff(JSType expected, JSType found) {
MismatchInfo mismatch = JSType.whyNotSubtypeOf(found, expected);
if (mismatch == null) {
return "Expected : " + expected + "\n"
+ "Found : " + found + "\n";
}
StringBuilder builder =
new StringBuilder("Expected : ")
.append(expected)
.append("\n" + "Found : ")
.append(found)
.append("\n" + "More details:\n");
if (mismatch.isPropMismatch()) {
builder
.append("Incompatible types for property ")
.append(mismatch.getPropName())
.append(".\n" + "Expected : ")
.append(mismatch.getExpectedType())
.append("\n" + "Found : ")
.append(mismatch.getFoundType());
} else if (mismatch.isMissingProp()) {
builder.append("The found type is missing property ").append(mismatch.getPropName());
} else if (mismatch.wantedRequiredFoundOptional()) {
builder
.append("In found type, property ")
.append(mismatch.getPropName())
.append(" is optional but should be required.");
} else if (mismatch.isArgTypeMismatch()) {
builder
.append(
"The expected and found types are functions which have"
+ " incompatible types for argument ")
.append(mismatch.getArgIndex() + 1)
.append(".\n" + "Expected a supertype of : ")
.append(mismatch.getExpectedType())
.append("\n" + "but found : ")
.append(mismatch.getFoundType());
} else if (mismatch.isRetTypeMismatch()) {
builder
.append(
"The expected and found types are functions which have"
+ " incompatible return types.\n"
+ "Expected a subtype of : ")
.append(mismatch.getExpectedType())
.append("\n" + "but found : ")
.append(mismatch.getFoundType());
} else if (mismatch.isUnionTypeMismatch()) {
builder
.append("The found type is a union that includes an unexpected type: ")
.append(mismatch.getFoundType());
}
return builder.toString();
}
//////////////////////////////////////////////////////////////////////////////
// These functions return true iff they produce a warning
private boolean mayWarnAboutNonObject(
Node receiver, JSType recvType, JSType specializedType) {
// Can happen for IF tests that are never true
if (recvType.isBottom()) {
return true;
}
// The warning depends on whether we are testing for the existence of a
// property.
boolean isNotAnObject = !JSType.haveCommonSubtype(recvType, TOP_OBJECT);
boolean mayNotBeAnObject = !recvType.isSubtypeOf(TOP_OBJECT);
if (isNotAnObject
|| (!specializedType.isTrueOrTruthy() && !specializedType.isFalseOrFalsy()
&& mayNotBeAnObject)) {
warnings.add(JSError.make(receiver, PROPERTY_ACCESS_ON_NONOBJECT,
getPropNameForErrorMsg(receiver.getParent()),
recvType.toString()));
return true;
}
return false;
}
private String getPropNameForErrorMsg(Node propAccessNode) {
Preconditions.checkArgument(propAccessNode.isGetProp() || propAccessNode.isGetElem());
Node propNode = propAccessNode.getLastChild();
if (propNode.isString()) {
return propNode.getString();
} else if (propNode.isQualifiedName()) {
return "[" + propNode.getQualifiedName() + "]";
}
return "[unknown property]";
}
private boolean mayWarnAboutStructPropAccess(Node obj, JSType type) {
if (type.mayBeStruct()) {
warnings.add(JSError.make(obj, ILLEGAL_PROPERTY_ACCESS, "'[]'", "struct"));
return true;
}
return false;
}
private boolean mayWarnAboutDictPropAccess(Node obj, JSType type) {
if (type.mayBeDict()) {
warnings.add(JSError.make(obj, ILLEGAL_PROPERTY_ACCESS, "'.'", "dict"));
return true;
}
return false;
}
private boolean mayWarnAboutPropCreation(
QualifiedName pname, Node getProp, JSType recvType) {
Preconditions.checkArgument(getProp.isGetProp());
// For inferred formals used as objects, we don't warn about property
// creation. Consider:
// function f(obj) { obj.prop = 123; }
// f should accept objects without prop, so we don't require that obj
// already have prop.
if (recvType.isStructWithoutProp(pname)) {
warnings.add(JSError.make(getProp, ILLEGAL_PROPERTY_CREATION, pname.toString()));
return true;
}
return false;
}
private boolean mayWarnAboutInexistentProp(Node propAccessNode,
JSType recvType, QualifiedName propQname) {
Preconditions.checkState(propAccessNode.isGetProp() || propAccessNode.isGetElem());
String pname = propQname.toString();
if (propAccessNode.isGetElem()
// Loose types always "have" the properties accessed on them, because of
// type inference. If the property is not defined anywhere though,
// we still want to warn in that case.
|| !recvType.isLoose() && recvType.hasProp(propQname)) {
return false;
}
if (recvType.isUnknown() || recvType.isTrueOrTruthy() || recvType.isLoose()
|| (allowPropertyOnSubtypes
&& (recvType.mayContainUnknownObject() || recvType.isIObject()))) {
if (symbolTable.isPropertyDefined(pname)) {
return false;
}
warnings.add(JSError.make(
propAccessNode, INEXISTENT_PROPERTY, pname, "any type in the program"));
return true;
}
if (allowPropertyOnSubtypes && !recvType.isStruct()
&& recvType.isPropDefinedOnSubtype(propQname)) {
return false;
}
// To avoid giant types in the error message, we use a heuristic:
// if the receiver is a qualified name whose type is too long, we print
// the qualified name instead.
String recvTypeAsString = recvType.toString();
Node recv = propAccessNode.getFirstChild();
String errorMsg;
if (!recv.isQualifiedName()) {
errorMsg = recvTypeAsString;
} else if (recvTypeAsString.length() > 100) {
errorMsg = recv.getQualifiedName();
} else {
errorMsg = recv.getQualifiedName() + " of type " + recvTypeAsString;
}
DiagnosticType warningType = recvType.mayHaveProp(propQname)
? POSSIBLY_INEXISTENT_PROPERTY : INEXISTENT_PROPERTY;
warnings.add(JSError.make(propAccessNode, warningType, pname, errorMsg));
return true;
}
private boolean mayWarnAboutConst(Node n) {
Node lhs = n.getFirstChild();
if (lhs.isName() && this.currentScope.isConstVar(lhs.getString())) {
warnings.add(JSError.make(n, CONST_REASSIGNED));
return true;
}
return false;
}
private boolean mayWarnAboutConstProp(
Node propAccess, JSType recvType, QualifiedName pname) {
if (recvType.hasConstantProp(pname) &&
!propAccess.getBooleanProp(Node.CONSTANT_PROPERTY_DEF)) {
warnings.add(JSError.make(propAccess, CONST_PROPERTY_REASSIGNED));
return true;
}
return false;
}
private void mayWarnAboutGlobalThis(Node thisExpr, NTIScope currentScope) {
Preconditions.checkArgument(thisExpr.isThis());
if (this.currentScope.isTopLevel() || !this.currentScope.hasThis()) {
Node parent = thisExpr.getParent();
if ((parent.isGetProp() || parent.isGetElem())
// Don't warn for callbacks. Most of them are not annotated but THIS is
// bound to a legitimate object at runtime. They do lose typechecking
// for THIS however, but we won't warn.
&& !NodeUtil.isCallOrNewArgument(this.currentScope.getRoot())) {
warnings.add(JSError.make(thisExpr, GLOBAL_THIS));
}
}
}
private boolean mayWarnAboutBadIObjectIndex(Node n, JSType iobjectType,
JSType foundIndexType, JSType requiredIndexType) {
if (requiredIndexType.isBottom()) {
warnings.add(JSError.make(
n, BOTTOM_INDEX_TYPE, iobjectType.toString()));
return true;
}
if (!foundIndexType.isSubtypeOf(requiredIndexType)) {
warnings.add(JSError.make(
n, INVALID_INDEX_TYPE,
errorMsgWithTypeDiff(requiredIndexType, foundIndexType)));
return true;
}
return false;
}
// Don't always wrap getIndexedType calls with this function. Only do it when
// you want to pass around the result type and it has to be non-null.
private JSType getIndexedTypeOrUnknown(JSType t) {
JSType tmp = t.getIndexedType();
return tmp == null ? UNKNOWN : tmp;
}
private EnvTypePair analyzePropAccessFwd(Node receiver, String pname,
TypeEnv inEnv, JSType requiredType, JSType specializedType) {
QualifiedName propQname = new QualifiedName(pname);
Node propAccessNode = receiver.getParent();
EnvTypePair pair;
JSType reqObjType = pickReqObjType(propAccessNode);
JSType recvReqType, recvSpecType;
// First, analyze the receiver object.
if ((NodeUtil.isPropertyTest(compiler, propAccessNode) && !specializedType.isFalseOrFalsy())
|| (NodeUtil.isPropertyAbsenceTest(propAccessNode) && !specializedType.isTrueOrTruthy())
// The NodeUtil method doesn't use types, so it can't see that the
// else branch of "if (!x.prop)" is a property test.
|| specializedType.isTrueOrTruthy()) {
recvReqType = reqObjType;
pair = analyzeExprFwd(receiver, inEnv, recvReqType);
JSType subtypeWithProp = pair.type.findSubtypeWithProp(propQname);
if (subtypeWithProp.isBottom()) {
recvSpecType = reqObjType;
} else {
recvSpecType = subtypeWithProp;
}
if (specializedType.isTrueOrTruthy()) {
// This handles cases like: if (x.prop1 && x.prop1.prop2) { ... }
// In the THEN branch, the only thing we know about x.prop1 is that it
// has a truthy property, so x.prop1 should be a loose object to avoid
// spurious warnings.
recvSpecType = recvSpecType.withLoose().withProperty(propQname, specializedType);
} else {
recvSpecType = recvSpecType.withProperty(propQname, specializedType);
}
} else if (specializedType.isFalseOrFalsy()) {
recvReqType = recvSpecType = reqObjType;
} else {
recvReqType = reqObjType.withProperty(propQname, requiredType);
recvSpecType = reqObjType.withProperty(propQname, specializedType);
}
pair = analyzeExprFwd(receiver, inEnv, recvReqType, recvSpecType);
pair = mayWarnAboutNullableReferenceAndTighten(receiver, pair.type, recvSpecType, pair.env);
JSType recvType = pair.type.autobox();
if (recvType.isUnknown() || recvType.isTrueOrTruthy()) {
mayWarnAboutInexistentProp(propAccessNode, recvType, propQname);
return new EnvTypePair(pair.env, requiredType);
}
if (mayWarnAboutNonObject(receiver, recvType, specializedType)) {
return new EnvTypePair(pair.env, requiredType);
}
FunctionType ft = recvType.getFunTypeIfSingletonObj();
if (ft != null && (pname.equals("call") || pname.equals("apply"))) {
if (ft.isAbstract()) {
// We don't check if the parent of the property access is a call node.
// This catches calls that are a few nodes away, and also warns on .call/.apply
// accesses that do not result in calls (these should be very rare).
String funName = receiver.isQualifiedName() ? receiver.getQualifiedName() : "";
warnings.add(JSError.make(propAccessNode, ABSTRACT_SUPER_METHOD_NOT_CALLABLE, funName));
}
return new EnvTypePair(pair.env,
pname.equals("call")
? commonTypes.fromFunctionType(ft.transformByCallProperty())
: commonTypes.fromFunctionType(ft.transformByApplyProperty()));
}
if (this.convention.isSuperClassReference(pname)) {
if (ft != null && ft.isUniqueConstructor()) {
JSType result = ft.getSuperPrototype();
pair.type = result != null ? result : UNDEFINED;
return pair;
}
}
if (propAccessNode.isGetProp() &&
mayWarnAboutDictPropAccess(receiver, recvType)) {
return new EnvTypePair(pair.env, requiredType);
}
if (recvType.isTop()) {
recvType = TOP_OBJECT;
}
if (propAccessNode.getParent().isDelProp()
&& recvType.hasConstantProp(propQname)) {
warnings.add(JSError.make(
propAccessNode.getParent(), CONST_PROPERTY_DELETED, pname));
}
// Then, analyze the property access.
QualifiedName getterPname = new QualifiedName(JSType.createGetterPropName(pname));
if (recvType.hasProp(getterPname)) {
return new EnvTypePair(pair.env, recvType.getProp(getterPname));
}
JSType resultType = recvType.getProp(propQname);
if (resultType != null && resultType.isBottom()) {
warnings.add(JSError.make(propAccessNode, BOTTOM_PROP,
pname, recvType.toString()));
return new EnvTypePair(pair.env, UNKNOWN);
}
if (!propAccessNode.getParent().isExprResult()
&& !specializedType.isTrueOrTruthy()
&& !specializedType.isFalseOrFalsy()
&& !recvType.mayBeDict()
&& !mayWarnAboutInexistentProp(propAccessNode, recvType, propQname)
&& recvType.hasProp(propQname)
&& !resultType.isSubtypeOf(requiredType)
&& tightenPropertyTypeAndDontWarn(
receiver.isName() ? receiver.getString() : null,
propAccessNode,
recvType,
recvType.getDeclaredProp(propQname),
resultType, requiredType)) {
// Tighten the inferred type and don't warn.
// See analyzeNameFwd for explanation about types as lower/upper bounds.
resultType = resultType.specialize(requiredType);
LValueResultFwd lvr = analyzeLValueFwd(propAccessNode, inEnv, resultType);
TypeEnv updatedEnv = updateLvalueTypeInEnv(lvr.env, propAccessNode, lvr.ptr, resultType);
return new EnvTypePair(updatedEnv, resultType);
}
// We've already warned about missing props, and never want to return null.
if (resultType == null) {
resultType = UNKNOWN;
}
// Any potential type mismatch will be caught by the context
return new EnvTypePair(pair.env, resultType);
}
private TypeEnv updateLvalueTypeInEnv(
TypeEnv env, Node lvalue, QualifiedName qname, JSType type) {
Preconditions.checkNotNull(type);
switch (lvalue.getToken()) {
case NAME:
return envPutType(env, lvalue.getString(), type);
case THIS: {
JSType t = envGetType(env, THIS_ID);
// Don't specialize THIS in functions where it is unknown.
return t == null ? env : envPutType(env, THIS_ID, type);
}
case VAR: // Can happen iff its parent is a for/in.
Preconditions.checkState(lvalue.getParent().isForIn());
return envPutType(env, lvalue.getFirstChild().getString(), type);
case GETPROP:
case GETELEM: {
if (qname != null) {
String objName = qname.getLeftmostName();
QualifiedName props = qname.getAllButLeftmost();
JSType objType = envGetType(env, objName);
// TODO(dimvar): In analyzeNameFwd/Bwd, we are careful to not
// specialize namespaces, and we need the same check here. But
// currently, stopping specialization here causes tests to fail,
// because specializing the namespace is our way of updating its
// functions after computing summaries. The better solution is to
// retain Namespace instances after GTI instead of turning them into
// ObjectTypes, and then update those with the summaries and stop
// specializing here.
env = envPutType(env, objName, objType.withProperty(props, type));
}
return env;
}
default:
return env;
}
}
private TypeEnv collectTypesForFreeVarsFwd(Node n, TypeEnv env) {
Preconditions.checkArgument(n.isFunction()
|| n.isName() && NodeUtil.isCallOrNewTarget(n));
String fnName = n.isFunction() ? symbolTable.getFunInternalName(n) : n.getString();
NTIScope innerScope = this.currentScope.getScope(fnName);
for (String freeVar : innerScope.getOuterVars()) {
if (innerScope.getDeclaredTypeOf(freeVar) == null) {
FunctionType summary = summaries.get(innerScope).getFunType();
JSType outerType = envGetType(env, freeVar);
if (outerType == null) {
outerType = UNKNOWN;
}
JSType innerType = summary.getOuterVarPrecondition(freeVar);
if (// We don't warn if the innerType is a loose object, because we
// haven't found an easy way to avoid false positives.
!innerType.isLoose()
// If n is a function expression, we don't know where it's called,
// so we don't warn for uninitialized variables.
&& (n.isName() || n.isFunction() && !outerType.isUndefined())
&& !JSType.haveCommonSubtype(outerType, innerType)) {
warnings.add(JSError.make(n, CROSS_SCOPE_GOTCHA,
freeVar, outerType.toString(), innerType.toString()));
}
// If n is a callee node, we only want to keep the type in the callee.
// If n is a function expression, we're not sure if it gets called,
// so we join the types.
env = envPutType(env, freeVar,
n.isFunction() ? JSType.join(innerType, outerType) : innerType);
}
}
return env;
}
private EnvTypePair analyzeLooseCallNodeFwd(
Node callNode, TypeEnv inEnv, JSType requiredType) {
Preconditions.checkArgument(callNode.isCall() || callNode.isNew());
Node callee = callNode.getFirstChild();
FunctionTypeBuilder builder = new FunctionTypeBuilder(this.commonTypes);
TypeEnv tmpEnv = inEnv;
for (Node arg = callee.getNext(); arg != null; arg = arg.getNext()) {
EnvTypePair pair = analyzeExprFwd(arg, tmpEnv);
tmpEnv = pair.env;
builder.addReqFormal(pair.type);
}
JSType looseRetType = requiredType.isUnknown() ? BOTTOM : requiredType;
JSType looseFunctionType = commonTypes.fromFunctionType(
builder.addRetType(looseRetType).addLoose().buildFunction());
// Unsound if the arguments and callee have interacting side effects
EnvTypePair calleePair = analyzeExprFwd(
callee, tmpEnv, commonTypes.topFunction(), looseFunctionType);
FunctionType calleeType = calleePair.type.getFunType();
JSType result = calleeType.getReturnType();
return new EnvTypePair(calleePair.env,
isImpreciseType(result) ? requiredType : result);
}
private static boolean isImpreciseType(JSType t) {
return t.isBottom() || t.isTop() || t.isUnknown() || t.isUnion()
|| t.isTrueOrTruthy() || t.isFalseOrFalsy()
|| t.isLoose() || t.isNonClassyObject();
}
private EnvTypePair analyzeLooseCallNodeBwd(
Node callNode, TypeEnv outEnv, JSType retType) {
Preconditions.checkArgument(callNode.isCall() || callNode.isNew());
Preconditions.checkNotNull(retType);
Node callee = callNode.getFirstChild();
TypeEnv tmpEnv = outEnv;
FunctionTypeBuilder builder = new FunctionTypeBuilder(this.commonTypes);
Node target = callNode.getFirstChild();
for (Node arg = callNode.getLastChild(); arg != target; arg = arg.getPrevious()) {
EnvTypePair pair = analyzeExprBwd(arg, tmpEnv);
JSType argType = pair.type;
tmpEnv = pair.env;
// May wait until FWD to get more precise argument types.
builder.addReqFormal(isImpreciseType(argType) ? BOTTOM : argType);
}
JSType looseRetType = retType.isUnknown() ? BOTTOM : retType;
JSType looseFunctionType = commonTypes.fromFunctionType(
builder.addRetType(looseRetType).addLoose().buildFunction());
println("loose function type is ", looseFunctionType);
EnvTypePair calleePair = analyzeExprBwd(callee, tmpEnv, looseFunctionType);
return new EnvTypePair(calleePair.env, retType);
}
private EnvTypePair analyzeExprBwd(Node expr, TypeEnv outEnv) {
return analyzeExprBwd(expr, outEnv, UNKNOWN);
}
/**
* For now, we won't emit any warnings bwd.
*/
private EnvTypePair analyzeExprBwd(
Node expr, TypeEnv outEnv, JSType requiredType) {
Preconditions.checkArgument(requiredType != null, "Required type null at: %s", expr);
Preconditions.checkArgument(!requiredType.isBottom());
switch (expr.getToken()) {
case EMPTY: // can be created by a FOR with empty condition
return new EnvTypePair(outEnv, UNKNOWN);
case FUNCTION: {
String fnName = symbolTable.getFunInternalName(expr);
return new EnvTypePair(outEnv, envGetType(outEnv, fnName));
}
case FALSE:
case NULL:
case NUMBER:
case STRING:
case TRUE:
return new EnvTypePair(outEnv, scalarValueToType(expr.getToken()));
case OBJECTLIT:
return analyzeObjLitBwd(expr, outEnv, requiredType);
case THIS: {
// TODO(blickly): Infer a loose type for THIS if we're in a function.
if (!this.currentScope.hasThis()) {
return new EnvTypePair(outEnv, UNKNOWN);
}
JSType thisType = this.currentScope.getDeclaredTypeOf(THIS_ID);
return new EnvTypePair(outEnv, thisType);
}
case SUPER:
// NOTE(dimvar): analyzing SUPER in the backward direction doesn't give
// us anything useful at the moment.
return new EnvTypePair(outEnv, UNKNOWN);
case NAME:
return analyzeNameBwd(expr, outEnv, requiredType);
case INC:
case DEC:
case BITNOT:
case NEG: // Unary operations on numbers
return analyzeExprBwd(expr.getFirstChild(), outEnv, NUMBER);
case POS: {
EnvTypePair pair = analyzeExprBwd(expr.getFirstChild(), outEnv);
pair.type = NUMBER;
return pair;
}
case TYPEOF: {
EnvTypePair pair = analyzeExprBwd(expr.getFirstChild(), outEnv);
pair.type = STRING;
return pair;
}
case INSTANCEOF: {
TypeEnv env = analyzeExprBwd(
expr.getLastChild(), outEnv, commonTypes.topFunction()).env;
EnvTypePair pair = analyzeExprBwd(expr.getFirstChild(), env);
pair.type = BOOLEAN;
return pair;
}
case BITOR:
case BITAND:
case BITXOR:
case DIV:
case LSH:
case MOD:
case MUL:
case RSH:
case SUB:
case URSH:
return analyzeBinaryNumericOpBwd(expr, outEnv);
case ADD:
return analyzeAddBwd(expr, outEnv, requiredType);
case OR:
case AND:
return analyzeLogicalOpBwd(expr, outEnv);
case SHEQ:
case SHNE:
case EQ:
case NE:
return analyzeEqNeBwd(expr, outEnv);
case LT:
case GT:
case LE:
case GE:
return analyzeLtGtBwd(expr, outEnv);
case ASSIGN:
return analyzeAssignBwd(expr, outEnv, requiredType);
case ASSIGN_ADD:
return analyzeAssignAddBwd(expr, outEnv, requiredType);
case ASSIGN_BITOR:
case ASSIGN_BITXOR:
case ASSIGN_BITAND:
case ASSIGN_LSH:
case ASSIGN_RSH:
case ASSIGN_URSH:
case ASSIGN_SUB:
case ASSIGN_MUL:
case ASSIGN_DIV:
case ASSIGN_MOD:
return analyzeAssignNumericOpBwd(expr, outEnv);
case GETPROP: {
Preconditions.checkState(
!NodeUtil.isAssignmentOp(expr.getParent()) ||
!NodeUtil.isLValue(expr));
if (expr.getBooleanProp(Node.ANALYZED_DURING_GTI)) {
return new EnvTypePair(outEnv, requiredType);
}
return analyzePropAccessBwd(expr.getFirstChild(),
expr.getLastChild().getString(), outEnv, requiredType);
}
case HOOK:
return analyzeHookBwd(expr, outEnv, requiredType);
case CALL:
case NEW:
return analyzeCallNewBwd(expr, outEnv, requiredType);
case COMMA: {
EnvTypePair pair = analyzeExprBwd(
expr.getLastChild(), outEnv, requiredType);
pair.env = analyzeExprBwd(expr.getFirstChild(), pair.env).env;
return pair;
}
case NOT: {
EnvTypePair pair = analyzeExprBwd(expr.getFirstChild(), outEnv);
pair.type = pair.type.negate();
return pair;
}
case GETELEM:
return analyzeGetElemBwd(expr, outEnv, requiredType);
case VOID: {
EnvTypePair pair = analyzeExprBwd(expr.getFirstChild(), outEnv);
pair.type = UNDEFINED;
return pair;
}
case IN:
return analyzeInBwd(expr, outEnv);
case DELPROP: {
EnvTypePair pair = analyzeExprBwd(expr.getFirstChild(), outEnv);
pair.type = BOOLEAN;
return pair;
}
case VAR: { // Can happen iff its parent is a for/in.
Node vdecl = expr.getFirstChild();
String name = vdecl.getString();
// For/in can never have rhs of its VAR
Preconditions.checkState(!vdecl.hasChildren());
return new EnvTypePair(
envPutType(outEnv, name, UNKNOWN), UNKNOWN);
}
case REGEXP:
return new EnvTypePair(outEnv, commonTypes.getRegexpType());
case ARRAYLIT:
return analyzeArrayLitBwd(expr, outEnv);
case CAST:
EnvTypePair pair = analyzeExprBwd(expr.getFirstChild(), outEnv);
pair.type = symbolTable.getCastType(expr);
return pair;
default:
throw new RuntimeException(
"BWD: Unhandled expression type: "
+ expr.getToken()
+ " with parent: "
+ expr.getParent());
}
}
private EnvTypePair analyzeNameBwd(
Node expr, TypeEnv outEnv, JSType requiredType) {
String varName = expr.getString();
if (varName.equals("undefined")) {
return new EnvTypePair(outEnv, UNDEFINED);
}
JSType inferredType = envGetType(outEnv, varName);
println(varName, "'s inferredType: ", inferredType,
" requiredType: ", requiredType);
if (inferredType == null) {
return new EnvTypePair(outEnv, UNKNOWN);
}
JSType preciseType = inferredType.specialize(requiredType);
if ((this.currentScope.isUndeclaredFormal(varName)
|| this.currentScope.isUndeclaredOuterVar(varName))
&& preciseType.hasNonScalar()) {
preciseType = preciseType.withLoose();
}
println(varName, "'s preciseType: ", preciseType);
if (preciseType.isBottom()) {
// If there is a type mismatch, we can propagate the previously
// inferred type or the required type.
// Propagating the already inferred type means that the type of the
// variable is stable throughout the function body.
// Propagating the required type means that the type chosen for a
// formal is the one closest to the function header, which helps
// generate more intuitive warnings in the fwd direction.
// But there is a small chance that the different types of the same
// variable flow to other variables and this can also be a source of
// unintuitive warnings.
// It's a trade-off.
JSType declType = this.currentScope.getDeclaredTypeOf(varName);
preciseType = declType == null ? requiredType : declType;
}
return EnvTypePair.addBinding(outEnv, varName, preciseType);
}
private EnvTypePair analyzeBinaryNumericOpBwd(Node expr, TypeEnv outEnv) {
Node lhs = expr.getFirstChild();
Node rhs = expr.getLastChild();
TypeEnv rhsEnv = analyzeExprBwd(rhs, outEnv, NUMBER).env;
EnvTypePair pair = analyzeExprBwd(lhs, rhsEnv, NUMBER);
pair.type = NUMBER;
return pair;
}
private EnvTypePair analyzeAddBwd(Node expr, TypeEnv outEnv, JSType requiredType) {
Node lhs = expr.getFirstChild();
Node rhs = expr.getLastChild();
JSType operandType = requiredType.isNumber() ? NUMBER : UNKNOWN;
EnvTypePair rhsPair = analyzeExprBwd(rhs, outEnv, operandType);
EnvTypePair lhsPair = analyzeExprBwd(lhs, rhsPair.env, operandType);
lhsPair.type = JSType.plus(lhsPair.type, rhsPair.type);
return lhsPair;
}
private EnvTypePair analyzeLogicalOpBwd(Node expr, TypeEnv outEnv) {
Node lhs = expr.getFirstChild();
Node rhs = expr.getLastChild();
EnvTypePair rhsPair = analyzeExprBwd(rhs, outEnv);
// The types inferred for variables in the rhs are not always
// true for the rest of the function. Eg, in (!x || x.foo(123))
// x is non-null only in the rhs, not everywhere.
// For this reason, we don't reuse rhsPair.env when analyzing the lhs,
// to avoid incorrect propagation of these types.
EnvTypePair lhsPair = analyzeExprBwd(lhs, outEnv);
lhsPair.type = JSType.join(rhsPair.type, lhsPair.type);
return lhsPair;
}
private EnvTypePair analyzeEqNeBwd(Node expr, TypeEnv outEnv) {
TypeEnv rhsEnv = analyzeExprBwd(expr.getLastChild(), outEnv).env;
EnvTypePair pair = analyzeExprBwd(expr.getFirstChild(), rhsEnv);
pair.type = BOOLEAN;
return pair;
}
private EnvTypePair analyzeLtGtBwd(Node expr, TypeEnv outEnv) {
Node lhs = expr.getFirstChild();
Node rhs = expr.getLastChild();
EnvTypePair rhsPair = analyzeExprBwd(rhs, outEnv);
EnvTypePair lhsPair = analyzeExprBwd(lhs, rhsPair.env);
JSType meetType = JSType.meet(lhsPair.type, rhsPair.type);
if (meetType.isBottom()) {
// Type mismatch, the fwd direction will warn; don't reanalyze
lhsPair.type = BOOLEAN;
return lhsPair;
}
rhsPair = analyzeExprBwd(rhs, outEnv, meetType);
lhsPair = analyzeExprBwd(lhs, rhsPair.env, meetType);
lhsPair.type = BOOLEAN;
return lhsPair;
}
private EnvTypePair analyzeAssignBwd(
Node expr, TypeEnv outEnv, JSType requiredType) {
if (expr.getBooleanProp(Node.ANALYZED_DURING_GTI)) {
return new EnvTypePair(outEnv, requiredType);
}
Node lhs = expr.getFirstChild();
Node rhs = expr.getLastChild();
if (lhs.getBooleanProp(Node.ANALYZED_DURING_GTI)) {
return analyzeExprBwd(rhs, outEnv,
markAndGetTypeOfPreanalyzedNode(lhs, outEnv, false));
}
// Here we analyze the LHS twice:
// Once to find out what should be removed for the slicedEnv,
// and again to take into account the side effects of the LHS itself.
LValueResultBwd lvalue = analyzeLValueBwd(lhs, outEnv, requiredType, true);
TypeEnv slicedEnv = lvalue.env;
JSType rhsReqType = specializeKeep2ndWhenBottom(lvalue.type, requiredType);
EnvTypePair pair = analyzeExprBwd(rhs, slicedEnv, rhsReqType);
pair.env = analyzeLValueBwd(lhs, pair.env, requiredType, true).env;
return pair;
}
private EnvTypePair analyzeAssignAddBwd(
Node expr, TypeEnv outEnv, JSType requiredType) {
Node lhs = expr.getFirstChild();
Node rhs = expr.getLastChild();
JSType lhsReqType = specializeKeep2ndWhenBottom(requiredType, NUMBER_OR_STRING);
LValueResultBwd lvalue = analyzeLValueBwd(lhs, outEnv, lhsReqType, false);
// if lhs is a string, rhs can still be a number
JSType rhsReqType = lvalue.type.isNumber() ? NUMBER : NUMBER_OR_STRING;
EnvTypePair pair = analyzeExprBwd(rhs, outEnv, rhsReqType);
pair.env = analyzeLValueBwd(lhs, pair.env, lhsReqType, false).env;
return pair;
}
private EnvTypePair analyzeAssignNumericOpBwd(Node expr, TypeEnv outEnv) {
Node lhs = expr.getFirstChild();
Node rhs = expr.getLastChild();
EnvTypePair pair = analyzeExprBwd(rhs, outEnv, NUMBER);
LValueResultBwd lvalue =
analyzeLValueBwd(lhs, pair.env, NUMBER, false);
return new EnvTypePair(lvalue.env, NUMBER);
}
private EnvTypePair analyzeHookBwd(
Node expr, TypeEnv outEnv, JSType requiredType) {
Node cond = expr.getFirstChild();
Node thenBranch = cond.getNext();
Node elseBranch = thenBranch.getNext();
EnvTypePair thenPair = analyzeExprBwd(thenBranch, outEnv, requiredType);
EnvTypePair elsePair = analyzeExprBwd(elseBranch, outEnv, requiredType);
return analyzeExprBwd(cond, TypeEnv.join(thenPair.env, elsePair.env));
}
private EnvTypePair analyzeCallNewBwd(
Node expr, TypeEnv outEnv, JSType requiredType) {
Preconditions.checkArgument(expr.isNew() || expr.isCall());
Node callee = expr.getFirstChild();
EnvTypePair pair = analyzeExprBwd(callee, outEnv, commonTypes.topFunction());
TypeEnv envAfterCallee = pair.env;
FunctionType funType = pair.type.getFunType();
if (funType == null) {
return analyzeCallNodeArgumentsBwd(expr, envAfterCallee);
} else if (funType.isLoose()) {
return analyzeLooseCallNodeBwd(expr, envAfterCallee, requiredType);
} else if (expr.isCall() && funType.isSomeConstructorOrInterface()
|| expr.isNew() && !funType.isSomeConstructorOrInterface()) {
return analyzeCallNodeArgumentsBwd(expr, envAfterCallee);
} else if (funType.isTopFunction()) {
return analyzeCallNodeArgumentsBwd(expr, envAfterCallee);
}
if (callee.isName() && !funType.isGeneric() && expr.isCall()) {
createDeferredCheckBwd(expr, requiredType);
}
int numArgs = expr.getChildCount() - 1;
if (numArgs < funType.getMinArity() || numArgs > funType.getMaxArity()) {
return analyzeCallNodeArgumentsBwd(expr, envAfterCallee);
}
if (funType.isGeneric()) {
Map<String, JSType> typeMap =
calcTypeInstantiationBwd(expr, funType, envAfterCallee);
funType = funType.instantiateGenerics(typeMap);
}
TypeEnv tmpEnv = envAfterCallee;
// In bwd direction, analyze arguments in reverse
Node target = expr.getFirstChild();
int i = expr.getChildCount() - 1;
for (Node arg = expr.getLastChild(); arg != target; arg = arg.getPrevious()) {
i--;
JSType formalType = funType.getFormalType(i);
// The type of a formal can be BOTTOM as the result of a join.
// Don't use this as a requiredType.
if (formalType.isBottom()) {
formalType = UNKNOWN;
}
tmpEnv = analyzeExprBwd(arg, tmpEnv, formalType).env;
// We don't need deferred checks for args in BWD
}
JSType retType =
expr.isNew() ? funType.getThisType() : funType.getReturnType();
return new EnvTypePair(tmpEnv, retType);
}
private EnvTypePair analyzeGetElemBwd(
Node expr, TypeEnv outEnv, JSType requiredType) {
Node receiver = expr.getFirstChild();
Node index = expr.getLastChild();
JSType reqObjType = pickReqObjType(expr);
EnvTypePair pair = analyzeExprBwd(receiver, outEnv, reqObjType);
JSType recvType = pair.type;
JSType indexType = recvType.getIndexType();
if (indexType != null) {
if (indexType.isBottom()) {
indexType = UNKNOWN;
}
pair = analyzeExprBwd(index, pair.env, indexType);
pair.type = getIndexedTypeOrUnknown(recvType);
return pair;
}
if (index.isString()) {
return analyzePropAccessBwd(receiver, index.getString(), outEnv, requiredType);
}
pair = analyzeExprBwd(index, outEnv);
pair = analyzeExprBwd(receiver, pair.env, reqObjType);
pair.type = requiredType;
return pair;
}
private EnvTypePair analyzeInBwd(Node expr, TypeEnv outEnv) {
Node lhs = expr.getFirstChild();
Node rhs = expr.getLastChild();
EnvTypePair pair = analyzeExprBwd(rhs, outEnv, pickReqObjType(expr));
pair = analyzeExprBwd(lhs, pair.env, NUMBER_OR_STRING);
pair.type = BOOLEAN;
return pair;
}
private EnvTypePair analyzeArrayLitBwd(Node expr, TypeEnv outEnv) {
TypeEnv env = outEnv;
JSType elementType = BOTTOM;
for (Node elm = expr.getLastChild(); elm != null; elm = elm.getPrevious()) {
EnvTypePair pair = analyzeExprBwd(elm, env);
env = pair.env;
elementType = JSType.join(elementType, pair.type);
}
if (elementType.isBottom()) {
elementType = UNKNOWN;
}
return new EnvTypePair(env, commonTypes.getArrayInstance(elementType));
}
private EnvTypePair analyzeCallNodeArgumentsBwd(
Node callNode, TypeEnv outEnv) {
TypeEnv env = outEnv;
Node target = callNode.getFirstChild();
for (Node arg = callNode.getLastChild(); arg != target; arg = arg.getPrevious()) {
env = analyzeExprBwd(arg, env).env;
}
return new EnvTypePair(env, UNKNOWN);
}
private void createDeferredCheckBwd(Node expr, JSType requiredType) {
Preconditions.checkArgument(expr.isCall());
Preconditions.checkArgument(expr.getFirstChild().isName());
String calleeName = expr.getFirstChild().getString();
// Local function definitions will be type-checked more
// exactly using their summaries, and don't need deferred checks
if (this.currentScope.isKnownFunction(calleeName)
&& !this.currentScope.isLocalFunDef(calleeName)
&& !this.currentScope.isExternalFunction(calleeName)) {
NTIScope s = this.currentScope.getScope(calleeName);
JSType expectedRetType;
if (s.getDeclaredFunctionType().getReturnType() == null) {
expectedRetType = requiredType;
} else {
// No deferred check if the return type is declared
expectedRetType = null;
}
println("Putting deferred check of function: ", calleeName,
" with ret: ", expectedRetType);
DeferredCheck dc = new DeferredCheck(
expr, expectedRetType, currentScope, s);
deferredChecks.put(expr, dc);
}
}
private EnvTypePair analyzePropAccessBwd(
Node receiver, String pname, TypeEnv outEnv, JSType requiredType) {
Node propAccessNode = receiver.getParent();
QualifiedName qname = new QualifiedName(pname);
JSType reqObjType = pickReqObjType(propAccessNode);
if (!NodeUtil.isPropertyTest(compiler, propAccessNode)) {
reqObjType = reqObjType.withProperty(qname, requiredType);
}
EnvTypePair pair = analyzeExprBwd(receiver, outEnv, reqObjType);
JSType receiverType = pair.type;
JSType propAccessType = receiverType.mayHaveProp(qname) ?
receiverType.getProp(qname) : requiredType;
pair.type = propAccessType;
return pair;
}
private EnvTypePair analyzeObjLitBwd(
Node objLit, TypeEnv outEnv, JSType requiredType) {
if (NodeUtil.isEnumDecl(objLit.getParent())) {
return analyzeEnumObjLitBwd(objLit, outEnv, requiredType);
}
TypeEnv env = outEnv;
JSType result = pickReqObjType(objLit);
for (Node prop = objLit.getLastChild();
prop != null;
prop = prop.getPrevious()) {
QualifiedName pname =
new QualifiedName(NodeUtil.getObjectLitKeyName(prop));
if (prop.isGetterDef() || prop.isSetterDef()) {
env = analyzeExprBwd(prop.getFirstChild(), env).env;
} else {
JSType jsdocType = symbolTable.getPropDeclaredType(prop);
JSType reqPtype;
if (jsdocType != null) {
reqPtype = jsdocType;
} else if (requiredType.mayHaveProp(pname)) {
reqPtype = requiredType.getProp(pname);
} else {
reqPtype = UNKNOWN;
}
EnvTypePair pair = analyzeExprBwd(prop.getFirstChild(), env, reqPtype);
result = result.withProperty(pname, pair.type);
env = pair.env;
}
}
return new EnvTypePair(env, result);
}
private EnvTypePair analyzeEnumObjLitBwd(
Node objLit, TypeEnv outEnv, JSType requiredType) {
if (objLit.getFirstChild() == null) {
return new EnvTypePair(outEnv, requiredType);
}
String pname = NodeUtil.getObjectLitKeyName(objLit.getFirstChild());
JSType enumeratedType =
requiredType.getProp(new QualifiedName(pname)).getEnumeratedTypeOfEnumElement();
if (enumeratedType == null) {
return new EnvTypePair(outEnv, requiredType);
}
TypeEnv env = outEnv;
for (Node prop = objLit.getLastChild();
prop != null;
prop = prop.getPrevious()) {
env = analyzeExprBwd(prop.getFirstChild(), env, enumeratedType).env;
}
return new EnvTypePair(env, requiredType);
}
private boolean isPropertyTestCall(Node expr) {
if (!expr.isCall()) {
return false;
}
return expr.getFirstChild().isQualifiedName()
&& this.convention.isPropertyTestFunction(expr);
}
private boolean isFunctionBind(Node expr, TypeEnv env, boolean isFwd) {
if (NodeUtil.isFunctionBind(expr)) {
return true;
}
if (!expr.isGetProp() || !expr.isQualifiedName()
|| !expr.getLastChild().getString().equals("bind")) {
return false;
}
Node recv = expr.getFirstChild();
JSType recvType = isFwd ? analyzeExprFwd(recv, env).type : analyzeExprBwd(recv, env).type;
return !recvType.isUnknown() && recvType.isSubtypeOf(commonTypes.topFunction());
}
private boolean isGoogTypeof(Node expr) {
if (!expr.isCall()) {
return false;
}
expr = expr.getFirstChild();
return expr.isGetProp() && expr.getFirstChild().isName() &&
expr.getFirstChild().getString().equals("goog") &&
expr.getLastChild().getString().equals("typeOf");
}
private JSType scalarValueToType(Token token) {
switch (token) {
case NUMBER:
return NUMBER;
case STRING:
return STRING;
case TRUE:
return TRUE_TYPE;
case FALSE:
return FALSE_TYPE;
case NULL:
return NULL;
default:
throw new RuntimeException("The token isn't a scalar value " + token);
}
}
private void warnInvalidOperand(
Node expr, Token operatorType, Object expected, Object actual) {
Preconditions.checkArgument(
(expected instanceof String) || (expected instanceof JSType));
Preconditions.checkArgument(
(actual instanceof String) || (actual instanceof JSType));
if (expected instanceof JSType && actual instanceof JSType) {
warnings.add(
JSError.make(
expr,
INVALID_OPERAND_TYPE,
operatorType.toString(),
errorMsgWithTypeDiff((JSType) expected, (JSType) actual)));
} else {
warnings.add(
JSError.make(
expr,
INVALID_OPERAND_TYPE,
operatorType.toString(),
"Expected : "
+ expected.toString()
+ "\n"
+ "Found : "
+ actual.toString()
+ "\n"));
}
}
private static class EnvTypePair {
TypeEnv env;
JSType type;
EnvTypePair(TypeEnv env, JSType type) {
this.env = env;
this.type = type;
}
static EnvTypePair addBinding(TypeEnv env, String varName, JSType type) {
return new EnvTypePair(envPutType(env, varName, type), type);
}
static EnvTypePair join(EnvTypePair p1, EnvTypePair p2) {
return new EnvTypePair(TypeEnv.join(p1.env, p2.env),
JSType.join(p1.type, p2.type));
}
}
private static JSType envGetType(TypeEnv env, String pname) {
Preconditions.checkArgument(!pname.contains("."));
return env.getType(pname);
}
private static JSType envGetTypeOfQname(TypeEnv env, QualifiedName qname) {
JSType leftmostType = envGetType(env, qname.getLeftmostName());
if (qname.isIdentifier()) {
return leftmostType;
}
return leftmostType == null ? null : leftmostType.getProp(qname.getAllButLeftmost());
}
private static TypeEnv envPutType(TypeEnv env, String varName, JSType type) {
Preconditions.checkArgument(!varName.contains("."));
return env.putType(varName, type);
}
private static class LValueResultFwd {
TypeEnv env;
JSType type;
JSType declType;
QualifiedName ptr;
LValueResultFwd(
TypeEnv env, JSType type, JSType declType, QualifiedName ptr) {
Preconditions.checkNotNull(type);
this.env = env;
this.type = type;
this.declType = declType;
this.ptr = ptr;
}
}
// Some expressions are analyzed during GTI, so they're skipped here.
// But we must annotate them with a type anyway.
private JSType markAndGetTypeOfPreanalyzedNode(Node qnameNode, TypeEnv env, boolean isFwd) {
switch (qnameNode.getToken()) {
case NAME:
case THIS: {
JSType result = envGetType(env, qnameNode.isThis() ? THIS_ID : qnameNode.getString());
Preconditions.checkNotNull(result, "Null declared type at node: %s", qnameNode);
if (isFwd) {
maybeSetTypeI(qnameNode, result);
}
return result;
}
case GETPROP: {
JSType recvType = markAndGetTypeOfPreanalyzedNode(qnameNode.getFirstChild(), env, isFwd);
String pname = qnameNode.getLastChild().getString();
JSType result = null;
if (recvType.isSubtypeOf(TOP_OBJECT)) {
result = recvType.getProp(new QualifiedName(pname));
}
if (result == null) {
warnings.add(JSError.make(qnameNode, UNKNOWN_NAMESPACE_PROPERTY,
qnameNode.getQualifiedName()));
return UNKNOWN;
}
Preconditions.checkNotNull(result, "Null declared type@%s", qnameNode);
if (isFwd) {
maybeSetTypeI(qnameNode, result);
}
return result;
}
default:
throw new RuntimeException(
"markAndGetTypeOfPreanalyzedNode: unexpected node "
+ compiler.toSource(qnameNode)
+ " with token "
+ qnameNode.getToken());
}
}
private void maybeSetTypeI(Node n, JSType t) {
TypeI oldType = n.getTypeI();
Preconditions.checkState(oldType == null || oldType instanceof JSType);
// When creating a function summary, we set a precise type on the function's
// name node. Since we're visiting inner scopes first, the name node is
// revisited after the function's scope is analyzed, and its type then can
// be less precise. So, we keep the summary type.
// TODO(dimvar): Look into why the name node has a less precise type in the
// outer scope; we've already computed a good type for it, don't lose it.
if (oldType == null) {
n.setTypeI(t);
}
}
private LValueResultFwd analyzeLValueFwd(
Node expr, TypeEnv inEnv, JSType type) {
return analyzeLValueFwd(expr, inEnv, type, false);
}
private LValueResultFwd analyzeLValueFwd(
Node expr, TypeEnv inEnv, JSType type, boolean insideQualifiedName) {
LValueResultFwd lvalResult = null;
switch (expr.getToken()) {
case THIS: {
mayWarnAboutGlobalThis(expr, currentScope);
if (this.currentScope.hasThis()) {
lvalResult = new LValueResultFwd(inEnv, envGetType(inEnv, THIS_ID),
this.currentScope.getDeclaredTypeOf(THIS_ID),
new QualifiedName(THIS_ID));
} else {
lvalResult = new LValueResultFwd(inEnv, UNKNOWN, null, null);
}
break;
}
case NAME: {
String varName = expr.getString();
JSType varType = analyzeExprFwd(expr, inEnv).type;
lvalResult = new LValueResultFwd(inEnv, varType,
this.currentScope.getDeclaredTypeOf(varName),
varType.hasNonScalar() ? new QualifiedName(varName) : null);
break;
}
case GETPROP:
case GETELEM: {
Node obj = expr.getFirstChild();
Node prop = expr.getLastChild();
QualifiedName pname = expr.isGetProp() || prop.isString()
? new QualifiedName(prop.getString()) : null;
LValueResultFwd recvLvalue = analyzeReceiverLvalFwd(obj, pname, inEnv, type);
if (!recvLvalue.type.isSubtypeOf(TOP_OBJECT)) {
EnvTypePair pair = analyzeExprFwd(prop, recvLvalue.env, type);
lvalResult = new LValueResultFwd(pair.env, type, null, null);
break;
}
JSType indexType = recvLvalue.type.getIndexType();
// (1) A getelem where the receiver is an IObject
if (expr.isGetElem() && indexType != null) {
lvalResult = analyzeIObjectElmLvalFwd(prop, recvLvalue, indexType);
break;
}
// (2) A getelem where the prop is a string literal is like a getprop
if (expr.isGetProp() || prop.isString()) {
lvalResult = analyzePropLValFwd(obj, pname, recvLvalue, type, insideQualifiedName);
break;
}
// (3) All other getelems
// TODO(dimvar): there is some recomputation here; the receiver will be
// analyzed again. Some more refactoring can fix this.
EnvTypePair pair = analyzeExprFwd(expr, recvLvalue.env, type);
lvalResult = new LValueResultFwd(pair.env, pair.type, null, null);
break;
}
case VAR:
{ // Can happen iff its parent is a for/in.
Preconditions.checkState(expr.getParent().isForIn());
Node vdecl = expr.getFirstChild();
String name = vdecl.getString();
// For/in can never have rhs of its VAR
Preconditions.checkState(!vdecl.hasChildren());
return new LValueResultFwd(inEnv, STRING, null, new QualifiedName(name));
}
default: {
// Expressions that aren't lvalues should be handled because they may
// be, e.g., the left child of a getprop.
// We must check that they are not the direct lvalues.
Preconditions.checkState(insideQualifiedName);
EnvTypePair pair = analyzeExprFwd(expr, inEnv, type);
return new LValueResultFwd(pair.env, pair.type, null, null);
}
}
maybeSetTypeI(expr, lvalResult.type);
mayWarnAboutUnknownType(expr, lvalResult.type);
return lvalResult;
}
private LValueResultFwd analyzeIObjectElmLvalFwd(
Node prop, LValueResultFwd recvLvalue, JSType indexType) {
EnvTypePair pair = analyzeExprFwd(
prop, recvLvalue.env, indexType.isBottom() ? UNKNOWN : indexType);
if (mayWarnAboutBadIObjectIndex(prop, recvLvalue.type, pair.type, indexType)) {
return new LValueResultFwd(pair.env, UNKNOWN, null, null);
}
JSType inferred = getIndexedTypeOrUnknown(recvLvalue.type);
JSType declared = null;
if (recvLvalue.declType != null) {
JSType receiverAdjustedDeclType =
recvLvalue.declType.removeType(NULL_OR_UNDEFINED);
declared = receiverAdjustedDeclType.getIndexedType();
}
return new LValueResultFwd(pair.env, inferred, declared, null);
}
private EnvTypePair mayWarnAboutNullableReferenceAndTighten(
Node obj, JSType recvType, JSType maybeSpecType, TypeEnv inEnv) {
if (!recvType.isUnknown()
&& !recvType.isTop()
&& (NULL.isSubtypeOf(recvType)
|| UNDEFINED.isSubtypeOf(recvType))) {
JSType minusNull = recvType.removeType(NULL_OR_UNDEFINED);
if (!minusNull.isBottom()) {
if (this.reportNullDeref) {
warnings.add(JSError.make(
obj, NULLABLE_DEREFERENCE, recvType.toString()));
}
TypeEnv outEnv = inEnv;
if (obj.isQualifiedName()) {
QualifiedName qname = QualifiedName.fromNode(obj);
if (maybeSpecType != null && maybeSpecType.isSubtypeOf(minusNull)) {
minusNull = maybeSpecType;
}
outEnv = updateLvalueTypeInEnv(inEnv, obj, qname, minusNull);
}
return new EnvTypePair(outEnv, minusNull);
}
}
return new EnvTypePair(inEnv, recvType);
}
private LValueResultFwd analyzePropLValFwd(Node obj, QualifiedName pname,
LValueResultFwd recvLvalue, JSType requiredType, boolean insideQualifiedName) {
Preconditions.checkArgument(pname.isIdentifier());
TypeEnv inEnv = recvLvalue.env;
JSType recvType = recvLvalue.type;
if (!recvType.isUnion() && !recvType.isSingletonObj()) {
// The lvalue is a subtype of TOP_OBJECT, but does not contain an object
// yet, eg, it is ?, truthy, or bottom.
recvType = TOP_OBJECT.withLoose();
}
Node propAccessNode = obj.getParent();
if (propAccessNode.isGetProp() &&
propAccessNode.getParent().isAssign() &&
mayWarnAboutPropCreation(pname, propAccessNode, recvType)) {
return new LValueResultFwd(inEnv, requiredType, null, null);
}
if (!insideQualifiedName
&& mayWarnAboutConstProp(propAccessNode, recvType, pname)) {
return new LValueResultFwd(inEnv, requiredType, null, null);
}
if (!recvType.hasProp(pname)) {
// Warn for inexistent prop either on the non-top-level of a qualified
// name, or for assignment ops that won't create a new property.
if (insideQualifiedName || !propAccessNode.getParent().isAssign()) {
mayWarnAboutInexistentProp(propAccessNode, recvType, pname);
if (!recvType.isLoose()) {
return new LValueResultFwd(inEnv, requiredType, null, null);
}
}
if (recvType.isLoose()) {
// For loose objects, create the inner property if it doesn't exist.
recvType = recvType.withProperty(pname, UNKNOWN);
inEnv = updateLvalueTypeInEnv(inEnv, obj, recvLvalue.ptr, recvType);
}
}
if (propAccessNode.isGetElem()) {
mayWarnAboutStructPropAccess(obj, recvType);
} else if (propAccessNode.isGetProp()) {
mayWarnAboutDictPropAccess(obj, recvType);
}
QualifiedName setterPname =
new QualifiedName(JSType.createSetterPropName(pname.getLeftmostName()));
if (recvType.hasProp(setterPname)) {
FunctionType funType = recvType.getProp(setterPname).getFunType();
Preconditions.checkNotNull(funType);
JSType formalType = funType.getFormalType(0);
Preconditions.checkState(!formalType.isBottom());
return new LValueResultFwd(inEnv, formalType, formalType, null);
}
QualifiedName ptr = recvLvalue.ptr == null
? null : QualifiedName.join(recvLvalue.ptr, pname);
return recvType.mayHaveProp(pname)
? new LValueResultFwd(
inEnv, recvType.getProp(pname), recvType.getDeclaredProp(pname), ptr)
: new LValueResultFwd(inEnv, UNKNOWN, null, ptr);
}
private LValueResultFwd analyzeReceiverLvalFwd(
Node obj, QualifiedName pname, TypeEnv inEnv, JSType propType) {
// pname is null when the property name is not known.
Preconditions.checkArgument(pname == null || pname.isIdentifier());
JSType reqObjType = pickReqObjType(obj.getParent());
if (pname != null) {
reqObjType = reqObjType.withProperty(pname, propType);
}
LValueResultFwd lvalue = analyzeLValueFwd(obj, inEnv, reqObjType, true);
EnvTypePair pair = mayWarnAboutNullableReferenceAndTighten(
obj, lvalue.type, null, lvalue.env);
JSType lvalueType = pair.type;
if (lvalueType.isEnumElement()) {
lvalueType = lvalueType.getEnumeratedTypeOfEnumElement();
}
if (!lvalueType.isSubtypeOf(TOP_OBJECT)) {
warnings.add(JSError.make(obj, ADDING_PROPERTY_TO_NON_OBJECT,
getPropNameForErrorMsg(obj.getParent()), lvalueType.toString()));
}
lvalue.type = lvalueType;
lvalue.env = pair.env;
return lvalue;
}
private static class LValueResultBwd {
TypeEnv env;
JSType type;
QualifiedName ptr;
LValueResultBwd(TypeEnv env, JSType type, QualifiedName ptr) {
Preconditions.checkNotNull(type);
this.env = env;
this.type = type;
this.ptr = ptr;
}
}
private LValueResultBwd analyzeLValueBwd(
Node expr, TypeEnv outEnv, JSType type, boolean doSlicing) {
return analyzeLValueBwd(expr, outEnv, type, doSlicing, false);
}
/** When {@code doSlicing} is set, remove the lvalue from the returned env */
private LValueResultBwd analyzeLValueBwd(Node expr, TypeEnv outEnv,
JSType type, boolean doSlicing, boolean insideQualifiedName) {
switch (expr.getToken()) {
case THIS:
case NAME: {
EnvTypePair pair = analyzeExprBwd(expr, outEnv, type);
String name = expr.getQualifiedName();
JSType declType = this.currentScope.getDeclaredTypeOf(name);
if (doSlicing) {
pair.env = envPutType(pair.env, name,
declType != null ? declType : UNKNOWN);
}
return new LValueResultBwd(pair.env, pair.type,
pair.type.hasNonScalar() ? new QualifiedName(name) : null);
}
case GETPROP: {
Node obj = expr.getFirstChild();
QualifiedName pname =
new QualifiedName(expr.getLastChild().getString());
return analyzePropLValBwd(obj, pname, outEnv, type, doSlicing);
}
case GETELEM: {
if (expr.getLastChild().isString()) {
Node obj = expr.getFirstChild();
QualifiedName pname =
new QualifiedName(expr.getLastChild().getString());
return analyzePropLValBwd(obj, pname, outEnv, type, doSlicing);
}
EnvTypePair pair = analyzeExprBwd(expr, outEnv, type);
return new LValueResultBwd(pair.env, pair.type, null);
}
default: {
// Expressions that aren't lvalues should be handled because they may
// be, e.g., the left child of a getprop.
// We must check that they are not the direct lvalues.
Preconditions.checkState(insideQualifiedName);
EnvTypePair pair = analyzeExprBwd(expr, outEnv, type);
return new LValueResultBwd(pair.env, pair.type, null);
}
}
}
private LValueResultBwd analyzePropLValBwd(Node obj, QualifiedName pname,
TypeEnv outEnv, JSType type, boolean doSlicing) {
Preconditions.checkArgument(pname.isIdentifier());
JSType reqObjType =
pickReqObjType(obj.getParent()).withProperty(pname, type);
LValueResultBwd lvalue =
analyzeLValueBwd(obj, outEnv, reqObjType, false, true);
if (lvalue.ptr != null) {
lvalue.ptr = QualifiedName.join(lvalue.ptr, pname);
if (doSlicing) {
String objName = lvalue.ptr.getLeftmostName();
QualifiedName props = lvalue.ptr.getAllButLeftmost();
JSType objType = envGetType(lvalue.env, objName);
// withoutProperty only removes inferred properties
JSType slicedObjType = objType.withoutProperty(props);
lvalue.env = envPutType(lvalue.env, objName, slicedObjType);
}
}
lvalue.type = lvalue.type.mayHaveProp(pname) ?
lvalue.type.getProp(pname) : UNKNOWN;
return lvalue;
}
private JSType pickReqObjType(Node expr) {
Token exprKind = expr.getToken();
switch (exprKind) {
case OBJECTLIT: {
JSDocInfo jsdoc = expr.getJSDocInfo();
if (jsdoc != null && jsdoc.makesStructs()) {
return this.commonTypes.getTopStruct();
}
if (jsdoc != null && jsdoc.makesDicts()) {
return this.commonTypes.getTopDict();
}
return this.commonTypes.getEmptyObjectLiteral();
}
case FOR:
case FOR_IN:
Preconditions.checkState(expr.isForIn());
return TOP_OBJECT;
case GETPROP:
case GETELEM:
case IN:
return TOP_OBJECT;
default:
throw new RuntimeException("Unhandled node for pickReqObjType: " + exprKind);
}
}
private static String getReadableCalleeName(Node expr) {
return expr.isQualifiedName() ? expr.getQualifiedName() : "";
}
private static JSType specializeKeep2ndWhenBottom(
JSType toBeSpecialized, JSType fallback) {
JSType specializedType = toBeSpecialized.specialize(fallback);
return specializedType.isBottom() ? fallback : specializedType;
}
TypeEnv getEntryTypeEnv() {
return getOutEnv(this.cfg.getEntry());
}
private class DeferredCheck {
final Node callSite;
final NTIScope callerScope;
final NTIScope calleeScope;
// Null types means that they were declared
// (and should have been checked during inference)
JSType expectedRetType;
List<JSType> argTypes;
DeferredCheck(
Node callSite,
JSType expectedRetType,
NTIScope callerScope,
NTIScope calleeScope) {
this.callSite = callSite;
this.expectedRetType = expectedRetType;
this.callerScope = callerScope;
this.calleeScope = calleeScope;
}
void updateReturn(JSType expectedRetType) {
if (this.expectedRetType != null) {
this.expectedRetType = JSType.meet(this.expectedRetType, expectedRetType);
}
}
void updateArgTypes(List<JSType> argTypes) {
this.argTypes = argTypes;
}
private void runCheck(
Map<NTIScope, JSType> summaries, WarningReporter warnings) {
FunctionType fnSummary = summaries.get(this.calleeScope).getFunType();
println(
"Running deferred check of function: ", calleeScope.getReadableName(),
" with FunctionSummary of: ", fnSummary, " and callsite ret: ",
expectedRetType, " args: ", argTypes);
if (this.expectedRetType != null &&
!fnSummary.getReturnType().isSubtypeOf(this.expectedRetType)) {
warnings.add(JSError.make(
this.callSite, INVALID_INFERRED_RETURN_TYPE,
errorMsgWithTypeDiff(
this.expectedRetType, fnSummary.getReturnType())));
}
int i = 0;
Node argNode = callSite.getSecondChild();
// this.argTypes can be null if in the fwd direction the analysis of the
// call return prematurely, eg, because of a WRONG_ARGUMENT_COUNT.
if (this.argTypes == null) {
return;
}
for (JSType argType : this.argTypes) {
JSType formalType = fnSummary.getFormalType(i);
if (argNode.isName() && callerScope.isKnownFunction(argNode.getString())) {
argType = summaries.get(callerScope.getScope(argNode.getString()));
}
if (argType != null) {
if (argType.isSubtypeOf(formalType)) {
registerImplicitUses(argNode, argType, formalType);
} else {
JSError error = JSError.make(argNode, INVALID_ARGUMENT_TYPE, Integer.toString(i + 1),
calleeScope.getReadableName(), errorMsgWithTypeDiff(formalType, argType));
registerMismatchAndWarn(error, argType, formalType);
}
}
i++;
argNode = argNode.getNext();
}
}
@Override
public boolean equals(Object o) {
Preconditions.checkArgument(o instanceof DeferredCheck);
DeferredCheck dc2 = (DeferredCheck) o;
return callSite == dc2.callSite &&
callerScope == dc2.callerScope &&
calleeScope == dc2.calleeScope &&
Objects.equals(expectedRetType, dc2.expectedRetType) &&
Objects.equals(argTypes, dc2.argTypes);
}
@Override
public int hashCode() {
return Objects.hash(
callSite, callerScope, calleeScope, expectedRetType, argTypes);
}
}
}