Skip to content

Commit 1dcb36c

Browse files
committed
add jspecify rules and ability for local suppressions file
1 parent 55aa783 commit 1dcb36c

41 files changed

Lines changed: 966 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package it.aboutbits.checkstyle;
2+
3+
import com.puppycrawl.tools.checkstyle.api.DetailAST;
4+
import com.puppycrawl.tools.checkstyle.api.FullIdent;
5+
import com.puppycrawl.tools.checkstyle.api.TokenTypes;
6+
7+
final class AnnotationNames {
8+
9+
private AnnotationNames() {
10+
}
11+
12+
/**
13+
* Returns the simple name of an ANNOTATION node, regardless of whether the
14+
* source uses an imported name (@Foo) or a fully-qualified name (@a.b.Foo).
15+
*/
16+
static String simpleName(DetailAST annotation) {
17+
var ident = annotation.findFirstToken(TokenTypes.IDENT);
18+
if (ident != null) {
19+
return ident.getText();
20+
}
21+
var dot = annotation.findFirstToken(TokenTypes.DOT);
22+
if (dot != null) {
23+
var last = dot.getLastChild();
24+
if (last != null && last.getType() == TokenTypes.IDENT) {
25+
return last.getText();
26+
}
27+
}
28+
return null;
29+
}
30+
31+
/**
32+
* Returns the source-text name of an ANNOTATION node: the simple name when
33+
* the source uses an imported form, or the full dotted name when written
34+
* inline as a fully-qualified annotation.
35+
*/
36+
static String fullName(DetailAST annotation) {
37+
var child = annotation.findFirstToken(TokenTypes.IDENT);
38+
if (child == null) {
39+
child = annotation.findFirstToken(TokenTypes.DOT);
40+
}
41+
return child == null ? null : FullIdent.createFullIdent(child).getText();
42+
}
43+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package it.aboutbits.checkstyle;
2+
3+
import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
4+
import com.puppycrawl.tools.checkstyle.api.DetailAST;
5+
import com.puppycrawl.tools.checkstyle.api.TokenTypes;
6+
7+
import java.util.LinkedHashSet;
8+
import java.util.Set;
9+
10+
public class JspecifyAnnotationOrderCheck extends AbstractCheck {
11+
12+
public static final String MSG_KEY = "jspecify.order.notLast";
13+
14+
private Set<String> closeAnnotations = new LinkedHashSet<>(Set.of("NullMarked", "NullUnmarked"));
15+
16+
public void setCloseAnnotations(String... names) {
17+
var next = new LinkedHashSet<String>();
18+
for (var n : names) {
19+
next.add(n.trim());
20+
}
21+
this.closeAnnotations = next;
22+
}
23+
24+
@Override
25+
public int[] getDefaultTokens() {
26+
return new int[]{
27+
TokenTypes.CLASS_DEF,
28+
TokenTypes.INTERFACE_DEF,
29+
TokenTypes.ENUM_DEF,
30+
TokenTypes.RECORD_DEF,
31+
};
32+
}
33+
34+
@Override
35+
public int[] getAcceptableTokens() {
36+
return getDefaultTokens();
37+
}
38+
39+
@Override
40+
public int[] getRequiredTokens() {
41+
return getDefaultTokens();
42+
}
43+
44+
@Override
45+
public void visitToken(DetailAST ast) {
46+
var modifiers = ast.findFirstToken(TokenTypes.MODIFIERS);
47+
if (modifiers == null) {
48+
return;
49+
}
50+
DetailAST lastAnnotation = null;
51+
var hasCloseAnnotation = false;
52+
String firstCloseName = null;
53+
for (var child = modifiers.getFirstChild(); child != null; child = child.getNextSibling()) {
54+
if (child.getType() != TokenTypes.ANNOTATION) {
55+
continue;
56+
}
57+
var name = AnnotationNames.simpleName(child);
58+
lastAnnotation = child;
59+
if (name != null && closeAnnotations.contains(name)) {
60+
hasCloseAnnotation = true;
61+
if (firstCloseName == null) {
62+
firstCloseName = name;
63+
}
64+
}
65+
}
66+
if (!hasCloseAnnotation || lastAnnotation == null) {
67+
return;
68+
}
69+
var lastName = AnnotationNames.simpleName(lastAnnotation);
70+
if (lastName == null || !closeAnnotations.contains(lastName)) {
71+
log(lastAnnotation, MSG_KEY, firstCloseName);
72+
}
73+
}
74+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package it.aboutbits.checkstyle;
2+
3+
import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
4+
import com.puppycrawl.tools.checkstyle.api.DetailAST;
5+
import com.puppycrawl.tools.checkstyle.api.FullIdent;
6+
import com.puppycrawl.tools.checkstyle.api.TokenTypes;
7+
8+
import java.util.HashSet;
9+
import java.util.Set;
10+
11+
public class JspecifyInlineTypeUseCheck extends AbstractCheck {
12+
13+
public static final String MSG_KEY = "jspecify.inline.notInline";
14+
15+
private static final String JSPECIFY_PACKAGE = "org.jspecify.annotations";
16+
private static final String JSPECIFY_PACKAGE_DOT = JSPECIFY_PACKAGE + ".";
17+
private static final Set<String> TYPE_USE_ANNOTATIONS = Set.of("Nullable", "NonNull");
18+
19+
private Set<String> jspecifySimpleNamesInScope = new HashSet<>();
20+
21+
@Override
22+
public int[] getDefaultTokens() {
23+
return new int[]{
24+
TokenTypes.METHOD_DEF,
25+
TokenTypes.VARIABLE_DEF,
26+
TokenTypes.PARAMETER_DEF,
27+
};
28+
}
29+
30+
@Override
31+
public int[] getAcceptableTokens() {
32+
return getDefaultTokens();
33+
}
34+
35+
@Override
36+
public int[] getRequiredTokens() {
37+
return getDefaultTokens();
38+
}
39+
40+
@Override
41+
public void beginTree(DetailAST rootAST) {
42+
jspecifySimpleNamesInScope = new HashSet<>();
43+
for (var node = rootAST.getFirstChild(); node != null; node = node.getNextSibling()) {
44+
if (node.getType() != TokenTypes.IMPORT) {
45+
continue;
46+
}
47+
var first = node.getFirstChild();
48+
if (first == null) {
49+
continue;
50+
}
51+
var text = FullIdent.createFullIdent(first).getText();
52+
if (text.equals(JSPECIFY_PACKAGE + ".*")) {
53+
jspecifySimpleNamesInScope.addAll(TYPE_USE_ANNOTATIONS);
54+
} else if (text.startsWith(JSPECIFY_PACKAGE_DOT)) {
55+
var simple = text.substring(JSPECIFY_PACKAGE_DOT.length());
56+
if (TYPE_USE_ANNOTATIONS.contains(simple)) {
57+
jspecifySimpleNamesInScope.add(simple);
58+
}
59+
}
60+
}
61+
}
62+
63+
@Override
64+
public void visitToken(DetailAST ast) {
65+
var modifiers = ast.findFirstToken(TokenTypes.MODIFIERS);
66+
var type = ast.findFirstToken(TokenTypes.TYPE);
67+
if (modifiers == null || type == null) {
68+
return;
69+
}
70+
for (var child = modifiers.getFirstChild(); child != null; child = child.getNextSibling()) {
71+
if (child.getType() != TokenTypes.ANNOTATION) {
72+
continue;
73+
}
74+
var simple = AnnotationNames.simpleName(child);
75+
if (simple == null || !TYPE_USE_ANNOTATIONS.contains(simple)) {
76+
continue;
77+
}
78+
if (!isJspecifyAnnotation(child, simple)) {
79+
continue;
80+
}
81+
if (child.getLineNo() < type.getLineNo()) {
82+
log(child, MSG_KEY, simple);
83+
}
84+
}
85+
}
86+
87+
private boolean isJspecifyAnnotation(DetailAST annotation, String simpleName) {
88+
var fullName = AnnotationNames.fullName(annotation);
89+
if (fullName == null) {
90+
return false;
91+
}
92+
if (fullName.contains(".")) {
93+
return fullName.equals(JSPECIFY_PACKAGE_DOT + simpleName);
94+
}
95+
return jspecifySimpleNamesInScope.contains(simpleName);
96+
}
97+
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
package it.aboutbits.checkstyle;
2+
3+
import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
4+
import com.puppycrawl.tools.checkstyle.api.DetailAST;
5+
import com.puppycrawl.tools.checkstyle.api.TokenTypes;
6+
7+
import java.util.ArrayList;
8+
9+
public class JspecifyMapStructMapperAnnotationCheck extends AbstractCheck {
10+
11+
public static final String MSG_KEY = "jspecify.mapstruct.annotation.invalid";
12+
13+
private String mapperAnnotationName = "Mapper";
14+
private String annotateWithAnnotationName = "AnnotateWith";
15+
private String nullUnmarkedAnnotationName = "NullUnmarked";
16+
17+
public void setMapperAnnotationName(String name) {
18+
this.mapperAnnotationName = name;
19+
}
20+
21+
public void setAnnotateWithAnnotationName(String name) {
22+
this.annotateWithAnnotationName = name;
23+
}
24+
25+
public void setNullUnmarkedAnnotationName(String name) {
26+
this.nullUnmarkedAnnotationName = name;
27+
}
28+
29+
@Override
30+
public int[] getDefaultTokens() {
31+
return new int[]{TokenTypes.INTERFACE_DEF};
32+
}
33+
34+
@Override
35+
public int[] getAcceptableTokens() {
36+
return getDefaultTokens();
37+
}
38+
39+
@Override
40+
public int[] getRequiredTokens() {
41+
return getDefaultTokens();
42+
}
43+
44+
@Override
45+
public void visitToken(DetailAST ast) {
46+
var modifiers = ast.findFirstToken(TokenTypes.MODIFIERS);
47+
if (modifiers == null) {
48+
return;
49+
}
50+
var annotations = new ArrayList<DetailAST>();
51+
var hasMapper = false;
52+
for (var child = modifiers.getFirstChild(); child != null; child = child.getNextSibling()) {
53+
if (child.getType() != TokenTypes.ANNOTATION) {
54+
continue;
55+
}
56+
annotations.add(child);
57+
if (mapperAnnotationName.equals(AnnotationNames.simpleName(child))) {
58+
hasMapper = true;
59+
}
60+
}
61+
if (!hasMapper) {
62+
return;
63+
}
64+
if (annotations.size() < 2) {
65+
logInvalid(ast);
66+
return;
67+
}
68+
var last = annotations.get(annotations.size() - 1);
69+
var secondLast = annotations.get(annotations.size() - 2);
70+
var lastOk = nullUnmarkedAnnotationName.equals(AnnotationNames.simpleName(last));
71+
var secondLastOk = annotateWithAnnotationName.equals(AnnotationNames.simpleName(secondLast))
72+
&& annotateWithArgumentReferences(secondLast, nullUnmarkedAnnotationName);
73+
if (!lastOk || !secondLastOk) {
74+
logInvalid(ast);
75+
}
76+
}
77+
78+
private void logInvalid(DetailAST ast) {
79+
var name = ast.findFirstToken(TokenTypes.IDENT);
80+
log(ast, MSG_KEY, name != null ? name.getText() : "");
81+
}
82+
83+
private static boolean annotateWithArgumentReferences(DetailAST annotation, String expectedClassName) {
84+
for (var child = annotation.getFirstChild(); child != null; child = child.getNextSibling()) {
85+
var found = switch (child.getType()) {
86+
case TokenTypes.EXPR -> classLiteralName(child);
87+
case TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR -> memberValuePairClassLiteralName(child);
88+
default -> null;
89+
};
90+
if (expectedClassName.equals(found)) {
91+
return true;
92+
}
93+
}
94+
return false;
95+
}
96+
97+
private static String memberValuePairClassLiteralName(DetailAST pair) {
98+
var ident = pair.findFirstToken(TokenTypes.IDENT);
99+
if (ident == null || !"value".equals(ident.getText())) {
100+
return null;
101+
}
102+
var expr = pair.findFirstToken(TokenTypes.EXPR);
103+
return expr == null ? null : classLiteralName(expr);
104+
}
105+
106+
private static String classLiteralName(DetailAST expr) {
107+
var dot = expr.findFirstToken(TokenTypes.DOT);
108+
if (dot == null) {
109+
return null;
110+
}
111+
var classLiteral = dot.findFirstToken(TokenTypes.LITERAL_CLASS);
112+
if (classLiteral == null) {
113+
return null;
114+
}
115+
var ident = dot.findFirstToken(TokenTypes.IDENT);
116+
if (ident != null) {
117+
return ident.getText();
118+
}
119+
var nestedDot = dot.findFirstToken(TokenTypes.DOT);
120+
if (nestedDot != null) {
121+
var last = nestedDot.getLastChild();
122+
if (last != null && last.getType() == TokenTypes.IDENT) {
123+
return last.getText();
124+
}
125+
}
126+
return null;
127+
}
128+
}

0 commit comments

Comments
 (0)