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

com.google.javascript.jscomp.ConformanceRules 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 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.jscomp;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.ImmutableSet.toImmutableSet;

import com.google.common.annotations.GwtIncompatible;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Ascii;
import com.google.common.base.Joiner;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSetMultimap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Sets;
import com.google.common.reflect.TypeToken;
import com.google.javascript.jscomp.CheckConformance.InvalidRequirementSpec;
import com.google.javascript.jscomp.CheckConformance.Precondition;
import com.google.javascript.jscomp.CheckConformance.Rule;
import com.google.javascript.jscomp.CodingConvention.AssertionFunctionLookup;
import com.google.javascript.jscomp.Requirement.Severity;
import com.google.javascript.jscomp.Requirement.WhitelistEntry;
import com.google.javascript.jscomp.parsing.JsDocInfoParser;
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 com.google.javascript.rhino.Token;
import com.google.javascript.rhino.jstype.EnumElementType;
import com.google.javascript.rhino.jstype.FunctionType;
import com.google.javascript.rhino.jstype.JSType;
import com.google.javascript.rhino.jstype.JSTypeNative;
import com.google.javascript.rhino.jstype.JSTypeRegistry;
import com.google.javascript.rhino.jstype.ObjectType;
import com.google.javascript.rhino.jstype.Property;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import org.jspecify.nullness.Nullable;

/**
 * Standard conformance rules. See
 * third_party/java_src/jscomp/java/com/google/javascript/jscomp/conformance.proto
 */
@GwtIncompatible("java.lang.reflect, java.util.regex")
public final class ConformanceRules {

  private static final AllowList ALL_TS_ALLOWLIST = createTsAllowlist();

  private static AllowList createTsAllowlist() {
    try {
      return new AllowList(ImmutableList.of(), ImmutableList.of(".*\\.closure\\.js"));
    } catch (Throwable t) {
      throw new AssertionError(t);
    }
  }

  private ConformanceRules() {}

  /**
   * Classes extending AbstractRule must return ConformanceResult from their checkConformance
   * implementation. For simple rules, the constants CONFORMANCE, POSSIBLE_VIOLATION, VIOLATION are
   * sufficient. However, for some rules additional clarification specific to the violation instance
   * is helpful, for that, an instance of this class can be created to associate a note with the
   * violation.
   */
  public static class ConformanceResult {
    ConformanceResult(ConformanceLevel level) {
      this(level, "");
    }

    ConformanceResult(ConformanceLevel level, String note) {
      this.level = level;
      this.note = note;
    }

    public final ConformanceLevel level;
    public final String note;

    // For CONFORMANCE rules that don't generate notes:
    public static final ConformanceResult CONFORMANCE =
        new ConformanceResult(ConformanceLevel.CONFORMANCE);
    public static final ConformanceResult POSSIBLE_VIOLATION =
        new ConformanceResult(ConformanceLevel.POSSIBLE_VIOLATION);
    private static final ConformanceResult POSSIBLE_VIOLATION_DUE_TO_LOOSE_TYPES =
        new ConformanceResult(
            ConformanceLevel.POSSIBLE_VIOLATION,
            "The type information available for this expression is too loose "
                + "to ensure conformance.");
    public static final ConformanceResult VIOLATION =
        new ConformanceResult(ConformanceLevel.VIOLATION);
  }

  /** Possible check check results */
  public static enum ConformanceLevel {
    // Nothing interesting detected.
    CONFORMANCE,
    // In the optionally typed world of the Closure Compiler type system
    // it is possible that detect patterns that match with looser types
    // that the target pattern.
    POSSIBLE_VIOLATION,
    // Definitely a violation.
    VIOLATION,
  }

  private static @Nullable Pattern buildPattern(List reqPatterns)
      throws InvalidRequirementSpec {
    if (reqPatterns == null || reqPatterns.isEmpty()) {
      return null;
    }

    // validate the patterns
    for (String reqPattern : reqPatterns) {
      try {
        Pattern.compile(reqPattern);
      } catch (PatternSyntaxException e) {
        throw new InvalidRequirementSpec("invalid regex pattern", e);
      }
    }

    Pattern pattern = null;
    try {
      String jointRegExp = "(" + Joiner.on("|").join(reqPatterns) + ")";
      pattern = Pattern.compile(jointRegExp);
    } catch (PatternSyntaxException e) {
      throw new RuntimeException("bad joined regexp", e);
    }
    return pattern;
  }

  private static class AllowList {
    final @Nullable ImmutableList prefixes;
    final @Nullable Pattern regexp;
    final @Nullable WhitelistEntry allowlistEntry;

    AllowList(List prefixes, List regexps) throws InvalidRequirementSpec {
      this.prefixes = ImmutableList.copyOf(prefixes);
      this.regexp = buildPattern(regexps);
      this.allowlistEntry = null;
    }

    AllowList(WhitelistEntry allowlistEntry) throws InvalidRequirementSpec {
      this.prefixes = ImmutableList.copyOf(allowlistEntry.getPrefixList());
      this.regexp = buildPattern(allowlistEntry.getRegexpList());
      this.allowlistEntry = allowlistEntry;
    }

    /**
     * Returns true if the given path matches one of the prefixes or regexps, and false otherwise
     */
    boolean matches(String path) {
      // If the path ends with .closure.js, it is probably a tsickle-generated file, and there may
      // be entries in the allow list for the TypeScript path
      String tsPath =
          path.endsWith(".closure.js")
              ? path.substring(0, path.length() - ".closure.js".length()) + ".ts"
              : null;
      if (prefixes != null) {
        for (String prefix : prefixes) {
          if (!path.isEmpty()
              && (path.startsWith(prefix) || (tsPath != null && tsPath.startsWith(prefix)))) {
            return true;
          }
        }
      }

      return regexp != null
          && (regexp.matcher(path).find() || (tsPath != null && regexp.matcher(tsPath).find()));
    }
  }

  /**
   * A conformance rule implementation to support things common to all rules such as allowlisting
   * and reporting.
   */
  public abstract static class AbstractRule implements Rule {
    final AbstractCompiler compiler;
    final String message;
    final Severity severity;
    final ImmutableList allowlists;
    final @Nullable AllowList onlyApplyTo;
    final boolean reportLooseTypeViolations;
    final TypeMatchingStrategy typeMatchingStrategy;
    final Requirement requirement;

    public AbstractRule(AbstractCompiler compiler, Requirement requirement)
        throws InvalidRequirementSpec {
      if (!requirement.hasErrorMessage()) {
        throw new InvalidRequirementSpec("missing message");
      }
      this.compiler = compiler;
      String message = requirement.getErrorMessage();
      if (requirement.getConfigFileCount() > 0) {
        message +=
            "\n  defined in " + Joiner.on("\n  extended by ").join(requirement.getConfigFileList());
      }
      this.message = message;
      if (requirement.getSeverity() == Severity.UNSPECIFIED) {
        severity = Severity.WARNING;
      } else {
        severity = requirement.getSeverity();
      }

      // build allowlists
      ImmutableList.Builder allowlistsBuilder = new ImmutableList.Builder<>();
      for (WhitelistEntry entry : requirement.getWhitelistEntryList()) {
        allowlistsBuilder.add(new AllowList(entry));
      }
      for (WhitelistEntry entry : requirement.getAllowlistEntryList()) {
        allowlistsBuilder.add(new AllowList(entry));
      }

      if (this.tsIsAllowlisted()) {
        allowlistsBuilder.add(ALL_TS_ALLOWLIST);
      }

      if (requirement.getWhitelistCount() > 0 || requirement.getWhitelistRegexpCount() > 0) {
        AllowList allowlist =
            new AllowList(requirement.getWhitelistList(), requirement.getWhitelistRegexpList());
        allowlistsBuilder.add(allowlist);
      }
      if (requirement.getAllowlistCount() > 0 || requirement.getAllowlistRegexpCount() > 0) {
        AllowList allowlist =
            new AllowList(requirement.getAllowlistList(), requirement.getAllowlistRegexpList());
        allowlistsBuilder.add(allowlist);
      }
      allowlists = allowlistsBuilder.build();

      if (requirement.getOnlyApplyToCount() > 0 || requirement.getOnlyApplyToRegexpCount() > 0) {
        onlyApplyTo =
            new AllowList(requirement.getOnlyApplyToList(), requirement.getOnlyApplyToRegexpList());
      } else {
        onlyApplyTo = null;
      }
      reportLooseTypeViolations = requirement.getReportLooseTypeViolations();
      typeMatchingStrategy = getTypeMatchingStrategy(requirement);
      this.requirement = requirement;
    }

    protected boolean tsIsAllowlisted() {
      return false;
    }

    private static TypeMatchingStrategy getTypeMatchingStrategy(Requirement requirement) {
      switch (requirement.getTypeMatchingStrategy()) {
        case LOOSE:
          return TypeMatchingStrategy.LOOSE;
        case STRICT_NULLABILITY:
          return TypeMatchingStrategy.STRICT_NULLABILITY;
        case SUBTYPES:
          return TypeMatchingStrategy.SUBTYPES;
        case EXACT:
          return TypeMatchingStrategy.EXACT;
        default:
          throw new IllegalStateException("Unknown TypeMatchingStrategy");
      }
    }

    /**
     * @return Whether the code represented by the Node conforms to the rule.
     */
    protected abstract ConformanceResult checkConformance(NodeTraversal t, Node n);

    /** Returns the first AllowList entry that matches the given path, and null otherwise. */
    private @Nullable AllowList findAllowListForPath(String path) {
      Optional pathRegex = compiler.getOptions().getConformanceRemoveRegexFromPath();
      if (pathRegex.isPresent()) {
        path = pathRegex.get().matcher(path).replaceFirst("");
      }

      for (AllowList allowlist : allowlists) {
        if (allowlist.matches(path)) {
          return allowlist;
        }
      }
      return null;
    }

    @Override
    public final void check(NodeTraversal t, Node n) {
      ConformanceResult result = checkConformance(t, n);
      if (result.level != ConformanceLevel.CONFORMANCE) {
        report(n, result);
      }
    }

    /**
     * Report a conformance warning for the given node.
     *
     * @param n The node representing the violating code.
     * @param result The result representing the confidence of the violation.
     */
    protected void report(Node n, ConformanceResult result) {
      DiagnosticType msg;
      if (severity == Severity.ERROR) {
        // Always report findings that are errors, even if the types are too loose to be certain.
        // TODO(bangert): If this causes problems, add another severity category that only
        // errors when certain.
        msg = CheckConformance.CONFORMANCE_ERROR;
      } else {
        if (result.level == ConformanceLevel.VIOLATION) {
          msg = CheckConformance.CONFORMANCE_VIOLATION;
        } else {
          msg = CheckConformance.CONFORMANCE_POSSIBLE_VIOLATION;
        }
      }
      String separator = (result.note.isEmpty()) ? "" : "\n";
      JSError err = JSError.make(n, msg, message, separator, result.note);

      String path = NodeUtil.getSourceName(n);
      AllowList allowlist = path != null ? findAllowListForPath(path) : null;
      boolean shouldReport =
          compiler
              .getErrorManager()
              .shouldReportConformanceViolation(
                  requirement,
                  allowlist != null
                      ? Optional.fromNullable(allowlist.allowlistEntry)
                      : Optional.absent(),
                  err);

      if (shouldReport && allowlist == null && (onlyApplyTo == null || onlyApplyTo.matches(path))) {
        compiler.report(err);
      }
    }
  }

  /**
   * No-op rule that never reports any violations.
   *
   * 

This exists so that, if a requirement becomes obsolete but is extended by other requirements * that can't all be simultaneously deleted, it can be changed to this rule, allowing it to be * effectively removed without breaking downstream builds. */ static final class NoOp extends AbstractRule { NoOp(AbstractCompiler compiler, Requirement requirement) throws InvalidRequirementSpec { super(compiler, requirement); } @Override protected ConformanceResult checkConformance(NodeTraversal t, Node n) { return ConformanceResult.CONFORMANCE; } } abstract static class AbstractTypeRestrictionRule extends AbstractRule { private final JSType nativeObjectType; private final JSType allowlistedTypes; private final AssertionFunctionLookup assertionFunctions; public AbstractTypeRestrictionRule(AbstractCompiler compiler, Requirement requirement) throws InvalidRequirementSpec { super(compiler, requirement); nativeObjectType = compiler.getTypeRegistry().getNativeType(JSTypeNative.OBJECT_TYPE); List allowlistedTypeNames = requirement.getValueList(); allowlistedTypes = union(allowlistedTypeNames); assertionFunctions = AssertionFunctionLookup.of(compiler.getCodingConvention().getAssertionFunctions()); } protected boolean isAllowlistedType(Node n) { if (allowlistedTypes != null && n.getJSType() != null) { JSType targetType = n.getJSType().restrictByNotNullOrUndefined(); if (targetType.isSubtypeOf(allowlistedTypes)) { return true; } } return false; } protected static boolean isKnown(Node n) { return !isUnknown(n) && !isBottom(n) && !isTemplateType(n); // TODO(johnlenz): Remove this restriction } protected boolean isNativeObjectType(Node n) { JSType type = n.getJSType().restrictByNotNullOrUndefined(); return type.equals(nativeObjectType); } protected static boolean isTop(Node n) { JSType type = n.getJSType(); return type != null && type.isAllType(); } protected static boolean isUnknown(Node n) { JSType type = n.getJSType(); return (type == null || type.isUnknownType()); } protected static boolean isTemplateType(Node n) { JSType type = n.getJSType(); if (type.isUnionType()) { for (JSType member : type.getUnionMembers()) { if (member.isTemplateType()) { return true; } } } return type.isTemplateType(); } private static boolean isBottom(Node n) { JSType type = n.getJSType().restrictByNotNullOrUndefined(); return type.isEmptyType(); } protected @Nullable JSType union(List typeNames) { JSTypeRegistry registry = compiler.getTypeRegistry(); List types = new ArrayList<>(); for (String typeName : typeNames) { JSType type = registry.getGlobalType(typeName); if (type != null) { types.add(type); } } if (types.isEmpty()) { return null; } else { return registry.createUnionType(types); } } protected boolean isAssertionCall(Node n) { if (n.isCall() && n.getFirstChild().isQualifiedName()) { Node target = n.getFirstChild(); return assertionFunctions.lookupByCallee(target) != null; } return false; } protected boolean isTypeImmediatelyTightened(Node n) { return isAssertionCall(n.getParent()) || n.getParent().isTypeOf() || /* casted node */ n.getJSTypeBeforeCast() != null; } protected boolean isUsed(Node n) { if (n.getParent().isName() || NodeUtil.isLhsByDestructuring(n)) { return false; } // Consider lvalues in assignment operations to be used iff the actual assignment // operation's result is used. e.g. for `a.b.c`: // USED: `alert(x = a.b.c);` // UNUSED: `x = a.b.c;` if (NodeUtil.isAssignmentOp(n.getParent())) { return NodeUtil.isExpressionResultUsed(n.getParent()); } return NodeUtil.isExpressionResultUsed(n); } } /** * Check that variables annotated as @const have an inferred type, if there is no type given * explicitly. */ static class InferredConstCheck extends AbstractRule { public InferredConstCheck(AbstractCompiler compiler, Requirement requirement) throws InvalidRequirementSpec { super(compiler, requirement); } @Override protected ConformanceResult checkConformance(NodeTraversal t, Node n) { JSDocInfo jsDoc = n.getJSDocInfo(); if (jsDoc != null && jsDoc.hasConstAnnotation() && jsDoc.getType() == null) { if (n.isAssign()) { n = n.getFirstChild(); } JSType type = n.getJSType(); if (type != null && type.isUnknownType() && !NodeUtil.isNamespaceDecl(n)) { return ConformanceResult.VIOLATION; } } return ConformanceResult.CONFORMANCE; } } /** Banned dependency rule */ static class BannedDependency extends AbstractRule { private final List paths; BannedDependency(AbstractCompiler compiler, Requirement requirement) throws InvalidRequirementSpec { super(compiler, requirement); paths = requirement.getValueList(); if (paths.isEmpty()) { throw new InvalidRequirementSpec("missing value"); } } @Override protected ConformanceResult checkConformance(NodeTraversal t, Node n) { if (n.isScript()) { String srcFile = n.getSourceFileName(); for (int i = 0; i < paths.size(); i++) { String path = paths.get(i); if (srcFile.startsWith(path)) { return ConformanceResult.VIOLATION; } } } return ConformanceResult.CONFORMANCE; } } /** Banned dependency via regex rule */ static class BannedDependencyRegex extends AbstractRule { private final @Nullable Pattern pathRegexp; BannedDependencyRegex(AbstractCompiler compiler, Requirement requirement) throws InvalidRequirementSpec { super(compiler, requirement); List pathRegexpList = requirement.getValueList(); if (pathRegexpList.isEmpty()) { throw new InvalidRequirementSpec("missing value (no banned dependency regexps)"); } pathRegexp = buildPattern(pathRegexpList); } @Override protected ConformanceResult checkConformance(NodeTraversal t, Node n) { if (n.isScript()) { String srcFile = n.getSourceFileName(); if (pathRegexp != null && pathRegexp.matcher(srcFile).find()) { return ConformanceResult.VIOLATION; } } return ConformanceResult.CONFORMANCE; } } /** Banned name rule */ static final class BannedName extends AbstractRule { private final Requirement.Type requirementType; private final ImmutableList qualifiedNames; private final ImmutableSet shortNames; BannedName(AbstractCompiler compiler, Requirement requirement) throws InvalidRequirementSpec { super(compiler, requirement); if (requirement.getValueCount() == 0) { throw new InvalidRequirementSpec("missing value"); } this.requirementType = requirement.getType(); ImmutableList.Builder qualifiedBuilder = ImmutableList.builder(); ImmutableSet.Builder shortBuilder = ImmutableSet.builder(); for (String name : requirement.getValueList()) { Node qualifiedName = NodeUtil.newQName(compiler, name); qualifiedBuilder.add(qualifiedName); shortBuilder.add(qualifiedName.getString()); } this.qualifiedNames = qualifiedBuilder.build(); this.shortNames = shortBuilder.build(); } private static final Precondition IS_CANDIDATE_NODE = new Precondition() { @Override public boolean shouldCheck(Node n) { switch (n.getToken()) { case GETPROP: return n.getFirstChild().isQualifiedName(); case NAME: return !n.getString().isEmpty(); default: return false; } } }; @Override public final Precondition getPrecondition() { return IS_CANDIDATE_NODE; } @Override protected ConformanceResult checkConformance(NodeTraversal t, Node n) { if (requirementType == Requirement.Type.BANNED_NAME_CALL && !ConformanceUtil.isCallTarget(n)) { return ConformanceResult.CONFORMANCE; } /** * Efficiently filter out nearly all candidates. * *

Ideally we could use a hashset containing the qualified names, but it turns out that * creating wrapper a object for every Node is more expensive than the loop below. */ if (!this.shortNames.contains(n.getString())) { return ConformanceResult.CONFORMANCE; } /** * Defer expensive infrequent checks. * *

These could be a precondition, but they don't usually need to be checked. Since they're * kind of expensive, it's cheaper overall to defer them. */ if (NodeUtil.isInSyntheticScript(n) || !isRootOfQualifiedNameGlobal(t, n)) { return ConformanceResult.CONFORMANCE; } for (int i = this.qualifiedNames.size() - 1; i >= 0; i--) { if (n.matchesQualifiedName(this.qualifiedNames.get(i))) { return ConformanceResult.VIOLATION; } } return ConformanceResult.CONFORMANCE; } private static boolean isRootOfQualifiedNameGlobal(NodeTraversal t, Node n) { String rootName = NodeUtil.getRootOfQualifiedName(n).getQualifiedName(); Var v = t.getScope().getVar(rootName); // TODO(b/189382837): Turn the nullness check back into an assertion once the bug is fixed. return v != null && v.isGlobal(); } } /** Banned property rule */ static final class BannedProperty extends AbstractRule { private enum RequirementPrecondition implements Precondition { BANNED_PROPERTY() { @Override public boolean shouldCheck(Node n) { switch (n.getToken()) { case STRING_KEY: case GETPROP: case GETELEM: case COMPUTED_PROP: return true; default: return false; } } }, BANNED_PROPERTY_WRITE() { @Override public boolean shouldCheck(Node n) { return NodeUtil.isLValue(n); } }, BANNED_PROPERTY_NON_CONSTANT_WRITE() { @Override public boolean shouldCheck(Node n) { if (!NodeUtil.isLValue(n)) { return false; } if (NodeUtil.isLhsOfAssign(n) && (NodeUtil.isLiteralValue(n.getNext(), /* includeFunctions= */ false) || NodeUtil.isSomeCompileTimeConstStringValue(n.getNext()))) { return false; } return true; } }, BANNED_PROPERTY_READ() { @Override public boolean shouldCheck(Node n) { return !NodeUtil.isLValue(n) && NodeUtil.isExpressionResultUsed(n); } }, BANNED_PROPERTY_CALL() { @Override public boolean shouldCheck(Node n) { return ConformanceUtil.isCallTarget(n); } }, } private final JSTypeRegistry registry; private final ImmutableSetMultimap props; private final RequirementPrecondition requirementPrecondition; BannedProperty(AbstractCompiler compiler, Requirement requirement) throws InvalidRequirementSpec { super(compiler, requirement); if (requirement.getValueCount() == 0) { throw new InvalidRequirementSpec("missing value"); } switch (requirement.getType()) { case BANNED_PROPERTY: this.requirementPrecondition = RequirementPrecondition.BANNED_PROPERTY; break; case BANNED_PROPERTY_READ: this.requirementPrecondition = RequirementPrecondition.BANNED_PROPERTY_READ; break; case BANNED_PROPERTY_WRITE: this.requirementPrecondition = RequirementPrecondition.BANNED_PROPERTY_WRITE; break; case BANNED_PROPERTY_NON_CONSTANT_WRITE: this.requirementPrecondition = RequirementPrecondition.BANNED_PROPERTY_NON_CONSTANT_WRITE; break; case BANNED_PROPERTY_CALL: this.requirementPrecondition = RequirementPrecondition.BANNED_PROPERTY_CALL; break; default: throw new AssertionError(requirement.getType()); } this.registry = compiler.getTypeRegistry(); ImmutableSetMultimap.Builder builder = ImmutableSetMultimap.builder(); for (String value : requirement.getValueList()) { String typename = ConformanceUtil.getClassFromDeclarationName(value); String property = ConformanceUtil.getPropertyFromDeclarationName(value); if (typename == null || property == null) { throw new InvalidRequirementSpec("bad prop value"); } JSType type = registry.getGlobalType(typename); if (type == null) { type = registry.resolveViaClosureNamespace(typename); } // If type doesn't exist in the copmilation, it can't be a violation. Also, bottom types // match everything, which is almost surely not what the check is intended to check against. if (type == null || type.isUnknownType() || type.isEmptyType()) { continue; } builder.put(property, type); } this.props = builder.build(); } @Override public final Precondition getPrecondition() { return this.requirementPrecondition; } @Override protected ConformanceResult checkConformance(NodeTraversal t, Node n) { ImmutableSet checkTypes = this.props.get(this.extractName(n)); if (checkTypes.isEmpty()) { return ConformanceResult.CONFORMANCE; } /** * Avoid type operations when possible. * *

checkTypes is almost always empty, and operations on unions can be expensive. */ JSType foundType = this.extractType(n); if (foundType == null) { return ConformanceResult.CONFORMANCE; } foundType = foundType.restrictByNotNullOrUndefined(); for (JSType checkType : checkTypes) { ConformanceResult result = this.matchTypes(foundType, checkType); if (result.level != ConformanceLevel.CONFORMANCE) { return result; } } return ConformanceResult.CONFORMANCE; } private ConformanceResult matchTypes(JSType foundType, JSType checkType) { ObjectType foundObj = foundType.toMaybeObjectType(); if (foundObj != null) { if (foundObj.isFunctionPrototypeType()) { FunctionType ownerFun = foundObj.getOwnerFunction(); if (ownerFun.isConstructor()) { foundType = ownerFun.getInstanceType(); } } else if (foundObj.isTemplatizedType()) { foundType = foundObj.getRawType(); } } if (foundType.isUnknownType() || foundType.isTemplateType() || foundType.isEmptyType() || foundType.isAllType() || foundType.equals(this.registry.getNativeType(JSTypeNative.OBJECT_TYPE))) { if (reportLooseTypeViolations) { return ConformanceResult.POSSIBLE_VIOLATION_DUE_TO_LOOSE_TYPES; } } else if (foundType.isSubtypeOf(checkType)) { return ConformanceResult.VIOLATION; } else if (checkType.isSubtypeWithoutStructuralTyping(foundType)) { if (matchesPrototype(checkType, foundType)) { return ConformanceResult.VIOLATION; } else if (reportLooseTypeViolations) { // Access of a banned property through a super class may be a violation return ConformanceResult.POSSIBLE_VIOLATION_DUE_TO_LOOSE_TYPES; } } return ConformanceResult.CONFORMANCE; } private boolean matchesPrototype(JSType type, JSType maybePrototype) { ObjectType methodClassObjectType = type.toMaybeObjectType(); if (methodClassObjectType != null) { if (methodClassObjectType.getImplicitPrototype().equals(maybePrototype)) { return true; } } return false; } private @Nullable JSType extractType(Node n) { switch (n.getToken()) { case GETELEM: case GETPROP: return n.getFirstChild().getJSType(); case STRING_KEY: case COMPUTED_PROP: { Node parent = n.getParent(); switch (parent.getToken()) { case OBJECT_PATTERN: case OBJECTLIT: return parent.getJSType(); case CLASS_MEMBERS: return null; default: throw new AssertionError(); } } default: return null; } } private @Nullable String extractName(Node n) { switch (n.getToken()) { case GETPROP: case STRING_KEY: return n.getString(); case GETELEM: { Node string = n.getSecondChild(); return string.isStringLit() ? string.getString() : null; } case COMPUTED_PROP: { Node string = n.getFirstChild(); return string.isStringLit() ? string.getString() : null; } default: return null; } } } /** Banned string rule */ static final class BannedStringRegex extends AbstractRule { private final Pattern stringPattern; public BannedStringRegex(AbstractCompiler compiler, Requirement requirement) throws InvalidRequirementSpec { super(compiler, requirement); if (requirement.getValueCount() == 0) { throw new InvalidRequirementSpec("missing value"); } ImmutableList.Builder builder = ImmutableList.builder(); for (String value : requirement.getValueList()) { if (value.trim().isEmpty()) { throw new InvalidRequirementSpec("empty strings or whitespace are not allowed"); } builder.add(value); } ImmutableList values = builder.build(); if (values.isEmpty()) { throw new InvalidRequirementSpec("missing value"); } Pattern stringRegex = buildPattern(values); this.stringPattern = stringRegex; } @Override protected ConformanceResult checkConformance(NodeTraversal traversal, Node node) { if (node == null) { return ConformanceResult.CONFORMANCE; } if (node.isStringLit()) { if (this.stringPattern.matcher(node.getString()).matches()) { return ConformanceResult.VIOLATION; } } else if (node.isTemplateLitString()) { if (this.stringPattern.matcher(node.getCookedString()).matches()) { return ConformanceResult.VIOLATION; } } return ConformanceResult.CONFORMANCE; } } private static class ConformanceUtil { static boolean isCallTarget(Node n) { Node parent = n.getParent(); return (parent.isCall() || parent.isNew()) && parent.getFirstChild() == n; } static boolean isLooseType(JSType type) { return type.isUnknownType() || type.isNoResolvedType() || type.isAllType(); } static JSType evaluateTypeString(AbstractCompiler compiler, String expression) throws InvalidRequirementSpec { Node typeNodes = JsDocInfoParser.parseTypeString(expression); if (typeNodes == null) { throw new InvalidRequirementSpec("bad type expression"); } JSTypeExpression typeExpr = new JSTypeExpression(typeNodes, "conformance"); return compiler.getTypeRegistry().evaluateTypeExpressionInGlobalScope(typeExpr); } /** * Validate the parameters and the 'this' type, of a new or call. * * @see TypeCheck#visitParameterList */ static boolean validateCall( AbstractCompiler compiler, Node callOrNew, FunctionType functionType, boolean isCallInvocation) { checkState(callOrNew.isCall() || callOrNew.isNew()); return validateParameterList(compiler, callOrNew, functionType, isCallInvocation) && validateThis(callOrNew, functionType, isCallInvocation); } private static boolean validateThis( Node callOrNew, FunctionType functionType, boolean isCallInvocation) { if (callOrNew.isNew()) { return true; } JSType thisType = functionType.getTypeOfThis(); if (thisType == null || thisType.isUnknownType()) { return true; } Node thisNode = isCallInvocation ? callOrNew.getSecondChild() : callOrNew.getFirstFirstChild(); JSType thisNodeType = thisNode.getJSType().restrictByNotNullOrUndefined(); return thisNodeType.isSubtypeOf(thisType); } private static boolean validateParameterList( AbstractCompiler compiler, Node callOrNew, FunctionType functionType, boolean isCallInvocation) { Node arg = callOrNew.getSecondChild(); // skip the function name if (isCallInvocation && arg != null) { arg = arg.getNext(); } // Get all the annotated types of the argument nodes ImmutableList.Builder argumentTypes = ImmutableList.builder(); for (; arg != null; arg = arg.getNext()) { JSType argType = arg.getJSType(); if (argType == null) { argType = compiler.getTypeRegistry().getNativeType(JSTypeNative.UNKNOWN_TYPE); } argumentTypes.add(argType); } return functionType.acceptsArguments(argumentTypes.build()); } /** Extracts the method name from a provided name. */ private static @Nullable String getPropertyFromDeclarationName(String specName) { String[] parts = specName.split("\\.prototype\\."); checkState(parts.length == 1 || parts.length == 2); if (parts.length == 2) { return parts[1]; } return null; } /** Extracts the class name from a provided name. */ private static @Nullable String getClassFromDeclarationName(String specName) { String tmp = specName; String[] parts = tmp.split("\\.prototype\\."); checkState(parts.length == 1 || parts.length == 2); if (parts.length == 2) { return parts[0]; } return null; } private static String removeTypeDecl(String specName) throws InvalidRequirementSpec { int index = specName.indexOf(':'); if (index < 1) { throw new InvalidRequirementSpec("value should be in the form NAME:TYPE"); } return specName.substring(0, index); } private static @Nullable String getTypeFromValue(String specName) { int index = specName.indexOf(':'); if (index < 1) { return null; } return specName.substring(index + 1); } private static @Nullable String inferStringValue(@Nullable Scope scope, Node node) { if (node == null) { return null; } switch (node.getToken()) { case STRING_KEY: case STRINGLIT: case TEMPLATELIT: case TEMPLATELIT_STRING: return NodeUtil.getStringValue(node); case NAME: if (scope == null) { return null; } String name = node.getString(); Var var = scope.getVar(name); if (var == null || !var.isConst()) { return null; } Node initialValue = var.getInitialValue(); return inferStringValue(var.getScope(), initialValue); case GETPROP: JSType type = node.getJSType(); if (type == null || !type.isEnumElementType()) { // For simplicity, only support enums. The JS style guide requires enums to be // effectively immutable and all enum items should be statically known. // See go/js-style#features-objects-enums. return null; } Node enumSource = type.toMaybeEnumElementType().getEnumType().getSource(); if (enumSource == null) { return null; } if (!enumSource.isObjectLit() && !enumSource.isClassMembers()) { return null; } return inferStringValue( null, NodeUtil.getFirstPropMatchingKey(enumSource, node.getString())); default: return null; } } private static boolean isXid(JSType type) { if (type == null) { return false; } EnumElementType enumElTy = type.toMaybeEnumElementType(); if (enumElTy != null && enumElTy.getEnumType().getReferenceName().equals("enum{xid.String}")) { return true; } return false; } private static boolean isEventHandlerAttrName(String attr) { return !attr.equals("on") && attr.startsWith("on"); } } /** Restricted name call rule */ static class RestrictedNameCall extends AbstractRule { private static class Restriction { final Node name; final FunctionType restrictedCallType; Restriction(Node name, FunctionType restrictedCallType) { this.name = name; this.restrictedCallType = restrictedCallType; } } private final ImmutableList restrictions; RestrictedNameCall(AbstractCompiler compiler, Requirement requirement) throws InvalidRequirementSpec { super(compiler, requirement); if (requirement.getValueCount() == 0) { throw new InvalidRequirementSpec("missing value"); } ImmutableList.Builder builder = ImmutableList.builder(); for (String value : requirement.getValueList()) { Node name = NodeUtil.newQName(compiler, getNameFromValue(value)); String restrictedDecl = ConformanceUtil.getTypeFromValue(value); if (name == null || restrictedDecl == null) { throw new InvalidRequirementSpec("bad prop value"); } FunctionType restrictedCallType = ConformanceUtil.evaluateTypeString(compiler, restrictedDecl).toMaybeFunctionType(); if (restrictedCallType == null) { throw new InvalidRequirementSpec("invalid conformance type"); } builder.add(new Restriction(name, restrictedCallType)); } restrictions = builder.build(); } @Override protected ConformanceResult checkConformance(NodeTraversal t, Node n) { if (ConformanceUtil.isCallTarget(n) && n.isQualifiedName()) { // TODO(johnlenz): restrict to global names for (int i = 0; i < restrictions.size(); i++) { Restriction r = restrictions.get(i); if (n.matchesQualifiedName(r.name)) { if (!ConformanceUtil.validateCall( compiler, n.getParent(), r.restrictedCallType, false)) { return ConformanceResult.VIOLATION; } } else if (n.isGetProp() && n.getString().equals("call") && n.getFirstChild().matchesQualifiedName(r.name)) { if (!ConformanceUtil.validateCall( compiler, n.getParent(), r.restrictedCallType, true)) { return ConformanceResult.VIOLATION; } } } } return ConformanceResult.CONFORMANCE; } private static @Nullable String getNameFromValue(String specName) { int index = specName.indexOf(':'); if (index < 1) { return null; } return specName.substring(0, index); } } /** Banned property call rule */ static class RestrictedMethodCall extends AbstractRule { private static class Restriction { final JSType type; final String property; final FunctionType restrictedCallType; Restriction(JSType type, String property, FunctionType restrictedCallType) { this.type = type; this.property = property; this.restrictedCallType = restrictedCallType; } } private final ImmutableList restrictions; RestrictedMethodCall(AbstractCompiler compiler, Requirement requirement) throws InvalidRequirementSpec { super(compiler, requirement); if (requirement.getValueCount() == 0) { throw new InvalidRequirementSpec("missing value"); } JSTypeRegistry registry = compiler.getTypeRegistry(); ImmutableList.Builder builder = ImmutableList.builder(); for (String value : requirement.getValueList()) { String type = ConformanceUtil.getClassFromDeclarationName(ConformanceUtil.removeTypeDecl(value)); String property = ConformanceUtil.getPropertyFromDeclarationName(ConformanceUtil.removeTypeDecl(value)); String restrictedDecl = ConformanceUtil.getTypeFromValue(value); if (type == null || property == null || restrictedDecl == null) { throw new InvalidRequirementSpec("bad prop value"); } FunctionType restrictedCallType = ConformanceUtil.evaluateTypeString(compiler, restrictedDecl).toMaybeFunctionType(); if (restrictedCallType == null) { throw new InvalidRequirementSpec("invalid conformance type"); } builder.add(new Restriction(registry.getGlobalType(type), property, restrictedCallType)); } restrictions = builder.build(); } @Override protected ConformanceResult checkConformance(NodeTraversal t, Node n) { if (!n.isGetProp() || !ConformanceUtil.isCallTarget(n)) { return ConformanceResult.CONFORMANCE; } for (int i = 0; i < restrictions.size(); i++) { Restriction r = restrictions.get(i); ConformanceResult result = ConformanceResult.CONFORMANCE; if (matchesProp(n, r)) { result = checkConformance(t, n, r, false); } else if (n.getString().equals("call") && matchesProp(n.getFirstChild(), r)) { // handle .call invocation result = checkConformance(t, n, r, true); } // TODO(johnlenz): should "apply" always be a possible violation? if (result.level != ConformanceLevel.CONFORMANCE) { return result; } } return ConformanceResult.CONFORMANCE; } private ConformanceResult checkConformance( NodeTraversal t, Node n, Restriction r, boolean isCallInvocation) { JSTypeRegistry registry = t.getCompiler().getTypeRegistry(); JSType methodClassType = r.type; Node lhs = isCallInvocation ? n.getFirstFirstChild() : n.getFirstChild(); if (methodClassType != null && lhs.getJSType() != null) { JSType targetType = lhs.getJSType().restrictByNotNullOrUndefined(); if (ConformanceUtil.isLooseType(targetType) || targetType.equals(registry.getNativeType(JSTypeNative.OBJECT_TYPE))) { if (reportLooseTypeViolations && !ConformanceUtil.validateCall( compiler, n.getParent(), r.restrictedCallType, isCallInvocation)) { return ConformanceResult.POSSIBLE_VIOLATION_DUE_TO_LOOSE_TYPES; } } else if (targetType.isSubtypeOf(methodClassType)) { if (!ConformanceUtil.validateCall( compiler, n.getParent(), r.restrictedCallType, isCallInvocation)) { return ConformanceResult.VIOLATION; } } } return ConformanceResult.CONFORMANCE; } private boolean matchesProp(Node n, Restriction r) { return n.isGetProp() && n.getString().equals(r.property); } } /** Restricted property write. */ static class RestrictedPropertyWrite extends AbstractRule { private static class Restriction { final JSType type; final String property; final JSType restrictedType; Restriction(JSType type, String property, JSType restrictedType) { this.type = type; this.property = property; this.restrictedType = restrictedType; } } private final ImmutableList restrictions; RestrictedPropertyWrite(AbstractCompiler compiler, Requirement requirement) throws InvalidRequirementSpec { super(compiler, requirement); if (requirement.getValueCount() == 0) { throw new InvalidRequirementSpec("missing value"); } JSTypeRegistry registry = compiler.getTypeRegistry(); ImmutableList.Builder builder = ImmutableList.builder(); for (String value : requirement.getValueList()) { String type = ConformanceUtil.getClassFromDeclarationName(ConformanceUtil.removeTypeDecl(value)); String property = ConformanceUtil.getPropertyFromDeclarationName(ConformanceUtil.removeTypeDecl(value)); String restrictedDecl = ConformanceUtil.getTypeFromValue(value); if (type == null || property == null || restrictedDecl == null) { throw new InvalidRequirementSpec("bad prop value"); } JSType restrictedType = ConformanceUtil.evaluateTypeString(compiler, restrictedDecl); builder.add(new Restriction(registry.getGlobalType(type), property, restrictedType)); } restrictions = builder.build(); } @Override protected ConformanceResult checkConformance(NodeTraversal t, Node n) { if (n.isGetProp() && NodeUtil.isLhsOfAssign(n)) { JSType rhsType = n.getNext().getJSType(); JSType targetType = n.getFirstChild().getJSType(); if (rhsType != null && targetType != null) { JSType targetNotNullType = null; for (Restriction r : restrictions) { if (n.getString().equals(r.property)) { if (!rhsType.isSubtypeOf(r.restrictedType)) { if (ConformanceUtil.isLooseType(targetType)) { if (reportLooseTypeViolations) { return ConformanceResult.POSSIBLE_VIOLATION_DUE_TO_LOOSE_TYPES; } } else { if (targetNotNullType == null) { targetNotNullType = targetType.restrictByNotNullOrUndefined(); } if (targetNotNullType.isSubtypeOf(r.type)) { return ConformanceResult.VIOLATION; } } } } } } } return ConformanceResult.CONFORMANCE; } } /** Banned Code Pattern rule */ static class BannedCodePattern extends AbstractRule { private final ImmutableList restrictions; BannedCodePattern(AbstractCompiler compiler, Requirement requirement) throws InvalidRequirementSpec { super(compiler, requirement); if (requirement.getValueCount() == 0) { throw new InvalidRequirementSpec("missing value"); } ImmutableList.Builder builder = ImmutableList.builder(); for (String value : requirement.getValueList()) { Node parseRoot = new JsAst(SourceFile.fromCode("