com.google.javascript.jscomp.CheckConformance Maven / Gradle / Ivy
Show all versions of closure-compiler Show documentation
/*
* 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.collect.ImmutableList.toImmutableList;
import com.google.common.annotations.GwtIncompatible;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.javascript.rhino.Node;
import com.google.protobuf.Descriptors;
import com.google.protobuf.TextFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.jspecify.nullness.Nullable;
/**
* Provides a framework for checking code against a set of user configured conformance rules. The
* rules are specified by the ConformanceConfig proto, which allows for both standard checks
* (forbidden properties, variables, or dependencies) and allow for more complex checks using custom
* rules than specify
*
* Conformance violations are both reported as compiler errors, and are also reported separately
* to the {cI gue@link ErrorManager}
*/
@GwtIncompatible("com.google.protobuf")
public final class CheckConformance implements NodeTraversal.Callback, CompilerPass {
static final DiagnosticType CONFORMANCE_ERROR =
DiagnosticType.error("JSC_CONFORMANCE_ERROR", "Violation: {0}{1}{2}");
static final DiagnosticType CONFORMANCE_VIOLATION =
DiagnosticType.warning("JSC_CONFORMANCE_VIOLATION", "Violation: {0}{1}{2}");
static final DiagnosticType CONFORMANCE_POSSIBLE_VIOLATION =
DiagnosticType.warning("JSC_CONFORMANCE_POSSIBLE_VIOLATION", "Possible violation: {0}{1}{2}");
static final DiagnosticType INVALID_REQUIREMENT_SPEC =
DiagnosticType.error(
"JSC_INVALID_REQUIREMENT_SPEC",
"Invalid requirement. Reason: {0}\nRequirement spec:\n{1}");
private final AbstractCompiler compiler;
private final ImmutableList categories;
public static interface Rule {
/**
* Return a precondition for this rule.
*
* This method will only be called once (per rule) during the creation of the
* CheckConformance pass. Therefore, the return must be constant.
*
*
Returning null means that there is no precondition. This is convenient, but can be a major
* performance hit.
*/
default @Nullable Precondition getPrecondition() {
return Precondition.CHECK_ALL;
}
/** Perform conformance check */
void check(NodeTraversal t, Node n);
}
/**
* A condition that must be true for a rule to possibly match a node.
*
*
Instances are used as keys to group rules with common preconditions. Grouping allows shared
* computation to be done only once per node, which is a substantial performance improvement.
*/
public static interface Precondition {
boolean shouldCheck(Node n);
public static final Precondition CHECK_ALL =
new Precondition() {
@Override
public boolean shouldCheck(Node n) {
return true;
}
};
}
private static final class Category {
final Precondition precondition;
final ImmutableList rules;
Category(Precondition precondition, ImmutableList rules) {
this.precondition = precondition;
this.rules = rules;
}
}
/** @param configs The rules to check. */
CheckConformance(AbstractCompiler compiler, ImmutableList configs) {
this.compiler = compiler;
// Initialize the map of functions to inspect for renaming candidates.
this.categories = initRules(compiler, configs);
}
@Override
public void process(Node externs, Node root) {
if (!this.categories.isEmpty()) {
NodeTraversal.traverseRoots(compiler, this, externs, root);
}
}
@Override
public final boolean shouldTraverse(NodeTraversal t, Node n, Node parent) {
// Don't inspect extern files
return !n.isScript() || isScriptOfInterest(t.getInput().getSourceFile());
}
private boolean isScriptOfInterest(SourceFile sf) {
return !sf.isWeak() && !sf.isExtern();
}
@Override
public void visit(NodeTraversal t, Node n, Node parent) {
/**
* Use counted loops and backward iteration for performance.
*
* These loops are run a huge number of times. The overhead of enhanced-for loops and even
* calling size() can add seconds of build time to large projects.
*/
for (int c = this.categories.size() - 1; c >= 0; c--) {
Category category = this.categories.get(c);
if (category.precondition.shouldCheck(n)) {
for (int r = category.rules.size() - 1; r >= 0; r--) {
category.rules.get(r).check(t, n);
}
}
}
}
/** Build the data structures need by this pass from the provided configurations. */
private static ImmutableList initRules(
AbstractCompiler compiler, ImmutableList configs) {
HashMultimap builder = HashMultimap.create();
List requirements = mergeRequirements(compiler, configs);
for (Requirement requirement : requirements) {
Rule rule = initRule(compiler, requirement);
if (rule != null) {
builder.put(rule.getPrecondition(), rule);
}
}
return builder.asMap().entrySet().stream()
.map((e) -> new Category(e.getKey(), ImmutableList.copyOf(e.getValue())))
.collect(toImmutableList());
}
private static final ImmutableSet EXTENDABLE_FIELDS =
ImmutableSet.of(
"config_file",
"extends",
"only_apply_to",
"only_apply_to_regexp",
"whitelist",
"whitelist_regexp",
"allowlist",
"allowlist_regexp",
"value");
/**
* Gets requirements from all configs. Merges allowlists/whitelists of requirements with 'extends'
* equal to 'rule_id' of other rule.
*/
static List mergeRequirements(
AbstractCompiler compiler, List configs) {
List builders = new ArrayList<>();
Map extendable = new HashMap<>();
for (ConformanceConfig config : configs) {
for (Requirement requirement : config.getRequirementList()) {
Requirement.Builder builder = requirement.toBuilder();
if (requirement.hasRuleId()) {
if (requirement.getRuleId().isEmpty()) {
reportInvalidRequirement(compiler, requirement, "empty rule_id");
continue;
}
if (extendable.containsKey(requirement.getRuleId())) {
reportInvalidRequirement(
compiler,
requirement,
"two requirements with the same rule_id: " + requirement.getRuleId());
continue;
}
extendable.put(requirement.getRuleId(), builder);
}
if (!requirement.hasExtends()) {
builders.add(builder);
}
}
}
for (ConformanceConfig config : configs) {
for (Requirement requirement : config.getRequirementList()) {
if (requirement.hasExtends()) {
Requirement.Builder existing = extendable.get(requirement.getExtends());
if (existing == null) {
reportInvalidRequirement(
compiler, requirement, "no requirement with rule_id: " + requirement.getExtends());
continue;
}
for (Descriptors.FieldDescriptor field : requirement.getAllFields().keySet()) {
if (!EXTENDABLE_FIELDS.contains(field.getName())) {
reportInvalidRequirement(
compiler, requirement, "extending rules allow only " + EXTENDABLE_FIELDS);
}
}
if (requirement.getValueCount() > 0 && !existing.getAllowExtendingValue()) {
reportInvalidRequirement(
compiler,
requirement,
"extending rule may not specify 'value' if base rule does not allow it");
}
existing
.addAllWhitelist(requirement.getWhitelistList())
.addAllWhitelistRegexp(requirement.getWhitelistRegexpList())
.addAllAllowlist(requirement.getAllowlistList())
.addAllAllowlistRegexp(requirement.getAllowlistRegexpList())
.addAllOnlyApplyTo(requirement.getOnlyApplyToList())
.addAllOnlyApplyToRegexp(requirement.getOnlyApplyToRegexpList())
.addAllWhitelistEntry(requirement.getWhitelistEntryList())
.addAllAllowlistEntry(requirement.getAllowlistEntryList())
.addAllValue(requirement.getValueList())
.addAllConfigFile(requirement.getConfigFileList());
}
}
}
List requirements = new ArrayList<>(builders.size());
for (Requirement.Builder builder : builders) {
removeDuplicates(builder);
requirements.add(builder.build());
}
return requirements;
}
private static void removeDuplicates(Requirement.Builder requirement) {
final ImmutableSet list1 = ImmutableSet.copyOf(requirement.getWhitelistList());
requirement.clearWhitelist().addAllWhitelist(list1);
final ImmutableSet allowlist = ImmutableSet.copyOf(requirement.getAllowlistList());
requirement.clearAllowlist().addAllAllowlist(allowlist);
final ImmutableSet list2 = ImmutableSet.copyOf(requirement.getWhitelistRegexpList());
requirement.clearWhitelistRegexp().addAllWhitelistRegexp(list2);
final ImmutableSet allowlistRegexp =
ImmutableSet.copyOf(requirement.getAllowlistRegexpList());
requirement.clearAllowlistRegexp().addAllAllowlistRegexp(allowlistRegexp);
final ImmutableSet list3 = ImmutableSet.copyOf(requirement.getOnlyApplyToList());
requirement.clearOnlyApplyTo().addAllOnlyApplyTo(list3);
final ImmutableSet list4 = ImmutableSet.copyOf(requirement.getOnlyApplyToRegexpList());
requirement.clearOnlyApplyToRegexp().addAllOnlyApplyToRegexp(list4);
}
private static @Nullable Rule initRule(AbstractCompiler compiler, Requirement requirement) {
try {
switch (requirement.getType()) {
case CUSTOM:
return new ConformanceRules.CustomRuleProxy(compiler, requirement);
case NO_OP:
return new ConformanceRules.NoOp(compiler, requirement);
case BANNED_CODE_PATTERN:
return new ConformanceRules.BannedCodePattern(compiler, requirement);
case BANNED_DEPENDENCY:
return new ConformanceRules.BannedDependency(compiler, requirement);
case BANNED_DEPENDENCY_REGEX:
return new ConformanceRules.BannedDependencyRegex(compiler, requirement);
case BANNED_ENHANCE:
return new ConformanceRules.BannedEnhance(compiler, requirement);
case BANNED_NAME:
case BANNED_NAME_CALL:
return new ConformanceRules.BannedName(compiler, requirement);
case BANNED_PROPERTY:
case BANNED_PROPERTY_READ:
case BANNED_PROPERTY_WRITE:
case BANNED_PROPERTY_NON_CONSTANT_WRITE:
case BANNED_PROPERTY_CALL:
return new ConformanceRules.BannedProperty(compiler, requirement);
case RESTRICTED_NAME_CALL:
return new ConformanceRules.RestrictedNameCall(compiler, requirement);
case RESTRICTED_METHOD_CALL:
return new ConformanceRules.RestrictedMethodCall(compiler, requirement);
case RESTRICTED_PROPERTY_WRITE:
return new ConformanceRules.RestrictedPropertyWrite(compiler, requirement);
case BANNED_STRING_REGEX:
return new ConformanceRules.BannedStringRegex(compiler, requirement);
}
throw new AssertionError();
} catch (InvalidRequirementSpec e) {
reportInvalidRequirement(compiler, requirement, e.getMessage());
return null;
}
}
public static class InvalidRequirementSpec extends Exception {
InvalidRequirementSpec(String message) {
super(message);
}
InvalidRequirementSpec(String message, Throwable cause) {
super(message, cause);
}
}
private static void reportInvalidRequirement(
AbstractCompiler compiler, Requirement requirement, String reason) {
compiler.report(
JSError.make(
INVALID_REQUIREMENT_SPEC, reason, TextFormat.printer().printToString(requirement)));
}
}