Support in/exclusions for LexicographicalAnnotationAttributeListingCheck

While there:
- Also enable more compiler checks.
- Add and apply the Google Java formatter. (Yeah, this should have been a
  separate commit...
This commit is contained in:
Stephan Schroevers
2017-10-25 23:25:16 +02:00
parent f2b393e72d
commit 151783e63f
11 changed files with 1437 additions and 1209 deletions

View File

@@ -279,6 +279,20 @@
</rules>
</configuration>
</plugin>
<plugin>
<groupId>com.theoryinpractise</groupId>
<artifactId>googleformatter-maven-plugin</artifactId>
<version>1.5.1</version>
<executions>
<execution>
<id>format-sources</id>
<phase>process-sources</phase>
<goals>
<goal>format</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>de.thetaphi</groupId>
<artifactId>forbiddenapis</artifactId>
@@ -665,59 +679,24 @@
<configuration>
<compilerArgs combine.children="append">
<arg>-Werror</arg>
<arg>-Xdoclint:reference</arg>
<arg>-Xlint:all</arg>
<!-- Some generated jOOQ code performs a
redundant cast. Until that is resolved we
suppress this warning. See
https://github.com/jOOQ/jOOQ/issues/6705. -->
<arg>-Xlint:-cast</arg>
<!-- Some of the dependencies we pull in
declare additional non-existent dependencies in
their MANIFEST.MF file. Since we're using Maven
this doesn't impact us. As such the resultant
compiler warnings are irrelevant. -->
<arg>-Xlint:-path</arg>
<!-- The Immutables.org annotation processor
does not handle all annotations present on the
classpath, and javac complains about this. That
doesn't make a lot of sense. From time to time
we should review whether this issue has been
<!-- Not all annotations present on the
classpath are handled by annotation processors,
and javac complains about this. That doesn't
make a lot of sense. From time to time we
should review whether this issue has been
resolved. -->
<arg>-Xlint:-processing</arg>
<!-- The JAXB-generated classes implement
java.io.Serializable, yet do not declare the
serialVersionUID field. Until that's solved, we
cannot enable "serial" warnings. -->
<arg>-Xlint:-serial</arg>
<!-- We want to enable almost all error-prone
bug pattern checkers, so we enable all and then
selectively deactivate some. -->
<arg>-XepAllDisabledChecksAsWarnings</arg>
<!-- The JAXB-generated classes exhibit some
error-prone bug patterns. Nothing we can do
about that, so we simply tell error-prone not
to warn about generated code. -->
<arg>-XepDisableWarningsInGeneratedCode</arg>
<!-- XXX: A whole bunch of JPA entities violate
this pattern. Revisit this once we phased out
Hibernate. See
https://picnic.atlassian.net/browse/PRP-6035. -->
<arg>-Xep:ConstructorInvokesOverridable:OFF</arg>
<!-- See https://github.com/google/error-prone/issues/655. -->
<arg>-Xep:ConstructorLeaksThis:OFF</arg>
<!-- See https://github.com/google/error-prone/issues/708. -->
<arg>-Xep:FieldMissingNullable:OFF</arg>
<!-- Disabled for now, but TBD. -->
<arg>-Xep:MethodCanBeStatic:OFF</arg>
<!-- See https://github.com/google/error-prone/issues/723. -->
<arg>-Xep:ReturnMissingNullable:OFF</arg>
<!-- Deals with an Android-specific limitation
not applicable to us. See also
https://github.com/google/error-prone/issues/488. -->
<arg>-Xep:StaticOrDefaultInterfaceMethod:OFF</arg>
<!-- Interesting idea, but way too much work for now. -->
<arg>-Xep:Var:OFF</arg>
<!-- The following bug patterns by default have
"SUGGESTION" severity. We raise them to the
"WARNING" level. See also

View File

@@ -0,0 +1,103 @@
package com.picnicinternational.errorprone.bugpatterns;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSetMultimap;
import com.google.common.collect.SetMultimap;
import java.io.Serializable;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
// XXX: Redefine using auto-value
// XXX: Document design decision that the project stays as close as possible to Error Prone.
// ^ ... and *therefore* uses Google Auto Value rather than Immutables.org.
/**
* A matcher of (annotation, attribute) pairs.
*
* <p>This class allows one to define a whitelist or blacklist of annotations or their attributes.
* Annotations are identified by their fully qualified name.
*/
final class AnnotationAttributeMatcher implements Serializable {
private static final long serialVersionUID = 1L;
private final boolean complement;
private final ImmutableSet<String> wholeTypes;
private final ImmutableSetMultimap<String, String> includedAttributes;
private final ImmutableSetMultimap<String, String> excludedAttributes;
private AnnotationAttributeMatcher(
boolean complement,
ImmutableSet<String> wholeTypes,
ImmutableSetMultimap<String, String> includedAttributes,
ImmutableSetMultimap<String, String> excludedAttributes) {
this.complement = complement;
this.wholeTypes = wholeTypes;
this.includedAttributes = includedAttributes;
this.excludedAttributes = excludedAttributes;
}
/**
* Creates an {@link AnnotationAttributeMatcher}.
*
* <p>Each string provided to this method must be of the form {@code
* "some.fully.qualified.AnnotationType"} or {@code
* "some.fully.qualified.AnnotationType#attribute"}.
*
* @param inclusions If specified, only the listed annotations or annotation attributes are
* matched.
* @param exclusions If specified, the listed annotations or annotation attributes are not
* matched.
* @return A non-{@code null} {@link AnnotationAttributeMatcher}.
*/
static AnnotationAttributeMatcher create(
Optional<ImmutableList<String>> inclusions, Optional<ImmutableList<String>> exclusions) {
Set<String> includedWholeTypes = new HashSet<>();
Set<String> excludedWholeTypes = new HashSet<>();
SetMultimap<String, String> includedAttributes = HashMultimap.create();
SetMultimap<String, String> excludedAttributes = HashMultimap.create();
inclusions.ifPresent(incl -> update(incl, includedWholeTypes, includedAttributes));
exclusions.ifPresent(excl -> update(excl, excludedWholeTypes, excludedAttributes));
includedWholeTypes.removeAll(excludedWholeTypes);
includedAttributes.keySet().removeAll(includedWholeTypes);
includedAttributes.keySet().removeAll(excludedWholeTypes);
excludedAttributes.forEach(includedAttributes::remove);
excludedAttributes.keySet().removeAll(excludedWholeTypes);
return new AnnotationAttributeMatcher(
!inclusions.isPresent(),
ImmutableSet.copyOf(inclusions.isPresent() ? includedWholeTypes : excludedWholeTypes),
ImmutableSetMultimap.copyOf(includedAttributes),
ImmutableSetMultimap.copyOf(excludedAttributes));
}
private static void update(
ImmutableList<String> enumeration,
Set<String> wholeTypes,
SetMultimap<String, String> attributeRestrictions) {
for (String entry : enumeration) {
int hash = entry.indexOf('#');
if (hash < 0) {
wholeTypes.add(entry);
} else {
String annotationType = entry.substring(0, hash);
String attribute = entry.substring(hash + 1);
attributeRestrictions.put(annotationType, attribute);
}
}
}
boolean matches(String annotationType, String attribute) {
if (this.complement) {
return !this.wholeTypes.contains(annotationType)
&& !this.excludedAttributes.containsEntry(annotationType, attribute);
}
return (this.wholeTypes.contains(annotationType)
&& !this.excludedAttributes.containsEntry(annotationType, attribute))
|| this.includedAttributes.containsEntry(annotationType, attribute);
}
}

View File

@@ -35,134 +35,132 @@ import java.util.function.ToLongFunction;
import java.util.stream.Stream;
import javax.annotation.CheckForNull;
// XXX: Add more documentation. Explain how this is useful in the face of refactoring to more specific types.
// XXX: Add more documentation. Explain how this is useful in the face of refactoring to more
// specific types.
// XXX: Change this checker's name?
@AutoService(BugChecker.class)
@BugPattern(
name = "PrimitiveComparison",
summary =
"Ensure invocations of `Comparator#comparing{,Double,Int,Long}` match the return type"
+ " of the provided function",
linkType = LinkType.NONE,
severity = SeverityLevel.WARNING,
tags = StandardTags.LIKELY_ERROR,
providesFix = ProvidesFix.REQUIRES_HUMAN_ATTENTION
name = "PrimitiveComparison",
summary =
"Ensure invocations of `Comparator#comparing{,Double,Int,Long}` match the return type"
+ " of the provided function",
linkType = LinkType.NONE,
severity = SeverityLevel.WARNING,
tags = StandardTags.LIKELY_ERROR,
providesFix = ProvidesFix.REQUIRES_HUMAN_ATTENTION
)
public class BoxingComparisonCheck extends BugChecker implements MethodInvocationTreeMatcher {
private static final Matcher<ExpressionTree> STATIC_MATCH = getStaticTargetMatcher();
private static final Matcher<ExpressionTree> INSTANCE_MATCH = getInstanceTargetMatcher();
private static final long serialVersionUID = 1L;
private static final Matcher<ExpressionTree> STATIC_MATCH = getStaticTargetMatcher();
private static final Matcher<ExpressionTree> INSTANCE_MATCH = getInstanceTargetMatcher();
@Override
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
boolean isStatic = STATIC_MATCH.matches(tree, state);
if (!isStatic && !INSTANCE_MATCH.matches(tree, state)) {
return Description.NO_MATCH;
}
Type potentiallyBoxedType = getPotentiallyBoxedReturnType(tree.getArguments().get(0));
if (potentiallyBoxedType == null) {
return Description.NO_MATCH;
}
String actualMethodName = ASTHelpers.getSymbol(tree).getSimpleName().toString();
String preferredMethodName = getPreferredMethod(state, potentiallyBoxedType, isStatic);
if (actualMethodName.equals(preferredMethodName)) {
return Description.NO_MATCH;
}
Description.Builder description = buildDescription(tree);
tryFix(description, tree, preferredMethodName);
return description.build();
@Override
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
boolean isStatic = STATIC_MATCH.matches(tree, state);
if (!isStatic && !INSTANCE_MATCH.matches(tree, state)) {
return Description.NO_MATCH;
}
private String getPreferredMethod(VisitorState state, Type cmpType, boolean isStatic) {
Types types = state.getTypes();
Symtab symtab = state.getSymtab();
if (types.isSubtype(cmpType, symtab.intType)) {
return isStatic ? "comparingInt" : "thenComparingInt";
}
if (types.isSubtype(cmpType, symtab.longType)) {
return isStatic ? "comparingLong" : "thenComparingLong";
}
if (types.isSubtype(cmpType, symtab.doubleType)) {
return isStatic ? "comparingDouble" : "thenComparingDouble";
}
return isStatic ? "comparing" : "thenComparing";
Type potentiallyBoxedType = getPotentiallyBoxedReturnType(tree.getArguments().get(0));
if (potentiallyBoxedType == null) {
return Description.NO_MATCH;
}
@CheckForNull
private Type getPotentiallyBoxedReturnType(ExpressionTree tree) {
switch (tree.getKind()) {
case LAMBDA_EXPRESSION:
/* Return the lambda expression's actual return type. */
return ASTHelpers.getType(((LambdaExpressionTree) tree).getBody());
case MEMBER_REFERENCE:
/* Return the method's declared return type. */
// XXX: Very fragile. Do better.
Type subType2 = ((JCMemberReference) tree).referentType;
return ((MethodType) subType2).getReturnType();
default:
/* This appears to be a genuine `{,ToInt,ToLong,ToDouble}Function`. */
return null;
}
String actualMethodName = ASTHelpers.getSymbol(tree).getSimpleName().toString();
String preferredMethodName = getPreferredMethod(state, potentiallyBoxedType, isStatic);
if (actualMethodName.equals(preferredMethodName)) {
return Description.NO_MATCH;
}
private void tryFix(
Description.Builder description,
MethodInvocationTree tree,
String preferredMethodName) {
ExpressionTree expr = tree.getMethodSelect();
switch (expr.getKind()) {
case IDENTIFIER:
SuggestedFix.Builder fix = SuggestedFix.builder();
fix.addStaticImport(
java.util.Comparator.class.getName() + '.' + preferredMethodName);
fix.replace(expr, preferredMethodName);
description.addFix(fix.build());
return;
case MEMBER_SELECT:
MemberSelectTree ms = (MemberSelectTree) tree.getMethodSelect();
description.addFix(
SuggestedFix.replace(ms, ms.getExpression() + "." + preferredMethodName));
return;
default:
throw new VerifyException("Unexpected type of expression: " + expr.getKind());
}
Description.Builder description = buildDescription(tree);
tryFix(description, tree, preferredMethodName);
return description.build();
}
private static String getPreferredMethod(VisitorState state, Type cmpType, boolean isStatic) {
Types types = state.getTypes();
Symtab symtab = state.getSymtab();
if (types.isSubtype(cmpType, symtab.intType)) {
return isStatic ? "comparingInt" : "thenComparingInt";
}
private static Matcher<ExpressionTree> getStaticTargetMatcher() {
MethodClassMatcher clazz = staticMethod().onClass(Comparator.class.getName());
return anyMatch(
unaryMethod(clazz, "comparingInt", ToIntFunction.class),
unaryMethod(clazz, "comparingLong", ToLongFunction.class),
unaryMethod(clazz, "comparingDouble", ToDoubleFunction.class),
unaryMethod(clazz, "comparing", Function.class));
if (types.isSubtype(cmpType, symtab.longType)) {
return isStatic ? "comparingLong" : "thenComparingLong";
}
private static Matcher<ExpressionTree> getInstanceTargetMatcher() {
MethodClassMatcher instance = instanceMethod().onDescendantOf(Comparator.class.getName());
return anyMatch(
unaryMethod(instance, "thenComparingInt", ToIntFunction.class),
unaryMethod(instance, "thenComparingLong", ToLongFunction.class),
unaryMethod(instance, "thenComparingDouble", ToDoubleFunction.class),
unaryMethod(instance, "thenComparing", Function.class));
if (types.isSubtype(cmpType, symtab.doubleType)) {
return isStatic ? "comparingDouble" : "thenComparingDouble";
}
@SafeVarargs
@SuppressWarnings("varargs")
private static Matcher<ExpressionTree> anyMatch(Matcher<ExpressionTree>... matchers) {
return (ExpressionTree t, VisitorState s) ->
Stream.of(matchers).anyMatch(m -> m.matches(t, s));
}
return isStatic ? "comparing" : "thenComparing";
}
private static Matcher<ExpressionTree> unaryMethod(
MethodClassMatcher classMatcher, String name, Class<?> paramType) {
return classMatcher.named(name).withParameters(paramType.getName());
@CheckForNull
private static Type getPotentiallyBoxedReturnType(ExpressionTree tree) {
switch (tree.getKind()) {
case LAMBDA_EXPRESSION:
/* Return the lambda expression's actual return type. */
return ASTHelpers.getType(((LambdaExpressionTree) tree).getBody());
case MEMBER_REFERENCE:
/* Return the method's declared return type. */
// XXX: Very fragile. Do better.
Type subType2 = ((JCMemberReference) tree).referentType;
return ((MethodType) subType2).getReturnType();
default:
/* This appears to be a genuine `{,ToInt,ToLong,ToDouble}Function`. */
return null;
}
}
private static void tryFix(
Description.Builder description, MethodInvocationTree tree, String preferredMethodName) {
ExpressionTree expr = tree.getMethodSelect();
switch (expr.getKind()) {
case IDENTIFIER:
SuggestedFix.Builder fix = SuggestedFix.builder();
fix.addStaticImport(java.util.Comparator.class.getName() + '.' + preferredMethodName);
fix.replace(expr, preferredMethodName);
description.addFix(fix.build());
return;
case MEMBER_SELECT:
MemberSelectTree ms = (MemberSelectTree) tree.getMethodSelect();
description.addFix(
SuggestedFix.replace(ms, ms.getExpression() + "." + preferredMethodName));
return;
default:
throw new VerifyException("Unexpected type of expression: " + expr.getKind());
}
}
private static Matcher<ExpressionTree> getStaticTargetMatcher() {
MethodClassMatcher clazz = staticMethod().onClass(Comparator.class.getName());
return anyMatch(
unaryMethod(clazz, "comparingInt", ToIntFunction.class),
unaryMethod(clazz, "comparingLong", ToLongFunction.class),
unaryMethod(clazz, "comparingDouble", ToDoubleFunction.class),
unaryMethod(clazz, "comparing", Function.class));
}
private static Matcher<ExpressionTree> getInstanceTargetMatcher() {
MethodClassMatcher instance = instanceMethod().onDescendantOf(Comparator.class.getName());
return anyMatch(
unaryMethod(instance, "thenComparingInt", ToIntFunction.class),
unaryMethod(instance, "thenComparingLong", ToLongFunction.class),
unaryMethod(instance, "thenComparingDouble", ToDoubleFunction.class),
unaryMethod(instance, "thenComparing", Function.class));
}
@SafeVarargs
@SuppressWarnings("varargs")
private static Matcher<ExpressionTree> anyMatch(Matcher<ExpressionTree>... matchers) {
return (ExpressionTree t, VisitorState s) -> Stream.of(matchers).anyMatch(m -> m.matches(t, s));
}
private static Matcher<ExpressionTree> unaryMethod(
MethodClassMatcher classMatcher, String name, Class<?> paramType) {
return classMatcher.named(name).withParameters(paramType.getName());
}
}

View File

@@ -27,117 +27,116 @@ import java.util.function.BiFunction;
// XXX: Also flag/drop trailing commas?
@AutoService(BugChecker.class)
@BugPattern(
name = "CanonicalAnnotationSyntax",
summary = "Omit redundant syntax from annotation declarations",
linkType = LinkType.NONE,
severity = SeverityLevel.SUGGESTION,
tags = StandardTags.STYLE,
providesFix = ProvidesFix.REQUIRES_HUMAN_ATTENTION
name = "CanonicalAnnotationSyntax",
summary = "Omit redundant syntax from annotation declarations",
linkType = LinkType.NONE,
severity = SeverityLevel.SUGGESTION,
tags = StandardTags.STYLE,
providesFix = ProvidesFix.REQUIRES_HUMAN_ATTENTION
)
public final class CanonicalAnnotationSyntaxCheck extends BugChecker
implements AnnotationTreeMatcher {
private static final ImmutableSet<BiFunction<AnnotationTree, VisitorState, Optional<Fix>>>
FIX_FACTORIES =
ImmutableSet.of(
CanonicalAnnotationSyntaxCheck::dropRedundantParentheses,
CanonicalAnnotationSyntaxCheck::dropRedundantValueAttribute,
CanonicalAnnotationSyntaxCheck::dropRedundantCurlies);
implements AnnotationTreeMatcher {
private static final long serialVersionUID = 1L;
private static final ImmutableSet<BiFunction<AnnotationTree, VisitorState, Optional<Fix>>>
FIX_FACTORIES =
ImmutableSet.of(
CanonicalAnnotationSyntaxCheck::dropRedundantParentheses,
CanonicalAnnotationSyntaxCheck::dropRedundantValueAttribute,
CanonicalAnnotationSyntaxCheck::dropRedundantCurlies);
@Override
public Description matchAnnotation(AnnotationTree tree, VisitorState state) {
return FIX_FACTORIES
.stream()
.map(op -> op.apply(tree, state))
.filter(Optional::isPresent)
.map(Optional::get)
.findFirst()
.map(fix -> buildDescription(tree).addFix(fix).build())
.orElse(Description.NO_MATCH);
@Override
public Description matchAnnotation(AnnotationTree tree, VisitorState state) {
return FIX_FACTORIES
.stream()
.map(op -> op.apply(tree, state))
.filter(Optional::isPresent)
.map(Optional::get)
.findFirst()
.map(fix -> buildDescription(tree).addFix(fix).build())
.orElse(Description.NO_MATCH);
}
private static Optional<Fix> dropRedundantParentheses(AnnotationTree tree, VisitorState state) {
if (!tree.getArguments().isEmpty()) {
/* Parentheses are necessary. */
return Optional.empty();
}
private static Optional<Fix> dropRedundantParentheses(AnnotationTree tree, VisitorState state) {
if (!tree.getArguments().isEmpty()) {
/* Parentheses are necessary. */
return Optional.empty();
}
String src = state.getSourceForNode(tree);
if (src == null) {
/* Without the source code there's not much we can do. */
return Optional.empty();
}
int parenIndex = src.indexOf('(');
if (parenIndex < 0) {
/* There are no redundant parentheses. */
return Optional.empty();
}
return Optional.of(SuggestedFix.replace(tree, src.substring(0, parenIndex)));
String src = state.getSourceForNode(tree);
if (src == null) {
/* Without the source code there's not much we can do. */
return Optional.empty();
}
private static Optional<Fix> dropRedundantValueAttribute(
AnnotationTree tree, VisitorState state) {
List<? extends ExpressionTree> args = tree.getArguments();
if (args.size() != 1) {
/* The `value` attribute, if specified, cannot be dropped. */
return Optional.empty();
}
ExpressionTree arg = args.get(0);
if (arg.getKind() != Kind.ASSIGNMENT) {
/* Evidently `value` isn't assigned to explicitly. */
return Optional.empty();
}
AssignmentTree assignment = (AssignmentTree) arg;
ExpressionTree variable = assignment.getVariable();
if (variable.getKind() != Kind.IDENTIFIER
|| !((IdentifierTree) variable).getName().contentEquals("value")
|| state.getSourceForNode(variable) == null) {
/* This is not an explicit assignment to the `value` attribute. */
return Optional.empty();
}
/* Replace the assignment with (the simplified representation of) just its value. */
ExpressionTree expr = assignment.getExpression();
return Optional.of(
SuggestedFix.replace(arg, simplifyAttributeValue(expr).orElseGet(expr::toString)));
int parenIndex = src.indexOf('(');
if (parenIndex < 0) {
/* There are no redundant parentheses. */
return Optional.empty();
}
private static Optional<Fix> dropRedundantCurlies(AnnotationTree tree, VisitorState state) {
List<SuggestedFix.Builder> fixes = new ArrayList<>();
for (ExpressionTree arg : tree.getArguments()) {
/*
* We'll try to simplify each assignment's RHS; for non-assignment we'll try to simplify
* the expression as a whole.
*/
ExpressionTree value =
(arg.getKind() == Kind.ASSIGNMENT)
? ((AssignmentTree) arg).getExpression()
: arg;
return Optional.of(SuggestedFix.replace(tree, src.substring(0, parenIndex)));
}
/* Store a fix for each expression that was successfully simplified. */
simplifyAttributeValue(value)
.ifPresent(expr -> fixes.add(SuggestedFix.builder().replace(value, expr)));
}
return fixes.stream().reduce(SuggestedFix.Builder::merge).map(SuggestedFix.Builder::build);
private static Optional<Fix> dropRedundantValueAttribute(
AnnotationTree tree, VisitorState state) {
List<? extends ExpressionTree> args = tree.getArguments();
if (args.size() != 1) {
/* The `value` attribute, if specified, cannot be dropped. */
return Optional.empty();
}
private static Optional<String> simplifyAttributeValue(ExpressionTree expr) {
if (expr.getKind() != Kind.NEW_ARRAY) {
/* There are no curly braces to be dropped here. */
return Optional.empty();
}
NewArrayTree newArray = (NewArrayTree) expr;
if (newArray.getInitializers().size() != 1) {
/* Only singleton arrays can be simplified. */
return Optional.empty();
}
/* Return the expression describing the array's sole element. */
return Optional.of(newArray.getInitializers().get(0).toString());
ExpressionTree arg = args.get(0);
if (arg.getKind() != Kind.ASSIGNMENT) {
/* Evidently `value` isn't assigned to explicitly. */
return Optional.empty();
}
AssignmentTree assignment = (AssignmentTree) arg;
ExpressionTree variable = assignment.getVariable();
if (variable.getKind() != Kind.IDENTIFIER
|| !((IdentifierTree) variable).getName().contentEquals("value")
|| state.getSourceForNode(variable) == null) {
/* This is not an explicit assignment to the `value` attribute. */
return Optional.empty();
}
/* Replace the assignment with (the simplified representation of) just its value. */
ExpressionTree expr = assignment.getExpression();
return Optional.of(
SuggestedFix.replace(arg, simplifyAttributeValue(expr).orElseGet(expr::toString)));
}
private static Optional<Fix> dropRedundantCurlies(AnnotationTree tree, VisitorState state) {
List<SuggestedFix.Builder> fixes = new ArrayList<>();
for (ExpressionTree arg : tree.getArguments()) {
/*
* We'll try to simplify each assignment's RHS; for non-assignment we'll try to simplify
* the expression as a whole.
*/
ExpressionTree value =
(arg.getKind() == Kind.ASSIGNMENT) ? ((AssignmentTree) arg).getExpression() : arg;
/* Store a fix for each expression that was successfully simplified. */
simplifyAttributeValue(value)
.ifPresent(expr -> fixes.add(SuggestedFix.builder().replace(value, expr)));
}
return fixes.stream().reduce(SuggestedFix.Builder::merge).map(SuggestedFix.Builder::build);
}
private static Optional<String> simplifyAttributeValue(ExpressionTree expr) {
if (expr.getKind() != Kind.NEW_ARRAY) {
/* There are no curly braces to be dropped here. */
return Optional.empty();
}
NewArrayTree newArray = (NewArrayTree) expr;
if (newArray.getInitializers().size() != 1) {
/* Only singleton arrays can be simplified. */
return Optional.empty();
}
/* Return the expression describing the array's sole element. */
return Optional.of(newArray.getInitializers().get(0).toString());
}
}

View File

@@ -11,25 +11,27 @@ import com.google.errorprone.matchers.Description;
import com.sun.source.tree.MethodTree;
// XXX: Disable until fixed.
//@AutoService(BugChecker.class)
// @AutoService(BugChecker.class)
@BugPattern(
name = "EmptyMethod",
summary = "Empty method can likely be deleted",
linkType = LinkType.NONE,
severity = SeverityLevel.WARNING,
tags = StandardTags.LIKELY_ERROR
name = "EmptyMethod",
summary = "Empty method can likely be deleted",
linkType = LinkType.NONE,
severity = SeverityLevel.WARNING,
tags = StandardTags.LIKELY_ERROR
)
public final class EmptyMethodCheck extends BugChecker implements MethodTreeMatcher {
// XXX: Restrict: only warn about:
// - static methods
// - methods that are provably not in an inheritance hierarchy.
// - public methods in a Mockito test class.
@Override
public Description matchMethod(MethodTree tree, VisitorState state) {
if (tree.getBody() == null || !tree.getBody().getStatements().isEmpty()) {
return Description.NO_MATCH;
}
private static final long serialVersionUID = 1L;
return buildDescription(tree).build();
// XXX: Restrict: only warn about:
// - static methods
// - methods that are provably not in an inheritance hierarchy.
// - public methods in a Mockito test class.
@Override
public Description matchMethod(MethodTree tree, VisitorState state) {
if (tree.getBody() == null || !tree.getBody().getStatements().isEmpty()) {
return Description.NO_MATCH;
}
return buildDescription(tree).build();
}
}

View File

@@ -9,6 +9,7 @@ import com.google.errorprone.BugPattern;
import com.google.errorprone.BugPattern.LinkType;
import com.google.errorprone.BugPattern.SeverityLevel;
import com.google.errorprone.BugPattern.StandardTags;
import com.google.errorprone.ErrorProneFlags;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker;
import com.google.errorprone.bugpatterns.BugChecker.AnnotationTreeMatcher;
@@ -28,88 +29,114 @@ import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;
// XXX: Add documentation. Explain that sorting reduces chances of conflicts and simplifies their resolution when they do happen.
// XXX: Add support for inclusions and exclusions.
// XXX: Add documentation. Explain that sorting reduces chances of conflicts and simplifies their
// resolution when they do happen.
@AutoService(BugChecker.class)
@BugPattern(
name = "LexicographicalAnnotationAttributeListing",
summary = "Where possible, sort annotation array attributes lexicographically",
linkType = LinkType.NONE,
severity = SeverityLevel.SUGGESTION,
tags = StandardTags.STYLE
name = "LexicographicalAnnotationAttributeListing",
summary = "Where possible, sort annotation array attributes lexicographically",
linkType = LinkType.NONE,
severity = SeverityLevel.SUGGESTION,
tags = StandardTags.STYLE
)
public final class LexicographicalAnnotationAttributeListingCheck extends BugChecker
implements AnnotationTreeMatcher {
@Override
public Description matchAnnotation(AnnotationTree tree, VisitorState state) {
return sortArrayElements(tree, state)
.map(fix -> buildDescription(tree).addFix(fix).build())
.orElse(Description.NO_MATCH);
implements AnnotationTreeMatcher {
private static final long serialVersionUID = 1L;
private static final String FLAG_PREFIX = "LexicographicalAnnotationAttributeListingCheck:";
private static final String INCLUDED_ANNOTATIONS_FLAG = FLAG_PREFIX + "Includes";
private static final String EXCLUDED_ANNOTATIONS_FLAG = FLAG_PREFIX + "Excludes";
private final AnnotationAttributeMatcher matcher;
public LexicographicalAnnotationAttributeListingCheck() {
this(ErrorProneFlags.empty());
}
public LexicographicalAnnotationAttributeListingCheck(ErrorProneFlags flags) {
this.matcher = getAnnotationAttributeMatcher(flags);
}
@Override
public Description matchAnnotation(AnnotationTree tree, VisitorState state) {
return sortArrayElements(tree, state)
.map(fix -> buildDescription(tree).addFix(fix).build())
.orElse(Description.NO_MATCH);
}
private Optional<Fix> sortArrayElements(AnnotationTree tree, VisitorState state) {
/**
* We loop over the array's attributes, trying to sort each array associated with a
* non-blacklisted attribute. A single compound fix, if any, is returned.
*/
String annotationType = ASTHelpers.getType(tree.getAnnotationType()).toString();
return tree.getArguments()
.stream()
.filter(a -> this.matcher.matches(annotationType, extractAttributeName(a)))
.map(expr -> extractArray(expr).flatMap(arr -> suggestSorting(arr, state)))
.filter(Optional::isPresent)
.map(Optional::get)
.reduce(SuggestedFix.Builder::merge)
.map(SuggestedFix.Builder::build);
}
private static String extractAttributeName(ExpressionTree expr) {
return (expr.getKind() == Kind.ASSIGNMENT)
? ASTHelpers.getSymbol(((AssignmentTree) expr).getVariable()).getSimpleName().toString()
: "value";
}
private static Optional<NewArrayTree> extractArray(ExpressionTree expr) {
if (expr.getKind() == Kind.ASSIGNMENT) {
return extractArray(((AssignmentTree) expr).getExpression());
}
private Optional<Fix> sortArrayElements(AnnotationTree tree, VisitorState state) {
List<SuggestedFix.Builder> fixes = new ArrayList<>();
return Optional.of(expr)
.filter(e -> e.getKind() == Kind.NEW_ARRAY)
.map(NewArrayTree.class::cast);
}
/*
* For each argument, see whether it contains an unsorted array and emit a suggested fix if
* so.
*/
for (ExpressionTree arg : tree.getArguments()) {
extractArray(arg).flatMap(arr -> suggestSorting(arr, state)).ifPresent(fixes::add);
}
/* Return a single compound fix, if any. */
return fixes.stream().reduce(SuggestedFix.Builder::merge).map(SuggestedFix.Builder::build);
private static Optional<SuggestedFix.Builder> suggestSorting(
NewArrayTree array, VisitorState state) {
if (array.getInitializers().size() < 2 || !canSort(array, state)) {
/* There's nothing to sort, or we don't want to sort. */
return Optional.empty();
}
private Optional<NewArrayTree> extractArray(ExpressionTree expr) {
if (expr.getKind() == Kind.ASSIGNMENT) {
return extractArray(((AssignmentTree) expr).getExpression());
}
return Optional.of(expr)
.filter(e -> e.getKind() == Kind.NEW_ARRAY)
.map(NewArrayTree.class::cast);
/* We're sorting based on each expression's string representation. */
List<String> expressions = new ArrayList<>();
for (ExpressionTree expr : array.getInitializers()) {
expressions.add(expr.toString());
}
private Optional<SuggestedFix.Builder> suggestSorting(NewArrayTree array, VisitorState state) {
if (array.getInitializers().size() < 2 || !canSort(array, state)) {
/* There's nothing to sort, or we don't want to sort. */
return Optional.empty();
}
/* We're sorting based on each expression's string representation. */
List<String> expressions = new ArrayList<>();
for (ExpressionTree expr : array.getInitializers()) {
expressions.add(expr.toString());
}
if (Comparators.isInOrder(expressions, naturalOrder())) {
/* In the (presumably) common case the elements are already sorted. */
return Optional.empty();
}
/* The elements aren't sorted yet. Do so now. */
expressions.sort(naturalOrder());
/* Re-assemble the expressions into an array. */
StringBuilder sb = new StringBuilder("{");
Joiner.on(", ").appendTo(sb, expressions);
sb.append("}");
return Optional.of(SuggestedFix.builder().replace(array, sb.toString()));
if (Comparators.isInOrder(expressions, naturalOrder())) {
/* In the (presumably) common case the elements are already sorted. */
return Optional.empty();
}
private boolean canSort(NewArrayTree array, VisitorState state) {
Type elemType = state.getTypes().elemtype(ASTHelpers.getType(array));
Symtab symtab = state.getSymtab();
/* The elements aren't sorted yet. Do so now. */
expressions.sort(naturalOrder());
/* For now we don't force sorting on numeric types. */
return Stream.of(
symtab.annotationType,
symtab.classType,
symtab.enumSym.type,
symtab.stringType)
.anyMatch(t -> ASTHelpers.isSubtype(elemType, t, state));
}
/* Re-assemble the expressions into an array. */
StringBuilder sb = new StringBuilder("{");
Joiner.on(", ").appendTo(sb, expressions);
sb.append("}");
return Optional.of(SuggestedFix.builder().replace(array, sb.toString()));
}
private static boolean canSort(NewArrayTree array, VisitorState state) {
Type elemType = state.getTypes().elemtype(ASTHelpers.getType(array));
Symtab symtab = state.getSymtab();
/* For now we don't force sorting on numeric types. */
return Stream.of(
symtab.annotationType, symtab.classType, symtab.enumSym.type, symtab.stringType)
.anyMatch(t -> ASTHelpers.isSubtype(elemType, t, state));
}
private static AnnotationAttributeMatcher getAnnotationAttributeMatcher(ErrorProneFlags flags) {
return AnnotationAttributeMatcher.create(
flags.getList(INCLUDED_ANNOTATIONS_FLAG), flags.getList(EXCLUDED_ANNOTATIONS_FLAG));
}
}

View File

@@ -0,0 +1,74 @@
package com.picnicinternational.errorprone.bugpatterns;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import com.google.common.collect.ImmutableList;
import java.util.Optional;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@RunWith(JUnit4.class)
public final class AnnotationAttributeMatcherTest {
@Test
public void testWithoutListings() {
AnnotationAttributeMatcher matcher =
AnnotationAttributeMatcher.create(Optional.empty(), Optional.empty());
assertTrue(matcher.matches("foo", "bar"));
}
@Test
public void testWithSingleFullAnnotationWhitelist() {
AnnotationAttributeMatcher matcher =
AnnotationAttributeMatcher.create(Optional.of(ImmutableList.of("foo")), Optional.empty());
assertTrue(matcher.matches("foo", "bar"));
assertTrue(matcher.matches("foo", "baz"));
assertFalse(matcher.matches("quux", "bar"));
}
@Test
public void testWithSingleAnnotationAttributeWhitelist() {
AnnotationAttributeMatcher matcher =
AnnotationAttributeMatcher.create(
Optional.of(ImmutableList.of("foo#bar")), Optional.empty());
assertTrue(matcher.matches("foo", "bar"));
assertFalse(matcher.matches("foo", "baz"));
assertFalse(matcher.matches("quux", "bar"));
}
@Test
public void testWithSingleFullAnnotationBlacklist() {
AnnotationAttributeMatcher matcher =
AnnotationAttributeMatcher.create(Optional.empty(), Optional.of(ImmutableList.of("foo")));
assertFalse(matcher.matches("foo", "bar"));
assertFalse(matcher.matches("foo", "baz"));
assertTrue(matcher.matches("quux", "bar"));
}
@Test
public void testWithSingleAnnotationAttributeBlacklist() {
AnnotationAttributeMatcher matcher =
AnnotationAttributeMatcher.create(
Optional.empty(), Optional.of(ImmutableList.of("foo#bar")));
assertFalse(matcher.matches("foo", "bar"));
assertTrue(matcher.matches("foo", "baz"));
assertTrue(matcher.matches("quux", "bar"));
}
@Test
public void testWithComplicatedConfiguration() {
AnnotationAttributeMatcher matcher =
AnnotationAttributeMatcher.create(
Optional.of(ImmutableList.of("foo", "bar", "baz", "baz#1", "baz#2", "quux#1")),
Optional.of(ImmutableList.of("foo", "baz#2")));
assertFalse(matcher.matches("foo", "1"));
assertFalse(matcher.matches("foo", "2"));
assertTrue(matcher.matches("bar", "1"));
assertTrue(matcher.matches("bar", "2"));
assertTrue(matcher.matches("baz", "1"));
assertFalse(matcher.matches("baz", "2"));
assertTrue(matcher.matches("quux", "1"));
assertFalse(matcher.matches("quux", "2"));
}
}

View File

@@ -9,137 +9,136 @@ import org.junit.runners.JUnit4;
@RunWith(JUnit4.class)
public final class CanonicalAnnotationSyntaxCheckTest {
private final CompilationTestHelper compilationTestHelper =
CompilationTestHelper.newInstance(CanonicalAnnotationSyntaxCheck.class, getClass());
private final BugCheckerRefactoringTestHelper refactoringTestHelper =
BugCheckerRefactoringTestHelper.newInstance(
new CanonicalAnnotationSyntaxCheck(), getClass());
private final CompilationTestHelper compilationTestHelper =
CompilationTestHelper.newInstance(CanonicalAnnotationSyntaxCheck.class, getClass());
private final BugCheckerRefactoringTestHelper refactoringTestHelper =
BugCheckerRefactoringTestHelper.newInstance(new CanonicalAnnotationSyntaxCheck(), getClass());
@Test
public void testIdentification() {
compilationTestHelper
.addSourceLines(
"pkg/A.java",
"package pkg;",
"",
"import pkg.A.Foo;",
"",
"interface A {",
" @interface Foo {",
" int[] value() default {};",
" int[] value2() default {};",
" }",
"",
" @pkg.A.Foo A minimal1();",
" @A.Foo A minimal2();",
" @Foo A minimal3();",
"",
" // BUG: Diagnostic contains:",
" @pkg.A.Foo() A functional1();",
" // BUG: Diagnostic contains:",
" @A.Foo() A functional2();",
" // BUG: Diagnostic contains:",
" @Foo() A functional3();",
"",
" @pkg.A.Foo(1) A simple1();",
" @A.Foo(1) A simple2();",
" @Foo(1) A simple3();",
"",
" // BUG: Diagnostic contains:",
" @pkg.A.Foo({1}) A singleton1();",
" // BUG: Diagnostic contains:",
" @A.Foo({1}) A singleton2();",
" // BUG: Diagnostic contains:",
" @Foo({1}) A singleton3();",
"",
" // BUG: Diagnostic contains:",
" @pkg.A.Foo(value = 1) A verbose1();",
" // BUG: Diagnostic contains:",
" @A.Foo(value = 1) A verbose2();",
" // BUG: Diagnostic contains:",
" @Foo(value = 1) A verbose3();",
"",
" @pkg.A.Foo(value2 = 2) A custom1();",
" @A.Foo(value2 = 2) A custom2();",
" @Foo(value2 = 2) A custom3();",
"",
" // BUG: Diagnostic contains:",
" @pkg.A.Foo(value2 = {2}) A customSingleton1();",
" // BUG: Diagnostic contains:",
" @A.Foo(value2 = {2}) A customSingleton2();",
" // BUG: Diagnostic contains:",
" @Foo(value2 = {2}) A customSingleton3();",
"",
" @pkg.A.Foo(value2 = {2, 2}) A customPair1();",
" @A.Foo(value2 = {2, 2}) A customPair2();",
" @Foo(value2 = {2, 2}) A customPair3();",
"",
" @pkg.A.Foo(value = 1, value2 = 2) A extended1();",
" @A.Foo(value = 1, value2 = 2) A extended2();",
" @Foo(value = 1, value2 = 2) A extended3();",
"}")
.doTest();
}
@Test
public void testIdentification() {
compilationTestHelper
.addSourceLines(
"pkg/A.java",
"package pkg;",
"",
"import pkg.A.Foo;",
"",
"interface A {",
" @interface Foo {",
" int[] value() default {};",
" int[] value2() default {};",
" }",
"",
" @pkg.A.Foo A minimal1();",
" @A.Foo A minimal2();",
" @Foo A minimal3();",
"",
" // BUG: Diagnostic contains:",
" @pkg.A.Foo() A functional1();",
" // BUG: Diagnostic contains:",
" @A.Foo() A functional2();",
" // BUG: Diagnostic contains:",
" @Foo() A functional3();",
"",
" @pkg.A.Foo(1) A simple1();",
" @A.Foo(1) A simple2();",
" @Foo(1) A simple3();",
"",
" // BUG: Diagnostic contains:",
" @pkg.A.Foo({1}) A singleton1();",
" // BUG: Diagnostic contains:",
" @A.Foo({1}) A singleton2();",
" // BUG: Diagnostic contains:",
" @Foo({1}) A singleton3();",
"",
" // BUG: Diagnostic contains:",
" @pkg.A.Foo(value = 1) A verbose1();",
" // BUG: Diagnostic contains:",
" @A.Foo(value = 1) A verbose2();",
" // BUG: Diagnostic contains:",
" @Foo(value = 1) A verbose3();",
"",
" @pkg.A.Foo(value2 = 2) A custom1();",
" @A.Foo(value2 = 2) A custom2();",
" @Foo(value2 = 2) A custom3();",
"",
" // BUG: Diagnostic contains:",
" @pkg.A.Foo(value2 = {2}) A customSingleton1();",
" // BUG: Diagnostic contains:",
" @A.Foo(value2 = {2}) A customSingleton2();",
" // BUG: Diagnostic contains:",
" @Foo(value2 = {2}) A customSingleton3();",
"",
" @pkg.A.Foo(value2 = {2, 2}) A customPair1();",
" @A.Foo(value2 = {2, 2}) A customPair2();",
" @Foo(value2 = {2, 2}) A customPair3();",
"",
" @pkg.A.Foo(value = 1, value2 = 2) A extended1();",
" @A.Foo(value = 1, value2 = 2) A extended2();",
" @Foo(value = 1, value2 = 2) A extended3();",
"}")
.doTest();
}
@Test
public void testReplacement() throws IOException {
refactoringTestHelper
.addInputLines(
"in/pkg/A.java",
"package pkg;",
"",
"import pkg.A.Foo;",
"",
"interface A {",
" @interface Foo {",
" int[] value() default {};",
" int[] value2() default {};",
" }",
"",
" @pkg.A.Foo() A functional1();",
" @A.Foo() A functional2();",
" @Foo() A functional3();",
"",
" @pkg.A.Foo(value = 1) A verbose1();",
" @A.Foo(value = 1) A verbose2();",
" @Foo(value = 1) A verbose3();",
"",
" @pkg.A.Foo(value = {1}) A moreVerbose1();",
" @A.Foo(value = {1}) A moreVerbose2();",
" @Foo(value = {1}) A moreVerbose3();",
"",
" @pkg.A.Foo(value = {1, 1}, value2 = {2}) A extended1();",
" @A.Foo(value = {1, 1}, value2 = {2}) A extended2();",
" @Foo(value = {1, 1}, value2 = {2}) A extended3();",
"}")
.addOutputLines(
"out/pkg/A.java",
"package pkg;",
"",
"import pkg.A.Foo;",
"",
"interface A {",
" @interface Foo {",
" int[] value() default {};",
" int[] value2() default {};",
" }",
"",
" @pkg.A.Foo A functional1();",
" @A.Foo A functional2();",
" @Foo A functional3();",
"",
" @pkg.A.Foo(1) A verbose1();",
" @A.Foo(1) A verbose2();",
" @Foo(1) A verbose3();",
"",
" @pkg.A.Foo(1) A moreVerbose1();",
" @A.Foo(1) A moreVerbose2();",
" @Foo(1) A moreVerbose3();",
"",
" @pkg.A.Foo(value = {1, 1}, value2 = 2) A extended1();",
" @A.Foo(value = {1, 1}, value2 = 2) A extended2();",
" @Foo(value = {1, 1}, value2 = 2) A extended3();",
"}")
.doTest();
}
@Test
public void testReplacement() throws IOException {
refactoringTestHelper
.addInputLines(
"in/pkg/A.java",
"package pkg;",
"",
"import pkg.A.Foo;",
"",
"interface A {",
" @interface Foo {",
" int[] value() default {};",
" int[] value2() default {};",
" }",
"",
" @pkg.A.Foo() A functional1();",
" @A.Foo() A functional2();",
" @Foo() A functional3();",
"",
" @pkg.A.Foo(value = 1) A verbose1();",
" @A.Foo(value = 1) A verbose2();",
" @Foo(value = 1) A verbose3();",
"",
" @pkg.A.Foo(value = {1}) A moreVerbose1();",
" @A.Foo(value = {1}) A moreVerbose2();",
" @Foo(value = {1}) A moreVerbose3();",
"",
" @pkg.A.Foo(value = {1, 1}, value2 = {2}) A extended1();",
" @A.Foo(value = {1, 1}, value2 = {2}) A extended2();",
" @Foo(value = {1, 1}, value2 = {2}) A extended3();",
"}")
.addOutputLines(
"out/pkg/A.java",
"package pkg;",
"",
"import pkg.A.Foo;",
"",
"interface A {",
" @interface Foo {",
" int[] value() default {};",
" int[] value2() default {};",
" }",
"",
" @pkg.A.Foo A functional1();",
" @A.Foo A functional2();",
" @Foo A functional3();",
"",
" @pkg.A.Foo(1) A verbose1();",
" @A.Foo(1) A verbose2();",
" @Foo(1) A verbose3();",
"",
" @pkg.A.Foo(1) A moreVerbose1();",
" @A.Foo(1) A moreVerbose2();",
" @Foo(1) A moreVerbose3();",
"",
" @pkg.A.Foo(value = {1, 1}, value2 = 2) A extended1();",
" @A.Foo(value = {1, 1}, value2 = 2) A extended2();",
" @Foo(value = {1, 1}, value2 = 2) A extended3();",
"}")
.doTest();
}
}

View File

@@ -7,42 +7,42 @@ import org.junit.runners.JUnit4;
@RunWith(JUnit4.class)
public final class EmptyMethodCheckTest {
private final CompilationTestHelper compilationTestHelper =
CompilationTestHelper.newInstance(EmptyMethodCheck.class, getClass());
private final CompilationTestHelper compilationTestHelper =
CompilationTestHelper.newInstance(EmptyMethodCheck.class, getClass());
@Test
public void testNegative() {
compilationTestHelper
.addSourceLines(
"A.java",
"class A {",
" Object m() {",
" return null;",
" }",
"",
" void m2() {",
" System.out.println(42);",
" }",
"",
" interface F {",
" void fun();",
" }",
"}")
.doTest();
}
@Test
public void testNegative() {
compilationTestHelper
.addSourceLines(
"A.java",
"class A {",
" Object m() {",
" return null;",
" }",
"",
" void m2() {",
" System.out.println(42);",
" }",
"",
" interface F {",
" void fun();",
" }",
"}")
.doTest();
}
@Test
public void testPositive() {
compilationTestHelper
.addSourceLines(
"A.java",
"class A {",
" // BUG: Diagnostic contains:",
" void m() {}",
"",
" // BUG: Diagnostic contains:",
" static void m2() {}",
"}")
.doTest();
}
@Test
public void testPositive() {
compilationTestHelper
.addSourceLines(
"A.java",
"class A {",
" // BUG: Diagnostic contains:",
" void m() {}",
"",
" // BUG: Diagnostic contains:",
" static void m2() {}",
"}")
.doTest();
}
}

View File

@@ -1,5 +1,6 @@
package com.picnicinternational.errorprone.bugpatterns;
import com.google.common.collect.ImmutableList;
import com.google.errorprone.BugCheckerRefactoringTestHelper;
import com.google.errorprone.CompilationTestHelper;
import java.io.IOException;
@@ -9,120 +10,166 @@ import org.junit.runners.JUnit4;
@RunWith(JUnit4.class)
public final class LexicographicalAnnotationAttributeListingCheckTest {
private final CompilationTestHelper compilationTestHelper =
CompilationTestHelper.newInstance(
LexicographicalAnnotationAttributeListingCheck.class, getClass());
private final BugCheckerRefactoringTestHelper refactoringTestHelper =
BugCheckerRefactoringTestHelper.newInstance(
new LexicographicalAnnotationAttributeListingCheck(), getClass());
private final CompilationTestHelper compilationTestHelper =
CompilationTestHelper.newInstance(
LexicographicalAnnotationAttributeListingCheck.class, getClass());
private final CompilationTestHelper restrictedCompilationTestHelper =
CompilationTestHelper.newInstance(
LexicographicalAnnotationAttributeListingCheck.class, getClass())
.setArgs(
ImmutableList.of(
"-XepOpt:LexicographicalAnnotationAttributeListingCheck:Includes=pkg.A.Foo,pkg.A.Bar",
"-XepOpt:LexicographicalAnnotationAttributeListingCheck:Excludes=pkg.A.Bar#value"));
private final BugCheckerRefactoringTestHelper refactoringTestHelper =
BugCheckerRefactoringTestHelper.newInstance(
new LexicographicalAnnotationAttributeListingCheck(), getClass());
@Test
public void testIdentification() {
compilationTestHelper
.addSourceLines(
"A.java",
"import static java.math.RoundingMode.UP;",
"import static java.math.RoundingMode.DOWN;",
"",
"import java.math.RoundingMode;",
"",
"interface A {",
" @interface Foo {",
" String[] value() default {};",
" Class<?>[] cls() default {};",
" RoundingMode[] enums() default {};",
" Bar[] anns() default {};",
" }",
"",
" @interface Bar {",
" String[] value() default {};",
" }",
"",
" @Foo({}) A noString();",
" @Foo({\"a\"}) A oneString();",
" @Foo({\"a\", \"b\"}) A sortedStrings();",
" // BUG: Diagnostic contains:",
" @Foo({\"b\", \"a\"}) A unsortedString();",
"",
" @Foo(cls = {}) A noClasses();",
" @Foo(cls = {int.class}) A oneClass();",
" @Foo(cls = {int.class, long.class}) A sortedClasses();",
" // BUG: Diagnostic contains:",
" @Foo(cls = {long.class, int.class}) A unsortedClasses();",
"",
" @Foo(enums = {}) A noEnums();",
" @Foo(enums = {DOWN}) A oneEnum();",
" @Foo(enums = {DOWN, UP}) A sortedEnums();",
" // BUG: Diagnostic contains:",
" @Foo(enums = {UP, DOWN}) A unsortedEnums();",
"",
" @Foo(anns = {}) A noAnns();",
" @Foo(anns = {@Bar(\"a\")}) A oneAnnd();",
" @Foo(anns = {@Bar(\"a\"), @Bar(\"b\")}) A sortedAnns();",
" // BUG: Diagnostic contains:",
" @Foo(anns = {@Bar(\"b\"), @Bar(\"a\")}) A unsortedAnns();",
" // BUG: Diagnostic contains:",
" @Foo(anns = {@Bar(\"a\"), @Bar({\"b\", \"a\"})}) A unsortedInnderAnns();",
"}")
.doTest();
}
@Test
public void testIdentification() {
compilationTestHelper
.addSourceLines(
"A.java",
"import static java.math.RoundingMode.UP;",
"import static java.math.RoundingMode.DOWN;",
"",
"import java.math.RoundingMode;",
"",
"interface A {",
" @interface Foo {",
" String[] value() default {};",
" Class<?>[] cls() default {};",
" RoundingMode[] enums() default {};",
" Bar[] anns() default {};",
" }",
"",
" @interface Bar {",
" String[] value() default {};",
" }",
"",
" @Foo({}) A noString();",
" @Foo({\"a\"}) A oneString();",
" @Foo({\"a\", \"b\"}) A sortedStrings();",
" // BUG: Diagnostic contains:",
" @Foo({\"b\", \"a\"}) A unsortedString();",
"",
" @Foo(cls = {}) A noClasses();",
" @Foo(cls = {int.class}) A oneClass();",
" @Foo(cls = {int.class, long.class}) A sortedClasses();",
" // BUG: Diagnostic contains:",
" @Foo(cls = {long.class, int.class}) A unsortedClasses();",
"",
" @Foo(enums = {}) A noEnums();",
" @Foo(enums = {DOWN}) A oneEnum();",
" @Foo(enums = {DOWN, UP}) A sortedEnums();",
" // BUG: Diagnostic contains:",
" @Foo(enums = {UP, DOWN}) A unsortedEnums();",
"",
" @Foo(anns = {}) A noAnns();",
" @Foo(anns = {@Bar(\"a\")}) A oneAnnd();",
" @Foo(anns = {@Bar(\"a\"), @Bar(\"b\")}) A sortedAnns();",
" // BUG: Diagnostic contains:",
" @Foo(anns = {@Bar(\"b\"), @Bar(\"a\")}) A unsortedAnns();",
" // BUG: Diagnostic contains:",
" @Foo(anns = {@Bar(\"a\"), @Bar({\"b\", \"a\"})}) A unsortedInnderAnns();",
"}")
.doTest();
}
// XXX: Note that in the output below in one instance redundant `value = ` assignments are introduced.
// Avoiding that might make the code too complex. Instead, users can have the `CanonicalAnnotationSyntaxCheck`
// correct the situation in a subsequent run.
@Test
public void testReplacement() throws IOException {
refactoringTestHelper
.addInputLines(
"in/A.java",
"import static java.math.RoundingMode.UP;",
"import static java.math.RoundingMode.DOWN;",
"",
"import java.math.RoundingMode;",
"",
"interface A {",
" @interface Foo {",
" String[] value() default {};",
" Class<?>[] cls() default {};",
" RoundingMode[] enums() default {};",
" Bar[] anns() default {};",
" }",
"",
" @interface Bar {",
" String[] value() default {};",
" }",
"",
" @Foo({\"b\", \"a\"}) A unsortedString();",
" @Foo(cls = {long.class, int.class}) A unsortedClasses();",
" @Foo(enums = {UP, DOWN}) A unsortedEnums();",
" @Foo(anns = {@Bar(\"b\"), @Bar(\"a\")}) A unsortedAnns();",
" @Foo(anns = {@Bar(\"a\"), @Bar({\"b\", \"a\"})}) A unsortedInnderAnns();",
"}")
.addOutputLines(
"out/A.java",
"import static java.math.RoundingMode.UP;",
"import static java.math.RoundingMode.DOWN;",
"",
"import java.math.RoundingMode;",
"",
"interface A {",
" @interface Foo {",
" String[] value() default {};",
" Class<?>[] cls() default {};",
" RoundingMode[] enums() default {};",
" Bar[] anns() default {};",
" }",
"",
" @interface Bar {",
" String[] value() default {};",
" }",
"",
" @Foo({\"a\", \"b\"}) A unsortedString();",
" @Foo(cls = {int.class, long.class}) A unsortedClasses();",
" @Foo(enums = {DOWN, UP}) A unsortedEnums();",
" @Foo(anns = {@Bar(value = \"a\"), @Bar(value = \"b\")}) A unsortedAnns();",
" @Foo(anns = {@Bar(\"a\"), @Bar({\"a\", \"b\"})}) A unsortedInnderAnns();",
"}")
.doTest();
}
// XXX: Note that in the output below in one instance redundant `value = ` assignments are
// introduced.
// Avoiding that might make the code too complex. Instead, users can have the
// `CanonicalAnnotationSyntaxCheck`
// correct the situation in a subsequent run.
@Test
public void testReplacement() throws IOException {
refactoringTestHelper
.addInputLines(
"in/A.java",
"import static java.math.RoundingMode.UP;",
"import static java.math.RoundingMode.DOWN;",
"",
"import java.math.RoundingMode;",
"",
"interface A {",
" @interface Foo {",
" String[] value() default {};",
" Class<?>[] cls() default {};",
" RoundingMode[] enums() default {};",
" Bar[] anns() default {};",
" }",
"",
" @interface Bar {",
" String[] value() default {};",
" }",
"",
" @Foo({\"b\", \"a\"}) A unsortedString();",
" @Foo(cls = {long.class, int.class}) A unsortedClasses();",
" @Foo(enums = {UP, DOWN}) A unsortedEnums();",
" @Foo(anns = {@Bar(\"b\"), @Bar(\"a\")}) A unsortedAnns();",
" @Foo(anns = {@Bar(\"a\"), @Bar({\"b\", \"a\"})}) A unsortedInnderAnns();",
"}")
.addOutputLines(
"out/A.java",
"import static java.math.RoundingMode.UP;",
"import static java.math.RoundingMode.DOWN;",
"",
"import java.math.RoundingMode;",
"",
"interface A {",
" @interface Foo {",
" String[] value() default {};",
" Class<?>[] cls() default {};",
" RoundingMode[] enums() default {};",
" Bar[] anns() default {};",
" }",
"",
" @interface Bar {",
" String[] value() default {};",
" }",
"",
" @Foo({\"a\", \"b\"}) A unsortedString();",
" @Foo(cls = {int.class, long.class}) A unsortedClasses();",
" @Foo(enums = {DOWN, UP}) A unsortedEnums();",
" @Foo(anns = {@Bar(value = \"a\"), @Bar(value = \"b\")}) A unsortedAnns();",
" @Foo(anns = {@Bar(\"a\"), @Bar({\"a\", \"b\"})}) A unsortedInnderAnns();",
"}")
.doTest();
}
@Test
public void testFiltering() {
/* Some violations are not flagged because they are not in- or excluded. */
restrictedCompilationTestHelper
.addSourceLines(
"pkg/A.java",
"package pkg;",
"",
"interface A {",
" @interface Foo {",
" String[] value() default {};",
" String[] value2() default {};",
" }",
"",
" @interface Bar {",
" String[] value() default {};",
" String[] value2() default {};",
" }",
"",
" @interface Baz {",
" String[] value() default {};",
" String[] value2() default {};",
" }",
"",
" // BUG: Diagnostic contains:",
" @Foo({\"b\", \"a\"}) A fooValue();",
" // BUG: Diagnostic contains:",
" @Foo(value2 = {\"b\", \"a\"}) A fooValue2();",
" @Bar({\"b\", \"a\"}) A barValue();",
" // BUG: Diagnostic contains:",
" @Bar(value2 = {\"b\", \"a\"}) A barValue2();",
" @Baz({\"b\", \"a\"}) A bazValue();",
" @Baz(value2 = {\"b\", \"a\"}) A bazValue2();",
"}")
.doTest();
}
}