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

com.google.javascript.jscomp.ijs.PotentialDeclaration Maven / Gradle / Ivy

/*
 * Copyright 2017 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.ijs;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;

import com.google.javascript.jscomp.AbstractCompiler;
import com.google.javascript.jscomp.NodeUtil;
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.JSDocInfo;
import com.google.javascript.rhino.JSTypeExpression;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.Token;
import javax.annotation.Nullable;

/**
 * Encapsulates something that could be a declaration.
 *
 * This includes:
 *   var/let/const declarations,
 *   function/class declarations,
 *   method declarations,
 *   assignments,
 *   goog.define calls,
 *   and even valueless property accesses (e.g. `/** @type {number} * / Foo.prototype.bar`)
 */
abstract class PotentialDeclaration {
  // The fully qualified name of the declaration.
  private final String fullyQualifiedName;
  // The LHS node of the declaration.
  private final Node lhs;
  // The RHS node of the declaration, if it exists.
  private final @Nullable Node rhs;

  private PotentialDeclaration(String fullyQualifiedName, Node lhs, @Nullable Node rhs) {
    this.fullyQualifiedName = checkNotNull(fullyQualifiedName);
    this.lhs = checkNotNull(lhs);
    this.rhs = rhs;
  }

  static PotentialDeclaration fromName(Node nameNode) {
    checkArgument(nameNode.isQualifiedName(), nameNode);
    Node rhs = NodeUtil.getRValueOfLValue(nameNode);
    if (ClassUtil.isThisProp(nameNode)) {
      String name = ClassUtil.getPrototypeNameOfThisProp(nameNode);
      return new ThisPropDeclaration(name, nameNode, rhs);
    }
    return new NameDeclaration(nameNode.getQualifiedName(), nameNode, rhs);
  }

  static PotentialDeclaration fromMethod(Node functionNode) {
    checkArgument(ClassUtil.isClassMethod(functionNode));
    String name = ClassUtil.getFullyQualifiedNameOfMethod(functionNode);
    return new MethodDeclaration(name, functionNode);
  }

  static PotentialDeclaration fromStringKey(Node stringKeyNode) {
    checkArgument(stringKeyNode.isStringKey());
    checkArgument(stringKeyNode.getParent().isObjectLit());
    String name = "this." + stringKeyNode.getString();
    if (stringKeyNode.getString().equals("properties")) {
      JSDocInfo objLitJsDoc = NodeUtil.getBestJSDocInfo(stringKeyNode.getParent());
      if (objLitJsDoc != null && objLitJsDoc.isPolymerBehavior()) {
        return new PolymerBehaviorPropertiesDeclaration(name, stringKeyNode);
      }
    }
    return new StringKeyDeclaration(name, stringKeyNode);
  }

  static PotentialDeclaration fromDefine(Node callNode) {
    checkArgument(NodeUtil.isCallTo(callNode, "goog.define"));
    return DefineDeclaration.from(callNode);
  }

  static PotentialDeclaration fromAlias(Node nameNode) {
    checkArgument(nameNode.isQualifiedName(), nameNode);
    return new AliasDeclaration(nameNode.getQualifiedName(), nameNode);
  }

  String getFullyQualifiedName() {
    return fullyQualifiedName;
  }

  Node getLhs() {
    return lhs;
  }

  @Nullable
  Node getRhs() {
    return rhs;
  }

  @Nullable
  JSDocInfo getJsDoc() {
    return NodeUtil.getBestJSDocInfo(lhs);
  }

  boolean isDetached() {
    for (Node current = lhs; current != null; current = current.getParent()) {
      if (current.isScript()) {
        return false;
      }
    }
    return true;
  }

  Node getRemovableNode() {
    return NodeUtil.getEnclosingStatement(lhs);
  }

  /**
   * Remove this "potential declaration" completely.
   * Usually, this is because the same symbol has already been declared in this file.
   */
  final void remove(AbstractCompiler compiler) {
    if (isDetached()) {
      return;
    }
    Node statement = getRemovableNode();
    NodeUtil.deleteNode(statement, compiler);
    statement.removeChildren();
  }

  /**
   * Simplify this declaration to only include what's necessary for typing.
   * Usually, this means removing the RHS and leaving a type annotation.
   */
  abstract void simplify(AbstractCompiler compiler);

  /**
   * A potential declaration that has a fully qualified name to describe it.
   * This includes things like:
   *   var/let/const/function/class declarations,
   *   assignments to a fully qualified name,
   *   and goog.module exports
   * This is the most common type of potential declaration.
   */
  private static class NameDeclaration extends PotentialDeclaration {

    NameDeclaration(String fullyQualifiedName, Node lhs, Node rhs) {
      super(fullyQualifiedName, lhs, rhs);
    }

    private void simplifyNamespace(AbstractCompiler compiler) {
      if (getRhs().isOr()) {
        Node objLit = getRhs().getLastChild().detach();
        getRhs().replaceWith(objLit);
        compiler.reportChangeToEnclosingScope(getLhs());
      }
    }

    private void simplifySymbol(AbstractCompiler compiler) {
      checkArgument(NodeUtil.isCallTo(getRhs(), "Symbol"));
      Node callNode = getRhs();
      while (callNode.hasMoreThanOneChild()) {
        NodeUtil.deleteNode(callNode.getLastChild(), compiler);
      }
    }

    @Override
    void simplify(AbstractCompiler compiler) {
      if (getRhs() == null || shouldPreserve()) {
        return;
      }
      Node nameNode = getLhs();
      JSDocInfo jsdoc = getJsDoc();
      if (jsdoc != null && jsdoc.hasEnumParameterType()) {
        super.simplifyEnumValues(compiler);
        return;
      }
      if (NodeUtil.isNamespaceDecl(nameNode)) {
        simplifyNamespace(compiler);
        return;
      }
      if (nameNode.matchesName("exports")) {
        // Replace the RHS of a default goog.module export with Unknown
        replaceRhsWithUnknown(getRhs());
        compiler.reportChangeToEnclosingScope(nameNode);
        return;
      }
      if (NodeUtil.isCallTo(getRhs(), "Symbol")) {
        simplifySymbol(compiler);
        return;
      }
      if (getLhs().getParent().isConst()) {
        jsdoc = JsdocUtil.markConstant(jsdoc);
      }
      // Just completely remove the RHS, and replace with a getprop.
      Node newStatement =
          NodeUtil.newQNameDeclaration(compiler, nameNode.getQualifiedName(), null, jsdoc);
      newStatement.useSourceInfoIfMissingFromForTree(nameNode);
      Node oldStatement = getRemovableNode();
      NodeUtil.deleteChildren(oldStatement, compiler);
      if (oldStatement.isExport()) {
        oldStatement.addChildToBack(newStatement);
      } else {
        oldStatement.replaceWith(newStatement);
      }
      compiler.reportChangeToEnclosingScope(newStatement);
    }

    private static void replaceRhsWithUnknown(Node rhs) {
      rhs.replaceWith(IR.cast(IR.number(0), JsdocUtil.getQmarkTypeJSDoc()).srcrefTree(rhs));
    }

    @Override
    boolean shouldPreserve() {
      Node rhs = getRhs();
      Node nameNode = getLhs();
      JSDocInfo jsdoc = getJsDoc();
      boolean isExport = isExportLhs(nameNode);
      return super.shouldPreserve()
          || isImportRhs(rhs)
          || (isExport && rhs != null && (rhs.isQualifiedName() || rhs.isObjectLit()))
          || (jsdoc != null && jsdoc.isConstructor() && rhs != null && rhs.isQualifiedName())
          || (rhs != null
              && rhs.isObjectLit()
              && !rhs.hasChildren()
              && (jsdoc == null || !JsdocUtil.hasAnnotatedType(jsdoc)))
          || (rhs != null && NodeUtil.isCallTo(rhs, "Polymer"))
          || isPolymerBehaviorAliasOrArray();
    }

    /**
     * Polymer Behaviors can take 3 forms:
     *
     * 
{@code
     * 1) /** @polymerBehavior *\/ export const MyBehavior = { ... };
     * 2) /** @polymerBehavior *\/ export const MyBehaviorAlias = MyBehavior;
     * 3) /** @polymerBehavior *\/ export const MyBehaviorArray = [Behavior1, Behavior2];
     * }
* * Form #1 will be simplified by PolymerBehaviorPropertiesDeclaration. Forms #2 and #3 need to * be preserved here as-is so that the PolymerPass can follow the name references. Other forms * annotated with @polymerBehavior are invalid and can be simplified or removed like any other * variable. */ boolean isPolymerBehaviorAliasOrArray() { JSDocInfo jsdoc = getJsDoc(); Node rhs = getRhs(); return jsdoc != null && jsdoc.isPolymerBehavior() && rhs != null && (rhs.isName() || rhs.isArrayLit()); } } /** * A declaration of a property on `this` inside a constructor. */ private static class ThisPropDeclaration extends PotentialDeclaration { private final Node insertionPoint; ThisPropDeclaration(String fullyQualifiedName, Node lhs, Node rhs) { super(fullyQualifiedName, lhs, rhs); Node thisPropDefinition = NodeUtil.getEnclosingStatement(lhs); this.insertionPoint = NodeUtil.getEnclosingStatement(thisPropDefinition.getParent()); } @Override void simplify(AbstractCompiler compiler) { if (shouldPreserve()) { return; } // Just completely remove the RHS, if present, and replace with a getprop. Node newStatement = NodeUtil.newQNameDeclaration(compiler, getFullyQualifiedName(), null, getJsDoc()); newStatement.useSourceInfoIfMissingFromForTree(getLhs()); NodeUtil.deleteNode(getRemovableNode(), compiler); if (insertionPoint.getParent() != null) { insertionPoint.getParent().addChildAfter(newStatement, insertionPoint); compiler.reportChangeToEnclosingScope(newStatement); } } } /** * A declaration declared by a call to `goog.define`. Note that a let, const, or var declaration * annotated with @define in its JSDoc and no 'goog.define' would be a NameDeclaration instead. */ private static class DefineDeclaration extends PotentialDeclaration { DefineDeclaration(String qualifiedName, Node lhs, Node rhs) { super(qualifiedName, lhs, rhs); } @Override void simplify(AbstractCompiler compiler) { JSDocInfo info = getJsDoc(); if (info != null && info.getType() != null) { Node newRhs = makeEmptyValueNode(info.getType()); if (newRhs != null) { getRhs().replaceWith(newRhs); compiler.reportChangeToEnclosingScope(newRhs); return; } } NodeUtil.deleteNode(getRemovableNode(), compiler); } static DefineDeclaration from(Node callNode) { // Match a few different forms, depending on the call node's parent: // 1. EXPR_RESULT: goog.define('foo', 1); // 2. ASSIGN: a.b = goog.define('c', 2); // 3. NAME: var x = goog.define('d', 3); switch (callNode.getParent().getToken()) { case EXPR_RESULT: return new DefineDeclaration( callNode.getSecondChild().getString(), callNode, callNode.getLastChild()); case ASSIGN: Node previous = callNode.getPrevious(); return new DefineDeclaration( previous.getQualifiedName(), previous, callNode.getLastChild()); case NAME: Node parent = callNode.getParent(); return new DefineDeclaration(parent.getString(), parent, callNode.getLastChild()); default: throw new IllegalStateException("Unexpected parent: " + callNode.getParent().getToken()); } } static Node makeEmptyValueNode(JSTypeExpression type) { Node n = type.getRoot(); while (n != null && !n.isString() && !n.isName()) { n = n.getFirstChild(); } switch (n != null ? n.getString() : "") { case "boolean": return new Node(Token.FALSE); case "number": return Node.newNumber(0); case "string": return Node.newString(""); default: return null; } } } /** * A declaration of a method defined using the ES6 method syntax or goog.defineClass. Note that * a method defined as an assignment to a prototype property would be a NameDeclaration instead. */ private static class MethodDeclaration extends PotentialDeclaration { MethodDeclaration(String name, Node functionNode) { super(name, functionNode.getParent(), functionNode); } @Override void simplify(AbstractCompiler compiler) {} @Override Node getRemovableNode() { return getLhs(); } } private static class StringKeyDeclaration extends PotentialDeclaration { StringKeyDeclaration(String name, Node stringKeyNode) { super(name, stringKeyNode, stringKeyNode.getLastChild()); } @Override void simplify(AbstractCompiler compiler) { if (shouldPreserve()) { return; } JSDocInfo jsdoc = getJsDoc(); if (jsdoc != null && jsdoc.hasEnumParameterType()) { super.simplifyEnumValues(compiler); return; } Node key = getLhs(); removeStringKeyValue(key); compiler.reportChangeToEnclosingScope(key); if (jsdoc == null || !jsdoc.containsDeclaration() || isConstToBeInferred()) { key.setJSDocInfo(JsdocUtil.getUnusableTypeJSDoc(jsdoc)); } } @Override boolean shouldPreserve() { return super.isDetached() || super.shouldPreserve() || !isInNamespace(); } private boolean isInNamespace() { Node stringKey = getLhs(); Node objLit = stringKey.getParent(); Node lvalue = NodeUtil.getBestLValue(objLit); if (lvalue == null) { return false; } JSDocInfo jsdoc = NodeUtil.getBestJSDocInfo(lvalue); return !isExportLhs(lvalue) && !JsdocUtil.hasAnnotatedType(jsdoc) && NodeUtil.isNamespaceDecl(lvalue); } @Override Node getRemovableNode() { return getLhs(); } } /** * Polymer Behaviors are mixin-like objects used in Polymer 1 for multiple inheritance. They are * also supported by Polymer 2 and 3 for backwards-compatibility, though their use is discouraged * in favor of regular JavaScript mixins. * *

Example: * *

{@code
   * \/** @polymerBehavior *\/
   * export const MyBehavior = {
   *   properties: {
   *     foo: String,
   *     bar: {
   *       type: Number,
   *       value: 123
   *     }
   *   },
   *   baz: function() {}
   * };
   * }
* *

For incremental compilation, it is important that the "properties" object is preserved, * because the PolymerPass injects the properties declared there onto the prototypes of the * Polymer elements that apply that behavior. Note that method signatures are already preserved so * don't need additional handling here. */ private static class PolymerBehaviorPropertiesDeclaration extends PotentialDeclaration { PolymerBehaviorPropertiesDeclaration(String name, Node stringKeyNode) { super(name, stringKeyNode, stringKeyNode.getLastChild()); } @Override void simplify(AbstractCompiler compiler) { if (isDetached()) { return; } Node propertiesObject = getRhs(); if (!propertiesObject.isObjectLit() || !propertiesObject.hasChildren()) { return; } for (Node propKey : propertiesObject.children()) { Node propDef = propKey.getOnlyChild(); // A property definition is either a function reference (e.g. String, Number), or another // object literal. If it's an object literal, only the "type" sub-property matters for type // checking, so we can delete everything else (which may include e.g. a "value" sub-property // with a function expression). if (propDef.isObjectLit()) { for (Node subProp : propDef.children()) { if (!subProp.getString().equals("type")) { NodeUtil.deleteNode(subProp, compiler); } } } } } @Override boolean shouldPreserve() { return true; } @Override Node getRemovableNode() { return getLhs(); } } private static class AliasDeclaration extends PotentialDeclaration { /** * @param name The alias name being declared. * @param lhs The NAME node that represents the name of the individual alias. */ AliasDeclaration(String name, Node lhs) { super(name, lhs, null); } @Override void simplify(AbstractCompiler compiler) { // Does not simplify } /** * If the declaration is a destructuring declaration: 1) If the lhs's destructuring pattern * parent has only one child, e.g. const {Foo} = x; returns the enclosing statement to remove * the entire statement. 2) If the parent has more than one children, e.g. const {Foo, Bar} = x; * returns the lhs so that when Foo is removed, const {Foo, Bar} = x; becomes const {Bar} = x; * Otherwise, returns the enclosing statement. */ @Override Node getRemovableNode() { Node lhs = getLhs(); if (lhs.getParent().isArrayPattern() && lhs.getParent().hasMoreThanOneChild()) { return lhs; } if (lhs.getGrandparent().isObjectPattern() && lhs.getGrandparent().hasMoreThanOneChild()) { return lhs.getParent(); } return NodeUtil.getEnclosingStatement(lhs); } @Override boolean isDefiniteDeclaration() { return true; } @Override boolean shouldPreserve() { return true; } } /** Remove values from enums */ private void simplifyEnumValues(AbstractCompiler compiler) { if (getRhs().isObjectLit() && getRhs().hasChildren()) { for (Node key : getRhs().children()) { removeStringKeyValue(key); } compiler.reportChangeToEnclosingScope(getRhs()); } } boolean isDefiniteDeclaration() { Node parent = getLhs().getParent(); switch (parent.getToken()) { case VAR: case LET: case CONST: case CLASS: case FUNCTION: return true; default: return isExportLhs(getLhs()) || (getJsDoc() != null && getJsDoc().containsDeclaration()) || (getRhs() != null && PotentialDeclaration.isTypedRhs(getRhs())); } } boolean shouldPreserve() { return getRhs() != null && isTypedRhs(getRhs()); } boolean isConstToBeInferred() { return isConstToBeInferred(getLhs()); } static boolean isConstToBeInferred(Node nameNode) { JSDocInfo jsdoc = NodeUtil.getBestJSDocInfo(nameNode); boolean isConst = nameNode.getParent().isConst() || isExportLhs(nameNode) || (jsdoc != null && jsdoc.isConstant()); return isConst && !JsdocUtil.hasAnnotatedType(jsdoc) && !NodeUtil.isNamespaceDecl(nameNode); } private static boolean isTypedRhs(Node rhs) { return rhs.isFunction() || rhs.isClass() || NodeUtil.isCallTo(rhs, "goog.defineClass") || (rhs.isQualifiedName() && rhs.matchesQualifiedName("goog.abstractMethod")) || (rhs.isQualifiedName() && rhs.matchesQualifiedName("goog.nullFunction")); } private static boolean isExportLhs(Node lhs) { return (lhs.isName() && lhs.matchesName("exports")) || (lhs.isGetProp() && lhs.getFirstChild().matchesName("exports")) || lhs.matchesQualifiedName("module.exports"); } static boolean isImportRhs(@Nullable Node rhs) { if (rhs == null || !rhs.isCall()) { return false; } Node callee = rhs.getFirstChild(); return callee.matchesQualifiedName("goog.require") || callee.matchesQualifiedName("goog.requireType") || callee.matchesQualifiedName("goog.forwardDeclare") || callee.matchesName("require"); } static boolean isAliasDeclaration(Node lhs, @Nullable Node rhs) { return !ClassUtil.isThisProp(lhs) && isConstToBeInferred(lhs) && rhs != null && rhs.isQualifiedName(); } private static void removeStringKeyValue(Node stringKey) { Node value = stringKey.getOnlyChild(); Node replacementValue = IR.number(0).srcrefTree(value); stringKey.replaceChild(value, replacementValue); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy