com.google.javascript.jscomp.disambiguate.AmbiguateProperties 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.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import com.google.common.collect.ImmutableSet;
import com.google.javascript.jscomp.AbstractCompiler;
import com.google.javascript.jscomp.CompilerPass;
import com.google.javascript.jscomp.DefaultNameGenerator;
import com.google.javascript.jscomp.GatherGetterAndSetterProperties;
import com.google.javascript.jscomp.InvalidatingTypes;
import com.google.javascript.jscomp.JSError;
import com.google.javascript.jscomp.NameGenerator;
import com.google.javascript.jscomp.NodeTraversal;
import com.google.javascript.jscomp.NodeTraversal.AbstractPostOrderCallback;
import com.google.javascript.jscomp.NodeUtil;
import com.google.javascript.jscomp.PropertyRenamingDiagnostics;
import com.google.javascript.jscomp.graph.AdjacencyGraph;
import com.google.javascript.jscomp.graph.Annotation;
import com.google.javascript.jscomp.graph.DiGraph;
import com.google.javascript.jscomp.graph.FixedPointGraphTraversal;
import com.google.javascript.jscomp.graph.GraphColoring;
import com.google.javascript.jscomp.graph.GraphColoring.GreedyGraphColoring;
import com.google.javascript.jscomp.graph.GraphNode;
import com.google.javascript.jscomp.graph.LowestCommonAncestorFinder;
import com.google.javascript.jscomp.graph.SubGraph;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.jstype.JSType;
import com.google.javascript.rhino.jstype.JSTypeNative;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.Comparator;
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;
/**
* Renames unrelated properties to the same name, using type information. This allows better
* compression as more properties can be given short names.
*
* Properties are considered unrelated if they are never referenced from the same type or from a
* subtype of each others' types, thus this pass is only effective if type checking is enabled.
*
*
Example:
* Foo.fooprop = 0;
* Foo.fooprop2 = 0;
* Bar.barprop = 0;
*
becomes:
* Foo.a = 0;
* Foo.b = 0;
* Bar.a = 0;
*
*/
public class AmbiguateProperties implements CompilerPass {
private static final Logger logger = Logger.getLogger(
AmbiguateProperties.class.getName());
private final AbstractCompiler compiler;
private final List stringNodesToRename = new ArrayList<>();
// Can't use these to start property names.
private final char[] reservedFirstCharacters;
// Can't use these at all in property names.
private final char[] reservedNonFirstCharacters;
/** Map from property name to Property object */
private final Map propertyMap = new LinkedHashMap<>();
/** Property names that don't get renamed */
private final ImmutableSet externedNames;
/** Names to which properties shouldn't be renamed, to avoid name conflicts */
private final Set quotedNames = new HashSet<>();
/** Map from original property name to new name. Only used by tests. */
private Map renamingMap = null;
private TypeFlattener flattener = null;
/**
* Sorts Property objects by their count, breaking ties alphabetically to ensure a deterministic
* total ordering.
*/
private static final Comparator FREQUENCY_COMPARATOR =
(Property p1, Property p2) -> {
if (p1.numOccurrences != p2.numOccurrences) {
return p2.numOccurrences - p1.numOccurrences;
}
return p1.oldName.compareTo(p2.oldName);
};
/** A set of types that invalidate properties from ambiguation. */
private final InvalidatingTypes invalidatingTypes;
public AmbiguateProperties(
AbstractCompiler compiler,
char[] reservedFirstCharacters,
char[] reservedNonFirstCharacters,
Set externProperties) {
checkState(compiler.getLifeCycleStage().isNormalized());
this.compiler = compiler;
this.reservedFirstCharacters = reservedFirstCharacters;
this.reservedNonFirstCharacters = reservedNonFirstCharacters;
this.invalidatingTypes = new InvalidatingTypes.Builder(compiler.getTypeRegistry())
.addAllTypeMismatches(compiler.getTypeMismatches())
.addAllTypeMismatches(compiler.getImplicitInterfaceUses())
.build();
this.externedNames =
ImmutableSet.builder().add("prototype").addAll(externProperties).build();
}
static AmbiguateProperties makePassForTesting(
AbstractCompiler compiler,
char[] reservedFirstCharacters,
char[] reservedNonFirstCharacters,
Set externProperties) {
AmbiguateProperties ap =
new AmbiguateProperties(
compiler, reservedFirstCharacters, reservedNonFirstCharacters, externProperties);
ap.renamingMap = new HashMap<>();
return ap;
}
Map getRenamingMap() {
checkNotNull(renamingMap);
return renamingMap;
}
@Override
public void process(Node externs, Node root) {
TypeFlattener flattener =
new TypeFlattener(compiler.getTypeRegistry(), this.invalidatingTypes::isInvalidating);
this.flattener = flattener;
// Find all property references and record the types on which they occur.
// Populate stringNodesToRename, propertyMap, quotedNames.
NodeTraversal.traverse(compiler, root, new ProcessPropertiesAndConstructors());
TypeGraphBuilder graphBuilder =
new TypeGraphBuilder(flattener, LowestCommonAncestorFinder::new);
graphBuilder.addAll(flattener.getAllKnownTypes());
DiGraph typeGraph = graphBuilder.build();
// Cache the set of all flat types as it is rebuilt per call to getAllKnownTypes (but wait
// until after the typeGraph is built, as the process of building it may create new FlatTypes)
ImmutableSet allTypes = flattener.getAllKnownTypes();
for (FlatType flatType : allTypes) {
flatType.getSubtypeIds().set(flatType.getId()); // Init subtyping as reflexive.
}
FixedPointGraphTraversal.newReverseTraversal(
(subtype, e, supertype) -> {
/**
* Cheap path for when we're sure there's going to be a change.
*
* Since bits only ever turn on, using more bits means there are definitely more
* elements. This prevents of from needing to check cardinality or equality, which
* would otherwise dominate the cost of computing the fixed point.
*
*
We're guaranteed to converge because the sizes will be euqal after the OR
* operation.
*/
if (subtype.getSubtypeIds().size() > supertype.getSubtypeIds().size()) {
supertype.getSubtypeIds().or(subtype.getSubtypeIds());
return true;
}
int startSize = supertype.getSubtypeIds().cardinality();
supertype.getSubtypeIds().or(subtype.getSubtypeIds());
return supertype.getSubtypeIds().cardinality() > startSize;
})
.computeFixedPoint(typeGraph);
// Fill in all transitive edges in subtyping graph per property
for (Property prop : propertyMap.values()) {
if (prop.relatedTypesSeeds == null) {
continue;
}
for (FlatType flatType : prop.relatedTypesSeeds.keySet()) {
prop.relatedTypes.or(flatType.getSubtypeIds());
}
prop.relatedTypesSeeds = null;
}
ImmutableSet.Builder reservedNames = ImmutableSet.builder()
.addAll(externedNames)
.addAll(quotedNames);
int numRenamedPropertyNames = 0;
int numSkippedPropertyNames = 0;
ArrayList nodes = new ArrayList<>(propertyMap.size());
for (Property prop : propertyMap.values()) {
if (prop.skipAmbiguating) {
++numSkippedPropertyNames;
reservedNames.add(prop.oldName);
} else {
++numRenamedPropertyNames;
nodes.add(new PropertyGraphNode(prop));
}
}
PropertyGraph propertyGraph = new PropertyGraph(nodes);
GraphColoring coloring =
new GreedyGraphColoring<>(propertyGraph, FREQUENCY_COMPARATOR);
int numNewPropertyNames = coloring.color();
// Generate new names for the properties that will be renamed.
NameGenerator nameGen =
new DefaultNameGenerator(
reservedNames.build(), "", reservedFirstCharacters, reservedNonFirstCharacters);
String[] colorMap = new String[numNewPropertyNames];
for (int i = 0; i < numNewPropertyNames; ++i) {
colorMap[i] = nameGen.generateNextName();
}
// Translate the color of each Property instance to a name.
for (PropertyGraphNode node : propertyGraph.getNodes()) {
node.getValue().newName = colorMap[node.getAnnotation().hashCode()];
if (renamingMap != null) {
renamingMap.put(node.getValue().oldName, node.getValue().newName);
}
}
// Actually assign the new names to the relevant STRING nodes in the AST.
for (Node n : stringNodesToRename) {
String oldName = n.getString();
Property p = propertyMap.get(oldName);
if (p != null && p.newName != null) {
checkState(oldName.equals(p.oldName));
if (!p.newName.equals(oldName)) {
n.setString(p.newName);
compiler.reportChangeToEnclosingScope(n);
}
}
}
// We may have renamed getter / setter properties.
// TODO(b/161947315): this shouldn't be the responsibility of AmbiguateProperties
GatherGetterAndSetterProperties.update(compiler, externs, root);
if (logger.isLoggable(Level.FINE)) {
logger.fine("Collapsed " + numRenamedPropertyNames + " properties into "
+ numNewPropertyNames + " and skipped renaming "
+ numSkippedPropertyNames + " properties.");
}
}
class PropertyGraph implements AdjacencyGraph {
private final ArrayList nodes;
PropertyGraph(ArrayList nodes) {
this.nodes = nodes;
}
@Override
public List getNodes() {
return nodes;
}
@Override
public int getNodeCount() {
return nodes.size();
}
@Override
public GraphNode getNode(Property property) {
throw new RuntimeException("PropertyGraph#getNode is never called.");
}
@Override
public SubGraph newSubGraph() {
return new PropertySubGraph();
}
@Override
public void clearNodeAnnotations() {
for (PropertyGraphNode node : nodes) {
node.setAnnotation(null);
}
}
@Override
public int getWeight(Property value) {
return value.numOccurrences;
}
}
/**
* A {@link SubGraph} that represents properties. The related types of
* the properties are used to efficiently calculate adjacency information.
*/
class PropertySubGraph implements SubGraph {
/** Types related to properties referenced in this subgraph. */
final BitSet relatedTypes = new BitSet();
/**
* Returns true if prop is in an independent set from all properties in this
* sub graph. That is, if none of its related types intersects with the
* related types for this sub graph.
*/
@Override
public boolean isIndependentOf(Property prop) {
return !this.relatedTypes.intersects(prop.relatedTypes);
}
/**
* Adds the node to the sub graph, adding all its related types to the
* related types for the sub graph.
*/
@Override
public void addNode(Property prop) {
this.relatedTypes.or(prop.relatedTypes);
}
}
static class PropertyGraphNode implements GraphNode {
Property property;
protected Annotation annotation;
PropertyGraphNode(Property property) {
this.property = property;
}
@Override
public Property getValue() {
return property;
}
@Override
@SuppressWarnings("unchecked")
public A getAnnotation() {
return (A) annotation;
}
@Override
public void setAnnotation(Annotation data) {
annotation = data;
}
}
private void reportInvalidRenameFunction(Node n, String functionName, String message) {
compiler.report(
JSError.make(
n, PropertyRenamingDiagnostics.INVALID_RENAME_FUNCTION, functionName, message));
}
private static final String WRONG_ARGUMENT_COUNT = " Must be called with 1 or 2 arguments.";
private static final String WANT_STRING_LITERAL = " The first argument must be a string literal.";
private static final String DO_NOT_WANT_PATH = " The first argument must not be a property path.";
/**
* Finds all property references, recording the types on which they occur, and records all
* constructors and their instance types in the {@link TypeFlattener}.
*/
private class ProcessPropertiesAndConstructors extends AbstractPostOrderCallback {
@Override
public void visit(NodeTraversal t, Node n, Node parent) {
switch (n.getToken()) {
case GETPROP:
processGetProp(n);
return;
case CALL:
processCall(n);
return;
case NAME:
// handle ES5-style classes
if (NodeUtil.isNameDeclaration(parent) || parent.isFunction()) {
flattener.flatten(getJSType(n));
}
return;
case OBJECTLIT:
case OBJECT_PATTERN:
processObjectLitOrPattern(n);
return;
case GETELEM:
processGetElem(n);
return;
case CLASS:
processClass(n);
return;
default:
// Nothing to do.
}
}
private void processGetProp(Node getProp) {
Node propNode = getProp.getSecondChild();
JSType type = getJSType(getProp.getFirstChild());
maybeMarkCandidate(propNode, type);
if (NodeUtil.isLhsOfAssign(getProp) || NodeUtil.isStatement(getProp.getParent())) {
flattener.flatten(type);
}
}
private void processCall(Node call) {
Node target = call.getFirstChild();
if (!target.isQualifiedName()) {
return;
}
String renameFunctionName = target.getOriginalQualifiedName();
if (renameFunctionName != null
&& compiler.getCodingConvention().isPropertyRenameFunction(renameFunctionName)) {
int childCount = call.getChildCount();
if (childCount != 2 && childCount != 3) {
reportInvalidRenameFunction(call, renameFunctionName, WRONG_ARGUMENT_COUNT);
return;
}
Node propName = call.getSecondChild();
if (!propName.isString()) {
reportInvalidRenameFunction(call, renameFunctionName, WANT_STRING_LITERAL);
return;
}
if (propName.getString().contains(".")) {
reportInvalidRenameFunction(call, renameFunctionName, DO_NOT_WANT_PATH);
return;
}
// Skip ambiguation for properties in renaming calls
// NOTE (lharker@) - I'm not sure if this behavior is necessary, or if we could safely
// ambiguate the property as long as we also updated the property renaming call
Property p = getProperty(propName.getString());
p.skipAmbiguating = true;
} else if (NodeUtil.isObjectDefinePropertiesDefinition(call)) {
Node typeObj = call.getSecondChild();
JSType type = getJSType(typeObj);
Node objectLiteral = typeObj.getNext();
if (!objectLiteral.isObjectLit()) {
return;
}
for (Node key : objectLiteral.children()) {
processObjectProperty(objectLiteral, key, type);
}
}
}
private void processObjectProperty(Node objectLit, Node key, JSType type) {
checkArgument(objectLit.isObjectLit() || objectLit.isObjectPattern(), objectLit);
switch (key.getToken()) {
case COMPUTED_PROP:
if (key.getFirstChild().isString()) {
// If this quoted prop name is statically determinable, ensure we don't rename some
// other property in a way that could conflict with it.
//
// This is largely because we store quoted member functions as computed properties and
// want to be consistent with how other quoted properties invalidate property names.
quotedNames.add(key.getFirstChild().getString());
}
break;
case MEMBER_FUNCTION_DEF:
case GETTER_DEF:
case SETTER_DEF:
case STRING_KEY:
if (key.isQuotedString()) {
// If this quoted prop name is statically determinable, ensure we don't rename some
// other property in a way that could conflict with it
quotedNames.add(key.getString());
} else {
maybeMarkCandidate(key, type);
}
break;
case OBJECT_REST:
case OBJECT_SPREAD:
break; // Nothing to do.
default:
throw new IllegalStateException(
"Unexpected child of " + objectLit.getToken() + ": " + key.toStringTree());
}
}
private void processObjectLitOrPattern(Node objectLit) {
// Object.defineProperties literals are handled at the CALL node, as we determine the type
// differently than for regular object literals.
if (objectLit.getParent().isCall()
&& NodeUtil.isObjectDefinePropertiesDefinition(objectLit.getParent())) {
return;
}
// The children of an OBJECTLIT node are keys, where the values
// are the children of the keys.
JSType type = getJSType(objectLit);
for (Node key = objectLit.getFirstChild(); key != null; key = key.getNext()) {
processObjectProperty(objectLit, key, type);
}
}
private void processGetElem(Node n) {
// If this is a quoted property access (e.g. x['myprop']), we need to
// ensure that we never rename some other property in a way that
// could conflict with this quoted name.
Node child = n.getLastChild();
if (child.isString()) {
quotedNames.add(child.getString());
}
}
private void processClass(Node classNode) {
JSType classConstructorType = getJSType(classNode);
flattener.flatten(classConstructorType);
JSType classPrototype =
classConstructorType.isFunctionType()
? classConstructorType.toMaybeFunctionType().getPrototype()
: compiler.getTypeRegistry().getNativeType(JSTypeNative.UNKNOWN_TYPE);
for (Node member : NodeUtil.getClassMembers(classNode).children()) {
if (member.isQuotedString()) {
// ignore get 'foo'() {} and prevent property name collisions
// Note that only getters/setters are represented as quoted strings, not 'foo'() {}
// see https://github.com/google/closure-compiler/issues/3071
quotedNames.add(member.getString());
continue;
} else if (member.isComputedProp()) {
// ignore ['foo']() {}
// for simple cases, we also prevent renaming collisions
if (member.getFirstChild().isString()) {
quotedNames.add(member.getFirstChild().getString());
}
continue;
} else if (NodeUtil.isEs6ConstructorMemberFunctionDef(member)) {
// don't rename `class C { constructor() {} }` !
// This only applies for ES6 classes, not generic properties called 'constructor', which
// is why it's handled in this method specifically.
continue;
}
JSType memberOwnerType = member.isStaticMember() ? classConstructorType : classPrototype;
// member could be a MEMBER_FUNCTION_DEF, GETTER_DEF, or SETTER_DEF
maybeMarkCandidate(member, memberOwnerType);
}
}
/**
* If a property node is eligible for renaming, stashes a reference to it
* and increments the property name's access count.
*
* @param n The STRING node for a property
*/
private void maybeMarkCandidate(Node n, JSType type) {
String name = n.getString();
if (!externedNames.contains(name)) {
stringNodesToRename.add(n);
recordProperty(name, type);
}
}
private Property recordProperty(String name, JSType type) {
Property prop = getProperty(name);
prop.addType(type);
return prop;
}
}
private Property getProperty(String name) {
Property prop = propertyMap.computeIfAbsent(name, Property::new);
return prop;
}
/**
* This method gets the JSType from the Node argument and verifies that it is
* present.
*/
private JSType getJSType(Node n) {
JSType type = n.getJSType();
if (type == null) {
// TODO(bradfordcsmith): This branch indicates a compiler bug. It should throw an exception.
return compiler.getTypeRegistry().getNativeType(JSTypeNative.UNKNOWN_TYPE);
} else {
return type;
}
}
/** Encapsulates the information needed for renaming a property. */
private class Property {
final String oldName;
String newName;
int numOccurrences;
boolean skipAmbiguating;
// All types upon which this property was directly accessed. For "a.b" this includes "a"'s type
IdentityHashMap relatedTypesSeeds = null;
// includes relatedTypesSeeds + all subtypes of those seed types. For example if this property
// was accessed off of Iterable, then this bitset will include Array as well.
final BitSet relatedTypes = new BitSet();
Property(String name) {
this.oldName = name;
}
/** Marks this type as related to this property */
void addType(JSType newType) {
if (skipAmbiguating) {
return;
}
++numOccurrences;
if (newType.isUnionType()) {
// Note(lharker): deleting this line causes testPredeclaredType to fail. Apparently
// invaliding types cannot tell that (null|a.forward.declared.type) is invalidating.
newType = newType.restrictByNotNullOrUndefined();
}
if (invalidatingTypes.isInvalidating(newType)) {
skipAmbiguating = true;
return;
}
if (relatedTypesSeeds == null) {
this.relatedTypesSeeds = new IdentityHashMap<>();
}
FlatType newFlatType = flattener.flatten(newType);
relatedTypesSeeds.put(newFlatType, 0);
}
}
}