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

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

import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.javascript.jscomp.DataFlowAnalysis.FlowState;
import com.google.javascript.jscomp.JoinOp.BinaryJoinOp;
import com.google.javascript.jscomp.NodeTraversal.AbstractPostOrderCallback;
import com.google.javascript.jscomp.graph.Annotation;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.Token;
import com.google.javascript.rhino.jstype.FunctionType;
import com.google.javascript.rhino.jstype.ObjectType;

import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.Stack;

/**
 * Checks that all paths through a constructor add properties in the same order.
 *
 * Background: one of the key elements of the design of V8 that improves
 * performance is that it attempts to discover the classes of the objects
 * created at run time.  Rather than implementing an object as a hash table,
 * most objects have a reference to a "hidden class" that stores the list of
 * properties so that the object itself can maintain only a list of property
 * values.  If many objects share the same hidden class, then this reduces
 * memory usage.  Furthermore, at a get-prop site, if only one hidden class ever
 * occurs for the receiver, then it can speed up the dispatch process, compiling
 * it into only 3 instructions.  (For more information, see this
 * video.)
 *
 * The key point relating to this pass is that a hidden class is not a set of
 * properties but rather an ordered list.  This is necesary so that iterating
 * over an object produces its keys in insertion order, as expected.  As a
 * result, if two different paths through the constructor generate different
 * ordered lists of properties, then the objects created via those paths will
 * have different hidden classes, defeating the optimizations described above.
 * This pass attempts to find and warn the user about such code.
 *
 */
class CheckPropertyOrder extends AbstractPostOrderCallback
    implements CompilerPass {
  static final DiagnosticType UNASSIGNED_PROPERTY = DiagnosticType.error(
      "UNASSIGNED_PROPERTY",
      "not all control paths assign property {1} in function {0}");
  static final DiagnosticType UNEQUAL_PROPERTIES = DiagnosticType.error(
      "UNEQUAL_PROPERTIES",
      "different control paths produce different (ordered) property lists:"
      + " {0} vs. {1}");

  private final AbstractCompiler compiler;
  private final CheckLevel level;
  private final boolean onlyOneError;
  private int errorCount;

  CheckPropertyOrder(AbstractCompiler compiler, CheckLevel level) {
    this(compiler, level, false);
  }

  CheckPropertyOrder(
      AbstractCompiler compiler, CheckLevel level, boolean onlyOneError) {
    this.compiler = compiler;
    this.level = level;
    this.onlyOneError = onlyOneError;
  }

  @Override
  public void process(Node externs, Node root) {
    NodeTraversal.traverse(compiler, root, this);
  }

  @Override
  public void visit(NodeTraversal t, Node n, Node parent) {
    // Look for both top-level functions and assignments of functions to
    // qualified names.
    Node func = null;
    String funcName = null;
    if (NodeUtil.isFunction(n) && isConstructor(n)) {
      func = n;
      funcName = n.getFirstChild().getString();
    } else if (NodeUtil.isAssign(n)
               && NodeUtil.isFunction(n.getFirstChild().getNext())
               && isConstructor(n)) {
      func = n.getFirstChild().getNext();
      funcName = n.getFirstChild().getQualifiedName();
    }

    if (func != null) {
      FunctionType funcType = (FunctionType) func.getJSType();
      checkConstructor(
          func, (funcType != null) ? funcType.getInstanceType() : null,
          t.getSourceName(), funcName);
    }
  }

  /** Determines whether the given node is jsdoc-ed as a constructor. */
  private static boolean isConstructor(Node n) {
    return (n.getJSDocInfo() != null) && n.getJSDocInfo().isConstructor();
  }

  @SuppressWarnings("unchecked")
  private void checkConstructor(Node func, ObjectType objType,
                                String sourceName, String funcName) {
    Preconditions.checkArgument(NodeUtil.isFunction(func));

    ControlFlowAnalysis cfa = new ControlFlowAnalysis(compiler, false, false);
    cfa.process(null, func.getFirstChild().getNext().getNext());
    ControlFlowGraph cfg = cfa.getCfg();

    new PropertyOrdersFlowAnalysis(cfa.getCfg()).analyze();

    Annotation ann = cfa.getCfg().getImplicitReturn().getAnnotation();
    List[] orders =
        ((FlowState) ann).getIn().getOrders();
    if (orders.length == 0) {
      throw new AssertionError(
          "no paths through constructor " + funcName + "?");
    }
    if (orders.length > 1) {
      report(sourceName, func, UNEQUAL_PROPERTIES,
             reverse(orders[0]).toString(), reverse(orders[1]).toString());
    }
    if (objType != null) {
      for (String propName : objType.getOwnPropertyNames()) {
        if (!orders[0].contains(propName)) {
          report(sourceName, func, UNASSIGNED_PROPERTY, funcName, propName);
        }
      }
    }
  }

  /** Reports the given error. Returns whether to continue. */
  private void report(
      String srcName, Node node, DiagnosticType type, String... args) {
    if (!onlyOneError || (++errorCount <= 1)) {
      compiler.report(JSError.make(srcName, node, level, type, args));
    }
  }

  /** Returns the given List in the reverse order. */
  private static  List reverse(List seq) {
    if (seq.isEmpty()) {
      return seq;
    }
    List rev = Lists.newArrayList(seq);
    Collections.reverse(seq);
    return rev;
  }

  private static class OrdersJoinOp extends BinaryJoinOp {
    @Override
    public PropertyOrders apply(PropertyOrders a, PropertyOrders b) {
      return new PropertyOrders(
          Sets.newHashSet(Sets.union(a.orders, b.orders)));
    }
  }

  /**
   * Stores all possible (ordered) lists of properties that may have been added
   * to this at some point in the code.
   *
   * The bottom element of the lattice is an empty set, meaning no
   * possibilities.  At the first statement in the constructor, we start with a
   * set containing only one possibility: an empty list of properties.  (Note
   * that we do not model the properties of the superclass, if any.)  Each
   * assignment, say, this.a = ... adds a to the end
   * of all lists in the set.  Joining two results in the union of the
   * possibilities of those results.
   *
   * For example, if the constructor body contained just the code 
   *   if (x) {
   *     this.a = 1;
   *     this.b = 2;
   *   } else {
   *     this.b = 2;
   *     this.a = 1;
   *   }
   * 
, then at the end of the constructor, the possibilities would * be {['a', 'b'], ['b', 'a']} because there are two possible orderings, * based on which path is taken in the if. */ private static class PropertyOrders implements LatticeElement { /** The bottom element of the lattice: an empty set of lists. */ public static final PropertyOrders EMPTY = new PropertyOrders(Sets.>newHashSet()); /** A set of possible ordered lists of properties. */ private final Set> orders; private PropertyOrders(Set> orders) { this.orders = orders; } @Override public boolean equals(Object other) { if (!(other instanceof PropertyOrders)) { return false; } return orders.equals(((PropertyOrders) other).orders); } /** * Returns a new set of possible orders with the given string added at the * end of each possibility. */ public PropertyOrders copyAndAdd(String propName) { Set> orders = Sets.newHashSet(); for (List order : this.orders) { if (!order.contains(propName)) { List nOrder = Lists.newArrayList(order); nOrder.add(propName); order = nOrder; } orders.add(order); } return new PropertyOrders(orders); } /** * Returns all possible orders over the first N properties that are * definitely assigned over all paths. */ @SuppressWarnings("unchecked") public List[] getOrders() { int minSize = Integer.MAX_VALUE; for (List seq : this.orders) { minSize = Math.min(minSize, seq.size()); } Set> orders = Sets.newHashSet(); for (List seq : this.orders) { //orders.add(seq.subList(seq.size() - minSize, seq.size())); orders.add(seq.subList(0, minSize)); } return orders.toArray( (List[]) new List[orders.size()]); } @Override public String toString() { return "{" + Joiner.on(", ").join(orders) + "}"; } } /** Implements a data flow analysis over PropertyOrders. */ private static class PropertyOrdersFlowAnalysis extends DataFlowAnalysis { public PropertyOrdersFlowAnalysis(ControlFlowGraph cfg) { super(cfg, new OrdersJoinOp()); } @Override public boolean isForward() { return true; } @Override public PropertyOrders createInitialEstimateLattice() { // An empty orders means no possibilities at all. return PropertyOrders.EMPTY; } @Override public PropertyOrders createEntryLattice() { // Initially, we have only one possibility: no properties. Set> orders = Sets.newHashSet(); orders.add(new Stack()); return new PropertyOrders(orders); } /** Computes the orders after executing the given node. */ @Override public PropertyOrders flowThrough(Node node, PropertyOrders input) { switch (node.getType()) { case Token.BLOCK: case Token.LABEL: case Token.FUNCTION: return input; case Token.IF: case Token.WHILE: case Token.DO: return flowThrough(NodeUtil.getConditionExpression(node), input); case Token.SWITCH: case Token.WITH: return flowThrough(node.getFirstChild(), input); case Token.FOR: if (node.getChildCount() == 4) { // Note that the post-loop expression is not considered here. That // is handled in the branched version above. Node pre = node.getFirstChild(), cond = pre.getNext(); return flowThrough(cond, flowThrough(pre, input)); } else { Node lhs = node.getFirstChild(), rhs = lhs.getNext(); return flowThrough(rhs, flowThrough(lhs, input)); } case Token.HOOK: Node cond = node.getFirstChild(); input = flowThrough(cond, input); Node ifTrue = cond.getNext(), ifFalse = ifTrue.getNext(); return join(flowThrough(ifTrue, input), flowThrough(ifFalse, input)); case Token.AND: case Token.OR: Node left = node.getFirstChild(), right = left.getNext(); input = flowThrough(left, input); return join(input, flowThrough(right, input)); case Token.ASSIGN: // If the left hand side is "this.x", then add x to the lists. Node lhs = node.getFirstChild(), rhs = lhs.getNext(); if (lhs.getType() == Token.GETPROP) { Node llhs = lhs.getFirstChild(), lrhs = llhs.getNext(); if ((llhs.getType() == Token.THIS) && (lrhs.getType() == Token.STRING) && (lrhs.getNext() == null)) { return flowThrough(rhs, input.copyAndAdd(lrhs.getString())); } } return flowThrough(rhs, flowThrough(lhs, input)); default: for (node = node.getFirstChild(); node != null; node = node.getNext()) { input = flowThrough(node, input); } return input; } } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy