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

com.google.javascript.jscomp.ProcessDefines Maven / Gradle / Ivy

There is a newer version: 9.0.8
Show newest version
/*
 * Copyright 2007 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.MoreObjects.firstNonNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.javascript.jscomp.ClosurePrimitiveErrors.INVALID_CLOSURE_CALL_SCOPE_ERROR;
import static com.google.javascript.rhino.jstype.JSTypeNative.NUMBER_STRING_BOOLEAN;
import static java.util.stream.Collectors.toCollection;

import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.google.javascript.jscomp.GlobalNamespace.Name;
import com.google.javascript.jscomp.GlobalNamespace.Ref;
import com.google.javascript.jscomp.base.Tri;
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 com.google.javascript.rhino.jstype.JSType;
import com.google.javascript.rhino.jstype.JSTypeRegistry;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.function.Supplier;
import javax.annotation.Nullable;

/**
 * Process variables annotated as {@code @define}. A define is
 * a special constant that may be overridden by later files and
 * manipulated by the compiler, much like C preprocessor {@code #define}s.
 */
class ProcessDefines implements CompilerPass {

  /**
   * Defines in this set will not be flagged with "unknown define" warnings. There are flags that
   * always set these defines, even when they might not be in the binary.
   */
  private static final ImmutableSet KNOWN_DEFINES =
      ImmutableSet.of("COMPILED", "goog.DEBUG", "$jscomp.ISOLATE_POLYFILLS");
  private static final Node GOOG_DEFINE = IR.getprop(IR.name("goog"), "define");

  private final AbstractCompiler compiler;
  private final JSTypeRegistry registry;
  private final ImmutableMap replacementValuesFromFlags;
  private final Mode mode;
  private final Supplier namespaceSupplier;
  private final boolean recognizeClosureDefines;

  private final LinkedHashSet knownDefineJsdocs = new LinkedHashSet<>();
  private final LinkedHashSet knownGoogDefineCalls = new LinkedHashSet<>();
  private final LinkedHashMap defineByDefineName = new LinkedHashMap<>();
  // from var CLOSURE_DEFINES = {
  private final LinkedHashMap replacementValuesFromClosureDefines =
      new LinkedHashMap<>();
  private final LinkedHashSet validDefineValueExpressions = new LinkedHashSet<>();

  private GlobalNamespace namespace;

  // Warnings

  static final DiagnosticType UNKNOWN_DEFINE_WARNING =
      DiagnosticType.warning("JSC_UNKNOWN_DEFINE_WARNING", "unknown @define variable {0}");

  // Errors

  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 INVALID_DEFINE_TYPE =
      DiagnosticType.error("JSC_INVALID_DEFINE_TYPE", "@define tag only permits primitive types");

  static final DiagnosticType INVALID_DEFINE_VALUE =
      DiagnosticType.error(
          "JSC_INVALID_DEFINE_VALUE", "invalid initialization value for @define {0}");

  static final DiagnosticType INVALID_DEFINE_LOCATION =
      DiagnosticType.error(
          "JSC_INVALID_DEFINE_LOCATION",
          "@define must be initalized on a static qualified name in global or module scope");

  static final DiagnosticType NON_CONST_DEFINE =
      DiagnosticType.error("JSC_NON_CONST_DEFINE", "@define {0} has already been set at {1}.");

  static final DiagnosticType CLOSURE_DEFINES_ERROR =
      DiagnosticType.error("JSC_CLOSURE_DEFINES_ERROR", "Invalid CLOSURE_DEFINES definition");

  static final DiagnosticType NON_GLOBAL_CLOSURE_DEFINES_ERROR =
      DiagnosticType.error(
          "JSC_NON_GLOBAL_CLOSURE_DEFINES_ERROR",
          "CLOSURE_DEFINES definition must be in top-level global scope");

  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.");

  /** Create a pass that overrides define constants. */
  private ProcessDefines(Builder builder) {
    this.mode = builder.mode;
    this.compiler = builder.compiler;
    this.registry = this.mode.check ? this.compiler.getTypeRegistry() : null;
    this.replacementValuesFromFlags = ImmutableMap.copyOf(builder.replacementValues);
    this.namespaceSupplier = builder.namespaceSupplier;
    this.recognizeClosureDefines = builder.recognizeClosureDefines;
  }

  enum Mode {
    CHECK(true, false),
    OPTIMIZE(false, true),
    CHECK_AND_OPTIMIZE(true, true);

    private final boolean check;
    private final boolean optimize;

    Mode(boolean check, boolean optimize) {
      this.check = check;
      this.optimize = optimize;
    }
  }

  /** Builder for ProcessDefines. */
  static class Builder {
    private final AbstractCompiler compiler;
    private final Map replacementValues = new LinkedHashMap<>();
    private Mode mode;
    private Supplier namespaceSupplier;
    private boolean recognizeClosureDefines = true;

    Builder(AbstractCompiler compiler) {
      this.compiler = compiler;
    }

    Builder putReplacements(Map replacementValues) {
      this.replacementValues.putAll(replacementValues);
      return this;
    }

    Builder setMode(Mode x) {
      this.mode = x;
      return this;
    }

    /**
     * Injects a pre-computed global namespace, so that the same namespace can be re-used for
     * multiple check passes. Accepts a supplier because the namespace may not exist at
     * pass-creation time.
     */
    Builder injectNamespace(Supplier namespaceSupplier) {
      this.namespaceSupplier = namespaceSupplier;
      return this;
    }

    Builder setRecognizeClosureDefines(boolean recognizeClosureDefines) {
      this.recognizeClosureDefines = recognizeClosureDefines;
      return this;
    }

    ProcessDefines build() {
      return new ProcessDefines(this);
    }
  }

  @Override
  public void process(Node externs, Node root) {
    this.initNamespace(externs, root);
    this.collectDefines();
    this.reportInvalidDefineLocations(root);
    this.collectValidDefineValueExpressions();
    this.validateDefineDeclarations();
    this.overrideDefines();
  }

  final ImmutableSet collectDefineNames(Node externs, Node root) {
    this.initNamespace(externs, root);
    this.collectDefines();

    return ImmutableSet.copyOf(this.defineByDefineName.keySet());
  }

  private void initNamespace(Node externs, Node root) {
    if (namespaceSupplier != null) {
      this.namespace = namespaceSupplier.get();
    }
    if (this.namespace == null) {
      this.namespace = new GlobalNamespace(compiler, externs, root);
    }
  }

  private void overrideDefines() {
    if (this.mode.optimize) {
      for (Define define : this.defineByDefineName.values()) {
        if (define.valueParent == null) {
          continue;
        }

        Node inputValue = this.getReplacementForDefine(define);
        if (inputValue == null || inputValue == define.value) {
          continue;
        }

        boolean changed =
            define.value == null
                || inputValue.getToken() != define.value.getToken()
                || !inputValue.isEquivalentTo(define.value);
        if (changed) {
          if (define.value == null) {
            define.valueParent.addChildToBack(inputValue.cloneTree());
          } else {
            define.value.replaceWith(inputValue.cloneTree());
          }

          compiler.reportChangeToEnclosingScope(define.valueParent);
        }
      }
    }

    if (this.mode.optimize) {
      Set unusedReplacements =
          Sets.difference(
              Sets.union(
                  this.replacementValuesFromFlags.keySet(),
                  this.replacementValuesFromClosureDefines.keySet()),
              Sets.union(KNOWN_DEFINES, this.defineByDefineName.keySet()));

      for (String unknownDefine : unusedReplacements) {
        compiler.report(JSError.make(UNKNOWN_DEFINE_WARNING, unknownDefine));
      }
    }
  }

  /**
   * Returns the replacement value for a @define, if any.
   *
   * 
    *
  1. First checks the flags/compiler options `--define=FOO=1` *
  2. If nothing was found, check for values in a "var CLOSURE_DEFINES = {'FOO': 1}` definition *
  3. If nothing was found, and this is defined via a goog.define call, replace the call with * the default value. */ @Nullable private Node getReplacementForDefine(Define define) { Node replacementFromFlags = this.replacementValuesFromFlags.get(define.defineName); if (replacementFromFlags != null) { return replacementFromFlags; } Node replacementFromClosureDefines = this.replacementValuesFromClosureDefines.get(define.defineName); if (replacementFromClosureDefines != null) { return replacementFromClosureDefines; } if (isGoogDefineCall(define.value) && define.value.getChildCount() == 3) { // Return the second argument of goog.define('name', false); return define.value.getChildAtIndex(2); } return null; } /** Only defines of literal number, string, or boolean are supported. */ private boolean isValidDefineType(JSTypeExpression expression) { JSType type = registry.evaluateTypeExpressionInGlobalScope(expression); return !type.isUnknownType() && type.isSubtypeOf(registry.getNativeType(NUMBER_STRING_BOOLEAN)); } /** Finds all defines, and creates a {@link Define} data structure for each one. */ private void collectDefines() { for (Name name : this.namespace.getAllSymbols()) { if (this.recognizeClosureDefines && name.getFullName().equals("CLOSURE_DEFINES")) { collectClosureDefinesValues(name); continue; } Ref declaration = this.selectDefineDeclaration(name); if (declaration == null) { continue; } int totalSets = name.getTotalSets(); Node valueParent = getValueParentForDefine(declaration); Node value = valueParent != null ? valueParent.getLastChild() : null; final String defineName; if (this.isGoogDefineCall(value) && this.verifyGoogDefine(value)) { Node nameNode = value.getSecondChild(); defineName = nameNode.getString(); } else { defineName = name.getFullName(); } Define existingDefine = this.defineByDefineName.putIfAbsent( defineName, new Define(defineName, name, declaration, valueParent, value)); if (existingDefine != null) { declaration = existingDefine.declaration; totalSets += existingDefine.name.getTotalSets(); } /** * We have to report this here because otherwise we don't remember which names have the same * define name. It's not worth it tracking a set of names, because it makes the rest of the * pass more complex. */ if (totalSets > 1) { for (Ref ref : name.getRefs()) { if (ref.isSet() && !ref.equals(declaration)) { this.compiler.report( JSError.make( ref.getNode(), NON_CONST_DEFINE, defineName, declaration.getNode().getLocation())); } } } } } @Nullable private Ref selectDefineDeclaration(Name name) { for (Ref ref : name.getRefs()) { // Make sure we don't select a local set as the declaration. if (!Ref.Type.SET_FROM_GLOBAL.equals(ref.type)) { continue; } Node refNode = ref.getNode(); if (!refNode.isQualifiedName()) { continue; } JSDocInfo jsdoc = NodeUtil.getBestJSDocInfo(refNode); if (jsdoc == null || !jsdoc.isDefine()) { continue; } this.knownDefineJsdocs.add(jsdoc); return ref; } return null; } private static Node getValueParentForDefine(Ref declaration) { // Note: this may be a NAME, a GETPROP, or even STRING_KEY or GETTER_DEF. We only care // about the first two, in which case the parent should be either VAR/CONST or ASSIGN. // We could accept STRING_KEY (i.e. `@define` on a property in an object literal), but // there's no reason to add another new way to do the same thing. Node declarationNode = declaration.getNode(); Node declarationParent = declarationNode.getParent(); if (declarationParent.isVar() || declarationParent.isConst()) { // Simple case of `var` or `const`. There's no reason to support `let` here, and we // don't explicitly check that it's not `let` anywhere else. checkState(declarationNode.isName(), declarationNode); return declarationNode; } else if (declarationParent.isAssign() && declarationNode.isFirstChildOf(declarationParent)) { // Assignment. Must either assign to a qualified name, or else be a different ref than // the declaration to not emit an error (we don't allow assignment before it's // declared). return declarationParent; } return null; } private void collectValidDefineValueExpressions() { LinkedHashSet namesToCheck = this.namespace.getAllSymbols().stream() .filter(ProcessDefines::isGlobalConst) .collect(toCollection(LinkedHashSet::new)); // All defines are implicitly valid in the values of other defines. for (Define define : this.defineByDefineName.values()) { namesToCheck.remove(define.name); define.name.getRefs().stream() .filter((r) -> !r.isSet()) .map(Ref::getNode) .forEachOrdered(this.validDefineValueExpressions::add); } // Do a breadth-first search of all const names to find those defined in terms of valid values. while (true) { LinkedHashSet namesToCheckAgain = new LinkedHashSet<>(); for (Name name : namesToCheck) { Node declValue = getConstantDeclValue(name.getDeclaration().getNode()); switch (isValidDefineValue(declValue)) { case TRUE: for (Ref ref : name.getRefs()) { this.validDefineValueExpressions.add(ref.getNode()); } break; case UNKNOWN: namesToCheckAgain.add(name); break; default: } } if (namesToCheckAgain.size() == namesToCheck.size()) { break; } else { namesToCheck = namesToCheckAgain; } } } private final void validateDefineDeclarations() { if (!this.mode.check) { return; } for (Define define : this.defineByDefineName.values()) { Node declarationNode = define.declaration.getNode(); if (!this.hasValidValue(define)) { compiler.report( JSError.make( firstNonNull(define.value, firstNonNull(define.valueParent, declarationNode)), INVALID_DEFINE_VALUE, define.defineName)); } /** * Process defines should not depend on check types being enabled, so we look for the JSDoc * instead of the inferred type. */ JSDocInfo jsdoc = NodeUtil.getBestJSDocInfo(declarationNode); if (jsdoc == null || !isValidDefineType(jsdoc.getType())) { compiler.report(JSError.make(declarationNode, INVALID_DEFINE_TYPE)); } } } /** Checks for misplaced @define and goog.define calls */ private void reportInvalidDefineLocations(Node root) { if (!this.mode.check) { return; } /** * This has to be done using a traversal because the global namespace doesn't record symbols * which only appear in local scopes. * *

    We don't check the externs because they can't contain local vars. */ NodeTraversal.builder() .setCompiler(this.compiler) .setCallback( (t, n, parent) -> { JSDocInfo jsdoc = n.getJSDocInfo(); if (jsdoc != null && jsdoc.isDefine() && this.knownDefineJsdocs.add(jsdoc)) { compiler.report(JSError.make(n, INVALID_DEFINE_LOCATION)); } if (isGoogDefineCall(n) && this.knownGoogDefineCalls.add(n)) { verifyGoogDefine(n); } if (n.matchesName("CLOSURE_DEFINES") && NodeUtil.isNameDeclaration(parent) && !NodeUtil.getEnclosingScopeRoot(n).isRoot()) { compiler.report(JSError.make(n, NON_GLOBAL_CLOSURE_DEFINES_ERROR)); } }) .traverse(root); } private void collectClosureDefinesValues(Name closureDefines) { // var CLOSURE_DEFINES = {}; for (Ref ref : closureDefines.getRefs()) { if (!ref.isSet()) { continue; } Node n = ref.getNode(); if (!(NodeUtil.isNameDeclaration(n.getParent()) && n.hasOneChild() && n.getFirstChild().isObjectLit())) { continue; } for (Node c = n.getFirstFirstChild(); c != null; c = c.getNext()) { if (c.isStringKey() && isValidClosureDefinesValue(c.getFirstChild())) { this.replacementValuesFromClosureDefines.put( c.getString(), c.getFirstChild().cloneNode()); } else if (this.mode.check) { compiler.report(JSError.make(n, CLOSURE_DEFINES_ERROR)); } } } } private static boolean isValidClosureDefinesValue(Node val) { // Values allowed in 'var CLOSURE_DEFINES = {' // Must be a subset of the values allowed for in // /** @define {...} */ var DEF = switch (val.getToken()) { case STRINGLIT: case NUMBER: case TRUE: case FALSE: return true; case NEG: return val.getFirstChild().isNumber(); default: return false; } } private boolean hasValidValue(Define define) { if (define.valueParent == null) { return false; } else if (define.valueParent.isFromExterns()) { return true; } else { return this.isValidDefineValue(define.value).toBoolean(false); } } private static boolean isGlobalConst(Name name) { return name.getTotalSets() == 1 && name.getDeclaration() != null && name.getDeclaration().type.equals(Ref.Type.SET_FROM_GLOBAL); } /** * Determines whether the given value may be assigned to a define. * * @param val The value being assigned. */ private Tri isValidDefineValue(@Nullable Node val) { if (val == null) { return Tri.FALSE; } switch (val.getToken()) { case STRINGLIT: case NUMBER: case TRUE: case FALSE: return Tri.TRUE; // Binary operators are only valid if both children are valid. case AND: case OR: case COALESCE: case ADD: case BITAND: case BITNOT: case BITOR: case BITXOR: case DIV: case EQ: case EXPONENT: case GE: case GT: case LE: case LSH: case LT: case MOD: case MUL: case NE: case RSH: case SHEQ: case SHNE: case SUB: case URSH: return isValidDefineValue(val.getFirstChild()).and(isValidDefineValue(val.getLastChild())); case HOOK: return isValidDefineValue(val.getFirstChild()) .and(isValidDefineValue(val.getSecondChild())) .and(isValidDefineValue(val.getLastChild())); // Unary operators are valid if the child is valid. case NOT: case NEG: case POS: return isValidDefineValue(val.getFirstChild()); // Names are valid if and only if they are defines themselves. case NAME: case GETPROP: if (val.isQualifiedName()) { return this.validDefineValueExpressions.contains(val) ? Tri.TRUE : Tri.UNKNOWN; } break; // Allow goog.define('XYZ', ) calls if and only if is valid. case CALL: if (!isGoogDefineCall(val)) { return Tri.FALSE; } if (!val.hasXChildren(3)) { // goog.define call with wrong arg count. Warn elsewhere and treat this call as valid. return Tri.TRUE; } return isValidDefineValue(val.getChildAtIndex(2)); default: break; } return Tri.FALSE; } /** * Checks whether the NAME node is inside either a CONST or a @const VAR. Returns the RHS node if * so, otherwise returns null. */ private static Node getConstantDeclValue(Node name) { Node parent = name.getParent(); if (parent == null) { return null; } if (name.isName()) { if (parent.isConst()) { return name.getFirstChild(); } else if (!parent.isVar()) { return null; } JSDocInfo jsdoc = NodeUtil.getBestJSDocInfo(name); return jsdoc != null && jsdoc.isConstant() ? name.getFirstChild() : null; } else if (name.isGetProp() && parent.isAssign()) { JSDocInfo jsdoc = NodeUtil.getBestJSDocInfo(name); return jsdoc != null && jsdoc.isConstant() ? name.getNext() : null; } return null; } private static final class Define { final String defineName; final Name name; /** * The connonical set ref with an `@define` or `goog.define`. * *

    This may not be the same as `name.getDeclaration()`. */ final Ref declaration; @Nullable final Node valueParent; @Nullable final Node value; public Define( String defineName, Name name, Ref declaration, @Nullable Node valueParent, @Nullable Node value) { checkState(valueParent == null || value == null || value.getParent() == valueParent); checkState(declaration.isSet()); checkState(declaration.name.equals(name)); this.defineName = defineName; this.name = name; this.declaration = declaration; this.valueParent = valueParent; this.value = value; } } private boolean isGoogDefineCall(Node node) { if (!this.recognizeClosureDefines) { return false; } if (node == null || !node.isCall()) { return false; } return node.getFirstChild().matchesQualifiedName(GOOG_DEFINE); } /** * 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 verifyGoogDefine(Node callNode) { this.knownGoogDefineCalls.add(callNode); Node parent = callNode.getParent(); Node methodName = callNode.getFirstChild(); Node args = callNode.getSecondChild(); // Calls to goog.define must be in the global hoist scope after module rewriting if (NodeUtil.getEnclosingFunction(callNode) != null) { 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.STRINGLIT)) { 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; } /** @return Whether the argument checked out okay */ private boolean verifyNotNull(Node methodName, Node arg) { if (arg == null) { compiler.report( JSError.make( methodName, ClosurePrimitiveErrors.NULL_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, ClosurePrimitiveErrors.TOO_MANY_ARGUMENTS_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, ClosurePrimitiveErrors.INVALID_ARGUMENT_ERROR, methodName.getQualifiedName())); return false; } return true; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy