Skip to content

Commit 5734aac

Browse files
committed
add tests and checks for custom annotations and improve validation
1 parent c87128b commit 5734aac

14 files changed

Lines changed: 177 additions & 12 deletions

src/main/java/it/aboutbits/checkstyle/JspecifyAnnotationOrderCheck.java

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -48,27 +48,25 @@ public void visitToken(DetailAST ast) {
4848
return;
4949
}
5050
DetailAST lastAnnotation = null;
51-
var hasCloseAnnotation = false;
51+
DetailAST firstCloseAnnotation = null;
5252
String firstCloseName = null;
5353
for (var child = modifiers.getFirstChild(); child != null; child = child.getNextSibling()) {
5454
if (child.getType() != TokenTypes.ANNOTATION) {
5555
continue;
5656
}
5757
var name = AnnotationNames.simpleName(child);
5858
lastAnnotation = child;
59-
if (name != null && closeAnnotations.contains(name)) {
60-
hasCloseAnnotation = true;
61-
if (firstCloseName == null) {
62-
firstCloseName = name;
63-
}
59+
if (name != null && closeAnnotations.contains(name) && firstCloseAnnotation == null) {
60+
firstCloseAnnotation = child;
61+
firstCloseName = name;
6462
}
6563
}
66-
if (!hasCloseAnnotation || lastAnnotation == null) {
64+
if (firstCloseAnnotation == null || lastAnnotation == null) {
6765
return;
6866
}
6967
var lastName = AnnotationNames.simpleName(lastAnnotation);
7068
if (lastName == null || !closeAnnotations.contains(lastName)) {
71-
log(lastAnnotation, MSG_KEY, firstCloseName);
69+
log(firstCloseAnnotation, MSG_KEY, firstCloseName);
7270
}
7371
}
7472
}

src/main/java/it/aboutbits/checkstyle/JspecifyMapStructMapperAnnotationCheck.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public void setNullUnmarkedAnnotationName(String name) {
2828

2929
@Override
3030
public int[] getDefaultTokens() {
31-
return new int[]{TokenTypes.INTERFACE_DEF};
31+
return new int[]{TokenTypes.INTERFACE_DEF, TokenTypes.CLASS_DEF};
3232
}
3333

3434
@Override
@@ -47,6 +47,9 @@ public void visitToken(DetailAST ast) {
4747
if (modifiers == null) {
4848
return;
4949
}
50+
if (ast.getType() == TokenTypes.CLASS_DEF && modifiers.findFirstToken(TokenTypes.ABSTRACT) == null) {
51+
return;
52+
}
5053
var annotations = new ArrayList<DetailAST>();
5154
var hasMapper = false;
5255
for (var child = modifiers.getFirstChild(); child != null; child = child.getNextSibling()) {
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
jspecify.toplevel.missing=Top-level type ''{0}'' must be annotated with @NullMarked or @NullUnmarked.
22
jspecify.order.notLast=@{0} must be the last annotation before the type declaration.
33
jspecify.inline.notInline=@{0} must appear inline before the type, not on its own line above the declaration.
4-
jspecify.mapstruct.annotation.invalid=MapStruct @Mapper interface ''{0}'' must end with @AnnotateWith(NullUnmarked.class) then @NullUnmarked as the last two annotations before ''interface''.
4+
jspecify.mapstruct.annotation.invalid=MapStruct @Mapper type ''{0}'' must end with @AnnotateWith(NullUnmarked.class) then @NullUnmarked as the last two annotations before the ''interface''/''abstract class'' keyword.

src/test/java/it/aboutbits/checkstyle/JspecifyAnnotationOrderCheckTest.java

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import org.junit.jupiter.api.Test;
44

55
import java.util.List;
6+
import java.util.Map;
67

78
import static org.junit.jupiter.api.Assertions.assertEquals;
89
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -18,21 +19,45 @@ void jspecifyAnnotationAsLastPasses() throws Exception {
1819
}
1920

2021
@Test
21-
void jspecifyAnnotationFollowedByOtherFails() throws Exception {
22+
void jspecifyAnnotationFollowedByOtherFailsAndHighlightsCloseAnnotation() throws Exception {
2223
var v = runCheck(JspecifyAnnotationOrderCheck.class, BASE + "BadInMiddle.java");
2324
assertEquals(1, v.size());
2425
assertTrue(v.get(0).contains("NullMarked"), v.toString());
26+
assertTrue(v.get(0).startsWith("6:"), "violation should be reported on the @NullMarked line: " + v);
2527
}
2628

2729
@Test
28-
void jspecifyAnnotationAsFirstFails() throws Exception {
30+
void jspecifyAnnotationAsFirstFailsAndHighlightsCloseAnnotation() throws Exception {
2931
var v = runCheck(JspecifyAnnotationOrderCheck.class, BASE + "BadFirst.java");
3032
assertEquals(1, v.size());
33+
assertTrue(v.get(0).startsWith("5:"), "violation should be reported on the @NullMarked line: " + v);
3134
}
3235

3336
@Test
3437
void noJspecifyAnnotationDoesNotFire() throws Exception {
3538
var v = runCheck(JspecifyAnnotationOrderCheck.class, BASE + "NoJspecify.java");
3639
assertEquals(List.of(), v);
3740
}
41+
42+
@Test
43+
void customCloseAnnotationsArePicked() throws Exception {
44+
var properties = Map.of("closeAnnotations", "Custom,AnotherClose");
45+
46+
var ok = runCheck(JspecifyAnnotationOrderCheck.class, BASE + "CustomCloseLast.java", properties);
47+
assertEquals(List.of(), ok);
48+
49+
var bad = runCheck(JspecifyAnnotationOrderCheck.class, BASE + "CustomCloseInMiddle.java", properties);
50+
assertEquals(1, bad.size());
51+
assertTrue(bad.get(0).contains("Custom"), bad.toString());
52+
}
53+
54+
@Test
55+
void closeAnnotationsAreTrimmed() throws Exception {
56+
var v = runCheck(
57+
JspecifyAnnotationOrderCheck.class,
58+
BASE + "GoodLast.java",
59+
Map.of("closeAnnotations", " NullMarked , NullUnmarked ")
60+
);
61+
assertEquals(List.of(), v);
62+
}
3863
}

src/test/java/it/aboutbits/checkstyle/JspecifyMapStructMapperAnnotationCheckTest.java

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import org.junit.jupiter.api.Test;
44

55
import java.util.List;
6+
import java.util.Map;
67

78
import static org.junit.jupiter.api.Assertions.assertEquals;
89
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -50,4 +51,61 @@ void interfaceWithoutMapperIsIgnored() throws Exception {
5051
runCheck(JspecifyMapStructMapperAnnotationCheck.class, BASE + "NonMapperInterfaceIgnored.java")
5152
);
5253
}
54+
55+
@Test
56+
void customMapperAnnotationNameIsHonored() throws Exception {
57+
var properties = Map.of("mapperAnnotationName", "MyMapper");
58+
59+
var ok = runCheck(JspecifyMapStructMapperAnnotationCheck.class, BASE + "CustomMapperGood.java", properties);
60+
assertEquals(List.of(), ok);
61+
62+
var bad = runCheck(JspecifyMapStructMapperAnnotationCheck.class, BASE + "CustomMapperBad.java", properties);
63+
assertEquals(1, bad.size());
64+
assertTrue(bad.get(0).contains("CustomMapperBad"), bad.toString());
65+
}
66+
67+
@Test
68+
void customAnnotateWithAnnotationNameIsHonored() throws Exception {
69+
var properties = Map.of("annotateWithAnnotationName", "MyAnnotateWith");
70+
71+
var ok = runCheck(JspecifyMapStructMapperAnnotationCheck.class, BASE + "CustomAnnotateWithGood.java", properties);
72+
assertEquals(List.of(), ok);
73+
74+
var bad = runCheck(JspecifyMapStructMapperAnnotationCheck.class, BASE + "GoodMapper.java", properties);
75+
assertEquals(1, bad.size(), bad.toString());
76+
}
77+
78+
@Test
79+
void customNullUnmarkedAnnotationNameIsHonored() throws Exception {
80+
var properties = Map.of("nullUnmarkedAnnotationName", "MyUnmarked");
81+
82+
var ok = runCheck(JspecifyMapStructMapperAnnotationCheck.class, BASE + "CustomNullUnmarkedGood.java", properties);
83+
assertEquals(List.of(), ok);
84+
85+
var bad = runCheck(JspecifyMapStructMapperAnnotationCheck.class, BASE + "GoodMapper.java", properties);
86+
assertEquals(1, bad.size(), bad.toString());
87+
}
88+
89+
@Test
90+
void wellOrderedAbstractClassMapperPasses() throws Exception {
91+
assertEquals(
92+
List.of(),
93+
runCheck(JspecifyMapStructMapperAnnotationCheck.class, BASE + "GoodAbstractMapper.java")
94+
);
95+
}
96+
97+
@Test
98+
void abstractClassMapperWithSwappedOrderFails() throws Exception {
99+
var v = runCheck(JspecifyMapStructMapperAnnotationCheck.class, BASE + "BadAbstractMapperOrderSwapped.java");
100+
assertEquals(1, v.size());
101+
assertTrue(v.get(0).contains("BadAbstractMapperOrderSwapped"), v.toString());
102+
}
103+
104+
@Test
105+
void nonAbstractClassWithMapperAnnotationIsIgnored() throws Exception {
106+
assertEquals(
107+
List.of(),
108+
runCheck(JspecifyMapStructMapperAnnotationCheck.class, BASE + "NonAbstractClassWithMapperAnnotationIgnored.java")
109+
);
110+
}
53111
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package fixtures.r2;
2+
3+
@Custom
4+
@Deprecated
5+
public class CustomCloseInMiddle {
6+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package fixtures.r2;
2+
3+
@Deprecated
4+
@Custom
5+
public class CustomCloseLast {
6+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package fixtures.r4;
2+
3+
import org.jspecify.annotations.NullUnmarked;
4+
import org.mapstruct.AnnotateWith;
5+
import org.mapstruct.Mapper;
6+
7+
@Mapper
8+
@NullUnmarked
9+
@AnnotateWith(NullUnmarked.class)
10+
public abstract class BadAbstractMapperOrderSwapped {
11+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package fixtures.r4;
2+
3+
import org.jspecify.annotations.NullUnmarked;
4+
import org.mapstruct.Mapper;
5+
6+
@Mapper
7+
@MyAnnotateWith(NullUnmarked.class)
8+
@NullUnmarked
9+
public interface CustomAnnotateWithGood {
10+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package fixtures.r4;
2+
3+
import org.jspecify.annotations.NullUnmarked;
4+
import org.mapstruct.AnnotateWith;
5+
6+
@MyMapper
7+
@NullUnmarked
8+
@AnnotateWith(NullUnmarked.class)
9+
public interface CustomMapperBad {
10+
}

0 commit comments

Comments
 (0)