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

com.google.javascript.jscomp.disambiguate.DisambiguateProperties 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 2019 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.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.ImmutableSortedMap.toImmutableSortedMap;
import static com.google.common.collect.ImmutableSortedSet.toImmutableSortedSet;
import static java.util.Comparator.comparingInt;
import static java.util.Comparator.naturalOrder;

import com.google.common.base.Supplier;
import com.google.common.collect.ComparisonChain;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSetMultimap;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.collect.ImmutableSortedSet;
import com.google.errorprone.annotations.Keep;
import com.google.gson.Gson;
import com.google.gson.stream.JsonWriter;
import com.google.javascript.jscomp.AbstractCompiler;
import com.google.javascript.jscomp.CompilerPass;
import com.google.javascript.jscomp.DiagnosticType;
import com.google.javascript.jscomp.GatherGetterAndSetterProperties;
import com.google.javascript.jscomp.JSError;
import com.google.javascript.jscomp.NodeTraversal;
import com.google.javascript.jscomp.colors.Color;
import com.google.javascript.jscomp.colors.ColorRegistry;
import com.google.javascript.jscomp.diagnostic.LogFile;
import com.google.javascript.jscomp.disambiguate.ColorGraphNode.PropAssociation;
import com.google.javascript.jscomp.graph.DiGraph;
import com.google.javascript.jscomp.graph.DiGraph.DiGraphEdge;
import com.google.javascript.jscomp.graph.DiGraph.DiGraphNode;
import com.google.javascript.jscomp.graph.FixedPointGraphTraversal;
import com.google.javascript.jscomp.graph.LowestCommonAncestorFinder;
import com.google.javascript.rhino.Node;
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;

/** Assembles the various parts of the diambiguator to execute them as a compiler pass. */
public final class DisambiguateProperties implements CompilerPass {

  public static final DiagnosticType PROPERTY_INVALIDATION =
      DiagnosticType.error(
          "JSC_DISAMBIGUATE2_PROPERTY_INVALIDATION",
          "Property ''{0}'' was required to be disambiguated but was invalidated."
              + "{1}");

  private static final Gson GSON = new Gson();

  private final AbstractCompiler compiler;
  private final ImmutableSet propertiesThatMustDisambiguate;
  private final ColorRegistry registry;

  public DisambiguateProperties(
      AbstractCompiler compiler, ImmutableSet propertiesThatMustDisambiguate) {
    this.compiler = compiler;
    this.propertiesThatMustDisambiguate = propertiesThatMustDisambiguate;
    this.registry = this.compiler.getColorRegistry();
  }

  @Override
  public void process(Node externs, Node root) {
    checkArgument(externs.getParent() == root.getParent());

    ColorGraphNodeFactory flattener = ColorGraphNodeFactory.createFactory(this.registry);
    ColorFindPropertyReferences findRefs =
        new ColorFindPropertyReferences(
            flattener, this.compiler.getCodingConvention()::isPropertyRenameFunction);
    ColorGraphBuilder graphBuilder =
        new ColorGraphBuilder(flattener, LowestCommonAncestorFinder::new, this.registry);
    ClusterPropagator propagator = new ClusterPropagator();
    UseSiteRenamer renamer =
        new UseSiteRenamer(
            /* mutationCb= */ this.compiler::reportChangeToEnclosingScope);

    NodeTraversal.traverse(this.compiler, externs.getParent(), findRefs);
    LinkedHashMap propIndex = findRefs.getPropertyIndex();

    invalidateWellKnownProperties(propIndex);
    this.logForDiagnostics(
        "prop_refs",
        // use a StreamedJsonProducer instead of building up the entire json string at once to
        // prevent OOMs for very large projects.
        new LogFile.StreamedJsonProducer() {
          @Override
          public void writeJson(JsonWriter writer) throws IOException {
            ImmutableSortedSet propRefsJson =
                propIndex.values().stream()
                    .map(PropertyReferenceIndexJson::new)
                    .collect(toImmutableSortedSet(naturalOrder()));

            writer.beginObject();
            for (PropertyReferenceIndexJson propRef : propRefsJson) {
              propRef.writeJson(writer);
            }
            writer.endObject();
          }
        });

    graphBuilder.addAll(flattener.getAllKnownTypes());
    DiGraph graph = graphBuilder.build();

    // Model legacy behavior from the old (pre-January 2021) disambiguator.
    // TODO(b/177695515): delete this section.
    for (ColorGraphNode colorGraphNode : flattener.getAllKnownTypes()) {
      if (graph.getOutEdges(colorGraphNode).isEmpty()) {
        // Skipping leaf types improves code size, especially as "namespace" types are all leaf
        // types and will often have their declared properties collapsed into variables.
        continue;
      }
      if (colorGraphNode.getColor().isUnion()) {
        // Only need this step for SINGLE ColorGraphNodes because
        // and we will add properties of each alternate elsewhere in this loop.
        continue;
      }
      registerOwnDeclaredProperties(colorGraphNode, propIndex);
    }

    this.logForDiagnostics(
        "graph",
        () ->
            graph.getNodes().stream()
                .map(TypeNodeJson::new)
                .sorted(comparingInt((x) -> x.index))
                .collect(toImmutableList()));

    invalidateBasedOnType(flattener);

    FixedPointGraphTraversal.newTraversal(propagator).computeFixedPoint(graph);

    for (PropertyClustering prop : propIndex.values()) {
      renamer.renameUses(prop);
      if (prop.isInvalidated() && this.propertiesThatMustDisambiguate.contains(prop.getName())) {
        this.compiler.report(this.createInvalidationError(prop));
      }
    }

    this.logForDiagnostics("renaming_index", () -> buildRenamingIndex(propIndex, renamer));
    this.logForDiagnostics("mismatches", this.registry.getMismatchLocationsForDebugging()::inverse);

    GatherGetterAndSetterProperties.update(this.compiler, externs, root);
  }

  private JSError createInvalidationError(PropertyClustering prop) {
    String additionalContext = "";

    return JSError.make(
        null,
        -1,
        -1,
        DisambiguateProperties.PROPERTY_INVALIDATION,
        prop.getName(),
        additionalContext);
  }

  private static ImmutableMap buildRenamingIndex(
      LinkedHashMap props, UseSiteRenamer renamer) {
    ImmutableSetMultimap newNames = renamer.getRenamingIndex();
    return props.values().stream()
        .collect(
            toImmutableSortedMap(
                naturalOrder(),
                PropertyClustering::getName,
                prop ->
                    prop.isInvalidated()
                        ? prop.getLastInvalidation()
                        : ImmutableSortedSet.copyOf(newNames.get(prop.getName()))));
  }

  private static void invalidateWellKnownProperties(
      LinkedHashMap propIndex) {
    /**
     * Expand this list as needed; it wasn't created exhaustively.
     *
     * 

Good candidates are: props accessed by builtin functions, props accessed by syntax sugar, * props used in strange ways by the language spec, etc. * *

TODO(b/169899789): consider instead omitting these properties entirely from the serialized * colors format. */ ImmutableList names = ImmutableList.of("prototype", "constructor", "then"); for (String name : names) { propIndex .computeIfAbsent(name, PropertyClustering::new) .invalidate(Invalidation.wellKnownProperty()); } } private void invalidateBasedOnType(ColorGraphNodeFactory flattener) { for (ColorGraphNode type : flattener.getAllKnownTypes()) { if (type.getColor().isInvalidating()) { for (PropertyClustering prop : type.getAssociatedProps().keySet()) { prop.invalidate(Invalidation.invalidatingType(type.getIndex())); } } else { invalidateNonDeclaredPropertyAccesses(type); } } } /** * Invalidate all property accesses that cause "missing property" warnings. * *

This behavior is not inherently necessary for correctness. It only exists because the older * version of the disambiguator invalidated these properties and some projects began to rely on * this. * *

TODO(b/177695515): delete this method */ private void invalidateNonDeclaredPropertyAccesses(ColorGraphNode colorGraphNode) { Color color = colorGraphNode.getColor(); checkArgument( !color.isInvalidating(), "Not applicable to invalidating types. All their properties are invalidated"); for (PropertyClustering prop : colorGraphNode.getAssociatedProps().keySet()) { if (prop.isInvalidated()) { continue; // Skip unnecessary `hasProperty` lookups which can be expensive. } if (!mayHaveProperty(color, prop.getName())) { prop.invalidate(Invalidation.undeclaredAccess(colorGraphNode.getIndex())); } } } /** * Returns true if the color or any of its ancestors has the given property * *

If this is a union, returns true if /any/ union alternate has the property. * *

TODO(b/177695515): delete this method */ private boolean mayHaveProperty(Color color, String propertyName) { try { if (!this.mayHavePropertySeenSet.add(color)) { return false; } if (color.isUnion()) { return color.getUnionElements().stream() .anyMatch(element -> mayHaveProperty(element, propertyName)); } if (color.getOwnProperties().contains(propertyName)) { return true; } return this.registry.getDisambiguationSupertypes(color).stream() .anyMatch(element -> mayHaveProperty(element, propertyName)); } finally { this.mayHavePropertySeenSet.remove(color); } } private final LinkedHashSet mayHavePropertySeenSet = new LinkedHashSet<>(); private void registerOwnDeclaredProperties( ColorGraphNode colorGraphNode, Map propIndex) { checkArgument(!colorGraphNode.getColor().isUnion()); // For each type, get the list of its "own properties" and add them to its clustering. This // is only to mimic the behavior of the old disambiguator and could be removed if we were // confident we could update all existing code to be compatible. for (String propName : colorGraphNode.getColor().getOwnProperties()) { PropertyClustering prop = propIndex.get(propName); if (prop == null) { // Ignore declared properties without any visible references to rename. continue; } else if (prop.isInvalidated()) { continue; } prop.getClusters().add(colorGraphNode); colorGraphNode.getAssociatedProps().put(prop, PropAssociation.TYPE_SYSTEM); } } private void logForDiagnostics(String name, Supplier data) { try (LogFile log = this.compiler.createOrReopenLog(this.getClass(), name + ".log")) { log.logJson(data); } } private void logForDiagnostics(String name, LogFile.StreamedJsonProducer data) { try (LogFile log = this.compiler.createOrReopenLog(this.getClass(), name + ".log")) { log.logJson(data); } } private static final class PropertyReferenceIndexJson implements Comparable { final String name; final ImmutableSortedSet refs; PropertyReferenceIndexJson(PropertyClustering prop) { this.name = prop.getName(); if (prop.isInvalidated()) { this.refs = ImmutableSortedSet.of(); } else { this.refs = prop.getUseSites().entrySet().stream() .map((e) -> new PropertyReferenceJson(e.getKey(), e.getValue())) .collect(toImmutableSortedSet(naturalOrder())); } } @Override public int compareTo(PropertyReferenceIndexJson x) { return this.name.compareTo(x.name); } private void writeJson(JsonWriter writer) throws IOException { // creates an entry such as: // foo: {name: 'foo', refs: [{location: 'bar.js:3:4', receiverIndex: 2}]} writer.name(this.name); writer.beginObject(); writer.name("name").value(this.name); writer.name("refs").beginArray(); for (PropertyReferenceJson ref : this.refs) { GSON.toJson(ref, PropertyReferenceJson.class, writer); } writer.endArray(); writer.endObject(); } } private static final class PropertyReferenceJson implements Comparable { final String location; final int receiverIndex; PropertyReferenceJson(Node location, ColorGraphNode receiver) { this.location = location.getLocation(); this.receiverIndex = receiver.getIndex(); } @Override public int compareTo(PropertyReferenceJson x) { return ComparisonChain.start() .compare(this.receiverIndex, x.receiverIndex) .compare(this.location, x.location) .result(); } } private static final class TypeNodeJson { final int index; // These fields are used reflectively via GSON. @Keep final boolean invalidating; @Keep final String colorId; @Keep final ImmutableSortedSet edges; @Keep final ImmutableSortedMap props; TypeNodeJson(DiGraphNode n) { ColorGraphNode t = n.getValue(); this.index = t.getIndex(); this.colorId = t.getColor().getId().toString(); this.invalidating = t.getColor().isInvalidating(); this.edges = n.getOutEdges().stream() .map(TypeEdgeJson::new) .collect(toImmutableSortedSet(naturalOrder())); this.props = t.getAssociatedProps().entrySet().stream() .collect( toImmutableSortedMap( naturalOrder(), e -> e.getKey().getName(), Map.Entry::getValue)); } } private static final class TypeEdgeJson implements Comparable { final int dest; // This field is used reflectively via GSON. @Keep final Object value; TypeEdgeJson(DiGraphEdge e) { this.dest = e.getDestination().getValue().getIndex(); this.value = e.getValue(); } @Override public int compareTo(TypeEdgeJson x) { checkArgument(this.dest != x.dest); return this.dest - x.dest; } } }