All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.javascript.jscomp.AmbiguateProperties Maven / Gradle / Ivy

There is a newer version: 9.0.8
Show newest version
/*
 * Copyright 2008 The Closure Compiler Authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.javascript.jscomp;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;

import com.google.common.base.Joiner;
import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;
import com.google.common.collect.ImmutableSet;
import com.google.javascript.jscomp.NodeTraversal.AbstractPostOrderCallback;
import com.google.javascript.jscomp.graph.AdjacencyGraph;
import com.google.javascript.jscomp.graph.Annotation;
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.SubGraph;
import com.google.javascript.rhino.FunctionTypeI;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.ObjectTypeI;
import com.google.javascript.rhino.TypeI;
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.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; * * */ 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 as property names. private final char[] reservedFirstCharacters; // Can't use these as property names. private final char[] reservedNonFirstCharacters; /** Map from property name to Property object */ private final Map propertyMap = new HashMap<>(); /** Property names that don't get renamed */ private final Set 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; /** * Sorts Property objects by their count, breaking ties alphabetically to * ensure a deterministic total ordering. */ private static final Comparator FREQUENCY_COMPARATOR = new Comparator() { @Override public int compare(Property p1, Property p2) { if (p1.numOccurrences != p2.numOccurrences) { return p2.numOccurrences - p1.numOccurrences; } return p1.oldName.compareTo(p2.oldName); } }; /** A map from TypeI to a unique representative Integer. */ private final BiMap intForType = HashBiMap.create(); /** A map from TypeI to JSTypeBitSet representing the types related to the type. */ private final Map relatedBitsets = new HashMap<>(); /** A set of types that invalidate properties from ambiguation. */ private final InvalidatingTypes invalidatingTypes; /** * Prefix of properties to skip renaming. These should be renamed in the * RenameProperties pass. */ static final String SKIP_PREFIX = "JSAbstractCompiler"; AmbiguateProperties( AbstractCompiler compiler, char[] reservedFirstCharacters, char[] reservedNonFirstCharacters) { checkState(compiler.getLifeCycleStage().isNormalized()); this.compiler = compiler; this.reservedFirstCharacters = reservedFirstCharacters; this.reservedNonFirstCharacters = reservedNonFirstCharacters; this.invalidatingTypes = new InvalidatingTypes.Builder(compiler.getTypeIRegistry()) .addTypesInvalidForPropertyRenaming() .addAllTypeMismatches(compiler.getTypeMismatches()) .addAllTypeMismatches(compiler.getImplicitInterfaceUses()) .build(); this.externedNames = compiler.getExternProperties(); } static AmbiguateProperties makePassForTesting( AbstractCompiler compiler, char[] reservedFirstCharacters, char[] reservedNonFirstCharacters) { AmbiguateProperties ap = new AmbiguateProperties(compiler, reservedFirstCharacters, reservedNonFirstCharacters); ap.renamingMap = new HashMap<>(); return ap; } Map getRenamingMap() { checkNotNull(renamingMap); return renamingMap; } /** Returns an integer that uniquely identifies a JSType. */ private int getIntForType(TypeI type) { // Templatized types don't exist at runtime, so collapse to raw type if (type != null && type.isGenericObjectType()) { type = type.toMaybeObjectType().getRawType(); } if (intForType.containsKey(type)) { return intForType.get(type).intValue(); } int newInt = intForType.size() + 1; intForType.put(type, newInt); return newInt; } @Override public void process(Node externs, Node root) { // Find all property references and record the types on which they occur. // Populate stringNodesToRename, propertyMap, quotedNames. NodeTraversal.traverseEs6(compiler, root, new ProcessProperties()); 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 graph = new PropertyGraph(nodes); GraphColoring coloring = new GreedyGraphColoring<>(graph, 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 : graph.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); } } } if (logger.isLoggable(Level.FINE)) { logger.fine("Collapsed " + numRenamedPropertyNames + " properties into " + numNewPropertyNames + " and skipped renaming " + numSkippedPropertyNames + " properties."); } } private BitSet getRelatedTypesOnNonUnion(TypeI type) { // All of the types we encounter should have been added to the // relatedBitsets via computeRelatedTypes. if (relatedBitsets.containsKey(type)) { return relatedBitsets.get(type); } else { throw new RuntimeException("Related types should have been computed for" + " type: " + type + " but have not been."); } } /** * Adds subtypes - and implementors, in the case of interfaces - of the type * to its JSTypeBitSet of related types. Union types are decomposed into their * alternative types. * *

The 'is related to' relationship is best understood graphically. Draw an * arrow from each instance type to the prototype of each of its * subclass. Draw an arrow from each prototype to its instance type. Draw an * arrow from each interface to its implementors. A type is related to another * if there is a directed path in the graph from the type to other. Thus, the * 'is related to' relationship is reflexive and transitive. * *

Example with Foo extends Bar which extends Baz and Bar implements I: *

{@code
   * Foo -> Bar.prototype -> Bar -> Baz.prototype -> Baz
   *                          ^
   *                          |
   *                          I
   * }
* *

Note that we don't need to correctly handle the relationships between * functions, because the function type is invalidating (i.e. its properties * won't be ambiguated). */ private void computeRelatedTypes(TypeI type) { if (type.isUnionType()) { type = type.restrictByNotNullOrUndefined(); if (type.isUnionType()) { for (TypeI alt : type.getUnionMembers()) { computeRelatedTypes(alt); } return; } } if (relatedBitsets.containsKey(type)) { // We only need to generate the bit set once. return; } JSTypeBitSet related = new JSTypeBitSet(intForType.size()); relatedBitsets.put(type, related); related.set(getIntForType(type)); // A prototype is related to its instance. if (type.isPrototypeObject()) { FunctionTypeI maybeCtor = type.toMaybeObjectType().getOwnerFunction(); if (maybeCtor.isConstructor() || maybeCtor.isInterface()) { addRelatedInstance(maybeCtor, related); } return; } // A class/interface is related to its subclasses/implementors. FunctionTypeI constructor = type.toMaybeObjectType().getConstructor(); if (constructor != null) { for (FunctionTypeI subType : constructor.getDirectSubTypes()) { addRelatedInstance(subType, related); } } } /** * Adds the instance of the given constructor, its implicit prototype and all * its related types to the given bit set. */ private void addRelatedInstance(FunctionTypeI constructor, JSTypeBitSet related) { checkArgument(constructor.hasInstanceType(), "Constructor %s without instance type.", constructor); ObjectTypeI instanceType = constructor.getInstanceType(); related.set(getIntForType(instanceType.getPrototypeObject())); computeRelatedTypes(instanceType); related.or(relatedBitsets.get(instanceType)); } 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. */ JSTypeBitSet relatedTypes = new JSTypeBitSet(intForType.size()); /** * 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 !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) { 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 public Annotation getAnnotation() { return annotation; } @Override public void setAnnotation(Annotation data) { annotation = data; } } private void reportInvalidRenameFunction(Node n, String functionName, String message) { compiler.report( JSError.make( n, DisambiguateProperties.Warnings.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. */ private class ProcessProperties extends AbstractPostOrderCallback { @Override public void visit(NodeTraversal t, Node n, Node parent) { switch (n.getToken()) { case GETPROP: { Node propNode = n.getSecondChild(); TypeI type = getTypeI(n.getFirstChild()); maybeMarkCandidate(propNode, type); return; } case CALL: { Node target = n.getFirstChild(); if (!target.isQualifiedName()) { return; } String renameFunctionName = target.getOriginalQualifiedName(); if (renameFunctionName != null && compiler.getCodingConvention().isPropertyRenameFunction(renameFunctionName)) { int childCount = n.getChildCount(); if (childCount != 2 && childCount != 3) { reportInvalidRenameFunction(n, renameFunctionName, WRONG_ARGUMENT_COUNT); return; } Node propName = n.getSecondChild(); if (!propName.isString()) { reportInvalidRenameFunction(n, renameFunctionName, WANT_STRING_LITERAL); return; } if (propName.getString().contains(".")) { reportInvalidRenameFunction(n, renameFunctionName, DO_NOT_WANT_PATH); return; } maybeMarkCandidate(propName, getTypeI(n.getSecondChild())); } else if (NodeUtil.isObjectDefinePropertiesDefinition(n)) { Node typeObj = n.getSecondChild(); TypeI type = getTypeI(typeObj); Node objectLiteral = typeObj.getNext(); if (!objectLiteral.isObjectLit()) { return; } for (Node key : objectLiteral.children()) { if (key.isQuotedString()) { quotedNames.add(key.getString()); } else { maybeMarkCandidate(key, type); } } } return; } case OBJECTLIT: // Object.defineProperties literals are handled at the CALL node. if (n.getParent().isCall() && NodeUtil.isObjectDefinePropertiesDefinition(n.getParent())) { return; } // The children of an OBJECTLIT node are keys, where the values // are the children of the keys. TypeI type = getTypeI(n); for (Node key = n.getFirstChild(); key != null; key = key.getNext()) { // We only want keys that were unquoted. // Keys are STRING, GET, SET if (key.isQuotedString()) { // Ensure that we never rename some other property in a way // that could conflict with this quoted key. quotedNames.add(key.getString()); } else { maybeMarkCandidate(key, type); } } return; case GETELEM: // 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()); } return; default: // Nothing to do. } } /** * 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, TypeI type) { String name = n.getString(); if (!externedNames.contains(name)) { stringNodesToRename.add(n); recordProperty(name, type); } } private Property recordProperty(String name, TypeI type) { Property prop = getProperty(name); prop.addType(type); return prop; } } private Property getProperty(String name) { Property prop = propertyMap.get(name); if (prop == null) { prop = new Property(name); propertyMap.put(name, prop); } return prop; } /** * This method gets the JSType from the Node argument and verifies that it is * present. */ private TypeI getTypeI(Node n) { if (n == null) { return compiler.getTypeIRegistry().getNativeType(JSTypeNative.UNKNOWN_TYPE); } TypeI type = n.getTypeI(); if (type == null) { // TODO(user): This branch indicates a compiler bug, not worthy of // halting the compilation but we should log this and analyze to track // down why it happens. This is not critical and will be resolved over // time as the type checker is extended. return compiler.getTypeIRegistry().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; JSTypeBitSet relatedTypes = new JSTypeBitSet(intForType.size()); Property(String name) { this.oldName = name; // Properties with this suffix are handled in RenameProperties. if (name.startsWith(SKIP_PREFIX)) { skipAmbiguating = true; } } /** Add this type to this property, calculating */ void addType(TypeI newType) { if (skipAmbiguating) { return; } ++numOccurrences; if (newType.isUnionType()) { newType = newType.restrictByNotNullOrUndefined(); if (newType.isUnionType()) { for (TypeI alt : newType.getUnionMembers()) { addNonUnionType(alt); } return; } } addNonUnionType(newType); } private void addNonUnionType(TypeI newType) { if (skipAmbiguating || invalidatingTypes.isInvalidating(newType)) { skipAmbiguating = true; return; } ObjectTypeI maybeObj = newType.toMaybeObjectType(); if (maybeObj != null) { newType = maybeObj.withoutStrayProperties(); } if (!relatedTypes.get(getIntForType(newType))) { computeRelatedTypes(newType); relatedTypes.or(getRelatedTypesOnNonUnion(newType)); } } } // A BitSet that stores type info. Adds pretty-print routines. private class JSTypeBitSet extends BitSet { private static final long serialVersionUID = 1L; private JSTypeBitSet(int size) { super(size); } private JSTypeBitSet() { super(); } /** * Pretty-printing, for diagnostic purposes. */ @Override public String toString() { int from = 0; int current = 0; List types = new ArrayList<>(); while (-1 != (current = nextSetBit(from))) { types.add(intForType.inverse().get(current).toString()); from = current + 1; } return Joiner.on(" && ").join(types); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy