Skip to content

Commit ff019c7

Browse files
authored
SONARJAVA-3904 Fix highlighting (#3699)
1 parent ace9f4c commit ff019c7

9 files changed

Lines changed: 194 additions & 23 deletions

File tree

java-frontend/src/main/java/org/sonar/java/ast/visitors/SyntaxHighlighterVisitor.java

Lines changed: 48 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,19 +25,25 @@
2525
import java.util.EnumMap;
2626
import java.util.List;
2727
import java.util.Map;
28+
import java.util.Optional;
2829
import java.util.Set;
2930
import java.util.stream.Collectors;
3031
import org.sonar.api.batch.sensor.highlighting.NewHighlighting;
3132
import org.sonar.api.batch.sensor.highlighting.TypeOfText;
3233
import org.sonar.java.SonarComponents;
3334
import org.sonar.java.ast.api.JavaKeyword;
3435
import org.sonar.java.ast.api.JavaRestrictedKeyword;
36+
import org.sonar.java.model.ModifiersUtils;
3537
import org.sonar.java.model.declaration.ClassTreeImpl;
3638
import org.sonar.plugins.java.api.JavaFileScannerContext;
3739
import org.sonar.plugins.java.api.tree.AnnotationTree;
40+
import org.sonar.plugins.java.api.tree.ClassTree;
41+
import org.sonar.plugins.java.api.tree.Modifier;
42+
import org.sonar.plugins.java.api.tree.ModifiersTree;
3843
import org.sonar.plugins.java.api.tree.SyntaxToken;
3944
import org.sonar.plugins.java.api.tree.SyntaxTrivia;
4045
import org.sonar.plugins.java.api.tree.Tree;
46+
import org.sonar.plugins.java.api.tree.YieldStatementTree;
4147

4248
public class SyntaxHighlighterVisitor extends SubscriptionVisitor {
4349

@@ -51,7 +57,7 @@ public class SyntaxHighlighterVisitor extends SubscriptionVisitor {
5157

5258
public SyntaxHighlighterVisitor(SonarComponents sonarComponents) {
5359
this.sonarComponents = sonarComponents;
54-
60+
5561
keywords = Collections.unmodifiableSet(Arrays.stream(JavaKeyword.keywordValues()).collect(Collectors.toSet()));
5662
restrictedKeywords = Collections.unmodifiableSet(Arrays.stream(JavaRestrictedKeyword.restrictedKeywordValues()).collect(Collectors.toSet()));
5763

@@ -71,9 +77,19 @@ public SyntaxHighlighterVisitor(SonarComponents sonarComponents) {
7177
@Override
7278
public List<Tree.Kind> nodesToVisit() {
7379
List<Tree.Kind> list = new ArrayList<>(typesByKind.keySet());
74-
list.add(Tree.Kind.MODULE);
7580
list.add(Tree.Kind.TOKEN);
7681
list.add(Tree.Kind.TRIVIA);
82+
// modules have their own set of restricted keywords
83+
list.add(Tree.Kind.MODULE);
84+
// 'yield' is a restricted keyword
85+
list.add(Tree.Kind.YIELD_STATEMENT);
86+
// 'record' is a restricted keyword
87+
list.add(Tree.Kind.RECORD);
88+
// sealed classes comes with restricted keyword 'permits', applying on classes and interfaces
89+
list.add(Tree.Kind.CLASS);
90+
list.add(Tree.Kind.INTERFACE);
91+
// sealed classes comes with restricted modifiers 'sealed' and 'non-sealed', applying on classes and interfaces
92+
list.add(Tree.Kind.MODIFIERS);
7793
return Collections.unmodifiableList(list);
7894
}
7995

@@ -88,15 +104,36 @@ public void scanFile(JavaFileScannerContext context) {
88104

89105
@Override
90106
public void visitNode(Tree tree) {
91-
if (tree.is(Tree.Kind.MODULE)) {
92-
withinModule = true;
93-
return;
94-
}
95-
if (tree.is(Tree.Kind.ANNOTATION)) {
96-
AnnotationTree annotationTree = (AnnotationTree) tree;
97-
highlight(annotationTree.atToken(), annotationTree.annotationType(), typesByKind.get(Tree.Kind.ANNOTATION));
98-
} else {
99-
highlight(tree, typesByKind.get(tree.kind()));
107+
switch (tree.kind()) {
108+
case MODULE:
109+
withinModule = true;
110+
return;
111+
case ANNOTATION:
112+
AnnotationTree annotationTree = (AnnotationTree) tree;
113+
highlight(annotationTree.atToken(), annotationTree.annotationType(), typesByKind.get(Tree.Kind.ANNOTATION));
114+
return;
115+
case YIELD_STATEMENT:
116+
// 'yield' is a 'restricted identifier' (JSL16, $3.9) only acting as keyword in a yield statement
117+
Optional.ofNullable(((YieldStatementTree) tree).yieldKeyword()).ifPresent(yieldKeyword -> highlight(yieldKeyword, TypeOfText.KEYWORD));
118+
return;
119+
case RECORD:
120+
// 'record' is a 'restricted identifier' (JSL16, $3.9) only acting as keyword in a record declaration
121+
highlight(((ClassTree) tree).declarationKeyword(), TypeOfText.KEYWORD);
122+
return;
123+
case CLASS:
124+
case INTERFACE:
125+
// 'permits' is a 'restricted identifier' (JSL16, $3.9) only acting as keyword in a class/interface declaration
126+
Optional.ofNullable(((ClassTree) tree).permitsKeyword()).ifPresent(permitsKeyword -> highlight(permitsKeyword, TypeOfText.KEYWORD));
127+
return;
128+
case MODIFIERS:
129+
// 'sealed' and 'non-sealed' are 'restricted identifier' (JSL16, $3.9) only acting as keyword in a class declaration
130+
ModifiersTree modifiers = (ModifiersTree) tree;
131+
ModifiersUtils.findModifier(modifiers, Modifier.SEALED).ifPresent(modifier -> highlight(modifier, TypeOfText.KEYWORD));
132+
ModifiersUtils.findModifier(modifiers, Modifier.NON_SEALED).ifPresent(modifier -> highlight(modifier, TypeOfText.KEYWORD));
133+
return;
134+
default:
135+
highlight(tree, typesByKind.get(tree.kind()));
136+
return;
100137
}
101138
}
102139

java-frontend/src/main/java/org/sonar/java/model/JParser.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -640,9 +640,14 @@ private ClassTreeImpl convertTypeDeclaration(TypeDeclaration e, ModifiersTreeImp
640640
.complete(modifiers, declarationKeyword, name)
641641
.completeTypeParameters(convertTypeParameters(e.typeParameters()));
642642

643-
if (e.getAST().isPreviewEnabled()) {
643+
if (e.getAST().isPreviewEnabled() && !e.permittedTypes().isEmpty()) {
644644
// TODO final in Java 17? relates to sealed classes
645-
convertSeparatedTypeList(e.permittedTypes(), t.permittedTypes());
645+
List permittedTypes = e.permittedTypes();
646+
InternalSyntaxToken permitsKeyword = firstTokenBefore((Type) permittedTypes.get(0), TerminalTokens.TokenNameRestrictedIdentifierpermits);
647+
QualifiedIdentifierListTreeImpl classPermittedTypes = QualifiedIdentifierListTreeImpl.emptyList();
648+
649+
convertSeparatedTypeList(permittedTypes, classPermittedTypes);
650+
t.completePermittedTypes(permitsKeyword, classPermittedTypes);
646651
}
647652

648653
if (!e.isInterface() && e.getSuperclassType() != null) {

java-frontend/src/main/java/org/sonar/java/model/declaration/ClassTreeImpl.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,9 @@ public class ClassTreeImpl extends JavaTree implements ClassTree {
6262
@Nullable
6363
private SyntaxToken implementsKeyword;
6464
private ListTree<TypeTree> superInterfaces;
65-
private final ListTree<TypeTree> permittedTypes = QualifiedIdentifierListTreeImpl.emptyList();
65+
@Nullable
66+
private SyntaxToken permitsKeyword;
67+
private ListTree<TypeTree> permittedTypes;
6668
@Nullable
6769
public ITypeBinding typeBinding;
6870

@@ -74,6 +76,7 @@ public ClassTreeImpl(Kind kind, SyntaxToken openBraceToken, List<Tree> members,
7476
this.modifiers = ModifiersTreeImpl.emptyModifiers();
7577
this.typeParameters = new TypeParameterListTreeImpl();
7678
this.superInterfaces = QualifiedIdentifierListTreeImpl.emptyList();
79+
this.permittedTypes = QualifiedIdentifierListTreeImpl.emptyList();
7780
}
7881

7982
public ClassTreeImpl complete(ModifiersTreeImpl modifiers, SyntaxToken declarationKeyword, IdentifierTree name) {
@@ -104,6 +107,12 @@ public ClassTreeImpl completeInterfaces(SyntaxToken keyword, QualifiedIdentifier
104107
return this;
105108
}
106109

110+
public ClassTreeImpl completePermittedTypes(SyntaxToken permitsKeyword, QualifiedIdentifierListTreeImpl permittedTypes) {
111+
this.permitsKeyword = permitsKeyword;
112+
this.permittedTypes = permittedTypes;
113+
return this;
114+
}
115+
107116
public ClassTreeImpl completeAtToken(InternalSyntaxToken atToken) {
108117
this.atToken = atToken;
109118
return this;
@@ -151,6 +160,11 @@ public ListTree<TypeTree> superInterfaces() {
151160
return superInterfaces;
152161
}
153162

163+
@Override
164+
public SyntaxToken permitsKeyword() {
165+
return permitsKeyword;
166+
}
167+
154168
@Override
155169
public ListTree<TypeTree> permittedTypes() {
156170
return permittedTypes;

java-frontend/src/main/java/org/sonar/plugins/java/api/tree/ClassTree.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,14 @@ public interface ClassTree extends StatementTree {
8686

8787
ListTree<TypeTree> superInterfaces();
8888

89+
/**
90+
* @since Java 15
91+
* @deprecated Preview Feature
92+
*/
93+
@Deprecated
94+
@Nullable
95+
SyntaxToken permitsKeyword();
96+
8997
/**
9098
* @since Java 15
9199
* @deprecated Preview Feature
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package org.foo;
2+
3+
record Foo(int a, String s) {
4+
static int record;
5+
Foo {
6+
assert a > 42;
7+
assert s.length() > 42;
8+
}
9+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package org.foo;
2+
3+
public class A {
4+
public sealed interface Shape permits Circle, Rectangle, Square, Diamond {
5+
default void foo(int non, int sealed) {
6+
// bugs in ECJ - should compile without spaces
7+
int permits = non - sealed;
8+
}
9+
10+
default void foo() { }
11+
}
12+
13+
public final class Circle implements Shape { }
14+
public non-sealed class Rectangle implements Shape { }
15+
public final class Square implements Shape { }
16+
public record Diamond() implements Shape { }
17+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package org.foo;
2+
3+
public class A {
4+
public boolean block() {
5+
boolean yield = false;
6+
return switch (Bool.random()) {
7+
case TRUE -> {
8+
System.out.println("Bool true");
9+
yield true;
10+
}
11+
case FALSE -> {
12+
System.out.println("Bool false");
13+
yield false;
14+
}
15+
case FILE_NOT_FOUND -> {
16+
var ex = new IllegalStateException("Ridiculous");
17+
throw ex;
18+
}
19+
default -> yield;
20+
};
21+
}
22+
23+
public enum Bool {
24+
TRUE, FALSE, FILE_NOT_FOUND;
25+
26+
public static Bool random() {
27+
return TRUE;
28+
}
29+
}
30+
}

java-frontend/src/test/java/org/sonar/java/ast/visitors/SyntaxHighlighterVisitorTest.java

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@
4242
import org.sonar.java.classpath.ClasspathForMain;
4343
import org.sonar.java.classpath.ClasspathForTest;
4444
import org.sonar.java.model.JavaVersionImpl;
45-
import org.sonar.plugins.java.api.JavaCheck;
4645

4746
import static org.assertj.core.api.Assertions.assertThat;
4847
import static org.mockito.Mockito.mock;
@@ -85,7 +84,7 @@ void parse_error() throws Exception {
8584
@ValueSource(strings = {"\n", "\r\n", "\r"})
8685
void test_different_end_of_line(String eol) throws IOException {
8786
this.eol = eol;
88-
InputFile inputFile = generateDefaultTestFile();
87+
InputFile inputFile = generateTestFile("src/test/files/highlighter/Example.java");
8988
scan(inputFile);
9089
verifyHighlighting(inputFile);
9190
}
@@ -192,13 +191,62 @@ void text_block() throws Exception {
192191
assertThatHasBeenHighlighted(componentKey, 8, 12, 10, 8, TypeOfText.STRING);
193192
}
194193

195-
private void scan(InputFile inputFile) {
196-
JavaFrontend frontend = new JavaFrontend(new JavaVersionImpl(10), null, null, null, null, new JavaCheck[] {syntaxHighlighterVisitor});
197-
frontend.scan(Collections.singletonList(inputFile), Collections.emptyList(), Collections.emptyList());
194+
/**
195+
* Java 15
196+
*/
197+
@Test
198+
void switch_expression() throws Exception {
199+
this.eol = "\n";
200+
InputFile inputFile = generateTestFile("src/test/files/highlighter/SwitchExpression.java");
201+
scan(inputFile);
202+
203+
String componentKey = inputFile.key();
204+
assertThatHasBeenHighlighted(componentKey, 9, 9, 9, 14, TypeOfText.KEYWORD); // yield true
205+
assertThatHasBeenHighlighted(componentKey, 13, 9, 13, 14, TypeOfText.KEYWORD); // yield false
206+
assertThatHasBeenHighlighted(componentKey, 19, 7, 19, 14, TypeOfText.KEYWORD); // default
207+
assertThatHasNotBeenHighlighted(componentKey, 19, 18, 19, 23); // yield as identifier
208+
}
209+
210+
/**
211+
* Java 16
212+
*/
213+
@Test
214+
void records() throws Exception {
215+
this.eol = "\n";
216+
InputFile inputFile = generateTestFile("src/test/files/highlighter/Records.java");
217+
scan(inputFile);
218+
219+
String componentKey = inputFile.key();
220+
assertThatHasBeenHighlighted(componentKey, 3, 1, 3, 7, TypeOfText.KEYWORD); // record
221+
assertThatHasNotBeenHighlighted(componentKey, 4, 14, 4, 20); // record as identifier
222+
}
223+
224+
/**
225+
* Java 16 (second preview)
226+
*/
227+
@Test
228+
void sealed_classes() throws Exception {
229+
this.eol = "\n";
230+
InputFile inputFile = generateTestFile("src/test/files/highlighter/SealedClass.java");
231+
scan(inputFile);
232+
233+
String componentKey = inputFile.key();
234+
assertThatHasBeenHighlighted(componentKey, 4, 19, 4, 25, TypeOfText.KEYWORD); // sealed
235+
assertThatHasNotBeenHighlighted(componentKey, 5, 35, 5, 41); // sealed as variable name
236+
237+
assertThatHasBeenHighlighted(componentKey, 4, 33, 4, 40, TypeOfText.KEYWORD); // permits
238+
assertThatHasNotBeenHighlighted(componentKey, 7, 11, 7, 18); // permits as variable name
239+
240+
assertThatHasBeenHighlighted(componentKey, 14, 10, 14, 20, TypeOfText.KEYWORD); // non-sealed
241+
// TODO fixme ECJ bug? should not require spaces
242+
assertThatHasNotBeenHighlighted(componentKey, 7, 21, 7, 23); // non-sealed as expression
243+
244+
assertThatHasBeenHighlighted(componentKey, 16, 10, 16, 16, TypeOfText.KEYWORD); // record
198245
}
199246

200-
private InputFile generateDefaultTestFile() throws IOException {
201-
return generateTestFile("src/test/files/highlighter/Example.java");
247+
private void scan(InputFile inputFile) {
248+
JavaFrontend frontend = new JavaFrontend(new JavaVersionImpl(), null, null, null, null, syntaxHighlighterVisitor);
249+
frontend.scan(Collections.singletonList(inputFile), Collections.emptyList(), Collections.emptyList());
202250
}
203251

204252
private InputFile generateTestFile(String sourceFile) throws IOException {

java-frontend/src/test/java/org/sonar/java/model/JParserSemanticTest.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@
8585
import org.sonar.plugins.java.api.tree.VariableTree;
8686

8787
import static org.assertj.core.api.Assertions.assertThat;
88+
import static org.sonar.java.model.assertions.TreeAssert.assertThat;
8889
import static org.sonar.java.model.assertions.TypeAssert.assertThat;
8990
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
9091

@@ -1021,15 +1022,17 @@ void declaration_sealed_class() {
10211022
assertThat(c.modifiers()).hasSize(1);
10221023
ModifierKeywordTree m = (ModifierKeywordTree) c.modifiers().get(0);
10231024
assertThat(m.modifier()).isEqualTo(Modifier.SEALED);
1024-
assertThat(m.firstToken().text()).isEqualTo("sealed");
1025+
assertThat(m.firstToken()).is("sealed");
1026+
1027+
assertThat(c.permitsKeyword()).is("permits");
10251028
assertThat(c.permittedTypes()).hasSize(2);
10261029

10271030
cu = test("non-sealed class Square extends Shape { }");
10281031
c = (ClassTreeImpl) cu.types().get(0);
10291032
assertThat(c.modifiers()).hasSize(1);
10301033
m = (ModifierKeywordTree) c.modifiers().get(0);
10311034
assertThat(m.modifier()).isEqualTo(Modifier.NON_SEALED);
1032-
assertThat(c.modifiers().get(0).firstToken().text()).isEqualTo("non-sealed");
1035+
assertThat(c.modifiers().get(0).firstToken()).is("non-sealed");
10331036
}
10341037

10351038
/**

0 commit comments

Comments
 (0)