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.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.Iterator;
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));
      }

      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);
      }
      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: 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) { Iterator arguments = callOrNew.children().iterator(); arguments.next(); // skip the function name if (isCallInvocation && arguments.hasNext()) { arguments.next(); } // Get all the annotated types of the argument nodes ImmutableList.Builder argumentTypes = ImmutableList.builder(); while (arguments.hasNext()) { JSType argType = arguments.next().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() && n.getLastChild().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 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 (NodeUtil.isNormalGet(n) && ConformanceUtil.isCallTarget(n) && n.getLastChild().isString()) { 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.getLastChild().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.getLastChild().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.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 (n.getLastChild().getString() == 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("