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

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); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy