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

com.google.javascript.refactoring.Matchers 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.

The newest version!
/*
 * Copyright 2014 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.refactoring;

import com.google.javascript.jscomp.NodeUtil;
import com.google.javascript.rhino.JSDocInfo;
import com.google.javascript.rhino.JSDocInfo.Visibility;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.jstype.JSType;
import com.google.javascript.rhino.jstype.JSTypeNative;
import org.jspecify.nullness.Nullable;

/**
 * Class that contains common Matchers that are useful to everyone.
 */
public final class Matchers {
  // TODO(mknichel): Make sure all this code works with goog.scope.

  /**
   * Returns a Matcher that matches every node.
   */
  public static Matcher anything() {
    return new Matcher() {
      @Override public boolean matches(Node node, NodeMetadata metadata) {
        return true;
      }
    };
  }

  /**
   * Returns a Matcher that returns true only if all of the provided
   * matchers match.
   */
  public static Matcher allOf(final Matcher... matchers) {
    return new Matcher() {
      @Override public boolean matches(Node node, NodeMetadata metadata) {
        for (Matcher m : matchers) {
          if (!m.matches(node, metadata)) {
            return false;
          }
        }
        return true;
      }
    };
  }

  /**
   * Returns a Matcher that returns true if any of the provided matchers match.
   */
  public static Matcher anyOf(final Matcher... matchers) {
    return new Matcher() {
      @Override public boolean matches(Node node, NodeMetadata metadata) {
        for (Matcher m : matchers) {
          if (m.matches(node, metadata)) {
            return true;
          }
        }
        return false;
      }
    };
  }

  /**
   * Returns a Matcher that matches the opposite of the provided matcher.
   */
  public static Matcher not(final Matcher matcher) {
    return new Matcher() {
      @Override public boolean matches(Node node, NodeMetadata metadata) {
        return !matcher.matches(node, metadata);
      }
    };
  }

  /**
   * Returns a matcher that matches any constructor definitions.
   */
  public static Matcher constructor() {
    return constructor(null);
  }

  /**
   * Returns a matcher that matches constructor definitions of the specified name.
   *
   * @param name The name of the class constructor to match.
   */
  public static Matcher constructor(final @Nullable String name) {
    return new Matcher() {
      @Override public boolean matches(Node node, NodeMetadata metadata) {
        JSDocInfo info = node.getJSDocInfo();
        if (info != null && info.isConstructor()) {
          Node firstChild = node.getFirstChild();
          // TODO(mknichel): Make sure this works with the following cases:
          // ns = {
          //   /** @constructor */
          //   name: function() {}
          // }
          if (name == null) {
            return true;
          }
          if ((firstChild.isGetProp() || firstChild.isName())
              && firstChild.matchesQualifiedName(name)) {
            return true;
          }
        }
        return false;
      }
    };
  }

  /**
   * Returns a Matcher that matches constructing new objects. This will match
   * the NEW node of the JS Compiler AST.
   */
  public static Matcher newClass() {
    return new Matcher() {
      @Override public boolean matches(Node node, NodeMetadata metadata) {
        return node.isNew();
      }
    };
  }

  /**
   * Returns a Matcher that matches constructing objects of the provided class
   * name. This will match the NEW node of the JS Compiler AST.
   * @param className The name of the class to return matching NEW nodes.
   */
  public static Matcher newClass(final String className) {
    return new Matcher() {
      @Override public boolean matches(Node node, NodeMetadata metadata) {
        if (!node.isNew()) {
          return false;
        }
        JSType providedJsType = getJsType(metadata, className);
        if (providedJsType == null) {
          return false;
        }

        JSType jsType = node.getJSType();
        if (jsType == null) {
          return false;
        }
        jsType = jsType.restrictByNotNullOrUndefined();
        return areTypesEquivalentIgnoringGenerics(jsType, providedJsType);
      }
    };
  }

  /**
   * Returns a Matcher that matches any function call.
   */
  public static Matcher functionCall() {
    return functionCall(null);
  }

  /**
   * Returns a Matcher that matches all nodes that are function calls that match the provided name.
   *
   * @param name The name of the function to match. For non-static functions, this must be the fully
   *     qualified name that includes the type of the object. For instance: {@code
   *     ns.AppContext.prototype.get} will match {@code appContext.get} and {@code this.get} when
   *     called from the AppContext class.
   */
  public static Matcher functionCall(final @Nullable String name) {
    return new Matcher() {
      @Override
      public boolean matches(Node node, NodeMetadata metadata) {
        // TODO(mknichel): Handle the case when functions are applied through .call or .apply.
        return node.isCall() && propertyAccess(name).matches(node.getFirstChild(), metadata);
      }
    };
  }

  /**
   * Returns a Matcher that matches any function call that has the given
   * number of arguments.
   */
  public static Matcher functionCallWithNumArgs(final int numArgs) {
    return new Matcher() {
      @Override
      public boolean matches(Node node, NodeMetadata metadata) {
        return node.isCall() && node.hasXChildren(numArgs + 1);
      }
    };
  }

  /**
   * Returns a Matcher that matches any function call that has the given
   * number of arguments and the given name.
   * @param name The name of the function to match. For non-static functions,
   *     this must be the fully qualified name that includes the type of the
   *     object. For instance: {@code ns.AppContext.prototype.get} will match
   *     {@code appContext.get} and {@code this.get} when called from the
   *     AppContext class.
   */
  public static Matcher functionCallWithNumArgs(final String name, final int numArgs) {
    return allOf(functionCallWithNumArgs(numArgs), functionCall(name));
  }

  public static Matcher googRequirelike(final String namespace) {
    return (Node node, NodeMetadata metadata) ->
        googRequirelike().matches(node, metadata)
            && node.getSecondChild().isStringLit()
            && node.getSecondChild().getString().equals(namespace);
  }

  public static Matcher googRequirelike() {
    return anyOf(googRequire(), googRequireType(), googForwardDeclare());
  }

  public static Matcher googRequire() {
    return functionCall("goog.require");
  }

  public static Matcher googRequireType() {
    return functionCall("goog.requireType");
  }

  public static Matcher googForwardDeclare() {
    return functionCall("goog.forwardDeclare");
  }

  public static Matcher googModule() {
    return functionCall("goog.module");
  }

  public static Matcher googModuleOrProvide() {
    return anyOf(googModule(), functionCall("goog.provide"));
  }

  /**
   * Returns a Matcher that matches any property access.
   */
  public static Matcher propertyAccess() {
    return propertyAccess(null);
  }

  /**
   * Returns a Matcher that matches nodes representing a GETPROP access of an object property.
   *
   * @param name The name of the property to match. For non-static properties, this must be the
   *     fully qualified name that includes the type of the object. For instance: {@code
   *     ns.AppContext.prototype.root} will match {@code appContext.root} and {@code this.root} when
   *     accessed from the AppContext.
   */
  public static Matcher propertyAccess(final @Nullable String name) {
    return new Matcher() {
      @Override
      public boolean matches(Node node, NodeMetadata metadata) {
        if (node.isGetProp()) {
          if (name == null) {
            return true;
          }
          if (node.matchesQualifiedName(name)) {
            return true;
          } else if (name.contains(".prototype.")) {
            return matchesPrototypeInstanceVar(node, metadata, name);
          }
        }
        return false;
      }
    };
  }

  /**
   * Returns a Matcher that matches definitions of any enum.
   */
  public static Matcher enumDefinition() {
    return new Matcher() {
      @Override public boolean matches(Node node, NodeMetadata metadata) {
        JSType jsType = node.getJSType();
        return jsType != null && jsType.isEnumType();
      }
    };
  }

  /**
   * Returns a Matcher that matches definitions of an enum of the given type.
   */
  public static Matcher enumDefinitionOfType(final String type) {
    return new Matcher() {
      @Override
      public boolean matches(Node node, NodeMetadata metadata) {
        JSType providedJsType = getJsType(metadata, type);
        if (providedJsType == null) {
          return false;
        }
        providedJsType = providedJsType.restrictByNotNullOrUndefined();

        JSType jsType = node.getJSType();
        return jsType != null
            && jsType.isEnumType()
            && providedJsType.equals(jsType.toMaybeEnumType().getElementsType().getPrimitiveType());
      }
    };
  }

  /**
   * Returns a Matcher that matches an ASSIGN node where the RHS of the assignment matches the given
   * rhsMatcher.
   */
  public static Matcher assignmentWithRhs(final Matcher rhsMatcher) {
    return new Matcher() {
      @Override public boolean matches(Node node, NodeMetadata metadata) {
        return node.isAssign() && rhsMatcher.matches(node.getLastChild(), metadata);
      }
    };
  }

  /**
   * Returns a Matcher that matches a declaration of a variable on the
   * prototype of a class.
   */
  public static Matcher prototypeVariableDeclaration() {
    return matcherForPrototypeDeclaration(/* requireFunctionType= */ false);
  }

  /**
   * Returns a Matcher that matches a declaration of a method on the
   * prototype of a class.
   */
  public static Matcher prototypeMethodDeclaration() {
    return matcherForPrototypeDeclaration(/* requireFunctionType= */ true);
  }

  /**
   * Returns a Matcher that matches nodes that contain JS Doc that specify the
   * {@code @type} annotation equivalent to the provided type.
   */
  public static Matcher jsDocType(final String type) {
    return new Matcher() {
      @Override
      public boolean matches(Node node, NodeMetadata metadata) {
        JSType providedJsType = getJsType(metadata, type);
        if (providedJsType == null) {
          return false;
        }
        providedJsType = providedJsType.restrictByNotNullOrUndefined();
        // The JSDoc for a variable declaration is on the VAR/CONST/LET node, but the type only
        // exists on the NAME node.
        // TODO(mknichel): Make NodeUtil.getBestJSDoc public and use that.
        JSDocInfo jsDoc =
            NodeUtil.isNameDeclaration(node.getParent())
                ? node.getParent().getJSDocInfo()
                : node.getJSDocInfo();
        JSType jsType = node.getJSType();
        return jsDoc != null
            && jsDoc.hasType()
            && jsType != null
            && providedJsType.equals(jsType.restrictByNotNullOrUndefined());
      }
    };
  }

  /**
   * Returns a Matcher that matches against properties that are declared in the constructor.
   */
  public static Matcher constructorPropertyDeclaration() {
    return new Matcher() {
      @Override public boolean matches(Node node, NodeMetadata metadata) {
        // This will match against code that looks like:
        // /** @constructor */
        // function constructor() {
        //   this.variable = 3;
        // }
        if (!node.isAssign()
            || !node.getFirstChild().isGetProp()
            || !node.getFirstFirstChild().isThis()) {
          return false;
        }
        while (node != null && !node.isFunction()) {
          node = node.getParent();
        }
        if (node != null && node.isFunction()) {
          JSDocInfo jsDoc = NodeUtil.getBestJSDocInfo(node);
          if (jsDoc != null) {
            return jsDoc.isConstructor();
          }
        }
        return false;
      }
    };
  }

  /**
   * Returns a Matcher that matches against nodes that are declared {@code @private}.
   */
  public static Matcher isPrivate() {
    return new Matcher() {
      @Override public boolean matches(Node node, NodeMetadata metadata) {
        JSDocInfo jsDoc = NodeUtil.getBestJSDocInfo(node);
        if (jsDoc != null) {
          return jsDoc.getVisibility() == Visibility.PRIVATE;
        }
        return false;
      }
    };
  }

  private static JSType getJsType(NodeMetadata metadata, String type) {
    return metadata.getCompiler().getTypeRegistry().getGlobalType(type);
  }

  private static JSType getJsType(NodeMetadata metadata, JSTypeNative nativeType) {
    return metadata.getCompiler().getTypeRegistry().getNativeType(nativeType);
  }

  private static boolean areTypesEquivalentIgnoringGenerics(JSType a, JSType b) {
    boolean equivalent = a.equals(b);
    if (equivalent) {
      return true;
    }
    if (a.isTemplatizedType()) {
      return a.toMaybeTemplatizedType().getReferencedType().equals(b);
    }
    return false;
  }

  /**
   * Checks to see if the node represents an access of an instance variable
   * on an object given a prototype declaration of an object. For instance,
   * {@code ns.AppContext.prototype.get} will match {@code appContext.get}
   * or {@code this.get} when accessed from within the AppContext object.
   */
  private static boolean matchesPrototypeInstanceVar(Node node, NodeMetadata metadata,
      String name) {
    String[] parts = name.split(".prototype.");
    String className = parts[0];
    String propertyName = parts[1];
    JSType providedJsType = getJsType(metadata, className);
    if (providedJsType == null) {
      return false;
    }
    JSType jsType = null;
    if (node.hasChildren()) {
      jsType = node.getFirstChild().getJSType();
    }
    if (jsType == null) {
      return false;
    }
    jsType = jsType.restrictByNotNullOrUndefined();
    if (!jsType.isUnknownType()
        && !jsType.isAllType()
        && jsType.isSubtypeOf(providedJsType)) {
      if (node.isName() && propertyName.equals(node.getString())) {
        return true;
      } else if (node.isGetProp() && propertyName.equals(node.getString())) {
        return true;
      }
    }
    return false;
  }

  private static Matcher matcherForPrototypeDeclaration(final boolean requireFunctionType) {
    return new Matcher() {
      @Override
      public boolean matches(Node node, NodeMetadata metadata) {
        // TODO(mknichel): Figure out which node is the best to return for this
        // function: the GETPROP node, or the ASSIGN node when the property is
        // being assigned to.
        // TODO(mknichel): Support matching:
        // foo.prototype = {
        //   bar: 1
        // };
        Node firstChild = node.getFirstChild();
        if (node.isGetProp()
            && firstChild.isGetProp()
            && "prototype".equals(firstChild.getString())) {
          JSType fnJsType = getJsType(metadata, JSTypeNative.FUNCTION_FUNCTION_TYPE);
          JSType jsType = node.getJSType();
          if (jsType == null) {
            return false;
          } else if (requireFunctionType) {
            return jsType.canCastTo(fnJsType);
          } else {
            return !jsType.canCastTo(fnJsType);
          }
        }
        return false;
      }
    };
  }

  // TODO(mknichel): Add matchers for:
  // - Constructor with argument types
  // - Function call with argument types
  // - Function definitions.
  // - Property definitions, references
  // - IsStatic
  // - JsDocMatcher

  /** Prevent instantiation. */
  private Matchers() {}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy