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

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

Go to download

Closure Compiler is a JavaScript optimizing compiler. It parses your JavaScript, analyzes it, removes dead code and rewrites and minimizes what's left. It also checks syntax, variable references, and types, and warns about common JavaScript pitfalls. It is used in many of Google's JavaScript apps, including Gmail, Google Web Search, Google Maps, and Google Docs.

There is a newer version: v20240317
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.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.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.colors.Color;
import com.google.javascript.jscomp.colors.ColorRegistry;
import com.google.javascript.jscomp.colors.StandardColors;
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 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;
import org.jspecify.nullness.Nullable;

/**
 * Renames unrelated properties to the same name, using {@link Color}s provided by the typechecker.
 * This allows better compression as more properties can be given short names.
 *
 * 

Properties are considered unrelated if they are never referenced from the same color or from a * subtype of each others' colors, 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<>(); private final ColorRegistry colorRegistry; /** Map from original property name to new name. Only used by tests. */ private @Nullable Map renamingMap = null; private @Nullable ColorGraphNodeFactory graphNodeFactory = 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); }; public AmbiguateProperties( AbstractCompiler compiler, char[] reservedFirstCharacters, char[] reservedNonFirstCharacters, Set externProperties) { checkState(compiler.getLifeCycleStage().isNormalized()); this.compiler = compiler; this.reservedFirstCharacters = reservedFirstCharacters; this.reservedNonFirstCharacters = reservedNonFirstCharacters; this.externedNames = ImmutableSet.builder().add("prototype").addAll(externProperties).build(); this.colorRegistry = compiler.getColorRegistry(); } 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) { this.graphNodeFactory = ColorGraphNodeFactory.createFactory(this.colorRegistry); // Find all property references and record the types on which they occur. // Populate stringNodesToRename, propertyMap, quotedNames. NodeTraversal.traverse(compiler, root, new ProcessPropertiesAndConstructors()); ColorGraphBuilder graphBuilder = new ColorGraphBuilder( graphNodeFactory, LowestCommonAncestorFinder::new, this.colorRegistry); graphBuilder.addAll(graphNodeFactory.getAllKnownTypes()); DiGraph colorGraph = graphBuilder.build(); for (ColorGraphNode node : graphNodeFactory.getAllKnownTypes()) { node.getSubtypeIndices().set(node.getIndex()); // 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.getSubtypeIndices().size() > supertype.getSubtypeIndices().size()) { supertype.getSubtypeIndices().or(subtype.getSubtypeIndices()); return true; } int startSize = supertype.getSubtypeIndices().cardinality(); supertype.getSubtypeIndices().or(subtype.getSubtypeIndices()); return supertype.getSubtypeIndices().cardinality() > startSize; }) .computeFixedPoint(colorGraph); // Fill in all transitive edges in subtyping graph per property for (Property prop : propertyMap.values()) { if (prop.relatedColorsSeeds == null) { continue; } for (ColorGraphNode color : prop.relatedColorsSeeds.keySet()) { prop.relatedColors.or(color.getSubtypeIndices()); } prop.relatedColorsSeeds = 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."); } } static 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. */ static 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.relatedColors); } /** * 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.relatedColors); } } 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(@Nullable Annotation data) { annotation = data; } } /** * Finds all property references, recording the types on which they occur, and records all * constructors and their instance types in the {@link ColorGraphNodeFactory}. */ private class ProcessPropertiesAndConstructors extends AbstractPostOrderCallback { @Override public void visit(NodeTraversal t, Node n, Node parent) { switch (n.getToken()) { case GETPROP: case OPTCHAIN_GETPROP: processGetProp(n); return; case CALL: processCall(n); return; case NAME: // handle ES5-style classes if (NodeUtil.isNameDeclaration(parent) || parent.isFunction()) { graphNodeFactory.createNode(getColor(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) { Color type = getColor(getProp.getFirstChild()); maybeMarkCandidate(getProp, type); if (NodeUtil.isLhsOfAssign(getProp) || NodeUtil.isStatement(getProp.getParent())) { graphNodeFactory.createNode(type); } } private void processCall(Node call) { Node target = call.getFirstChild(); if (!target.isQualifiedName()) { return; } if (compiler.getCodingConvention().isPropertyRenameFunction(target)) { Node propName = call.getSecondChild(); if (propName == null || !propName.isStringLit()) { 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(); Color type = getColor(typeObj); Node objectLiteral = typeObj.getNext(); if (!objectLiteral.isObjectLit()) { return; } for (Node key = objectLiteral.getFirstChild(); key != null; key = key.getNext()) { processObjectProperty(objectLiteral, key, type); } } } private void processObjectProperty(Node objectLit, Node key, Color type) { checkArgument(objectLit.isObjectLit() || objectLit.isObjectPattern(), objectLit); switch (key.getToken()) { case COMPUTED_PROP: if (key.getFirstChild().isStringLit()) { // 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.isQuotedStringKey()) { // 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. Color type = getColor(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.isStringLit()) { quotedNames.add(child.getString()); } } private void processClass(Node classNode) { Color classConstructorType = getColor(classNode); graphNodeFactory.createNode(classConstructorType); // In theory all CLASS colors should be a function with a known prototype, but in // practice typecasts mean that this is not always the case. ImmutableSet possiblePrototypes = classConstructorType.getPrototypes(); Color classPrototype = possiblePrototypes.isEmpty() ? StandardColors.UNKNOWN : Color.createUnion(possiblePrototypes); for (Node member = NodeUtil.getClassMembers(classNode).getFirstChild(); member != null; member = member.getNext()) { if (member.isQuotedStringKey()) { // 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() || member.isComputedFieldDef()) { // ignore ['foo']() {} // for simple cases, we also prevent renaming collisions if (member.getFirstChild().isStringLit()) { 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; } Color memberOwnerColor; if (member.isStaticMember()) { memberOwnerColor = classConstructorType; } else if (member.isMemberFieldDef()) { ImmutableSet possibleInstances = classConstructorType.getInstanceColors(); memberOwnerColor = possibleInstances.isEmpty() ? StandardColors.UNKNOWN : Color.createUnion(possibleInstances); } else { checkState(member.isMemberFunctionDef() || member.isGetterDef() || member.isSetterDef()); memberOwnerColor = classPrototype; } // member could be a MEMBER_FUNCTION_DEF, MEMBER_FIELD_DEF, GETTER_DEF, or SETTER_DEF maybeMarkCandidate(member, memberOwnerColor); } } /** * 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, Color type) { String name = n.getString(); if (!externedNames.contains(name)) { stringNodesToRename.add(n); recordProperty(name, type); } } private Property recordProperty(String name, Color color) { Property prop = getProperty(name); prop.addRelatedColor(color); return prop; } } private Property getProperty(String name) { return propertyMap.computeIfAbsent(name, Property::new); } /** This method gets the Color from the Node argument or UNKNOWN if not present. */ private Color getColor(Node n) { Color type = n.getColor(); if (type == null) { // TODO(bradfordcsmith): This branch indicates a compiler bug. It should throw an exception. return StandardColors.UNKNOWN; } else { return type; } } /** Encapsulates the information needed for renaming a property. */ private class Property { final String oldName; String newName; int numOccurrences; boolean skipAmbiguating; // All colors upon which this property was directly accessed. For "a.b" this includes "a"'s type @Nullable IdentityHashMap relatedColorsSeeds = null; // includes relatedTypesSeeds + all subtypes of those seed colors. For example if this property // was accessed off of Iterable, then this bitset will include Array as well. final BitSet relatedColors = new BitSet(); Property(String name) { this.oldName = name; } /** Marks this color as related to this property */ void addRelatedColor(Color color) { if (skipAmbiguating) { return; } ++numOccurrences; if (color.isInvalidating() || color.getPropertiesKeepOriginalName()) { skipAmbiguating = true; return; } if (relatedColorsSeeds == null) { this.relatedColorsSeeds = new IdentityHashMap<>(); } ColorGraphNode newColorGraphNode = graphNodeFactory.createNode(color); relatedColorsSeeds.put(newColorGraphNode, 0); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy