/* * Copyright 2015 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.base.Preconditions; import com.google.common.base.Predicate; import com.google.common.collect.Iterables; import com.google.javascript.rhino.JSDocInfo; import com.google.javascript.rhino.Node; import com.google.javascript.rhino.TypeIEnv; import com.google.javascript.rhino.jstype.JSType; import com.google.javascript.rhino.jstype.ObjectType; import com.google.javascript.rhino.jstype.StaticTypedScope; import com.google.javascript.rhino.jstype.StaticTypedSlot; import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; /** * TypedScope contains information about variables and their types. * Scopes can be nested, a scope points back to its parent scope. * <p> * TypedScope is also used as a lattice element for flow-sensitive type inference. * As a lattice element, a scope is viewed as a map from names to types. A name * not in the map is considered to have the bottom type. The join of two maps m1 * and m2 is the map of the union of names with {@link JSType#getLeastSupertype} * to meet the m1 type and m2 type. * * @see NodeTraversal * @see DataFlowAnalysis * * Several methods in this class, such as {@code isBlockScope} throw an exception when called. * The reason for this is that we want to shadow methods from the parent class, to avoid calling * them accidentally. */ public class TypedScope extends Scope implements StaticTypedScope<JSType>, TypeIEnv<JSType> { private final Map<String, TypedVar> vars = new LinkedHashMap<>(); private final TypedScope parent; /** Whether this is a bottom scope for the purposes of type inference. */ private final boolean isBottom; // Scope.java contains an arguments field. // We haven't added it here because it's unused by the passes that need typed scopes. private static final Predicate<TypedVar> DECLARATIVELY_UNBOUND_VARS_WITHOUT_TYPES = new Predicate<TypedVar>() { @Override public boolean apply(TypedVar var) { return var.getParentNode() != null && var.getType() == null && var.getParentNode().isVar() && !var.isExtern(); } }; TypedScope(TypedScope parent, Node rootNode) { super(parent, rootNode); this.parent = parent; this.isBottom = false; } /** * Creates a empty Scope (bottom of the lattice). * @param rootNode Typically a FUNCTION node or the global BLOCK node. * @param isBottom Whether this is the bottom of a lattice. Otherwise, * it must be a global scope. */ private TypedScope(Node rootNode, boolean isBottom) { super(rootNode); this.parent = null; this.isBottom = isBottom; } static TypedScope createGlobalScope(Node rootNode) { return new TypedScope(rootNode, false); } static TypedScope createLatticeBottom(Node rootNode) { return new TypedScope(rootNode, true); } /** Whether this is the bottom of the lattice. */ boolean isBottom() { return isBottom; } @Override public int getDepth() { return depth; } @Override public Node getRootNode() { return rootNode; } @Override public TypedScope getParent() { return parent; } @Override TypedScope getGlobalScope() { TypedScope result = this; while (result.getParent() != null) { result = result.getParent(); } return result; } @Override public StaticTypedScope<JSType> getParentScope() { return parent; } /** * Gets the type of {@code this} in the current scope. */ @Override public JSType getTypeOfThis() { if (isGlobal()) { return ObjectType.cast(rootNode.getJSType()); } Preconditions.checkState(rootNode.isFunction()); JSType nodeType = rootNode.getJSType(); if (nodeType != null && nodeType.isFunctionType()) { return nodeType.toMaybeFunctionType().getTypeOfThis(); } else { // Executed when the current scope has not been typechecked. return null; } } @Override Var declare(String name, Node nameNode, CompilerInput input) { throw new IllegalStateException( "Method declare(untyped) cannot be called on typed scopes."); } TypedVar declare(String name, Node nameNode, JSType type, CompilerInput input) { return declare(name, nameNode, type, input, true); } TypedVar declare(String name, Node nameNode, JSType type, CompilerInput input, boolean inferred) { Preconditions.checkState(name != null && !name.isEmpty()); TypedVar var = new TypedVar(inferred, name, nameNode, type, this, vars.size(), input); vars.put(name, var); return var; } @Override void undeclare(Var var) { TypedVar tvar = (TypedVar) var; Preconditions.checkState(tvar.scope == this); Preconditions.checkState(vars.get(tvar.name) == tvar); vars.remove(tvar.name); } @Override public TypedVar getSlot(String name) { return getVar(name); } @Override public TypedVar getOwnSlot(String name) { return vars.get(name); } @Override public TypedVar getVar(String name) { TypedScope scope = this; while (scope != null) { TypedVar var = scope.vars.get(name); if (var != null) { return var; } // Recurse up the parent Scope scope = scope.parent; } return null; } @Override public Var getArgumentsVar() { throw new IllegalStateException("Method getArgumentsVar cannot be called on typed scopes."); } @Override public boolean isDeclared(String name, boolean recurse) { TypedScope scope = this; while (true) { if (scope.vars.containsKey(name)) { return true; } if (scope.parent != null && recurse) { scope = scope.parent; continue; } return false; } } @Override public Iterable<TypedVar> getVarIterable() { return vars.values(); } @Override public Iterable<TypedVar> getAllSymbols() { return Collections.unmodifiableCollection(vars.values()); } @Override public int getVarCount() { return vars.size(); } @Override public boolean isGlobal() { return parent == null; } @Override public boolean isLocal() { return parent != null; } public Iterable<TypedVar> getDeclarativelyUnboundVarsWithoutTypes() { return Iterables.filter(getVarIterable(), DECLARATIVELY_UNBOUND_VARS_WITHOUT_TYPES); } static interface TypeResolver { void resolveTypes(); } private TypeResolver typeResolver; /** Resolve all type references. Only used on typed scopes. */ void resolveTypes() { if (typeResolver != null) { typeResolver.resolveTypes(); typeResolver = null; } } void setTypeResolver(TypeResolver resolver) { this.typeResolver = resolver; } @Override public boolean isBlockScope() { // TypedScope is not ES6 compatible yet, so always return false for now. return false; } @Override public boolean isFunctionBlockScope() { throw new IllegalStateException( "Method isFunctionBlockScope cannot be called on typed scopes."); } @Override public Scope getClosestHoistScope() { throw new IllegalStateException( "Method getClosestHoistScope cannot be called on typed scopes."); } @SuppressWarnings("unchecked") @Override public JSType getType(String typeName) { StaticTypedSlot<JSType> slot = getSlot(typeName); return slot == null ? null : slot.getType(); } @Override public JSDocInfo getJsdocOfTypeDeclaration(String typeName) { StaticTypedSlot<JSType> slot = getSlot(typeName); return slot == null ? null : slot.getJSDocInfo(); } }