Fix LexicographicalAnnotationAttributeListing string sorting (#281)

In particular, the empty string is now ordered first.
This commit is contained in:
Bastien Diederichs
2022-10-07 14:34:14 +02:00
committed by GitHub
parent bd73243c34
commit 57836103ea
2 changed files with 28 additions and 24 deletions

View File

@@ -1,5 +1,6 @@
package tech.picnic.errorprone.bugpatterns;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
import static com.google.errorprone.BugPattern.StandardTags.STYLE;
@@ -9,6 +10,7 @@ import static java.util.stream.Collectors.joining;
import static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
import com.google.auto.service.AutoService;
import com.google.common.base.Splitter;
import com.google.common.collect.Comparators;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
@@ -71,6 +73,12 @@ public final class LexicographicalAnnotationAttributeListing extends BugChecker
private static final String FLAG_PREFIX = "LexicographicalAnnotationAttributeListing:";
private static final String INCLUDED_ANNOTATIONS_FLAG = FLAG_PREFIX + "Includes";
private static final String EXCLUDED_ANNOTATIONS_FLAG = FLAG_PREFIX + "Excludes";
/**
* The splitter applied to string-typed annotation arguments prior to lexicographical sorting. By
* splitting on {@code =}, strings that represent e.g. inline Spring property declarations are
* properly sorted by key, then value.
*/
private static final Splitter STRING_ARGUMENT_SPLITTER = Splitter.on('=');
private final AnnotationAttributeMatcher matcher;
@@ -126,7 +134,7 @@ public final class LexicographicalAnnotationAttributeListing extends BugChecker
}
List<? extends ExpressionTree> actualOrdering = array.getInitializers();
ImmutableList<? extends ExpressionTree> desiredOrdering = doSort(actualOrdering, state);
ImmutableList<? extends ExpressionTree> desiredOrdering = doSort(actualOrdering);
if (actualOrdering.equals(desiredOrdering)) {
/* In the (presumably) common case the elements are already sorted. */
return Optional.empty();
@@ -156,12 +164,12 @@ public final class LexicographicalAnnotationAttributeListing extends BugChecker
}
private static ImmutableList<? extends ExpressionTree> doSort(
Iterable<? extends ExpressionTree> elements, VisitorState state) {
Iterable<? extends ExpressionTree> elements) {
// XXX: Perhaps we should use `Collator` with `.setStrength(Collator.PRIMARY)` and
// `getCollationKey`. Not clear whether that's worth the hassle at this point.
return ImmutableList.sortedCopyOf(
comparing(
e -> getStructure(e, state),
LexicographicalAnnotationAttributeListing::getStructure,
Comparators.lexicographical(
Comparators.lexicographical(
String.CASE_INSENSITIVE_ORDER.thenComparing(naturalOrder())))),
@@ -173,38 +181,34 @@ public final class LexicographicalAnnotationAttributeListing extends BugChecker
* performed. This approach disregards e.g. irrelevant whitespace. It also allows special
* structure within string literals to be respected.
*/
private static ImmutableList<ImmutableList<String>> getStructure(
ExpressionTree array, VisitorState state) {
private static ImmutableList<ImmutableList<String>> getStructure(ExpressionTree array) {
ImmutableList.Builder<ImmutableList<String>> nodes = ImmutableList.builder();
new TreeScanner<Void, Void>() {
@Nullable
@Override
public Void visitIdentifier(IdentifierTree node, @Nullable Void ctx) {
nodes.add(tokenize(node));
return super.visitIdentifier(node, ctx);
public Void visitIdentifier(IdentifierTree node, @Nullable Void unused) {
nodes.add(ImmutableList.of(node.getName().toString()));
return super.visitIdentifier(node, unused);
}
@Nullable
@Override
public Void visitLiteral(LiteralTree node, @Nullable Void ctx) {
nodes.add(tokenize(node));
return super.visitLiteral(node, ctx);
public Void visitLiteral(LiteralTree node, @Nullable Void unused) {
Object value = ASTHelpers.constValue(node);
nodes.add(
value instanceof String
? STRING_ARGUMENT_SPLITTER.splitToStream((String) value).collect(toImmutableList())
: ImmutableList.of(String.valueOf(value)));
return super.visitLiteral(node, unused);
}
@Nullable
@Override
public Void visitPrimitiveType(PrimitiveTypeTree node, @Nullable Void ctx) {
nodes.add(tokenize(node));
return super.visitPrimitiveType(node, ctx);
}
private ImmutableList<String> tokenize(Tree node) {
/*
* Tokens are split on `=` so that e.g. inline Spring property declarations are properly
* sorted by key, then value.
*/
return ImmutableList.copyOf(SourceCode.treeToString(node, state).split("=", -1));
public Void visitPrimitiveType(PrimitiveTypeTree node, @Nullable Void unused) {
nodes.add(ImmutableList.of(node.getPrimitiveTypeKind().toString()));
return super.visitPrimitiveType(node, unused);
}
}.scan(array, null);

View File

@@ -188,7 +188,7 @@ final class LexicographicalAnnotationAttributeListingTest {
" String[] value() default {};",
" }",
"",
" @Foo({\"b\", \"a\"})",
" @Foo({\" \", \"\", \"b\", \"a\"})",
" A unsortedString();",
"",
" @Foo(cls = {long.class, int.class})",
@@ -225,7 +225,7 @@ final class LexicographicalAnnotationAttributeListingTest {
" String[] value() default {};",
" }",
"",
" @Foo({\"a\", \"b\"})",
" @Foo({\"\", \" \", \"a\", \"b\"})",
" A unsortedString();",
"",
" @Foo(cls = {int.class, long.class})",