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

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

/*
 * 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.checkNotNull;
import static com.google.common.base.Preconditions.checkState;

import com.google.common.annotations.GwtIncompatible;
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.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.Rule;
import com.google.javascript.jscomp.CodingConvention.AssertionFunctionLookup;
import com.google.javascript.jscomp.Requirement.Severity;
import com.google.javascript.jscomp.Requirement.Type;
import com.google.javascript.jscomp.parsing.JsDocInfoParser;
import com.google.javascript.rhino.JSDocInfo;
import com.google.javascript.rhino.JSDocInfo.Visibility;
import com.google.javascript.rhino.JSTypeExpression;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.Token;
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.Objects;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import javax.annotation.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,
  }

  @Nullable
  private static 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 {
    @Nullable final ImmutableList prefixes;
    @Nullable final Pattern regexp;
    @Nullable final Requirement.WhitelistEntry allowlistEntry;

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

    AllowList(Requirement.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 (prefixes != null) {
        for (String prefix : prefixes) {
          if (!path.isEmpty() && path.startsWith(prefix)) {
            return true;
          }
        }
      }

      return regexp != null && regexp.matcher(path).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;
    @Nullable final 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 (Requirement.WhitelistEntry entry : requirement.getWhitelistEntryList()) {
        allowlistsBuilder.add(new AllowList(entry));
      }
      for (Requirement.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. */
    @Nullable
    private 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 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 { @Nullable private final 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 class BannedName extends AbstractRule { private final Requirement.Type requirementType; private final ImmutableList names; BannedName(AbstractCompiler compiler, Requirement requirement) throws InvalidRequirementSpec { super(compiler, requirement); if (requirement.getValueCount() == 0) { throw new InvalidRequirementSpec("missing value"); } requirementType = requirement.getType(); ImmutableList.Builder builder = ImmutableList.builder(); for (String name : requirement.getValueList()) { builder.add(NodeUtil.newQName(compiler, name)); } names = builder.build(); } @Override protected ConformanceResult checkConformance(NodeTraversal t, Node n) { if (isCandidateNode(n)) { if (requirementType == Type.BANNED_NAME_CALL) { if (!ConformanceUtil.isCallTarget(n)) { return ConformanceResult.CONFORMANCE; } } for (int i = 0; i < names.size(); i++) { Node nameNode = names.get(i); if (n.matchesQualifiedName(nameNode) && isRootOfQualifiedNameGlobal(t, n)) { if (NodeUtil.isInSyntheticScript(n)) { return ConformanceResult.CONFORMANCE; } else { return ConformanceResult.VIOLATION; } } } } return ConformanceResult.CONFORMANCE; } private boolean isCandidateNode(Node n) { switch (n.getToken()) { case GETPROP: return n.getFirstChild().isQualifiedName(); case NAME: return !n.getString().isEmpty(); default: return false; } } private static boolean isRootOfQualifiedNameGlobal(NodeTraversal t, Node n) { String rootName = NodeUtil.getRootOfQualifiedName(n).getQualifiedName(); Var v = t.getScope().getVar(rootName); return v != null && v.isGlobal(); } } /** Banned property rule */ static class BannedProperty extends AbstractRule { private static class Property { final JSType type; final String property; Property(JSType type, String property) { this.type = checkNotNull(type); this.property = checkNotNull(property); } } private final JSTypeRegistry registry; private final ImmutableList props; private final Requirement.Type requirementType; 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: case BANNED_PROPERTY_READ: case BANNED_PROPERTY_WRITE: case BANNED_PROPERTY_NON_CONSTANT_WRITE: case BANNED_PROPERTY_CALL: break; default: throw new AssertionError(requirement.getType()); } this.requirementType = requirement.getType(); this.registry = compiler.getTypeRegistry(); ImmutableList.Builder builder = ImmutableList.builder(); List values = requirement.getValueList(); for (String value : values) { String typename = ConformanceUtil.getClassFromDeclarationName(value); String property = ConformanceUtil.getPropertyFromDeclarationName(value); if (typename == null || property == null) { throw new InvalidRequirementSpec("bad prop value"); } // Type doesn't exist in the copmilation, so it can't be a violation. JSType type = registry.getGlobalType(typename); if (type == null) { continue; } builder.add(new Property(type, property)); } this.props = builder.build(); } @Override protected ConformanceResult checkConformance(NodeTraversal t, Node n) { if (!this.isCandidatePropAccess(n)) { return ConformanceResult.CONFORMANCE; } Property srcProp = this.createSrcProperty(n); if (srcProp == null) { return ConformanceResult.CONFORMANCE; } for (Property checkProp : this.props) { ConformanceResult result = this.matchProps(srcProp, checkProp); if (result.level != ConformanceLevel.CONFORMANCE) { return result; } } return ConformanceResult.CONFORMANCE; } private ConformanceResult matchProps(Property srcProp, Property checkProp) { if (!Objects.equals(srcProp.property, checkProp.property)) { return ConformanceResult.CONFORMANCE; } JSType foundType = srcProp.type.restrictByNotNullOrUndefined(); 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(checkProp.type)) { return ConformanceResult.VIOLATION; } else if (checkProp.type.isSubtypeWithoutStructuralTyping(foundType)) { if (matchesPrototype(checkProp.type, 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; } /** * Determines if {@code n} is a potentially banned use of {@code prop}. * *

Specifically if the conformance requirement under consideration only bans assignment to * the property, {@code n} is only a candidate if it is an l-value. */ private boolean isCandidatePropAccess(Node propAccess) { switch (this.requirementType) { case BANNED_PROPERTY_WRITE: return NodeUtil.isLValue(propAccess); case BANNED_PROPERTY_NON_CONSTANT_WRITE: if (!NodeUtil.isLValue(propAccess)) { return false; } if (NodeUtil.isLhsOfAssign(propAccess) && (NodeUtil.isLiteralValue(propAccess.getNext(), false /* includeFunctions */) || NodeUtil.isSomeCompileTimeConstStringValue(propAccess.getNext()))) { return false; } return true; case BANNED_PROPERTY_READ: return !NodeUtil.isLValue(propAccess) && NodeUtil.isExpressionResultUsed(propAccess); case BANNED_PROPERTY_CALL: return ConformanceUtil.isCallTarget(propAccess); default: return true; } } private Property createSrcProperty(Node n) { final JSType receiverType; switch (n.getToken()) { case GETELEM: case GETPROP: receiverType = n.getFirstChild().getJSType(); break; case STRING_KEY: case COMPUTED_PROP: { Node parent = n.getParent(); switch (parent.getToken()) { case OBJECT_PATTERN: receiverType = parent.getJSType(); break; case OBJECTLIT: case CLASS_MEMBERS: receiverType = null; break; default: throw new AssertionError(); } } break; default: receiverType = null; break; } final String name; switch (n.getToken()) { case STRING_KEY: name = n.getString(); break; case GETPROP: name = Node.getGetpropString(n); break; case GETELEM: { Node string = n.getSecondChild(); name = string.isString() ? string.getString() : null; } break; case COMPUTED_PROP: { Node string = n.getFirstChild(); name = string.isString() ? string.getString() : null; } break; default: name = null; break; } return (receiverType == null || name == null) ? null : new Property(receiverType, name); } } 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.isUnresolved() || 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 String getPropertyFromDeclarationName(String specName) throws InvalidRequirementSpec { 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 String getClassFromDeclarationName(String specName) throws InvalidRequirementSpec { 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 String getTypeFromValue(String specName) { int index = specName.indexOf(':'); if (index < 1) { return null; } return specName.substring(index + 1); } } /** 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() && Node.getGetpropString(n).equals("call") && n.getFirstChild().matchesQualifiedName(r.name)) { if (!ConformanceUtil.validateCall( compiler, n.getParent(), r.restrictedCallType, true)) { return ConformanceResult.VIOLATION; } } } } return ConformanceResult.CONFORMANCE; } private static 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 (Node.getGetpropString(n).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() && Node.getGetpropString(n).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.intern(); 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 @SuppressWarnings("ReferenceEquality") // take advantage of string interning. 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 (Node.getGetpropString(n) == r.property) { // Both strings are interned. 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("