Skip to content

Commit e0f7699

Browse files
committed
Refine JSpecify support
Update formatter to ignore non JSpecify `@Nullable` annotations and relax checkstyle to allow `@Nullable` on the same line. Fixes gh-440
1 parent 4122a3e commit e0f7699

File tree

14 files changed

+339
-8
lines changed

14 files changed

+339
-8
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
/*
2+
* Copyright 2017-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.spring.javaformat.checkstyle.check;
18+
19+
import java.util.Arrays;
20+
import java.util.HashSet;
21+
import java.util.Set;
22+
23+
import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
24+
import com.puppycrawl.tools.checkstyle.api.DetailAST;
25+
import com.puppycrawl.tools.checkstyle.api.TokenTypes;
26+
import com.puppycrawl.tools.checkstyle.checks.annotation.AnnotationLocationCheck;
27+
import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
28+
29+
/**
30+
* Spring variant of {@link AnnotationLocationCheck}.
31+
*
32+
* @author Phillip Webb
33+
*/
34+
public class SpringAnnotationLocationCheck extends AbstractCheck {
35+
36+
private static final Set<String> JSPECIFY_ANNOTATION_NAMES = new HashSet<>(
37+
Arrays.asList("NonNull", "Nullable", "NullMarked", "NullUnmarked"));
38+
39+
@Override
40+
public int[] getDefaultTokens() {
41+
return new int[] { TokenTypes.CLASS_DEF, TokenTypes.INTERFACE_DEF, TokenTypes.PACKAGE_DEF,
42+
TokenTypes.ENUM_CONSTANT_DEF, TokenTypes.ENUM_DEF, TokenTypes.METHOD_DEF, TokenTypes.CTOR_DEF,
43+
TokenTypes.VARIABLE_DEF, TokenTypes.RECORD_DEF, TokenTypes.COMPACT_CTOR_DEF, };
44+
}
45+
46+
@Override
47+
public int[] getAcceptableTokens() {
48+
return new int[] { TokenTypes.CLASS_DEF, TokenTypes.INTERFACE_DEF, TokenTypes.PACKAGE_DEF,
49+
TokenTypes.ENUM_CONSTANT_DEF, TokenTypes.ENUM_DEF, TokenTypes.METHOD_DEF, TokenTypes.CTOR_DEF,
50+
TokenTypes.VARIABLE_DEF, TokenTypes.ANNOTATION_DEF, TokenTypes.ANNOTATION_FIELD_DEF,
51+
TokenTypes.RECORD_DEF, TokenTypes.COMPACT_CTOR_DEF, };
52+
}
53+
54+
@Override
55+
public int[] getRequiredTokens() {
56+
return CommonUtil.EMPTY_INT_ARRAY;
57+
}
58+
59+
@Override
60+
public void visitToken(DetailAST ast) {
61+
if (ast.getType() != TokenTypes.VARIABLE_DEF || ast.getParent().getType() == TokenTypes.OBJBLOCK) {
62+
DetailAST node = ast.findFirstToken(TokenTypes.MODIFIERS);
63+
node = (node != null) ? node : ast.findFirstToken(TokenTypes.ANNOTATIONS);
64+
checkAnnotations(node, getExpectedAnnotationIndentation(node));
65+
}
66+
}
67+
68+
private int getExpectedAnnotationIndentation(DetailAST node) {
69+
return node.getColumnNo();
70+
}
71+
72+
private void checkAnnotations(DetailAST node, int correctIndentation) {
73+
DetailAST annotation = node.getFirstChild();
74+
while (annotation != null && annotation.getType() == TokenTypes.ANNOTATION) {
75+
checkAnnotation(correctIndentation, annotation);
76+
annotation = annotation.getNextSibling();
77+
}
78+
}
79+
80+
private void checkAnnotation(int correctIndentation, DetailAST annotation) {
81+
String annotationName = getAnnotationName(annotation);
82+
if (!isCorrectLocation(annotation) && !isJSpecifyAnnotation(annotationName)) {
83+
log(annotation, AnnotationLocationCheck.MSG_KEY_ANNOTATION_LOCATION_ALONE, annotationName);
84+
}
85+
else if (annotation.getColumnNo() != correctIndentation && !hasNodeBefore(annotation)) {
86+
log(annotation, AnnotationLocationCheck.MSG_KEY_ANNOTATION_LOCATION, annotationName,
87+
annotation.getColumnNo(), correctIndentation);
88+
}
89+
}
90+
91+
private String getAnnotationName(DetailAST annotation) {
92+
DetailAST identNode = annotation.findFirstToken(TokenTypes.IDENT);
93+
if (identNode == null) {
94+
identNode = annotation.findFirstToken(TokenTypes.DOT).findFirstToken(TokenTypes.IDENT);
95+
}
96+
return identNode.getText();
97+
}
98+
99+
private boolean isCorrectLocation(DetailAST annotation) {
100+
return !hasNodeBeside(annotation);
101+
}
102+
103+
private boolean hasNodeBeside(DetailAST annotation) {
104+
return hasNodeBefore(annotation) || hasNodeAfter(annotation);
105+
}
106+
107+
private boolean hasNodeBefore(DetailAST annotation) {
108+
int annotationLineNo = annotation.getLineNo();
109+
DetailAST previousNode = annotation.getPreviousSibling();
110+
return (previousNode != null) && (annotationLineNo == previousNode.getLineNo());
111+
}
112+
113+
private boolean hasNodeAfter(DetailAST annotation) {
114+
int annotationLineNo = annotation.getLineNo();
115+
DetailAST nextNode = annotation.getNextSibling();
116+
nextNode = (nextNode != null) ? nextNode : annotation.getParent().getNextSibling();
117+
return annotationLineNo == nextNode.getLineNo();
118+
}
119+
120+
private boolean isJSpecifyAnnotation(String annotationName) {
121+
return JSPECIFY_ANNOTATION_NAMES.contains(annotationName);
122+
}
123+
124+
}

spring-javaformat/spring-javaformat-checkstyle/src/main/resources/io/spring/javaformat/checkstyle/check/messages.properties

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
annotation.location=Annotation ''{0}'' have incorrect indentation level {1}, expected level should be {2}.
2+
annotation.location.alone=Annotation ''{0}'' should be alone on line.
13
catch.singleLetter=Single letter catch variable (use "ex" instead).
24
catch.wideEye=''o_O'' catch variable (use "ex" instead).
35
header.unexpected=Unexpected header.

spring-javaformat/spring-javaformat-checkstyle/src/main/resources/io/spring/javaformat/checkstyle/spring-checkstyle.xml

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,7 @@
2424
<module name="com.puppycrawl.tools.checkstyle.checks.annotation.MissingOverrideCheck" />
2525
<module name="com.puppycrawl.tools.checkstyle.checks.annotation.MissingDeprecatedCheck" />
2626
<module name="com.puppycrawl.tools.checkstyle.checks.annotation.PackageAnnotationCheck" />
27-
<module name="com.puppycrawl.tools.checkstyle.checks.annotation.AnnotationLocationCheck">
28-
<property name="allowSamelineSingleParameterlessAnnotation"
29-
value="false" />
30-
</module>
27+
<module name="io.spring.javaformat.checkstyle.check.SpringAnnotationLocationCheck" />
3128

3229
<!-- Block Checks -->
3330
<module name="com.puppycrawl.tools.checkstyle.checks.blocks.EmptyBlockCheck">
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
+0 errors
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* Copyright 2017-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import org.jspecify.annotations.Nullable;
18+
19+
/**
20+
* This is a valid example.
21+
*
22+
* @author Phillip Webb
23+
*/
24+
public class AnnotationOnNewLine {
25+
26+
@Override
27+
public String toString() {
28+
return "";
29+
}
30+
31+
@Nullable String test1() {
32+
return "";
33+
}
34+
35+
@Override
36+
@Nullable String test2() {
37+
return "";
38+
}
39+
40+
@Override
41+
public @Nullable String test3() {
42+
return "";
43+
}
44+
45+
}

spring-javaformat/spring-javaformat-formatter-tests/src/test/java/io/spring/javaformat/formatter/AbstractFormatterTests.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import java.nio.charset.StandardCharsets;
2222
import java.nio.file.Files;
2323
import java.util.ArrayList;
24+
import java.util.Comparator;
2425
import java.util.List;
2526

2627
import io.spring.javaformat.config.JavaBaseline;
@@ -60,6 +61,7 @@ protected static Item[] items(String expectedOverride) {
6061
addItem(items, javaBaseline, source, expected, config);
6162
}
6263
}
64+
items.sort(Comparator.comparing(Item::getName));
6365
return items.toArray(new Item[0]);
6466
}
6567

@@ -139,6 +141,10 @@ private JavaFormatConfig loadConfig(JavaBaseline javaBaseline, File configFile)
139141
return JavaFormatConfig.of(javaBaseline, config.getIndentationStyle());
140142
}
141143

144+
String getName() {
145+
return this.source.getName();
146+
}
147+
142148
public File getSource() {
143149
return this.source;
144150
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package example;
2+
3+
import com.example.Nullable;
4+
5+
/**
6+
* Nullable.
7+
*
8+
* @author Phillip Webb
9+
* @since 1.0.0
10+
*/
11+
public interface ExampleNullables {
12+
13+
@Override
14+
@Nullable
15+
String myMethod(String param);
16+
17+
@Override
18+
public @Nullable String myPublicMethod(String param);
19+
20+
Object myArrayMethod(@Nullable String string, @Nullable String @Nullable [] array,
21+
@Nullable String @Nullable... varargs);
22+
23+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package example;
2+
3+
import org.jspecify.annotations.*;
4+
5+
/**
6+
* Nullable.
7+
*
8+
* @author Phillip Webb
9+
* @since 1.0.0
10+
*/
11+
public interface ExampleNullables {
12+
13+
@Override
14+
@Nullable String myMethod(String param);
15+
16+
@Override
17+
public @Nullable String myPublicMethod(String param);
18+
19+
Object myArrayMethod(@Nullable String string, @Nullable String @Nullable [] array,
20+
@Nullable String @Nullable ... varargs);
21+
22+
}

spring-javaformat/spring-javaformat-formatter-tests/src/test/resources/expected/nullable.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ public interface ExampleNullables {
1616
@Override
1717
public @Nullable String myPublicMethod(String param);
1818

19-
Object myArrayMethod(@Nullable String @Nullable [] array, @Nullable String @Nullable ... varargs);
19+
Object myArrayMethod(@Nullable String string, @Nullable String @Nullable [] array,
20+
@Nullable String @Nullable ... varargs);
2021

2122
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package example;
2+
3+
import com.example.Nullable;
4+
5+
/**
6+
* Nullable.
7+
*
8+
* @author Phillip Webb
9+
* @since 1.0.0
10+
*/
11+
public interface ExampleNullables {
12+
13+
@Override @Nullable String myMethod(String param);
14+
15+
@Override public @Nullable String myPublicMethod(String param);
16+
17+
Object myArrayMethod(@Nullable String string, @Nullable String @Nullable [] array, @Nullable
18+
String @Nullable ... varargs);
19+
20+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package example;
2+
3+
import org.jspecify.annotations.*;
4+
5+
/**
6+
* Nullable.
7+
*
8+
* @author Phillip Webb
9+
* @since 1.0.0
10+
*/
11+
public interface ExampleNullables {
12+
13+
@Override @Nullable String myMethod(String param);
14+
15+
@Override public @Nullable String myPublicMethod(String param);
16+
17+
Object myArrayMethod(@Nullable String string, @Nullable String @Nullable [] array, @Nullable
18+
String @Nullable ... varargs);
19+
20+
}

spring-javaformat/spring-javaformat-formatter-tests/src/test/resources/source/nullable.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ public interface ExampleNullables {
1414

1515
@Override public @Nullable String myPublicMethod(String param);
1616

17-
Object myArrayMethod(@Nullable String @Nullable [] array, @Nullable
17+
Object myArrayMethod(@Nullable String string, @Nullable String @Nullable [] array, @Nullable
1818
String @Nullable ... varargs);
1919

2020
}

0 commit comments

Comments
 (0)