/*
* Copyright 2015 Google Inc. All Rights Reserved.
*
* 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.errorprone;
import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableSet;
import com.google.errorprone.BugPattern.SeverityLevel;
import com.google.errorprone.BugPattern.Suppressibility;
import com.google.errorprone.bugpatterns.BugChecker;
import com.google.errorprone.matchers.Description;
import com.sun.source.tree.Tree;
import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import javax.annotation.CheckReturnValue;
/**
* An accessor for information about a single bug checker, including the metadata in the check's
* {@code @BugPattern} annotation and the class that implements the check.
*/
public class BugCheckerInfo implements Serializable {
/**
* The BugChecker class.
*/
private final Class<? extends BugChecker> checker;
/**
* The canonical name of this check. Corresponds to the {@code name} attribute from its
* {@code BugPattern} annotation.
*/
private final String canonicalName;
/**
* Additional identifiers for this check, to be checked for in @SuppressWarnings annotations.
* Corresponds to the canonical name plus all {@code altName}s from its {@code BugPattern}
* annotation.
*/
private final ImmutableSet<String> allNames;
/**
* The error message to print in compiler diagnostics when this check triggers. Corresponds to the
* {@code summary} attribute from its {@code BugPattern}.
*/
private final String message;
/**
* The default type of diagnostic (error or warning) to emit when this check triggers.
*/
private final SeverityLevel defaultSeverity;
/**
* The link URL to display in the diagnostic message when this check triggers. Computed from the
* {@code link} and {@code linkType} attributes from its {@code BugPattern}. May be null if no
* link should be displayed.
*/
private final String linkUrl;
/**
* Whether this check may be suppressed. Corresponds to the {@code suppressibility} attribute from
* its {@code BugPattern}.
*/
private final Suppressibility suppressibility;
/**
* A set of custom suppression annotations for this check. Computed from the
* {@code suppressibility} and {@code customSuppressionAnnotations} attributes from its {@code
* BugPattern}. May be empty if there are no custom suppression annotations for this check.
*/
private final Set<Class<? extends Annotation>> customSuppressionAnnotations;
public static BugCheckerInfo create(Class<? extends BugChecker> checker) {
BugPattern pattern =
checkNotNull(
checker.getAnnotation(BugPattern.class),
"BugCheckers must be annotated with @BugPattern");
checkArgument(
!(Modifier.isAbstract(checker.getModifiers())
|| Modifier.isInterface(checker.getModifiers())),
"%s must be a concrete class",
checker);
try {
BugPatternValidator.validate(pattern);
} catch (ValidationException e) {
throw new IllegalStateException(e);
}
return new BugCheckerInfo(checker, pattern);
}
private BugCheckerInfo(Class<? extends BugChecker> checker, BugPattern pattern) {
this.checker = checker;
canonicalName = pattern.name();
allNames = ImmutableSet.<String>builder().add(canonicalName).add(pattern.altNames()).build();
message = pattern.summary();
defaultSeverity = pattern.severity();
linkUrl = createLinkUrl(pattern);
suppressibility = pattern.suppressibility();
if (suppressibility == Suppressibility.CUSTOM_ANNOTATION) {
customSuppressionAnnotations =
new HashSet<>(Arrays.asList(pattern.customSuppressionAnnotations()));
} else {
customSuppressionAnnotations = Collections.<Class<? extends Annotation>>emptySet();
}
}
private BugCheckerInfo(
Class<? extends BugChecker> checker,
String canonicalName,
ImmutableSet<String> allNames,
String message,
SeverityLevel defaultSeverity,
String linkUrl,
Suppressibility suppressibility,
Set<Class<? extends Annotation>> customSuppressionAnnotations) {
this.checker = checker;
this.canonicalName = canonicalName;
this.allNames = allNames;
this.message = message;
this.defaultSeverity = defaultSeverity;
this.linkUrl = linkUrl;
this.suppressibility = suppressibility;
this.customSuppressionAnnotations = customSuppressionAnnotations;
}
/**
* @return a BugCheckerInfo with the same information as this class, except that its default
* severity is the passed in paramter. If this checker's current defaultSeverity is the same
* as the argument, return this.
*/
public BugCheckerInfo withCustomDefaultSeverity(SeverityLevel defaultSeverity) {
if (defaultSeverity == this.defaultSeverity) {
return this;
}
return new BugCheckerInfo(
checker,
canonicalName,
allNames,
message,
defaultSeverity,
linkUrl,
suppressibility,
customSuppressionAnnotations);
}
private static final String URL_FORMAT = "http://errorprone.info/bugpattern/%s";
private static String createLinkUrl(BugPattern pattern) {
switch (pattern.linkType()) {
case AUTOGENERATED:
return String.format(URL_FORMAT, pattern.name());
case CUSTOM:
// annotation.link() must be provided.
if (pattern.link().isEmpty()) {
throw new IllegalStateException(
"If linkType element of @BugPattern is CUSTOM, "
+ "a link element must also be provided.");
}
return pattern.link();
case NONE:
return null;
default:
throw new IllegalStateException(
"Unexpected value for linkType element of @BugPattern: " + pattern.linkType());
}
}
/**
* Returns a new builder for {@link Description}s.
*
* @param node the node where the error is
* @param checker the {@code BugChecker} instance that is producing this {@code Description}
*/
@CheckReturnValue
public static Description.Builder buildDescriptionFromChecker(Tree node, BugChecker checker) {
return Description.builder(
Preconditions.checkNotNull(node),
checker.canonicalName(),
checker.linkUrl(),
checker.defaultSeverity(),
checker.message());
}
public String canonicalName() {
return canonicalName;
}
public Set<String> allNames() {
return allNames;
}
public String message() {
return message;
}
public SeverityLevel defaultSeverity() {
return defaultSeverity;
}
public SeverityLevel severity(Map<String, SeverityLevel> severities) {
return firstNonNull(severities.get(canonicalName), defaultSeverity);
}
public String linkUrl() {
return linkUrl;
}
public Suppressibility suppressibility() {
return suppressibility;
}
public Set<Class<? extends Annotation>> customSuppressionAnnotations() {
return customSuppressionAnnotations;
}
public Class<? extends BugChecker> checkerClass() {
return checker;
}
@Override
public int hashCode() {
return checker.hashCode();
}
@Override
public boolean equals(Object o) {
if (!(o instanceof BugCheckerInfo)) {
return false;
}
return checker.equals(((BugCheckerInfo) o).checker);
}
@Override
public String toString() {
return canonicalName;
}
}