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

com.google.javascript.jscomp.ProcessClosurePrimitives 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 2006 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 static com.google.common.base.Preconditions.checkState;
import static com.google.javascript.jscomp.ClosurePrimitiveErrors.INVALID_CLOSURE_CALL_SCOPE_ERROR;

import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.javascript.jscomp.NodeTraversal.AbstractPostOrderCallback;
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.JSDocInfo;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.Token;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import javax.annotation.Nullable;

/**
 * Performs some Closure-specific simplifications including rewriting goog.base, goog.addDependency.
 *
 * 

Adds forwardDeclared and goog.defined names to the compiler. */ class ProcessClosurePrimitives extends AbstractPostOrderCallback implements HotSwapCompilerPass { static final DiagnosticType NULL_ARGUMENT_ERROR = DiagnosticType.error( "JSC_NULL_ARGUMENT_ERROR", "method \"{0}\" called without an argument"); static final DiagnosticType EXPECTED_OBJECTLIT_ERROR = DiagnosticType.error( "JSC_EXPECTED_OBJECTLIT_ERROR", "method \"{0}\" expected an object literal argument"); static final DiagnosticType EXPECTED_STRING_ERROR = DiagnosticType.error( "JSC_EXPECTED_STRING_ERROR", "method \"{0}\" expected a string argument"); static final DiagnosticType INVALID_ARGUMENT_ERROR = DiagnosticType.error( "JSC_INVALID_ARGUMENT_ERROR", "method \"{0}\" called with invalid argument"); static final DiagnosticType INVALID_STYLE_ERROR = DiagnosticType.error( "JSC_INVALID_CSS_NAME_MAP_STYLE_ERROR", "Invalid CSS name map style {0}"); static final DiagnosticType TOO_MANY_ARGUMENTS_ERROR = DiagnosticType.error( "JSC_TOO_MANY_ARGUMENTS_ERROR", "method \"{0}\" called with more than one argument"); static final DiagnosticType WEAK_NAMESPACE_TYPE = DiagnosticType.warning( "JSC_WEAK_NAMESPACE_TYPE", "Provided symbol declared with type Object. This is rarely useful. " + "For more information see " + "https://github.com/google/closure-compiler/wiki/A-word-about-the-type-Object"); static final DiagnosticType CLASS_NAMESPACE_ERROR = DiagnosticType.error( "JSC_CLASS_NAMESPACE_ERROR", "\"{0}\" cannot be both provided and declared as a class. Try var {0} = class '{'...'}'"); static final DiagnosticType FUNCTION_NAMESPACE_ERROR = DiagnosticType.error( "JSC_FUNCTION_NAMESPACE_ERROR", "\"{0}\" cannot be both provided and declared as a function"); static final DiagnosticType INVALID_PROVIDE_ERROR = DiagnosticType.error( "JSC_INVALID_PROVIDE_ERROR", "\"{0}\" is not a valid {1} qualified name"); static final DiagnosticType INVALID_DEFINE_NAME_ERROR = DiagnosticType.error( "JSC_INVALID_DEFINE_NAME_ERROR", "\"{0}\" is not a valid JS identifier name"); static final DiagnosticType MISSING_DEFINE_ANNOTATION = DiagnosticType.error( "JSC_INVALID_MISSING_DEFINE_ANNOTATION", "Missing @define annotation"); static final DiagnosticType XMODULE_REQUIRE_ERROR = DiagnosticType.warning( "JSC_XMODULE_REQUIRE_ERROR", "namespace \"{0}\" is required in module {2} but provided in module {1}." + " Is module {2} missing a dependency on module {1}?"); static final DiagnosticType NON_STRING_PASSED_TO_SET_CSS_NAME_MAPPING_ERROR = DiagnosticType.error( "JSC_NON_STRING_PASSED_TO_SET_CSS_NAME_MAPPING_ERROR", "goog.setCssNameMapping only takes an object literal with string values"); static final DiagnosticType INVALID_CSS_RENAMING_MAP = DiagnosticType.warning( "INVALID_CSS_RENAMING_MAP", "Invalid entries in css renaming map: {0}"); static final DiagnosticType BASE_CLASS_ERROR = DiagnosticType.error( "JSC_BASE_CLASS_ERROR", "incorrect use of {0}.base: {1}"); static final DiagnosticType CLOSURE_DEFINES_ERROR = DiagnosticType.error( "JSC_CLOSURE_DEFINES_ERROR", "Invalid CLOSURE_DEFINES definition"); static final DiagnosticType DEFINE_CALL_WITHOUT_ASSIGNMENT = DiagnosticType.error( "JSC_DEFINE_CALL_WITHOUT_ASSIGNMENT", "The result of a goog.define call must be assigned as an isolated statement."); static final DiagnosticType INVALID_FORWARD_DECLARE = DiagnosticType.error("JSC_INVALID_FORWARD_DECLARE", "Malformed goog.forwardDeclare"); static final DiagnosticType CLOSURE_CALL_CANNOT_BE_ALIASED_ERROR = DiagnosticType.error( "JSC_CLOSURE_CALL_CANNOT_BE_ALIASED_ERROR", "Closure primitive method {0} may not be aliased"); static final DiagnosticType CLOSURE_CALL_CANNOT_BE_ALIASED_OUTSIDE_MODULE_ERROR = DiagnosticType.error( "JSC_CLOSURE_CALL_CANNOT_BE_ALIASED_ERROR", "Closure primitive method {0} may not be aliased outside a module (ES " + "module, CommonJS module, or goog.module)"); /** The root Closure namespace */ static final String GOOG = "goog"; private final AbstractCompiler compiler; private final Set knownClosureSubclasses = new HashSet<>(); private final Set exportedVariables = new HashSet<>(); private final PreprocessorSymbolTable preprocessorSymbolTable; private final List defineCalls = new ArrayList<>(); ProcessClosurePrimitives( AbstractCompiler compiler, @Nullable PreprocessorSymbolTable preprocessorSymbolTable) { this.compiler = compiler; this.preprocessorSymbolTable = preprocessorSymbolTable; } Set getExportedVariableNames() { return exportedVariables; } @Override public void process(Node externs, Node root) { // Replace and validate other Closure primitives NodeTraversal.traverseRoots(compiler, this, externs, root); for (Node n : defineCalls) { replaceGoogDefines(n); } } /** * @param n */ private void replaceGoogDefines(Node n) { Node parent = n.getParent(); String name = n.getSecondChild().getString(); Node value = n.getChildAtIndex(2).detach(); switch (parent.getToken()) { case NAME: parent.setDefineName(name); n.replaceWith(value); compiler.reportChangeToEnclosingScope(parent); break; case ASSIGN: checkState(n == parent.getLastChild()); parent.getFirstChild().setDefineName(name); n.replaceWith(value); compiler.reportChangeToEnclosingScope(parent); break; default: throw new IllegalStateException("goog.define outside of NAME, or ASSIGN"); } } @Override public void hotSwapScript(Node scriptRoot, Node originalRoot) { // TODO(bashir): Implement a real hot-swap version instead and make it fully // consistent with the full version. this.compiler.process(this); } @Override public void visit(NodeTraversal t, Node n, Node parent) { switch (n.getToken()) { case CALL: Node left = n.getFirstChild(); if (left.isGetProp()) { Node name = left.getFirstChild(); if (name.isName() && GOOG.equals(name.getString())) { // For the sake of simplicity, we report code changes // when we see a provides/requires, and don't worry about // reporting the change when we actually do the replacement. String methodName = name.getNext().getString(); switch (methodName) { case "define": processDefineCall(t, n, parent); break; case "inherits": // Note: inherits is allowed in local scope processInheritsCall(n); break; case "exportSymbol": // Note: exportSymbol is allowed in local scope Node arg = left.getNext(); if (arg.isString()) { String argString = arg.getString(); int dot = argString.indexOf('.'); if (dot == -1) { exportedVariables.add(argString); } else { exportedVariables.add(argString.substring(0, dot)); } } break; case "addDependency": if (validateUnaliasablePrimitiveCall(t, n, methodName)) { processAddDependency(n, parent); } break; case "setCssNameMapping": processSetCssNameMapping(n, parent); break; case "forwardDeclare": if (validatePrimitiveCallWithMessage( t, n, methodName, ProcessClosurePrimitives.CLOSURE_CALL_CANNOT_BE_ALIASED_OUTSIDE_MODULE_ERROR)) { processForwardDeclare(n); } break; default: // fall out } } else if (left.getLastChild().getString().equals("base")) { // maybe an "base" setup by goog.inherits maybeProcessClassBaseCall(n); } } break; case ASSIGN: case NAME: if (n.isName() && n.getString().equals("CLOSURE_DEFINES")) { handleClosureDefinesValues(n); } break; default: break; } } /** * Verifies that a) the call is in the top level of a file and b) the return value is unused * *

This method is for primitives that never return a value. */ private boolean validateUnaliasablePrimitiveCall(NodeTraversal t, Node n, String methodName) { return validatePrimitiveCallWithMessage(t, n, methodName, CLOSURE_CALL_CANNOT_BE_ALIASED_ERROR); } /** * @param methodName list of primitive types classed together with this one * @param invalidAliasingError which DiagnosticType to emit if this call is aliased. this depends * on whether the primitive is sometimes aliasiable in a module or never aliasable. */ private boolean validatePrimitiveCallWithMessage( NodeTraversal t, Node n, String methodName, DiagnosticType invalidAliasingError) { // Ignore invalid primitives if we didn't strip module sugar. if (compiler.getOptions().shouldPreserveGoogModule()) { return true; } if (!t.inGlobalHoistScope() && !t.inModuleScope()) { compiler.report(JSError.make(n, INVALID_CLOSURE_CALL_SCOPE_ERROR)); return false; } else if (!n.getParent().isExprResult() && !t.inModuleScope() && !"goog.define".equals(methodName)) { // If the call is in the global hoist scope, but the result is used compiler.report(JSError.make(n, invalidAliasingError, GOOG + "." + methodName)); return false; } return true; } private void handleClosureDefinesValues(Node n) { // var CLOSURE_DEFINES = {}; if (NodeUtil.isNameDeclaration(n.getParent()) && n.hasOneChild() && n.getFirstChild().isObjectLit()) { HashMap builder = new HashMap<>(); builder.putAll(compiler.getDefaultDefineValues()); for (Node c : n.getFirstChild().children()) { if (c.isStringKey() && isValidDefineValue(c.getFirstChild())) { builder.put(c.getString(), c.getFirstChild().cloneTree()); } else { reportBadClosureCommonDefinesDefinition(c); } } compiler.setDefaultDefineValues(ImmutableMap.copyOf(builder)); } } static boolean isValidDefineValue(Node val) { switch (val.getToken()) { case STRING: case NUMBER: case TRUE: case FALSE: return true; case NEG: return val.getFirstChild().isNumber(); default: return false; } } /** Handles a goog.define call. */ private void processDefineCall(NodeTraversal t, Node n, Node parent) { Node left = n.getFirstChild(); Node args = left.getNext(); if (verifyDefine(t, parent, left, args)) { Node nameNode = args; maybeAddNameToSymbolTable(left); maybeAddNameToSymbolTable(nameNode); this.defineCalls.add(n); } } private void maybeProcessClassBaseCall(Node n) { // Two things must hold for every base call: // 1) We must be calling it on "this". // 2) We must be calling it on a prototype method of the same name as // the one we're in, OR we must be calling it from a constructor. // If both of those things are true, then we can rewrite: //

    // function Foo() {
    //   Foo.base(this);
    // }
    // goog.inherits(Foo, BaseFoo);
    // Foo.prototype.bar = function() {
    //   Foo.base(this, 'bar', 1);
    // };
    // 
// as the easy-to-optimize: //
    // function Foo() {
    //   BaseFoo.call(this);
    // }
    // goog.inherits(Foo, BaseFoo);
    // Foo.prototype.bar = function() {
    //   Foo.superClass_.bar.call(this, 1);
    // };
    //
    // Most of the logic here is just to make sure the AST's
    // structure is what we expect it to be.

    Node callTarget = n.getFirstChild();
    Node baseContainerNode = callTarget.getFirstChild();
    if (!baseContainerNode.isUnscopedQualifiedName()) {
      // Some unknown "base" method.
      return;
    }
    String baseContainer = callTarget.getFirstChild().getQualifiedName();

    Node enclosingFnNameNode = getEnclosingDeclNameNode(n);
    if (enclosingFnNameNode == null || !enclosingFnNameNode.isUnscopedQualifiedName()) {
      // some unknown container method or a MEMBER_FUNCTION_DEF.
      if (knownClosureSubclasses.contains(baseContainer)) {
        reportBadBaseMethodUse(n, baseContainer, "Could not find enclosing method.");
      } else if (baseUsedInClass(n)) {
        Node clazz = NodeUtil.getEnclosingClass(n);
        // TODO(lharker): this check ignores class expressions like "foo.Bar = class extends X {}"
        if ((clazz.getFirstChild().isName()
                && clazz.getFirstChild().getString().equals(baseContainer))
            || (clazz.getSecondChild().isName()
                && clazz.getSecondChild().getString().equals(baseContainer))) {
          reportBadBaseMethodUse(
              n,
              clazz.getFirstChild().getString(),
              "base method is not allowed in ES6 class. Use super instead.");
        }
      }
      return;
    }

    if (baseUsedInClass(n)) {
      Node clazz = NodeUtil.getEnclosingClass(n);
      String name = NodeUtil.getBestLValueName(clazz);
      reportBadBaseMethodUse(
          n, name, "base method is not allowed in ES6 class. Use super instead.");
      return;
    }

    String enclosingQname = enclosingFnNameNode.getQualifiedName();
    if (!enclosingQname.contains(".prototype.")) {
      rewriteBaseCallInConstructor(enclosingQname, baseContainer, n, enclosingFnNameNode);
    } else {
      rewriteBaseCallInMethod(enclosingQname, baseContainer, n, enclosingFnNameNode);
    }
  }

  private void rewriteBaseCallInConstructor(
      String enclosingQname, String baseContainer, Node n, Node enclosingFnNameNode) {
    // Check if this is some other "base" method.
    if (!enclosingQname.equals(baseContainer)) {
      // Report misuse of "base" methods from other known classes.
      if (knownClosureSubclasses.contains(baseContainer)) {
        reportBadBaseMethodUse(
            n, baseContainer, "Must be used within " + baseContainer + " methods");
      }
      return;
    }

    // Determine if this is a class with a "base" method created by
    // goog.inherits.
    Node enclosingParent = enclosingFnNameNode.getParent();
    Node baseClassNode =
        findGoogInheritsCall(
            enclosingParent.isAssign() ? enclosingParent.getParent() : enclosingParent);
    if (baseClassNode == null) {
      // If there is no "goog.inherits", this might be some other "base" method.
      return;
    }

    // This is the expected method, validate its parameters.
    Node callee = n.getFirstChild();
    Node thisArg = callee.getNext();
    if (thisArg == null || !thisArg.isThis()) {
      reportBadBaseMethodUse(n, baseContainer, "First argument must be 'this'.");
      return;
    }

    // Handle methods.
    Node methodNameNode = thisArg.getNext();
    if (methodNameNode == null
        || !methodNameNode.isString()
        || !methodNameNode.getString().equals("constructor")) {
      reportBadBaseMethodUse(n, baseContainer, "Second argument must be 'constructor'.");
      return;
    }

    // We're good to go.
    n.replaceChild(
        callee,
        NodeUtil.newQName(
            compiler,
            baseClassNode.getQualifiedName() + ".call",
            callee,
            enclosingQname + ".base"));
    n.removeChild(methodNameNode);
    compiler.reportChangeToEnclosingScope(n);
  }

  private void rewriteBaseCallInMethod(
      String enclosingQname, String baseContainer, Node n, Node enclosingFnNameNode) {
    if (!knownClosureSubclasses.contains(baseContainer)) {
      // Can't determine if this is a known "class" that has a known "base" method.
      return;
    }

    boolean misuseOfBase =
        !enclosingFnNameNode.getFirstFirstChild().matchesQualifiedName(baseContainer);
    if (misuseOfBase) {
      // Report misuse of "base" methods from other known classes.
      reportBadBaseMethodUse(n, baseContainer, "Must be used within " + baseContainer + " methods");
      return;
    }

    // The super class is known.
    Node callee = n.getFirstChild();
    Node thisArg = callee.getNext();
    if (thisArg == null || !thisArg.isThis()) {
      reportBadBaseMethodUse(n, baseContainer, "First argument must be 'this'.");
      return;
    }

    // Handle methods.
    Node methodNameNode = thisArg.getNext();
    if (methodNameNode == null || !methodNameNode.isString()) {
      reportBadBaseMethodUse(n, baseContainer, "Second argument must name a method.");
      return;
    }

    String methodName = methodNameNode.getString();
    String ending = ".prototype." + methodName;
    if (enclosingQname == null || !enclosingQname.endsWith(ending)) {
      reportBadBaseMethodUse(n, baseContainer, "Enclosing method does not match " + methodName);
      return;
    }

    // We're good to go.
    Node className = enclosingFnNameNode.getFirstFirstChild();
    n.replaceChild(
        callee,
        NodeUtil.newQName(
            compiler,
            className.getQualifiedName() + ".superClass_." + methodName + ".call",
            callee,
            enclosingQname + ".base"));
    n.removeChild(methodNameNode);
    compiler.reportChangeToEnclosingScope(n);
  }

  private static Node findGoogInheritsCall(Node ctorDeclarationNode) {
    Node maybeInheritsExpr = ctorDeclarationNode.getNext();

    while (maybeInheritsExpr != null
        && (maybeInheritsExpr.isEmpty() || NodeUtil.isExprAssign(maybeInheritsExpr))) {
      // Skip empty nodes (e.g. those created by an unnecessary semicolon) and potential aliases,
      // e.g. "exports = Ctor;" in a goog.module.
      maybeInheritsExpr = maybeInheritsExpr.getNext();
    }
    Node baseClassNode = null;
    if (maybeInheritsExpr != null && NodeUtil.isExprCall(maybeInheritsExpr)) {
      Node callNode = maybeInheritsExpr.getFirstChild();
      if (callNode.getFirstChild().matchesQualifiedName("goog.inherits")
          && callNode.getLastChild().isQualifiedName()) {
        baseClassNode = callNode.getLastChild();
      }
    }
    return baseClassNode;
  }

  /**
   * Processes the goog.inherits call.
   */
  private void processInheritsCall(Node n) {
    if (n.hasXChildren(3)) {
      Node subClass = n.getSecondChild();
      Node superClass = subClass.getNext();
      if (subClass.isUnscopedQualifiedName() && superClass.isUnscopedQualifiedName()) {
        knownClosureSubclasses.add(subClass.getQualifiedName());
      }
    }
  }

  /**
   * Returns the qualified name node of the function whose scope we're in,
   * or null if it cannot be found.
   */
  private static Node getEnclosingDeclNameNode(Node n) {
    Node fn = NodeUtil.getEnclosingFunction(n);
    return fn == null ? null : NodeUtil.getNameNode(fn);
  }

  /** Verify if goog.base call is used in a class */
  private static boolean baseUsedInClass(Node n) {
    for (Node curr = n; curr != null; curr = curr.getParent()){
      if (curr.isClassMembers()) {
        return true;
      }
    }
    return false;
  }

  /** Reports an incorrect use of super-method calling. */
  private void reportBadBaseMethodUse(Node n, String className, String extraMessage) {
    compiler.report(JSError.make(n, BASE_CLASS_ERROR, className, extraMessage));
  }

  /** Reports an incorrect CLOSURE_DEFINES definition. */
  private void reportBadClosureCommonDefinesDefinition(Node n) {
    compiler.report(JSError.make(n, CLOSURE_DEFINES_ERROR));
  }

  /**
   * Processes a call to goog.setCssNameMapping(). Either the argument to goog.setCssNameMapping()
   * is valid, in which case it will be used to create a CssRenamingMap for the compiler of this
   * CompilerPass, or it is invalid and a JSCompiler error will be reported.
   *
   * @see #visit(NodeTraversal, Node, Node)
   */
  private void processSetCssNameMapping(Node n, Node parent) {
    Node left = n.getFirstChild();
    Node arg = left.getNext();
    if (verifySetCssNameMapping(left, arg)) {
      // Translate OBJECTLIT into SubstitutionMap. All keys and
      // values must be strings, or an error will be thrown.
      final Map cssNames = new HashMap<>();

      for (Node key = arg.getFirstChild(); key != null;
          key = key.getNext()) {
        Node value = key.getFirstChild();
        if (!key.isStringKey()
            || value == null
            || !value.isString()) {
          compiler.report(JSError.make(n, NON_STRING_PASSED_TO_SET_CSS_NAME_MAPPING_ERROR));
          return;
        }
        cssNames.put(key.getString(), value.getString());
      }

      String styleStr = "BY_PART";
      if (arg.getNext() != null) {
        styleStr = arg.getNext().getString();
      }

      final CssRenamingMap.Style style;
      try {
        style = CssRenamingMap.Style.valueOf(styleStr);
      } catch (IllegalArgumentException e) {
        compiler.report(JSError.make(n, INVALID_STYLE_ERROR, styleStr));
        return;
      }

      if (style == CssRenamingMap.Style.BY_PART) {
        // Make sure that no keys contain -'s
        List errors = new ArrayList<>();
        for (String key : cssNames.keySet()) {
          if (key.contains("-")) {
            errors.add(key);
          }
        }
        if (!errors.isEmpty()) {
          compiler.report(JSError.make(n, INVALID_CSS_RENAMING_MAP, errors.toString()));
        }
      } else if (style == CssRenamingMap.Style.BY_WHOLE) {
        // Verifying things is a lot trickier here. We just do a quick
        // n^2 check over the map which makes sure that if "a-b" in
        // the map, then map(a-b) = map(a)-map(b).
        // To speed things up, only consider cases where len(b) <= 10
        List errors = new ArrayList<>();
        for (Map.Entry b : cssNames.entrySet()) {
          if (b.getKey().length() > 10) {
            continue;
          }
          for (Map.Entry a : cssNames.entrySet()) {
            String combined = cssNames.get(a.getKey() + "-" + b.getKey());
            if (combined != null && !combined.equals(a.getValue() + "-" + b.getValue())) {
              errors.add("map(" + a.getKey() + "-" + b.getKey() + ") != map("
                  + a.getKey() + ")-map(" + b.getKey() + ")");
            }
          }
        }
        if (!errors.isEmpty()) {
          compiler.report(JSError.make(n, INVALID_CSS_RENAMING_MAP, errors.toString()));
        }
      }

      CssRenamingMap cssRenamingMap = new CssRenamingMap() {
        @Override
        public String get(String value) {
          if (cssNames.containsKey(value)) {
            return cssNames.get(value);
          } else {
            return value;
          }
        }

        @Override
        public CssRenamingMap.Style getStyle() {
          return style;
        }
      };
      compiler.setCssRenamingMap(cssRenamingMap);
      compiler.reportChangeToEnclosingScope(parent);
      parent.detach();
    }
  }

  /**
   * Verifies that a goog.define method call has exactly two arguments, with the first a string
   * literal whose contents is a valid JS qualified name. Reports a compile error if it doesn't.
   *
   * @return Whether the argument checked out okay
   */
  private boolean verifyDefine(NodeTraversal t, Node parent, Node methodName, Node args) {
    // Calls to goog.define must be in the global hoist scope.  This is copied from
    // validate(Un)aliasablePrimitiveCall.
    // TODO(sdh): loosen this restriction if the results are assigned?
    if (!compiler.getOptions().shouldPreserveGoogModule()
        && !t.inGlobalHoistScope()
        && !t.inModuleScope()) {
      compiler.report(JSError.make(methodName.getParent(), INVALID_CLOSURE_CALL_SCOPE_ERROR));
      return false;
    }

    // It is an error for goog.define to show up anywhere except immediately after =.
    if (parent.isAssign() && parent.getParent().isExprResult()) {
      parent = parent.getParent();
    } else if (parent.isName() && NodeUtil.isNameDeclaration(parent.getParent())) {
      parent = parent.getParent();
    } else {
      compiler.report(JSError.make(methodName.getParent(), DEFINE_CALL_WITHOUT_ASSIGNMENT));
      return false;
    }

    // Verify first arg
    Node arg = args;
    if (!verifyNotNull(methodName, arg) || !verifyOfType(methodName, arg, Token.STRING)) {
      return false;
    }

    // Verify second arg
    arg = arg.getNext();
    if (!args.isFromExterns()
        && (!verifyNotNull(methodName, arg) || !verifyIsLast(methodName, arg))) {
      return false;
    }

    String name = args.getString();
    if (!NodeUtil.isValidQualifiedName(
        compiler.getOptions().getLanguageIn().toFeatureSet(), name)) {
      compiler.report(JSError.make(args, INVALID_DEFINE_NAME_ERROR, name));
      return false;
    }

    JSDocInfo info = (parent.isExprResult() ? parent.getFirstChild() : parent).getJSDocInfo();
    if (info == null || !info.isDefine()) {
      compiler.report(JSError.make(parent, MISSING_DEFINE_ANNOTATION));
      return false;
    }
    return true;
  }

  /**
   * Process a goog.addDependency() call and record any forward declarations.
   */
  private void processAddDependency(Node n, Node parent) {
    CodingConvention convention = compiler.getCodingConvention();
    List typeDecls =
        convention.identifyTypeDeclarationCall(n);

    // TODO(nnaze): Use of addDependency() should someday cause a warning
    // as we migrate users to explicit goog.forwardDeclare() calls.
    if (typeDecls != null) {
      for (String typeDecl : typeDecls) {
        compiler.forwardDeclareType(typeDecl);
      }
    }

    // We can't modify parent, so just create a node that will
    // get compiled out.
    Node emptyNode = IR.number(0);
    parent.replaceChild(n, emptyNode);
    compiler.reportChangeToEnclosingScope(emptyNode);
  }

  /** Process a goog.forwardDeclare() call and record the specified forward declaration. */
  private void processForwardDeclare(Node n) {
    if (!n.getParent().isExprResult()) {
      //  Ignore "const Foo = goog.forwardDeclare('my.Foo');". It's legal but does not actually
      // forward declare the type 'my.Foo'.
      return;
    }
    CodingConvention convention = compiler.getCodingConvention();

    String typeDeclaration = null;
    try {
      typeDeclaration = Iterables.getOnlyElement(
          convention.identifyTypeDeclarationCall(n));
    } catch (NullPointerException | NoSuchElementException | IllegalArgumentException e) {
      compiler.report(
          JSError.make(
              n,
              INVALID_FORWARD_DECLARE,
              "A single type could not identified for the goog.forwardDeclare statement"));
    }

    if (typeDeclaration != null) {
      compiler.forwardDeclareType(typeDeclaration);
    }
  }

  /** @return Whether the argument checked out okay */
  private boolean verifyNotNull(Node methodName, Node arg) {
    if (arg == null) {
      compiler.report(JSError.make(methodName, NULL_ARGUMENT_ERROR, methodName.getQualifiedName()));
      return false;
    }
    return true;
  }

  /** @return Whether the argument checked out okay */
  private boolean verifyOfType(Node methodName, Node arg, Token desiredType) {
    if (arg.getToken() != desiredType) {
      compiler.report(
          JSError.make(methodName, INVALID_ARGUMENT_ERROR, methodName.getQualifiedName()));
      return false;
    }
    return true;
  }

  /** @return Whether the argument checked out okay */
  private boolean verifyIsLast(Node methodName, Node arg) {
    if (arg.getNext() != null) {
      compiler.report(
          JSError.make(methodName, TOO_MANY_ARGUMENTS_ERROR, methodName.getQualifiedName()));
      return false;
    }
    return true;
  }

  /**
   * Verifies that setCssNameMapping is called with the correct methods.
   *
   * @return Whether the arguments checked out okay
   */
  private boolean verifySetCssNameMapping(Node methodName, Node firstArg) {
    DiagnosticType diagnostic = null;
    if (firstArg == null) {
      diagnostic = NULL_ARGUMENT_ERROR;
    } else if (!firstArg.isObjectLit()) {
      diagnostic = EXPECTED_OBJECTLIT_ERROR;
    } else if (firstArg.getNext() != null) {
      Node secondArg = firstArg.getNext();
      if (!secondArg.isString()) {
        diagnostic = EXPECTED_STRING_ERROR;
      } else if (secondArg.getNext() != null) {
        diagnostic = TOO_MANY_ARGUMENTS_ERROR;
      }
    }
    if (diagnostic != null) {
      compiler.report(JSError.make(methodName, diagnostic, methodName.getQualifiedName()));
      return false;
    }
    return true;
  }

  /** Add the given qualified name node to the symbol table. */
  private void maybeAddNameToSymbolTable(Node name) {
    if (preprocessorSymbolTable != null) {
      preprocessorSymbolTable.addReference(name);
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy