com.google.javascript.jscomp.disambiguate.DisambiguateProperties Maven / Gradle / Ivy
/*
* Copyright 2008 The Closure Compiler Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.javascript.jscomp.disambiguate;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.Multimap;
import com.google.common.collect.Table;
import com.google.common.collect.Tables;
import com.google.javascript.jscomp.AbstractCompiler;
import com.google.javascript.jscomp.CheckLevel;
import com.google.javascript.jscomp.CompilerPass;
import com.google.javascript.jscomp.DestructuredTarget;
import com.google.javascript.jscomp.GatherGetterAndSetterProperties;
import com.google.javascript.jscomp.InvalidatingTypes;
import com.google.javascript.jscomp.JSError;
import com.google.javascript.jscomp.NodeTraversal;
import com.google.javascript.jscomp.NodeTraversal.AbstractScopedCallback;
import com.google.javascript.jscomp.NodeUtil;
import com.google.javascript.jscomp.PropertyRenamingDiagnostics;
import com.google.javascript.jscomp.graph.StandardUnionFind;
import com.google.javascript.jscomp.graph.UnionFind;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.jstype.FunctionType;
import com.google.javascript.rhino.jstype.JSType;
import com.google.javascript.rhino.jstype.JSTypeNative;
import com.google.javascript.rhino.jstype.JSTypeRegistry;
import com.google.javascript.rhino.jstype.ObjectType;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
/**
* DisambiguateProperties renames properties to disambiguate between unrelated fields with the same
* name. Two properties are considered related if they share a definition on their prototype chains,
* or if they are potentially referenced together via union types.
*
* Renamimg only occurs if there are two or more distinct properties with the same name.
*
*
This pass allows other passes, such as inlining and code removal to take advantage of type
* information implicitly.
*
*
* Foo.a;
* Bar.a;
*
*
* will become
*
*
* Foo.Foo$a;
* Bar.Bar$a;
*
*
* NOTE(dimvar): For every property, this pass groups together the types that can't be
* disambiguated. If a type inherits from another type, their common properties can never be
* disambiguated, yet we have to compute this info once per property rather than just once in the
* pass. This is where the bulk of the time is spent. We have added many caches that help a lot, but
* it is probably worth it to revisit this pass and rewrite it in a way that does not compute the
* same thing over and over.
*/
public class DisambiguateProperties implements CompilerPass {
// To prevent the logs from filling up, we cap the number of warnings
// that we tell the user to fix per-property.
private static final int MAX_INVALIDATION_WARNINGS_PER_PROPERTY = 10;
private static final Logger logger = Logger.getLogger(
DisambiguateProperties.class.getName());
private static final Pattern NONWORD_PATTERN = Pattern.compile("[^\\w$]");
private final AbstractCompiler compiler;
private final InvalidatingTypes invalidatingTypes;
private final JSTypeRegistry registry;
/** Map of a type to all the related errors that invalidated the type for disambiguation. */
private final Multimap invalidationMap;
/**
* In practice any large code base will have thousands and thousands of
* type invalidations, which makes reporting all of the errors useless.
* However, certain properties are worth specifically guarding because of the
* large amount of code that can be removed as dead code. This list contains
* the properties (eg: "toString") that we care about; if any of these
* properties is invalidated it causes an error.
*/
private final Map propertiesToErrorFor;
// Use this cache to call FunctionType#getImplementedInterfaces
// or FunctionType#getExtendedInterfaces only once per constructor.
private Map> ancestorInterfaces;
// Cache calls to getRepresentativeType.
private final Table representativeTypeCache =
Tables.newCustomTable(new LinkedHashMap<>(), IdentityHashMap::new);
/**
* A sentinel value for cached computations of {@link #getRepresentativeType} which returned
* {@code null}.
*
* Use of a sentinel allows the value in {@link #representativeTypeCache} to encode that there
* is no useful result, obviating the need to first check {@code Table#contains}. That is, it
* ensures that within the cache, {@code null} always means "not yet computed".
*/
private final ObjectType representativeTypeSentinel;
private class Property {
/** The name of the property. */
final String name;
/**
* All top types on which the field exists, grouped together if related. See
* getRepresentativeType. If a property exists on a parent class and a subclass, only the parent
* class is recorded here.
*/
private UnionFind types;
/**
* A set of types for which renaming this field should be skipped. This
* list is first filled by fields defined in the externs file.
*/
Set typesToSkip = new HashSet<>();
/**
* If false, do not rename any instance of this field, as it has been referenced in an
* untracable way.
*/
boolean isValidForRenaming = true;
/**
* A map from nodes that need renaming to the highest type in the prototype
* chain containing the field for each node. In the case of a union, the
* type is the highest type of one of the types in the union.
*/
Map rootTypesByNode = new LinkedHashMap<>();
/**
* For every property p and type t, we only need to run recordInterfaces
* once. Use this cache to avoid needless calls.
*/
private final Set recordInterfacesCache = new HashSet<>();
Property(String name) {
this.name = name;
}
/** Returns the types on which this field is referenced. */
UnionFind getTypes() {
if (types == null) {
types = new StandardUnionFind<>();
}
return types;
}
/**
* Record that this property is referenced from this type.
*/
void addType(JSType type, JSType relatedType) {
checkState(this.isValidForRenaming, "Attempt to record an invalidated property: %s", name);
JSType top = getRepresentativeType(this.name, type);
if (invalidatingTypes.isInvalidating(top)) {
invalidate();
return;
}
if (isTypeToSkip(top)) {
addTypeToSkip(top);
}
if (relatedType == null) {
getTypes().add(top);
} else {
getTypes().union(top, relatedType);
}
recordInterfaces(type, top);
}
/** Records the given type as one to skip for this property. */
void addTypeToSkip(JSType type) {
for (JSType skipType : getTypesToSkipForType(type)) {
typesToSkip.add(skipType);
getTypes().union(skipType, type);
}
}
/** Invalidates any types related to invalid types. */
void expandTypesToSkip() {
// If we are not going to rename any properties, then we do not need to
// update the list of invalid types, as they are all invalid.
if (!this.wouldBeDisambiguatedByRenaming()) {
return;
}
for (int count = 0; count < 10; count++) {
// Make sure that the representative type for each type to skip is
// marked as being skipped.
Set rootTypesToSkip = new HashSet<>();
for (JSType subType : typesToSkip) {
rootTypesToSkip.add(types.find(subType));
}
typesToSkip.addAll(rootTypesToSkip);
Set newTypesToSkip = new HashSet<>();
Set allTypes = types.elements();
int originalTypesSize = allTypes.size();
for (JSType subType : allTypes) {
if (!typesToSkip.contains(subType) && typesToSkip.contains(types.find(subType))) {
newTypesToSkip.add(subType);
}
}
for (JSType newType : newTypesToSkip) {
addTypeToSkip(newType);
}
// If there were not any new types added, we are done here.
if (types.elements().size() == originalTypesSize) {
return;
}
}
throw new IllegalStateException("Stuck in loop expanding types to skip.");
}
/** Returns true if any instance of this property should be renamed. */
boolean wouldBeDisambiguatedByRenaming() {
return this.isValidForRenaming && types != null && types.allEquivalenceClasses().size() > 1;
}
/**
* Returns true if this property should be renamed on this type. expandTypesToSkip() should be
* called before this, if anything has been added to the typesToSkip list.
*/
boolean shouldRenameOnType(JSType type) {
return this.isValidForRenaming && !typesToSkip.contains(type);
}
/**
* Invalidates a field from renaming. Used for field references on an
* object with unknown type.
*/
boolean invalidate() {
boolean changed = this.isValidForRenaming;
this.isValidForRenaming = false;
types = null;
typesToSkip = null;
rootTypesByNode = null;
return changed;
}
/**
* Schedule the node to potentially be renamed.
*
* @param node the node to rename
* @return True if type was accepted without invalidation or if the property was already
* invalidated. False if this property was invalidated this time.
*/
boolean scheduleRenaming(Node node, JSType targetType) {
JSType type = processProperty(targetType);
if (this.isValidForRenaming) {
if (invalidatingTypes.isInvalidating(type)) {
invalidate();
return false;
}
rootTypesByNode.put(node, type);
}
return true;
}
@Nullable
private JSType processProperty(JSType type) {
return processProperty(type, null);
}
/**
* Processes a property, adding it to the list of properties to rename.
*
* @param type a type this property is known to be on, not necessarily the highest type
* @param relatedType only for use inside this function, other callers should pass null.
* @return a representative type for the property reference, which will be the highest type on
* the prototype chain of the provided type. In the case of a union type, it will be the
* highest type on the prototype chain of one of the members of the union. Returns null if
* the property is marked as not renamable or the given type is invalidated
*/
@Nullable
private JSType processProperty(JSType type, @Nullable JSType relatedType) {
type = type.restrictByNotNullOrUndefined();
if (!this.isValidForRenaming || invalidatingTypes.isInvalidating(type)) {
return null;
}
Iterable extends JSType> alternatives = getTypeAlternatives(type);
if (alternatives != null) {
JSType firstType = relatedType;
for (JSType subType : alternatives) {
JSType lastType = processProperty(subType, firstType);
if (firstType == null) {
firstType = lastType; // maybe null
}
}
return firstType;
} else {
JSType topType = getRepresentativeType(this.name, type);
if (invalidatingTypes.isInvalidating(topType)) {
return null;
}
this.addType(type, relatedType);
return topType;
}
}
/**
* Records that this property could be referenced from any interface that this type inherits
* from.
*
* If the property p is defined only on a subtype of constructor, then this method has no
* effect. But we tried modifying getRepresentativeType to tell us when the returned type is a
* subtype, and then skip those calls to recordInterface, and there was no speed-up. And it made
* the code harder to understand, so we don't do it.
*/
private void recordInterfaces(JSType type, JSType relatedType) {
@Nullable FunctionType constructor = getConstructor(type);
if (constructor == null || !recordInterfacesCache.add(type)) {
return;
}
Iterable interfaces =
ancestorInterfaces.computeIfAbsent(constructor, FunctionType::getAncestorInterfaces);
for (ObjectType itype : interfaces) {
JSType top = getRepresentativeType(name, itype);
if (top != null) {
addType(itype, relatedType);
}
// If this interface invalidated this property, return now.
if (!this.isValidForRenaming) {
return;
}
}
}
}
private final Map properties = new LinkedHashMap<>();
public DisambiguateProperties(
AbstractCompiler compiler, Map propertiesToErrorFor) {
this.compiler = compiler;
this.registry = compiler.getTypeRegistry();
this.representativeTypeSentinel =
this.registry.getNativeType(JSTypeNative.NO_OBJECT_TYPE).toMaybeObjectType();
this.propertiesToErrorFor = propertiesToErrorFor;
this.invalidationMap = propertiesToErrorFor.isEmpty() ? null : LinkedHashMultimap.create();
this.invalidatingTypes =
new InvalidatingTypes.Builder(registry)
.writeInvalidationsInto(this.invalidationMap)
.addAllTypeMismatches(compiler.getTypeMismatches())
.addAllTypeMismatches(compiler.getImplicitInterfaceUses())
.allowEnums()
.allowScalars()
.build();
}
@Override
public void process(Node externs, Node root) {
checkState(compiler.getLifeCycleStage().isNormalized());
this.ancestorInterfaces = new HashMap<>();
// Gather names of properties in externs; these properties can't be renamed.
NodeTraversal.traverse(compiler, externs, new FindExternProperties());
// Look at each unquoted property access and decide if that property will
// be renamed.
NodeTraversal.traverse(compiler, root, new FindRenameableProperties());
// Do the actual renaming.
renameProperties();
// Update any getters and setters we renamed.
// TODO(b/161947315): this shouldn't be the responsibility of DisambiguateProperties
GatherGetterAndSetterProperties.update(compiler, externs, root);
}
/** Returns the property for the given name, creating it if necessary. */
protected Property getProperty(String name) {
properties.computeIfAbsent(name, Property::new);
return properties.get(name);
}
/**
* Finds all properties defined in the externs file and sets them as ineligible for renaming from
* the type on which they are defined.
*/
private class FindExternProperties extends AbstractScopedCallback {
@Override
public void visit(NodeTraversal t, Node n, Node parent) {
// TODO(johnlenz): Support object-literal property definitions.
if (n.isGetProp()) {
Node recv = n.getFirstChild();
JSType recvType = getType(recv);
Property prop = getProperty(n.getLastChild().getString());
processExternsProperty(prop, recvType);
} else if (n.isClass()) {
JSType classType = n.getJSType();
JSType classInstanceType =
// the class type may not be a function type if it was in a cast, so treat it as unknown
classType.toMaybeFunctionType() != null
? classType.toMaybeFunctionType().getPrototype()
: registry.getNativeType(JSTypeNative.UNKNOWN_TYPE);
for (Node member : NodeUtil.getClassMembers(n).children()) {
// possibilities are:
// MEMBER_FUNCTION_DEF (could be static)
// COMPUTED_PROP (ignore)
// GETTER_DEF (could be static)
// SETTER_DEF (could be static)
if (member.isComputedProp() || member.isQuotedString()) {
// TODO(b/119682883): consider handling symbols here
continue;
}
String name = member.getString();
JSType ownerType = member.isStaticMember() ? classType : classInstanceType;
Property prop = getProperty(name);
processExternsProperty(prop, ownerType);
}
}
}
/**
* Marks an externs property as not-renamable, and possibly invalidates the property based on
* the type it is on
*
* @param ownerType The type of the object the property is on (e.g. `ns.a` for `ns.a.MyType;`)
*/
private void processExternsProperty(Property prop, JSType ownerType) {
if (invalidatingTypes.isInvalidating(ownerType)
// TODO(b/119886075): invalidating here when isStructuralInterfacePrototype is true is
// kind of arbitrary. We should only do it when the @record is implicitly implemented.
|| isStructuralInterfacePrototype(ownerType)) {
prop.invalidate();
} else if (prop.isValidForRenaming) {
prop.addTypeToSkip(ownerType);
// If this is a prototype property, then we want to skip assignments
// to the instance type as well. These assignments are not usually
// seen in the extern code itself, so we must handle them here.
JSType instanceType = getInstanceIfPrototype(ownerType);
if (instanceType != null) {
prop.getTypes().add(instanceType);
prop.typesToSkip.add(instanceType);
}
}
}
}
private void reportInvalidation(Node location, String propName, JSType receiver) {
CheckLevel level = propertiesToErrorFor.getOrDefault(propName, CheckLevel.OFF);
if (level.equals(CheckLevel.OFF)) {
return;
}
ArrayList locations = new ArrayList<>();
printErrorLocations(locations, receiver);
String locationsMsg =
locations.isEmpty() ? "This type is inherently invalidating" : String.join("\n", locations);
compiler.report(
JSError.make(
location,
level,
PropertyRenamingDiagnostics.INVALIDATION,
propName,
receiver.toString(),
locationsMsg));
}
private void printErrorLocations(List locations, JSType t) {
if (!t.isObjectType() || t.isAllType()) {
return;
}
if (t.isUnionType()) {
for (JSType alt : t.getUnionMembers()) {
printErrorLocations(locations, alt);
}
return;
}
invalidationMap.get(t).stream()
.limit(MAX_INVALIDATION_WARNINGS_PER_PROPERTY)
.map((l) -> l.getSourceFileName() + ":" + l.getLineno() + ":" + l.getCharno())
.forEachOrdered(locations::add);
}
/**
* Traverses the tree, building a map from field names to Nodes for all fields that can be
* renamed.
*/
private class FindRenameableProperties extends AbstractScopedCallback {
@Override
public void visit(NodeTraversal t, Node n, Node parent) {
if (n.isGetProp()) {
handleGetProp(n);
} else if (n.isObjectLit()) {
handleObjectLit(n);
} else if (n.isCall()) {
handleCall(n);
} else if (n.isClass()) {
handleClass(n);
} else if (n.isObjectPattern()) {
handleObjectPattern(n);
}
}
private void handleGetProp(Node n) {
String name = n.getLastChild().getString();
JSType type = getType(n.getFirstChild());
Property prop = getProperty(name);
if (!prop.scheduleRenaming(n.getLastChild(), type)) {
reportInvalidation(n, name, type);
}
}
private void handleObjectLit(Node n) {
// Object.defineProperties literals are handled at the CALL node.
if (n.getParent().isCall() && NodeUtil.isObjectDefinePropertiesDefinition(n.getParent())) {
return;
}
for (Node child = n.getFirstChild(); child != null; child = child.getNext()) {
switch (child.getToken()) {
case COMPUTED_PROP:
// These won't be renamed due to our assumptions. Ignore them.
case OBJECT_SPREAD:
// Ignore properties added via spread. All properties accessed from object literals are
// invalidated regardless, so we don't have to explicitly do that here. Additionally,
// even if we invalidated all the properties known to be on the spread type, there may
// be others we're unaware of, so it would be insufficient.
continue;
case STRING_KEY:
case MEMBER_FUNCTION_DEF:
case GETTER_DEF:
case SETTER_DEF:
if (child.isQuotedString()) {
continue; // These won't be renamed due to our assumptions. Ignore them.
}
// We should never see a mix of numbers and strings.
String name = child.getString();
JSType objlitType = getType(n);
Property prop = getProperty(name);
if (!prop.scheduleRenaming(child, objlitType)) {
reportInvalidation(n, name, objlitType);
}
break;
default:
throw new IllegalStateException(
"Unexpected child of OBJECTLIT: " + child.toStringTree());
}
}
}
/** Examines calls in case they are Object.defineProperties calls */
private void handleCall(Node call) {
Node target = call.getFirstChild();
if (!target.isQualifiedName()) {
return;
}
String functionName = target.getOriginalQualifiedName();
if (functionName != null
&& compiler.getCodingConvention().isPropertyRenameFunction(functionName)) {
handlePropertyRenameFunctionCall(call, functionName);
} else if (NodeUtil.isObjectDefinePropertiesDefinition(call)) {
handleObjectDefineProperties(call);
}
}
private void handleClass(Node classNode) {
JSType classType = classNode.getJSType();
JSType classInstanceType =
// the class type may not be a function type if it was in a cast, so treat it as unknown
classType.toMaybeFunctionType() != null
? classType.toMaybeFunctionType().getInstanceType()
: registry.getNativeType(JSTypeNative.UNKNOWN_TYPE);
for (Node member : NodeUtil.getClassMembers(classNode).children()) {
// possibilities are:
// MEMBER_FUNCTION_DEF (could be static)
// COMPUTED_PROP (ignore)
// GETTER_DEF (could be static)
// SETTER_DEF (could be static)
if (member.isComputedProp() || member.isQuotedString()) {
// TODO(b/119682883): consider handling symbols here
continue;
}
String name = member.getString();
Property prop = getProperty(name);
JSType ownerType = member.isStaticMember() ? classType : classInstanceType;
if (!prop.scheduleRenaming(member, ownerType)) {
reportInvalidation(classNode, name, ownerType);
}
}
}
private void handleObjectPattern(Node pattern) {
JSType objectPatternType = checkNotNull(pattern.getJSType());
for (DestructuredTarget target :
DestructuredTarget.createAllNonEmptyTargetsInPattern(
compiler.getTypeRegistry(), objectPatternType, pattern)) {
if (!target.hasStringKey()) {
// ignore computed properties and rest
// TODO(b/119682883): consider handling symbols here
continue;
}
Node stringKey = target.getStringKey();
if (stringKey.isQuotedString()) {
// Never rename quoted property accesses, e.g.
// const {'prop': localVar} = someObj;
continue;
}
String name = stringKey.getString();
Property prop = getProperty(name);
if (!prop.scheduleRenaming(stringKey, objectPatternType)) {
reportInvalidation(stringKey, name, objectPatternType);
}
}
}
private void handlePropertyRenameFunctionCall(Node call, String renameFunctionName) {
int childCount = call.getChildCount();
if (childCount != 2 && childCount != 3) {
compiler.report(
JSError.make(
call,
PropertyRenamingDiagnostics.INVALID_RENAME_FUNCTION,
renameFunctionName,
" Must be called with 1 or 2 arguments"));
return;
}
if (!call.getSecondChild().isString()) {
compiler.report(
JSError.make(
call,
PropertyRenamingDiagnostics.INVALID_RENAME_FUNCTION,
renameFunctionName,
" The first argument must be a string literal."));
return;
}
String propName = call.getSecondChild().getString();
if (propName.contains(".")) {
compiler.report(
JSError.make(
call,
PropertyRenamingDiagnostics.INVALID_RENAME_FUNCTION,
renameFunctionName,
" The first argument must not be a property path."));
return;
}
Node obj = call.getChildAtIndex(2);
JSType type = getType(obj);
Property prop = getProperty(propName);
if (!prop.scheduleRenaming(call.getSecondChild(), type)) {
reportInvalidation(obj, propName, type);
}
}
private void handleObjectDefineProperties(Node call) {
Node typeObj = call.getSecondChild();
JSType type = getType(typeObj);
Node objectLiteral = typeObj.getNext();
if (!objectLiteral.isObjectLit()) {
return;
}
for (Node key : objectLiteral.children()) {
switch (key.getToken()) {
case COMPUTED_PROP:
case OBJECT_SPREAD:
break;
case STRING_KEY:
case MEMBER_FUNCTION_DEF:
case GETTER_DEF:
case SETTER_DEF:
if (!key.isQuotedString()) {
Property prop = getProperty(key.getString());
prop.scheduleRenaming(key, type);
}
break;
default:
throw new IllegalStateException("Unrecognized child of object lit " + key);
}
}
}
}
/** Renames all properties with references on more than one type. */
void renameProperties() {
int propsRenamed = 0;
int propsSkipped = 0;
int instancesRenamed = 0;
int instancesSkipped = 0;
int singleTypeProps = 0;
Set reported = new HashSet<>();
for (Property prop : properties.values()) {
if (!prop.wouldBeDisambiguatedByRenaming()) {
if (!prop.isValidForRenaming) {
++propsSkipped;
} else {
++singleTypeProps;
}
continue;
}
UnionFind pTypes = prop.getTypes();
Map propNames = buildPropNames(prop);
++propsRenamed;
prop.expandTypesToSkip();
// This loop has poor locality, because instead of walking the AST,
// we iterate over all accesses of a property, which can be in very
// different places in the code.
for (Map.Entry entry : prop.rootTypesByNode.entrySet()) {
Node node = entry.getKey();
JSType rootType = entry.getValue();
if (prop.shouldRenameOnType(rootType)) {
String newName = propNames.get(pTypes.find(rootType));
node.setString(newName);
compiler.reportChangeToEnclosingScope(node);
++instancesRenamed;
continue;
}
++instancesSkipped;
CheckLevel checkLevelForProp = propertiesToErrorFor.getOrDefault(prop.name, CheckLevel.OFF);
if (!prop.isValidForRenaming
&& checkLevelForProp != CheckLevel.OFF
&& !reported.contains(prop.name)) {
reported.add(prop.name);
compiler.report(
JSError.make(
node,
checkLevelForProp,
PropertyRenamingDiagnostics.INVALIDATION_ON_TYPE,
prop.name,
rootType.toString(),
""));
}
}
}
if (logger.isLoggable(Level.FINE)) {
logger.fine("Renamed " + instancesRenamed + " instances of "
+ propsRenamed + " properties.");
logger.fine("Skipped renaming " + instancesSkipped + " invalidated "
+ "properties, " + propsSkipped + " instances of properties "
+ "that were skipped for specific types and " + singleTypeProps
+ " properties that were referenced from only one type.");
}
}
/**
* Chooses a name to use for renaming in each equivalence class and maps
* the representative type of that class to that name.
*/
private Map buildPropNames(Property prop) {
UnionFind pTypes = prop.getTypes();
String pname = prop.name;
Map names = new HashMap<>();
for (Set set : pTypes.allEquivalenceClasses()) {
checkState(!set.isEmpty());
JSType representative = pTypes.find(set.iterator().next());
String typeName = null;
for (JSType type : set) {
String typeString = type.toString();
if (typeName == null || typeString.compareTo(typeName) < 0) {
typeName = typeString;
}
}
String newName;
if ("{...}".equals(typeName)) {
newName = pname;
} else {
newName = NONWORD_PATTERN.matcher(typeName).replaceAll("_") + '$' + pname;
}
names.put(representative, newName);
}
return names;
}
/** Returns a map from field name to types for which it will be renamed. */
@VisibleForTesting
Multimap> getRenamedTypesForTesting() {
Multimap> ret = HashMultimap.create();
for (Map.Entry entry : properties.entrySet()) {
Property prop = entry.getValue();
if (prop.isValidForRenaming) {
for (Collection c : prop.getTypes().allEquivalenceClasses()) {
if (!c.isEmpty() && !prop.typesToSkip.contains(c.iterator().next())) {
ret.put(entry.getKey(), c);
}
}
}
}
return ret;
}
private JSType getType(Node node) {
if (node == null || node.getJSType() == null) {
return registry.getNativeType(JSTypeNative.UNKNOWN_TYPE);
}
return node.getJSType();
}
/**
* Returns a set of types that should be skipped given the given type. This is
* necessary for interfaces, as all super interfaces must also be skipped.
*/
private ImmutableSet getTypesToSkipForType(JSType type) {
type = type.restrictByNotNullOrUndefined();
if (type.isUnionType()) {
ImmutableSet.Builder types = ImmutableSet.builder();
types.add(type);
for (JSType alt : type.getUnionMembers()) {
types.addAll(getTypesToSkipForTypeNonUnion(alt));
}
return types.build();
} else if (type.isEnumElementType()) {
return getTypesToSkipForType(type.getEnumeratedTypeOfEnumElement());
}
return ImmutableSet.copyOf(getTypesToSkipForTypeNonUnion(type));
}
private Set getTypesToSkipForTypeNonUnion(JSType type) {
Set types = new HashSet<>();
JSType skipType = type;
while (skipType != null) {
types.add(skipType);
ObjectType objSkipType = skipType.toMaybeObjectType();
if (objSkipType != null) {
skipType = objSkipType.getImplicitPrototype();
} else {
break;
}
}
return types;
}
/**
* Determines whether the given type is one whose properties should not be
* considered for renaming.
*/
private boolean isTypeToSkip(JSType type) {
return type.isEnumType() || type.isBoxableScalar();
}
/**
* Returns the alternatives if this is a type that represents multiple
* types, and null if not. Union and interface types can correspond to
* multiple other types.
*/
private Iterable extends JSType> getTypeAlternatives(JSType type) {
if (type.isUnionType()) {
return type.getUnionMembers();
} else {
ObjectType objType = type.toMaybeObjectType();
FunctionType constructor = objType != null ? objType.getConstructor() : null;
if (constructor != null && constructor.isInterface()) {
List list = new ArrayList<>();
for (FunctionType impl : constructor.getDirectSubTypes()) {
list.add(impl.getInstanceType());
}
return list.isEmpty() ? null : list;
} else {
return null;
}
}
}
/**
* Returns the type representing an innate disambiguation cluster of types as defined by the
* declarations of properties named {@code field} against the type lattice.
*
* The representative type is the top-most type that declares {@code field} and which is above
* {@code type}. Therefore, for any type, its representative is always the same, and it is
* possible to compute disambiguations by only considering representatives.
*
*
Interface edges in the lattice, because they form a DAG and not a tree, are not walked when
* finding the representative. Interfaces must therefore be considered as being in distinct innate
* clusters.
*/
private ObjectType getRepresentativeType(String field, JSType type) {
if (type == null) {
return null;
}
ObjectType foundType = representativeTypeCache.get(field, type);
if (foundType != null) {
return foundType.equals(representativeTypeSentinel) ? null : foundType;
}
if (type.isEnumElementType()) {
foundType = getRepresentativeType(field, type.getEnumeratedTypeOfEnumElement());
representativeTypeCache.put(
field, type, foundType == null ? representativeTypeSentinel : foundType);
return foundType;
}
if (!type.isObjectType()) {
if (type.isBoxableScalar()) {
foundType = getRepresentativeType(field, type.autobox());
representativeTypeCache.put(
field, type, foundType == null ? representativeTypeSentinel : foundType);
return foundType;
} else {
representativeTypeCache.put(field, type, representativeTypeSentinel);
return null;
}
}
// Ignore the prototype itself at all times.
if ("prototype".equals(field)) {
representativeTypeCache.put(field, type, representativeTypeSentinel);
return null;
}
// We look up the prototype chain to find the highest place (if any) that
// this appears. This will make references to overridden properties look
// like references to the initial property, so they are renamed alike.
ObjectType objType = type.toMaybeObjectType();
if (objType != null) {
foundType = objType.getTopMostDefiningType(field);
}
// If the property does not exist on the referenced type but the original
// type is an object type, see if any subtype has the property.
if (foundType == null) {
JSType subtypeWithProp = type.getGreatestSubtypeWithProperty(field);
ObjectType maybeType = subtypeWithProp == null ? null : subtypeWithProp.toMaybeObjectType();
// getGreatestSubtypeWithProperty does not guarantee that the property
// is defined on the returned type, it just indicates that it might be,
// so we have to double check.
if (maybeType != null && maybeType.hasOwnProperty(field)) {
foundType = maybeType;
}
}
// Unwrap templatized types, they are not unique at runtime.
if (foundType != null && foundType.isTemplatizedType()) {
foundType = foundType.getRawType();
}
// Since disambiguation just looks at names, we must return a uniquely named type rather
// than an "equivalent" type. In particular, we must manually unwrap named types
// so that the returned type has the correct name.
if (foundType != null && foundType.isNamedType()) {
foundType = foundType.toMaybeNamedType().getReferencedType().toMaybeObjectType();
}
representativeTypeCache.put(
field, type, foundType == null ? representativeTypeSentinel : foundType);
return foundType;
}
private static boolean isStructuralInterfacePrototype(JSType type) {
if (!type.isFunctionPrototypeType()) {
return false;
}
FunctionType constructor = type.toObjectType().getOwnerFunction();
return constructor != null && constructor.isStructuralInterface();
}
/**
* Returns the corresponding instance if `maybePrototype` is a prototype of a constructor,
* otherwise null
*/
@Nullable
private static JSType getInstanceIfPrototype(JSType maybePrototype) {
if (maybePrototype.isFunctionPrototypeType()) {
FunctionType constructor = maybePrototype.toObjectType().getOwnerFunction();
if (constructor != null) {
if (!constructor.hasInstanceType()) {
// this can happen when adding to the prototype of a non-constructor function
return null;
}
return constructor.getInstanceType();
}
}
return null;
}
/**
* Return the constructor type of {@code type}, which holds {@code type}'s implemented interfaces.
*/
private FunctionType getConstructor(JSType type) {
ObjectType objType = type.toMaybeObjectType();
if (objType == null) {
return null;
}
if (objType.isFunctionType()) {
// Recall that the constructor of a constructor `A`, is not `A` itself, but rather `Function`.
// If `Function` implemented some interface, that interface would be conflated with all other
// ctors, however that currently isn't the case, and would be correct.
return (FunctionType) registry.getNativeType(JSTypeNative.FUNCTION_FUNCTION_TYPE);
} else if (objType.isFunctionPrototypeType()) {
// For the purposes of disambiguation, pretend that prototypes implement the interfaces of
// their instance type.
return objType.getOwnerFunction();
} else {
return objType.getConstructor();
}
}
}