-
Notifications
You must be signed in to change notification settings - Fork 39
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Introduce ImmutablesSortedSetComparator
check
#102
Changes from 14 commits
3b77e41
9066020
88f26eb
9c81adb
89812fd
aabf777
69d0b19
7061f57
f4f5c4d
7553dbc
3634106
5501429
402da2b
42c5bb7
42aae4c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
package tech.picnic.errorprone.bugpatterns; | ||
|
||
import static com.google.errorprone.BugPattern.LinkType.NONE; | ||
import static com.google.errorprone.BugPattern.SeverityLevel.ERROR; | ||
import static com.google.errorprone.BugPattern.StandardTags.LIKELY_ERROR; | ||
import static com.google.errorprone.matchers.Matchers.allOf; | ||
import static com.google.errorprone.matchers.Matchers.anyOf; | ||
import static com.google.errorprone.matchers.Matchers.enclosingClass; | ||
import static com.google.errorprone.matchers.Matchers.hasAnnotation; | ||
import static com.google.errorprone.matchers.Matchers.hasModifier; | ||
import static com.google.errorprone.matchers.Matchers.isSubtypeOf; | ||
import static com.google.errorprone.matchers.Matchers.methodReturns; | ||
import static com.google.errorprone.matchers.Matchers.not; | ||
|
||
import com.google.auto.service.AutoService; | ||
import com.google.errorprone.BugPattern; | ||
import com.google.errorprone.VisitorState; | ||
import com.google.errorprone.bugpatterns.BugChecker; | ||
import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher; | ||
import com.google.errorprone.fixes.SuggestedFix; | ||
import com.google.errorprone.fixes.SuggestedFixes; | ||
import com.google.errorprone.matchers.Description; | ||
import com.google.errorprone.matchers.Matcher; | ||
import com.sun.source.tree.MethodTree; | ||
import java.util.SortedSet; | ||
import javax.lang.model.element.Modifier; | ||
|
||
/** | ||
* A {@link BugChecker} which flags {@link SortedSet} property declarations inside | ||
* {@code @Value.Immutable}- and {@code @Value.Modifiable}-annotated types that lack a | ||
* {@code @Value.NaturalOrder} or {@code @Value.ReverseOrder} annotation. | ||
* | ||
* <p>Without such an annotation: | ||
* | ||
* <ul> | ||
* <li>deserialization of the enclosing type requires that the associated JSON property is | ||
* present, contrary to the way in which Immutables handles other collection properties; and | ||
* <li>different instances may use different comparator implementations (e.g. deserialization | ||
* would default to natural order sorting), potentially leading to subtle bugs. | ||
* </ul> | ||
*/ | ||
@AutoService(BugChecker.class) | ||
@BugPattern( | ||
summary = | ||
"`SortedSet` properties of a `@Value.Immutable` or `@Value.Modifiable` type must be " | ||
+ "annotated `@Value.NaturalOrder` or `@Value.ReverseOrder`", | ||
linkType = NONE, | ||
severity = ERROR, | ||
tags = LIKELY_ERROR) | ||
public final class ImmutablesSortedSetComparator extends BugChecker implements MethodTreeMatcher { | ||
private static final long serialVersionUID = 1L; | ||
private static final Matcher<MethodTree> METHOD_LACKS_ANNOTATION = | ||
allOf( | ||
methodReturns(isSubtypeOf(SortedSet.class)), | ||
anyOf( | ||
allOf( | ||
hasModifier(Modifier.ABSTRACT), | ||
enclosingClass( | ||
anyOf( | ||
hasAnnotation("org.immutables.value.Value.Immutable"), | ||
hasAnnotation("org.immutables.value.Value.Modifiable")))), | ||
hasAnnotation("org.immutables.value.Value.Default")), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does this annotation even take effect if the enclosing class is not annotated with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Didn't check, but suspect not. Still my reasoning was that "if it's present, then so should the other annotation". (If indeed |
||
not( | ||
anyOf( | ||
hasAnnotation("org.immutables.value.Value.NaturalOrder"), | ||
hasAnnotation("org.immutables.value.Value.ReverseOrder")))); | ||
|
||
@Override | ||
public Description matchMethod(MethodTree tree, VisitorState state) { | ||
if (!METHOD_LACKS_ANNOTATION.matches(tree, state)) { | ||
return Description.NO_MATCH; | ||
} | ||
|
||
SuggestedFix.Builder builder = SuggestedFix.builder(); | ||
String valueTypeIdentifier = | ||
SuggestedFixes.qualifyType(state, builder, "org.immutables.value.Value"); | ||
return describeMatch( | ||
tree, | ||
builder.prefixWith(tree, String.format("@%s.NaturalOrder ", valueTypeIdentifier)).build()); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,182 @@ | ||
package tech.picnic.errorprone.bugpatterns; | ||
|
||
import com.google.errorprone.BugCheckerRefactoringTestHelper; | ||
import com.google.errorprone.BugCheckerRefactoringTestHelper.TestMode; | ||
import com.google.errorprone.CompilationTestHelper; | ||
import org.junit.jupiter.api.Test; | ||
|
||
final class ImmutablesSortedSetComparatorTest { | ||
private final CompilationTestHelper compilationTestHelper = | ||
CompilationTestHelper.newInstance(ImmutablesSortedSetComparator.class, getClass()); | ||
private final BugCheckerRefactoringTestHelper refactoringTestHelper = | ||
BugCheckerRefactoringTestHelper.newInstance(ImmutablesSortedSetComparator.class, getClass()); | ||
|
||
@Test | ||
void identification() { | ||
compilationTestHelper | ||
.addSourceLines( | ||
"A.java", | ||
"import com.google.common.collect.ContiguousSet;", | ||
"import com.google.common.collect.ImmutableSet;", | ||
"import com.google.common.collect.ImmutableSortedSet;", | ||
"import java.util.NavigableSet;", | ||
"import java.util.Set;", | ||
"import java.util.SortedSet;", | ||
"import java.util.TreeSet;", | ||
"import org.immutables.value.Value;", | ||
"", | ||
"interface A {", | ||
" @Value.Immutable", | ||
" interface ImmutableInterface {", | ||
" Set<String> set();", | ||
"", | ||
" // BUG: Diagnostic contains:", | ||
" SortedSet<String> sortedSet();", | ||
"", | ||
" @Value.NaturalOrder", | ||
" SortedSet<String> sortedSet2();", | ||
" }", | ||
"", | ||
" @Value.Modifiable", | ||
" interface ModifiableInterfaceWithDefaults {", | ||
" @Value.Default", | ||
" default Set<Integer> set() {", | ||
" return new TreeSet<>();", | ||
" }", | ||
"", | ||
" @Value.Default", | ||
" // BUG: Diagnostic contains:", | ||
" default NavigableSet<Integer> navigableSet() {", | ||
" return new TreeSet<>();", | ||
" }", | ||
"", | ||
" @Value.Default", | ||
" @Value.ReverseOrder", | ||
" default NavigableSet<Integer> navigableSet2() {", | ||
" return new TreeSet<>();", | ||
" }", | ||
"", | ||
" default NavigableSet<Integer> nonPropertyNavigableSet() {", | ||
" return new TreeSet<>();", | ||
" }", | ||
" }", | ||
"", | ||
" interface NonImmutablesInterface {", | ||
" SortedSet<String> sortedSet();", | ||
" }", | ||
"", | ||
" @Value.Immutable", | ||
" abstract class AbstractImmutableWithDefaults {", | ||
" @Value.Default", | ||
" ImmutableSet<Integer> immutableSet() {", | ||
" return ImmutableSet.of();", | ||
" }", | ||
"", | ||
" @Value.Default", | ||
" // BUG: Diagnostic contains:", | ||
" ImmutableSortedSet<String> immutableSortedSet() {", | ||
" return ImmutableSortedSet.of();", | ||
" }", | ||
"", | ||
" @Value.Default", | ||
" @Value.NaturalOrder", | ||
" ImmutableSortedSet<String> immutableSortedSet2() {", | ||
" return ImmutableSortedSet.of();", | ||
" }", | ||
"", | ||
" ImmutableSortedSet<String> nonPropertyImmutableSortedSet() {", | ||
" return ImmutableSortedSet.of();", | ||
" }", | ||
" }", | ||
"", | ||
" @Value.Modifiable", | ||
" abstract class AbstractModifiable {", | ||
" abstract ImmutableSet<Integer> immutableSet();", | ||
"", | ||
" // BUG: Diagnostic contains:", | ||
" abstract ContiguousSet<Integer> contiguousSet();", | ||
"", | ||
" @Value.ReverseOrder", | ||
" abstract ContiguousSet<Integer> contiguousSet2();", | ||
" }", | ||
"", | ||
" abstract class AbstractNonImmutables {", | ||
" abstract SortedSet<Integer> sortedSet();", | ||
" }", | ||
"}") | ||
.doTest(); | ||
} | ||
|
||
@Test | ||
void replacement() { | ||
refactoringTestHelper | ||
.addInputLines( | ||
"A.java", | ||
"import com.google.common.collect.ImmutableSortedSet;", | ||
"import java.util.SortedSet;", | ||
"import org.immutables.value.Value;", | ||
"", | ||
"@Value.Immutable", | ||
"abstract class A {", | ||
" abstract ImmutableSortedSet<String> sortedSet();", | ||
"", | ||
" @Value.Modifiable", | ||
" interface B {", | ||
" SortedSet<String> sortedSet();", | ||
" }", | ||
"}") | ||
.addOutputLines( | ||
"A.java", | ||
"import com.google.common.collect.ImmutableSortedSet;", | ||
"import java.util.SortedSet;", | ||
"import org.immutables.value.Value;", | ||
"", | ||
"@Value.Immutable", | ||
"abstract class A {", | ||
" @Value.NaturalOrder", | ||
" abstract ImmutableSortedSet<String> sortedSet();", | ||
"", | ||
" @Value.Modifiable", | ||
" interface B {", | ||
" @Value.NaturalOrder", | ||
" SortedSet<String> sortedSet();", | ||
" }", | ||
"}") | ||
.doTest(TestMode.TEXT_MATCH); | ||
} | ||
|
||
@Test | ||
void replacementWithImportClash() { | ||
refactoringTestHelper | ||
.addInputLines( | ||
"MySpringService.java", | ||
"import com.google.common.collect.ImmutableSortedSet;", | ||
"import org.springframework.beans.factory.annotation.Value;", | ||
"", | ||
"class MySpringService {", | ||
" MySpringService(@Value(\"${someProperty}\") String prop) {}", | ||
" ;", | ||
"", | ||
" @org.immutables.value.Value.Immutable", | ||
" interface A {", | ||
" ImmutableSortedSet<String> sortedSet();", | ||
" }", | ||
"}") | ||
.addOutputLines( | ||
"MySpringService.java", | ||
"import com.google.common.collect.ImmutableSortedSet;", | ||
"import org.springframework.beans.factory.annotation.Value;", | ||
"", | ||
"class MySpringService {", | ||
" MySpringService(@Value(\"${someProperty}\") String prop) {}", | ||
" ;", | ||
"", | ||
" @org.immutables.value.Value.Immutable", | ||
" interface A {", | ||
" @org.immutables.value.Value.NaturalOrder", | ||
" ImmutableSortedSet<String> sortedSet();", | ||
" }", | ||
"}") | ||
.doTest(TestMode.TEXT_MATCH); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like this matcher construction a lot more. Thanks!