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

com.google.javascript.jscomp.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;

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.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.ObjectType;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
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; * * */ 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; /** * 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 JSType to a unique representative Integer. */ private final BiMap intForType = HashBiMap.create(); /** * A map from JSType to JSTypeBitSet representing the types related to the type. * *

A type is always related to itself. */ private final Map relatedBitsets = new HashMap<>(); /** A set of types that invalidate properties from ambiguation. */ private final InvalidatingTypes invalidatingTypes; 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.getTypeRegistry()) .addTypesInvalidForPropertyRenaming() .addAllTypeMismatches(compiler.getTypeMismatches()) .addAllTypeMismatches(compiler.getImplicitInterfaceUses()) .build(); this.externedNames = ImmutableSet.builder() .add("prototype") .addAll(compiler.getExternProperties()) .build(); } 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(JSType 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.traverse(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); } } } // We may have renamed getter / setter properties. GatherGettersAndSetterProperties.update(compiler, externs, root); if (logger.isLoggable(Level.FINE)) { logger.fine("Collapsed " + numRenamedPropertyNames + " properties into " + numNewPropertyNames + " and skipped renaming " + numSkippedPropertyNames + " properties."); } } private BitSet getRelatedTypesOnNonUnion(JSType type) { // All of the types we encounter should have been added to the // relatedBitsets via computeRelatedTypesForNonUnionType. 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. * *

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
   * }
* *

We also need to handle relationships between functions because of ES6 class-side inheritance * although the top Function type itself is invalidating. */ @SuppressWarnings("ReferenceEquality") private void computeRelatedTypesForNonUnionType(JSType type) { // This method could be expanded to handle union types if necessary, but currently no union // types are ever passed as input so the method doesn't have logic for union types checkState(!type.isUnionType(), type); 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.isFunctionPrototypeType()) { FunctionType maybeCtor = type.toMaybeObjectType().getOwnerFunction(); if (maybeCtor.isConstructor() || maybeCtor.isInterface()) { addRelatedInstance(maybeCtor, related); } return; } // A class/interface is related to its subclasses/implementors. FunctionType constructor = type.toMaybeObjectType().getConstructor(); if (constructor != null) { for (FunctionType subType : constructor.getDirectSubTypes()) { addRelatedInstance(subType, related); } } // We only specifically handle implicit prototypes of functions in the case of ES6 classes // For regular functions, the implicit prototype being Function.prototype does not matter // because the type `Function` is invalidating. // This may cause unexpected behavior for code that manually sets a prototype, e.g. // Object.setPrototypeOf(myFunction, prototypeObj); // but code like that should not be used with --ambiguate_properties or type-based optimizations FunctionType fnType = type.toMaybeFunctionType(); if (fnType != null) { for (FunctionType subType : fnType.getDirectSubTypes()) { // We record all subtypes of constructors, but don't care about old 'ES5-style' subtyping, // just ES6-style. This is equivalent to saying that the subtype constructor's implicit // prototype is the given type if (fnType == subType.getImplicitPrototype()) { addRelatedType(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(FunctionType constructor, JSTypeBitSet related) { checkArgument(constructor.hasInstanceType(), "Constructor %s without instance type.", constructor); ObjectType instanceType = constructor.getInstanceType(); addRelatedType(instanceType, related); } /** Adds the given type and all its related types to the given bit set. */ private void addRelatedType(JSType type, JSTypeBitSet related) { computeRelatedTypesForNonUnionType(type); related.or(relatedBitsets.get(type)); } 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: processGetProp(n); return; case CALL: processCall(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); } 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()) { if (key.isQuotedString()) { quotedNames.add(key.getString()); } else { maybeMarkCandidate(key, type); } } } } private void processObjectLitOrPattern(Node objectLit) { // Object.defineProperties literals are handled at the CALL node. 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()) { // We only want keys that were unquoted. // Keys are STRING, GET, SET if (key.isComputedProp()) { if (key.getFirstChild().isString()) { // Ensure that we never rename some other property in a way that could conflict with // this computed prop. This is largely because we store quoted member fns as // computed properties and want to be consistent with how other quoted properties // invalidate property names quotedNames.add(key.getFirstChild().getString()); } // Never rename computed properties } else 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); } } } 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); 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.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 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; JSTypeBitSet relatedTypes = new JSTypeBitSet(intForType.size()); Property(String name) { this.oldName = name; } /** Add this type to this property, calculating */ void addType(JSType newType) { if (skipAmbiguating) { return; } ++numOccurrences; if (newType.isUnionType()) { newType = newType.restrictByNotNullOrUndefined(); if (newType.isUnionType()) { for (JSType alt : newType.getUnionMembers()) { addNonUnionType(alt); } return; } } addNonUnionType(newType); } private void addNonUnionType(JSType newType) { if (skipAmbiguating || invalidatingTypes.isInvalidating(newType)) { skipAmbiguating = true; return; } if (!relatedTypes.get(getIntForType(newType))) { computeRelatedTypesForNonUnionType(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); } /** * 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 - 2025 Weber Informatics LLC | Privacy Policy