Skip to content

Commit 355abdc

Browse files
committed
Add JSpecifyPreparator to consistently format @Nullable
Fixes gh-435
1 parent 79d64af commit 355abdc

File tree

8 files changed

+235
-3
lines changed

8 files changed

+235
-3
lines changed

spring-javaformat-eclipse/io.spring.javaformat.eclipse/src/io/spring/javaformat/eclipse/projectsettings/org.eclipse.jdt.ui.prefs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
cleanup.add_all=false
12
cleanup.add_default_serial_version_id=true
23
cleanup.add_generated_serial_version_id=false
34
cleanup.add_missing_annotations=true

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2017-2023 the original author or authors.
2+
* Copyright 2017-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package example;
2+
3+
import org.jspecify.annotations.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 String myMethod(String param);
15+
16+
@Override
17+
public @Nullable String myPublicMethod(String param);
18+
19+
Object myArrayMethod(@Nullable String @Nullable [] array, @Nullable String @Nullable ... varargs);
20+
21+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package example;
2+
3+
import org.jspecify.annotations.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 @Nullable [] array, @Nullable
18+
String @Nullable ... varargs);
19+
20+
}

spring-javaformat/spring-javaformat-formatter/src/main/java/io/spring/javaformat/formatter/jdk17/eclipse/EclipseJdk17CodeFormatter.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2017-2024 the original author or authors.
2+
* Copyright 2017-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -43,6 +43,7 @@ public EclipseJdk17CodeFormatter(JavaFormatConfig javaFormatConfig) {
4343
this.appliedOptions = options;
4444
addPreparator(new JavadocLineBreakPreparator());
4545
addPreparator(new CodeLineBreakPreparator());
46+
addPreparator(new JSpecifyPreparator());
4647
}
4748

4849
@Override
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
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.formatter.jdk17.eclipse;
18+
19+
import java.util.Arrays;
20+
import java.util.HashSet;
21+
import java.util.List;
22+
import java.util.Set;
23+
24+
import io.spring.javaformat.eclipse.jdt.jdk17.core.dom.ASTNode;
25+
import io.spring.javaformat.eclipse.jdt.jdk17.core.dom.ASTVisitor;
26+
import io.spring.javaformat.eclipse.jdt.jdk17.core.dom.Annotation;
27+
import io.spring.javaformat.eclipse.jdt.jdk17.core.dom.IExtendedModifier;
28+
import io.spring.javaformat.eclipse.jdt.jdk17.core.dom.MethodDeclaration;
29+
import io.spring.javaformat.eclipse.jdt.jdk17.core.dom.SingleVariableDeclaration;
30+
import io.spring.javaformat.eclipse.jdt.jdk17.core.formatter.CodeFormatter;
31+
import io.spring.javaformat.eclipse.jdt.jdk17.internal.formatter.Preparator;
32+
import io.spring.javaformat.eclipse.jdt.jdk17.internal.formatter.TokenManager;
33+
34+
public class JSpecifyPreparator implements Preparator {
35+
36+
private static final Set<String> ANNOTATION_NAMES = new HashSet<>(
37+
Arrays.asList("NonNull", "Nullable", "NullMarked", "NullUnmarked"));
38+
39+
@Override
40+
public void apply(int kind, TokenManager tokenManager, ASTNode astRoot) {
41+
if ((kind & CodeFormatter.K_COMPILATION_UNIT) != 0) {
42+
ASTVisitor visitor = new Vistor(tokenManager);
43+
astRoot.accept(visitor);
44+
}
45+
}
46+
47+
private static class Vistor extends ASTVisitor {
48+
49+
private final TokenManager tokenManager;
50+
51+
Vistor(TokenManager tokenManager) {
52+
this.tokenManager = tokenManager;
53+
}
54+
55+
@Override
56+
@SuppressWarnings("unchecked")
57+
public boolean visit(MethodDeclaration node) {
58+
Annotation lastAnnotation = getLastAnnotation((List<IExtendedModifier>) node.modifiers());
59+
if (isJSpecifyAnnotation(lastAnnotation)) {
60+
this.tokenManager.lastTokenIn(lastAnnotation, -1).clearLineBreaksAfter();
61+
}
62+
return true;
63+
}
64+
65+
@Override
66+
@SuppressWarnings("unchecked")
67+
public void endVisit(SingleVariableDeclaration node) {
68+
if (node.isVarargs()) {
69+
List<Annotation> annotations = node.varargsAnnotations();
70+
Annotation lastAnnotation = getLastAnnotation(annotations);
71+
if (isJSpecifyAnnotation(lastAnnotation)) {
72+
this.tokenManager.lastTokenIn(lastAnnotation, -1).spaceAfter();
73+
}
74+
}
75+
}
76+
77+
private Annotation getLastAnnotation(List<? extends IExtendedModifier> modifiers) {
78+
Annotation annotation = null;
79+
for (IExtendedModifier modifier : modifiers) {
80+
if (!modifier.isAnnotation()) {
81+
return annotation;
82+
}
83+
annotation = (Annotation) modifier;
84+
}
85+
return annotation;
86+
}
87+
88+
private boolean isJSpecifyAnnotation(Annotation annotation) {
89+
return (annotation != null) && ANNOTATION_NAMES.contains(annotation.getTypeName().toString());
90+
}
91+
92+
}
93+
94+
}

spring-javaformat/spring-javaformat-formatter/src/main/java/io/spring/javaformat/formatter/jdk8/eclipse/EclipseJdk8CodeFormatter.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2017-2024 the original author or authors.
2+
* Copyright 2017-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -43,6 +43,7 @@ public EclipseJdk8CodeFormatter(JavaFormatConfig javaFormatConfig) {
4343
this.appliedOptions = options;
4444
addPreparator(new JavadocLineBreakPreparator());
4545
addPreparator(new CodeLineBreakPreparator());
46+
addPreparator(new JSpecifyPreparator());
4647
}
4748

4849
@Override
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
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.formatter.jdk8.eclipse;
18+
19+
import java.util.Arrays;
20+
import java.util.HashSet;
21+
import java.util.List;
22+
import java.util.Set;
23+
24+
import io.spring.javaformat.eclipse.jdt.jdk8.core.dom.ASTNode;
25+
import io.spring.javaformat.eclipse.jdt.jdk8.core.dom.ASTVisitor;
26+
import io.spring.javaformat.eclipse.jdt.jdk8.core.dom.Annotation;
27+
import io.spring.javaformat.eclipse.jdt.jdk8.core.dom.IExtendedModifier;
28+
import io.spring.javaformat.eclipse.jdt.jdk8.core.dom.MethodDeclaration;
29+
import io.spring.javaformat.eclipse.jdt.jdk8.core.dom.SingleVariableDeclaration;
30+
import io.spring.javaformat.eclipse.jdt.jdk8.core.formatter.CodeFormatter;
31+
import io.spring.javaformat.eclipse.jdt.jdk8.internal.formatter.Preparator;
32+
import io.spring.javaformat.eclipse.jdt.jdk8.internal.formatter.TokenManager;
33+
34+
public class JSpecifyPreparator implements Preparator {
35+
36+
private static final Set<String> ANNOTATION_NAMES = new HashSet<>(
37+
Arrays.asList("NonNull", "Nullable", "NullMarked", "NullUnmarked"));
38+
39+
@Override
40+
public void apply(int kind, TokenManager tokenManager, ASTNode astRoot) {
41+
if ((kind & CodeFormatter.K_COMPILATION_UNIT) != 0) {
42+
ASTVisitor visitor = new Vistor(tokenManager);
43+
astRoot.accept(visitor);
44+
}
45+
}
46+
47+
private static class Vistor extends ASTVisitor {
48+
49+
private final TokenManager tokenManager;
50+
51+
Vistor(TokenManager tokenManager) {
52+
this.tokenManager = tokenManager;
53+
}
54+
55+
@Override
56+
@SuppressWarnings("unchecked")
57+
public boolean visit(MethodDeclaration node) {
58+
Annotation lastAnnotation = getLastAnnotation((List<IExtendedModifier>) node.modifiers());
59+
if (isJSpecifyAnnotation(lastAnnotation)) {
60+
this.tokenManager.lastTokenIn(lastAnnotation, -1).clearLineBreaksAfter();
61+
}
62+
return true;
63+
}
64+
65+
@Override
66+
@SuppressWarnings("unchecked")
67+
public void endVisit(SingleVariableDeclaration node) {
68+
if (node.isVarargs()) {
69+
List<Annotation> annotations = node.varargsAnnotations();
70+
Annotation lastAnnotation = getLastAnnotation(annotations);
71+
if (isJSpecifyAnnotation(lastAnnotation)) {
72+
this.tokenManager.lastTokenIn(lastAnnotation, -1).spaceAfter();
73+
}
74+
}
75+
}
76+
77+
private Annotation getLastAnnotation(List<? extends IExtendedModifier> modifiers) {
78+
Annotation annotation = null;
79+
for (IExtendedModifier modifier : modifiers) {
80+
if (!modifier.isAnnotation()) {
81+
return annotation;
82+
}
83+
annotation = (Annotation) modifier;
84+
}
85+
return annotation;
86+
}
87+
88+
private boolean isJSpecifyAnnotation(Annotation annotation) {
89+
return (annotation != null) && ANNOTATION_NAMES.contains(annotation.getTypeName().toString());
90+
}
91+
92+
}
93+
94+
}

0 commit comments

Comments
 (0)