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

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

/*
 * Copyright 2020 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.checkNotNull;

import com.google.common.collect.ImmutableMap;
import com.google.javascript.jscomp.NodeTraversal.AbstractModuleCallback;
import com.google.javascript.jscomp.modules.ModuleMetadataMap;
import com.google.javascript.jscomp.modules.ModuleMetadataMap.ModuleMetadata;
import com.google.javascript.rhino.JSDocInfo;
import com.google.javascript.rhino.JSTypeExpression;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.QualifiedName;
import java.util.HashSet;
import javax.annotation.Nullable;

/** A pass to detect references to fully qualified Closure namespaces. */
public class CheckMissingRequires extends AbstractModuleCallback implements CompilerPass {

  public static final DiagnosticType MISSING_REQUIRE =
      DiagnosticType.warning(
          "JSC_MISSING_REQUIRE",
          "''{0}'' references a fully qualified namespace, which is disallowed by the style"
              + " guide.\nPlease add a goog.require, assign or destructure it into an alias, and "
              + "use the alias instead.");

  public static final DiagnosticType MISSING_REQUIRE_TYPE =
      DiagnosticType.warning(
          "JSC_MISSING_REQUIRE_TYPE",
          "''{0}'' references a fully qualified namespace, which is disallowed by the style"
              + " guide.\nPlease add a goog.requireType, assign or destructure it into an alias, "
              + "and use the alias instead.");

  /** The set of template parameter names found so far in the file currently being checked. */
  private final HashSet templateParamNames = new HashSet<>();

  /** The mapping from Closure namespace into the module that provides it. */
  private final ImmutableMap moduleByNamespace;

  public CheckMissingRequires(AbstractCompiler compiler, ModuleMetadataMap moduleMetadataMap) {
    super(compiler, moduleMetadataMap);
    this.moduleByNamespace = moduleMetadataMap.getModulesByGoogNamespace();
  }

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

  @Override
  public boolean shouldTraverse(
      NodeTraversal t, Node n, @Nullable ModuleMetadata currentModule, Node scopeRoot) {
    if (currentModule == null) {
      return true;
    }
    if (n == currentModule.rootNode() && !currentModule.isModule()) {
      // Only check inside modules.
      return false;
    }
    // Traverse nodes in preorder to collect `@template` parameter names before their use.
    visitNode(t, n, checkNotNull(currentModule));
    return true;
  }

  @Override
  public void visit(
      NodeTraversal t, Node n, @Nullable ModuleMetadata currentModule, @Nullable Node scopeRoot) {
    if (currentModule != null && n == currentModule.rootNode()) {
      // For this pass, template parameter names are only meaningful inside the file defining them.
      templateParamNames.clear();
    }
  }

  private void visitNode(NodeTraversal t, Node n, ModuleMetadata currentModule) {
    JSDocInfo info = n.getJSDocInfo();
    if (info != null) {
      visitJsDocInfo(t, currentModule, info);
    }
    if (n.isQualifiedName() && !n.getParent().isGetProp()) {
      QualifiedName qualifiedName = n.getQualifiedNameObject();
      String root = qualifiedName.getRoot();
      if (root.equals("this") || root.equals("super")) {
        return;
      }
      visitQualifiedName(
          t, n, currentModule, n.getQualifiedNameObject(), /* isStrongReference= */ true);
    }
  }

  private void visitJsDocInfo(NodeTraversal t, ModuleMetadata currentModule, JSDocInfo info) {
    // Collect template parameter names before checking, so that annotations on the same node that
    // reference the name are excluded from the check.
    templateParamNames.addAll(info.getTemplateTypeNames());
    templateParamNames.addAll(info.getTypeTransformations().keySet());
    if (info.hasType()) {
      visitJsDocExpr(t, currentModule, info.getType(), /* isStrongReference */ false);
    }
    for (String param : info.getParameterNames()) {
      if (info.hasParameterType(param)) {
        visitJsDocExpr(
            t, currentModule, info.getParameterType(param), /* isStrongReference=*/ false);
      }
    }
    if (info.hasReturnType()) {
      visitJsDocExpr(t, currentModule, info.getReturnType(), /* isStrongReference=*/ false);
    }
    if (info.hasEnumParameterType()) {
      visitJsDocExpr(t, currentModule, info.getEnumParameterType(), /* isStrongReference=*/ false);
    }
    if (info.hasTypedefType()) {
      visitJsDocExpr(t, currentModule, info.getTypedefType(), /* isStrongReference=*/ false);
    }
    if (info.hasThisType()) {
      visitJsDocExpr(t, currentModule, info.getThisType(), /* isStrongReference=*/ false);
    }
    if (info.hasBaseType()) {
      // Note that `@extends` requires a goog.require, not a goog.requireType.
      visitJsDocExpr(t, currentModule, info.getBaseType(), /* isStrongReference=*/ true);
    }
    for (JSTypeExpression expr : info.getExtendedInterfaces()) {
      // Note that `@extends` requires a goog.require, not a goog.requireType.
      visitJsDocExpr(t, currentModule, expr, /* isStrongReference=*/ true);
    }
    for (JSTypeExpression expr : info.getImplementedInterfaces()) {
      // Note that `@implements` requires a goog.require, not a goog.requireType.
      visitJsDocExpr(t, currentModule, expr, /* isStrongReference=*/ true);
    }
  }

  private void visitJsDocExpr(
      NodeTraversal t,
      ModuleMetadata currentModule,
      JSTypeExpression expr,
      boolean isStrongReference) {
    for (Node typeNode : expr.getAllTypeNodes()) {
      visitQualifiedName(
          t, typeNode, currentModule, QualifiedName.of(typeNode.getString()), isStrongReference);
    }
  }

  private void visitQualifiedName(
      NodeTraversal t,
      Node n,
      ModuleMetadata currentModule,
      QualifiedName qualifiedName,
      boolean isStrongReference) {
    if (qualifiedName.isSimple() && templateParamNames.contains(qualifiedName.getRoot())) {
      // This will produce a false negative when the same name is used in both template and
      // non-template capacity in the same file, and a false positive when the `@template` does not
      // precede the reference within the same source file (e.g. an ES5 ctor in a different file).
      return;
    }
    if (qualifiedName.isSimple() && qualifiedName.getRoot().equals("xid")) {
      // Specifically don't report the name 'xid', which is a function that is widely used
      // within Google without an accompanying goog.require, and which makes it hard to roll out
      // this check.
      // TODO(user): fix the remaining code involving xid and remove this workaround.
      return;
    }
    Var var = t.getScope().getVar(qualifiedName.getRoot());
    if (var != null && var.getScope().isLocal()) {
      // If the name refers to a nonexisting variable, the error will be caught elsewhere.
      // If it refers to a local variable, it's legal. Note that this includes variables introduced
      // by the aliasing and destructuring forms of `goog.require` and `goog.requireType`.
      return;
    }
    // Look for the longest prefix match against a provided namespace.
    while (qualifiedName != null) {
      String namespace = qualifiedName.join();
      if (namespace.equals("goog.module")) {
        // We must special case `goog.module` because Closure Library provides a namespace with that
        // name, but it's (confusingly) unrelated to the `goog.module` primitive.
        return;
      }
      ModuleMetadata module = moduleByNamespace.get(namespace);
      if (module != null) {
        // Do not report references to a namespace provided in the same file, but do not recurse
        // into parent namespaces either.
        // TODO(tjgq): Also check for these references.
        if (module.rootNode() != currentModule.rootNode()) {
          t.report(n, isStrongReference ? MISSING_REQUIRE : MISSING_REQUIRE_TYPE, namespace);
        }
        return;
      }
      qualifiedName = qualifiedName.getOwner();
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy