From b1cea932e8dbd38dcd0d6d60d9bfa8cf88418c6c Mon Sep 17 00:00:00 2001 From: Tomasz Tylenda Date: Thu, 22 Jan 2026 18:17:18 +0100 Subject: [PATCH 1/6] SONARJAVA-5978 Support Compact Source Files --- .../checks/HiddenFieldCheckCompactSample.java | 11 ++++ .../IndentationCheck_compactSource.java | 11 ++++ .../sonar/java/checks/HiddenFieldCheck.java | 10 +++- .../sonar/java/checks/IndentationCheck.java | 8 +-- .../java/checks/HiddenFieldCheckTest.java | 7 +++ .../java/checks/IndentationCheckTest.java | 8 +++ .../java/org/sonar/java/JavaFilesCache.java | 7 ++- .../main/java/org/sonar/java/Measurer.java | 22 +++++-- .../java/ast/visitors/PublicApiChecker.java | 3 +- .../java/org/sonar/java/model/JParser.java | 60 +++++++++++++------ .../java/model/declaration/ClassTreeImpl.java | 22 ++++--- .../org/sonar/plugins/java/api/tree/Tree.java | 7 +++ .../src/test/files/metrics/CompactSource.java | 10 ++++ .../org/sonar/java/JavaFilesCacheTest.java | 10 ++++ .../java/org/sonar/java/MeasurerTest.java | 5 ++ .../ast/visitors/PublicApiCheckerTest.java | 6 +- .../sonar/java/model/JParserSemanticTest.java | 36 +++++++++++ .../sonar/plugins/java/api/tree/TreeTest.java | 2 +- .../JavaFilesCacheTestFileCompact.java | 9 +++ 19 files changed, 213 insertions(+), 41 deletions(-) create mode 100644 java-checks-test-sources/default/src/main/java/checks/HiddenFieldCheckCompactSample.java create mode 100644 java-checks-test-sources/default/src/main/java/checks/IndentationCheck_compactSource.java create mode 100644 java-frontend/src/test/files/metrics/CompactSource.java create mode 100644 java-frontend/src/test/resources/JavaFilesCacheTestFileCompact.java diff --git a/java-checks-test-sources/default/src/main/java/checks/HiddenFieldCheckCompactSample.java b/java-checks-test-sources/default/src/main/java/checks/HiddenFieldCheckCompactSample.java new file mode 100644 index 00000000000..bba6654a94c --- /dev/null +++ b/java-checks-test-sources/default/src/main/java/checks/HiddenFieldCheckCompactSample.java @@ -0,0 +1,11 @@ +void main() { + int a = 5; + int x = 10; // Noncompliant {{Rename "x" which hides the field declared at line 11.}} +// ^ + float b = 3.14f; + System.out.println(a); + System.out.println(x); + System.out.println(b); +} + +int x = 20; diff --git a/java-checks-test-sources/default/src/main/java/checks/IndentationCheck_compactSource.java b/java-checks-test-sources/default/src/main/java/checks/IndentationCheck_compactSource.java new file mode 100644 index 00000000000..8dbf6add2cf --- /dev/null +++ b/java-checks-test-sources/default/src/main/java/checks/IndentationCheck_compactSource.java @@ -0,0 +1,11 @@ +// SONARJAVA-6028: FPs ahead. Only the line with "Too much." should be noncompliant. + +void main() { // Noncompliant + System.out.println("Just right."); // Noncompliant + if(true) { + System.out.println("Too much."); // Noncompliant + } +} + +class MyClass { // Noncompliant +} diff --git a/java-checks/src/main/java/org/sonar/java/checks/HiddenFieldCheck.java b/java-checks/src/main/java/org/sonar/java/checks/HiddenFieldCheck.java index fc17ece5ed2..a4e378f81a9 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/HiddenFieldCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/HiddenFieldCheck.java @@ -59,6 +59,7 @@ public List nodesToVisit() { Tree.Kind.INTERFACE, Tree.Kind.ANNOTATION_TYPE, Tree.Kind.RECORD, + Tree.Kind.IMPLICIT_CLASS, Tree.Kind.VARIABLE, Tree.Kind.METHOD, Tree.Kind.CONSTRUCTOR, @@ -130,7 +131,14 @@ private static boolean isInStaticInnerClass(VariableTree hiddenVariable, Variabl } private static boolean isClassTree(Tree tree) { - return tree.is(Tree.Kind.CLASS, Tree.Kind.ENUM, Tree.Kind.INTERFACE, Tree.Kind.ANNOTATION_TYPE, Tree.Kind.RECORD); + return tree.is( + Tree.Kind.CLASS, + Tree.Kind.ENUM, + Tree.Kind.INTERFACE, + Tree.Kind.ANNOTATION_TYPE, + Tree.Kind.RECORD, + Tree.Kind.IMPLICIT_CLASS + ); } @Override diff --git a/java-checks/src/main/java/org/sonar/java/checks/IndentationCheck.java b/java-checks/src/main/java/org/sonar/java/checks/IndentationCheck.java index 8cf83f7d780..991725c5fbd 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/IndentationCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/IndentationCheck.java @@ -72,13 +72,13 @@ public void scanFile(JavaFileScannerContext context) { @Override public void visitClass(ClassTree tree) { - // Exclude anonymous classes - boolean isAnonymous = tree.simpleName() == null; - if (!isAnonymous) { + // Exclude anonymous classes other than implicit classed of compact source files. + boolean isExcluded = tree.simpleName() == null && !tree.is(Kind.IMPLICIT_CLASS); + if (!isExcluded) { checkIndentation(Collections.singletonList(tree)); } int previousLevel = expectedLevel; - if (isAnonymous) { + if (isExcluded) { excludeIssueAtLine = LineUtils.startLine(tree.openBraceToken()); expectedLevel = Position.startOf(tree.closeBraceToken()).columnOffset(); } diff --git a/java-checks/src/test/java/org/sonar/java/checks/HiddenFieldCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/HiddenFieldCheckTest.java index 9e487de3970..2c9babac317 100644 --- a/java-checks/src/test/java/org/sonar/java/checks/HiddenFieldCheckTest.java +++ b/java-checks/src/test/java/org/sonar/java/checks/HiddenFieldCheckTest.java @@ -40,4 +40,11 @@ void test_records() { .verifyIssues(); } + @Test + void test_compact_source() { + CheckVerifier.newVerifier() + .onFile(mainCodeSourcesPath("checks/HiddenFieldCheckCompactSample.java")) + .withCheck(new HiddenFieldCheck()) + .verifyIssues(); + } } diff --git a/java-checks/src/test/java/org/sonar/java/checks/IndentationCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/IndentationCheckTest.java index 0e061217213..f325cae967f 100644 --- a/java-checks/src/test/java/org/sonar/java/checks/IndentationCheckTest.java +++ b/java-checks/src/test/java/org/sonar/java/checks/IndentationCheckTest.java @@ -57,4 +57,12 @@ void tolerates_line_breaking_control_characters() { .withCheck(new IndentationCheck()) .verifyNoIssues(); } + + @Test + void compact_source_file() { + CheckVerifier.newVerifier() + .onFile(mainCodeSourcesPath("checks/IndentationCheck_compactSource.java")) + .withCheck(new IndentationCheck()) + .verifyIssues(); + } } diff --git a/java-frontend/src/main/java/org/sonar/java/JavaFilesCache.java b/java-frontend/src/main/java/org/sonar/java/JavaFilesCache.java index b38bbc20ca4..31dad657255 100644 --- a/java-frontend/src/main/java/org/sonar/java/JavaFilesCache.java +++ b/java-frontend/src/main/java/org/sonar/java/JavaFilesCache.java @@ -50,6 +50,8 @@ public void scanFile(JavaFileScannerContext context) { currentClassKey.clear(); parent.clear(); anonymousInnerClassCounter.clear(); + // 0 stored on the top will be used for implicit anonymous classes in compact source files. + anonymousInnerClassCounter.push(0); scan(tree); } @@ -77,9 +79,10 @@ private String getClassKey(String className) { key = currentPackage + "/" + className; } if ("".equals(className) || (parent.peek() != null && parent.peek().is(Tree.Kind.METHOD))) { - // inner class declared within method + // inner class declared within method or unnamed implicit class in compact source file int count = anonymousInnerClassCounter.pop() + 1; - key = currentClassKey.peek() + "$" + count + className; + String prefix = currentClassKey.isEmpty() ? "" : currentClassKey.peek(); + key = prefix + "$" + count + className; anonymousInnerClassCounter.push(count); } else if (currentClassKey.peek() != null) { key = currentClassKey.peek() + "$" + className; diff --git a/java-frontend/src/main/java/org/sonar/java/Measurer.java b/java-frontend/src/main/java/org/sonar/java/Measurer.java index 37f0c7fcda9..c2bccbeb882 100644 --- a/java-frontend/src/main/java/org/sonar/java/Measurer.java +++ b/java-frontend/src/main/java/org/sonar/java/Measurer.java @@ -17,6 +17,7 @@ package org.sonar.java; import java.io.Serializable; +import java.util.ArrayList; import java.util.Arrays; import java.util.Deque; import java.util.LinkedList; @@ -38,6 +39,15 @@ public class Measurer extends SubscriptionVisitor { + private static final Tree.Kind[] CLASS_KINDS = new Tree.Kind[]{ + Tree.Kind.CLASS, + Tree.Kind.INTERFACE, + Tree.Kind.ENUM, + Tree.Kind.ANNOTATION_TYPE, + Tree.Kind.RECORD, + Tree.Kind.IMPLICIT_CLASS + }; + private final SensorContext sensorContext; private final NoSonarFilter noSonarFilter; private InputFile sonarFile; @@ -61,9 +71,13 @@ public void scanFile(JavaFileScannerContext context) { @Override public List nodesToVisit() { - return Arrays.asList(Tree.Kind.CLASS, Tree.Kind.INTERFACE, Tree.Kind.ENUM, Tree.Kind.ANNOTATION_TYPE, Tree.Kind.RECORD, - Tree.Kind.NEW_CLASS, Tree.Kind.ENUM_CONSTANT, - Tree.Kind.METHOD, Tree.Kind.CONSTRUCTOR); + List nodes = new ArrayList<>(Arrays.asList(CLASS_KINDS)); + nodes.addAll(Arrays.asList( + Tree.Kind.NEW_CLASS, + Tree.Kind.ENUM_CONSTANT, + Tree.Kind.METHOD, + Tree.Kind.CONSTRUCTOR)); + return nodes; } @@ -120,7 +134,7 @@ public void leaveNode(Tree tree) { } private static boolean isClassTree(Tree tree) { - return tree.is(Tree.Kind.CLASS, Tree.Kind.INTERFACE, Tree.Kind.ENUM, Tree.Kind.ANNOTATION_TYPE, Tree.Kind.RECORD); + return tree.is(CLASS_KINDS); } private void saveMetricOnFile(Metric metric, T value) { diff --git a/java-frontend/src/main/java/org/sonar/java/ast/visitors/PublicApiChecker.java b/java-frontend/src/main/java/org/sonar/java/ast/visitors/PublicApiChecker.java index 150930d1f9c..ab6a420c7b6 100644 --- a/java-frontend/src/main/java/org/sonar/java/ast/visitors/PublicApiChecker.java +++ b/java-frontend/src/main/java/org/sonar/java/ast/visitors/PublicApiChecker.java @@ -43,7 +43,8 @@ private PublicApiChecker() { Tree.Kind.INTERFACE, Tree.Kind.ENUM, Tree.Kind.ANNOTATION_TYPE, - Tree.Kind.RECORD + Tree.Kind.RECORD, + Tree.Kind.IMPLICIT_CLASS }; private static final Tree.Kind[] METHOD_KINDS = { diff --git a/java-frontend/src/main/java/org/sonar/java/model/JParser.java b/java-frontend/src/main/java/org/sonar/java/model/JParser.java index 0fb06ac7e3b..1c21b2ef35c 100644 --- a/java-frontend/src/main/java/org/sonar/java/model/JParser.java +++ b/java-frontend/src/main/java/org/sonar/java/model/JParser.java @@ -769,7 +769,27 @@ private ModuleDirectiveTree convertModuleDirective(ModuleDirective node) { } } + /** Converts implicitly declared unnamed class at the top level of a compact compilation unit. */ + private ClassTreeImpl convertUnnamedClassDeclaration(AbstractTypeDeclaration e) { + List members = new ArrayList<>(); + + for (Object o : e.bodyDeclarations()) { + processBodyDeclaration((BodyDeclaration) o, members); + } + + ClassTreeImpl t = new ClassTreeImpl(Tree.Kind.IMPLICIT_CLASS, members); + + t.typeBinding = e.resolveBinding(); + declaration(t.typeBinding, t); + + return t; + } + private ClassTreeImpl convertTypeDeclaration(AbstractTypeDeclaration e) { + if (e.getNodeType() == ASTNode.UNNAMED_CLASS) { + return convertUnnamedClassDeclaration(e); + } + List members = new ArrayList<>(); int leftBraceTokenIndex = findLeftBraceTokenIndex(e); @@ -815,7 +835,8 @@ private ClassTreeImpl convertTypeDeclaration(AbstractTypeDeclaration e) { private ClassTreeImpl convertTypeDeclaration(TypeDeclaration e, ModifiersTreeImpl modifiers, IdentifierTreeImpl name, InternalSyntaxToken openBraceToken, List members, InternalSyntaxToken closeBraceToken) { InternalSyntaxToken declarationKeyword = firstTokenBefore(e.getName(), e.isInterface() ? TerminalToken.TokenNameinterface : TerminalToken.TokenNameclass); - ClassTreeImpl t = new ClassTreeImpl(e.isInterface() ? Tree.Kind.INTERFACE : Tree.Kind.CLASS, openBraceToken, members, closeBraceToken) + ClassTreeImpl t = new ClassTreeImpl(e.isInterface() ? Tree.Kind.INTERFACE : Tree.Kind.CLASS, members) + .complete(openBraceToken, closeBraceToken) .complete(modifiers, declarationKeyword, name) .completeTypeParameters(convertTypeParameters(e.typeParameters())); @@ -845,14 +866,16 @@ private ClassTreeImpl convertEnumDeclaration(EnumDeclaration e, ModifiersTreeImp members.addAll(0, enumConstants); InternalSyntaxToken declarationKeyword = firstTokenBefore(e.getName(), TerminalToken.TokenNameenum); - return new ClassTreeImpl(Tree.Kind.ENUM, openBraceToken, members, closeBraceToken) + return new ClassTreeImpl(Tree.Kind.ENUM, members) + .complete(openBraceToken, closeBraceToken) .complete(modifiers, declarationKeyword, name); } private ClassTreeImpl convertAnnotationTypeDeclaration(AnnotationTypeDeclaration e, ModifiersTreeImpl modifiers, IdentifierTreeImpl name, InternalSyntaxToken openBraceToken, List members, InternalSyntaxToken closeBraceToken) { InternalSyntaxToken declarationKeyword = firstTokenBefore(e.getName(), TerminalToken.TokenNameinterface); - return new ClassTreeImpl(Tree.Kind.ANNOTATION_TYPE, openBraceToken, members, closeBraceToken) + return new ClassTreeImpl(Tree.Kind.ANNOTATION_TYPE, members) + .complete(openBraceToken, closeBraceToken) .complete(modifiers, declarationKeyword, name) .completeAtToken(firstTokenBefore(e.getName(), TerminalToken.TokenNameAT)); } @@ -865,7 +888,8 @@ private ClassTreeImpl convertRecordDeclaration(RecordDeclaration e, ModifiersTre InternalSyntaxToken closeParen = firstTokenAfter( recordComponents.isEmpty() ? e.getName() : (ASTNode) recordComponents.get(recordComponents.size() - 1), TerminalToken.TokenNameRPAREN); - return new ClassTreeImpl(Tree.Kind.RECORD, openBraceToken, members, closeBraceToken) + return new ClassTreeImpl(Tree.Kind.RECORD, members) + .complete(openBraceToken, closeBraceToken) .complete(modifiers, declarationKeyword, name) .completeTypeParameters(convertTypeParameters(e.typeParameters())) .completeRecordComponents(openParen, convertRecordComponents(e), closeParen); @@ -932,7 +956,7 @@ private static List superInterfaceTypes(AbstractTypeDeclaration e) { return ((EnumDeclaration) e).superInterfaceTypes(); case ASTNode.RECORD_DECLARATION: return ((RecordDeclaration) e).superInterfaceTypes(); - case ASTNode.ANNOTATION_TYPE_DECLARATION: + case ASTNode.ANNOTATION_TYPE_DECLARATION, ASTNode.UNNAMED_CLASS: return Collections.emptyList(); default: throw new IllegalStateException(ASTNode.nodeClassForType(e.getNodeType()).toString()); @@ -971,12 +995,12 @@ private EnumConstantTreeImpl processEnumConstantDeclaration(EnumConstantDeclarat for (Object o : e.getAnonymousClassDeclaration().bodyDeclarations()) { processBodyDeclaration((BodyDeclaration) o, members); } - classBody = new ClassTreeImpl( - Tree.Kind.CLASS, - firstTokenIn(e.getAnonymousClassDeclaration(), TerminalToken.TokenNameLBRACE), - members, - lastTokenIn(e.getAnonymousClassDeclaration(), TerminalToken.TokenNameRBRACE) - ); + classBody = new ClassTreeImpl(Tree.Kind.CLASS, members) + .complete( + firstTokenIn(e.getAnonymousClassDeclaration(), TerminalToken.TokenNameLBRACE), + lastTokenIn(e.getAnonymousClassDeclaration(), TerminalToken.TokenNameRBRACE) + ); + classBody.typeBinding = e.getAnonymousClassDeclaration().resolveBinding(); declaration(classBody.typeBinding, classBody); } @@ -1021,7 +1045,8 @@ private void processBodyDeclaration(BodyDeclaration node, List members) { case ASTNode.ANNOTATION_TYPE_DECLARATION, ASTNode.ENUM_DECLARATION, ASTNode.RECORD_DECLARATION, - ASTNode.TYPE_DECLARATION: + ASTNode.TYPE_DECLARATION, + ASTNode.UNNAMED_CLASS: lastTokenIndex = processTypeDeclaration((AbstractTypeDeclaration) node, members); break; case ASTNode.ANNOTATION_TYPE_MEMBER_DECLARATION: @@ -2175,12 +2200,11 @@ private NewClassTreeImpl convertClassInstanceCreation(ClassInstanceCreation e) { for (Object o : e.getAnonymousClassDeclaration().bodyDeclarations()) { processBodyDeclaration((BodyDeclaration) o, members); } - classBody = new ClassTreeImpl( - Tree.Kind.CLASS, - firstTokenIn(e.getAnonymousClassDeclaration(), TerminalToken.TokenNameLBRACE), - members, - lastTokenIn(e.getAnonymousClassDeclaration(), TerminalToken.TokenNameRBRACE) - ); + classBody = new ClassTreeImpl(Tree.Kind.CLASS, members). + complete( + firstTokenIn(e.getAnonymousClassDeclaration(), TerminalToken.TokenNameLBRACE), + lastTokenIn(e.getAnonymousClassDeclaration(), TerminalToken.TokenNameRBRACE) + ); classBody.typeBinding = e.getAnonymousClassDeclaration().resolveBinding(); declaration(classBody.typeBinding, classBody); } diff --git a/java-frontend/src/main/java/org/sonar/java/model/declaration/ClassTreeImpl.java b/java-frontend/src/main/java/org/sonar/java/model/declaration/ClassTreeImpl.java index f8b96f74954..ee68b697082 100644 --- a/java-frontend/src/main/java/org/sonar/java/model/declaration/ClassTreeImpl.java +++ b/java-frontend/src/main/java/org/sonar/java/model/declaration/ClassTreeImpl.java @@ -43,9 +43,11 @@ public class ClassTreeImpl extends JavaTree implements ClassTree { private final Kind kind; - private final SyntaxToken openBraceToken; + @Nullable + private SyntaxToken openBraceToken; private final List members; - private final SyntaxToken closeBraceToken; + @Nullable + private SyntaxToken closeBraceToken; private ModifiersTree modifiers; private SyntaxToken atToken; private SyntaxToken declarationKeyword; @@ -69,17 +71,21 @@ public class ClassTreeImpl extends JavaTree implements ClassTree { @Nullable public ITypeBinding typeBinding; - public ClassTreeImpl(Kind kind, SyntaxToken openBraceToken, List members, SyntaxToken closeBraceToken) { + public ClassTreeImpl(Kind kind, List members) { this.kind = kind; - this.openBraceToken = openBraceToken; this.members = orderMembers(kind, members); - this.closeBraceToken = closeBraceToken; this.modifiers = ModifiersTreeImpl.emptyModifiers(); this.typeParameters = new TypeParameterListTreeImpl(); this.superInterfaces = QualifiedIdentifierListTreeImpl.emptyList(); this.permittedTypes = QualifiedIdentifierListTreeImpl.emptyList(); } + public ClassTreeImpl complete(SyntaxToken openBraceToken, SyntaxToken closeBraceToken) { + this.openBraceToken = openBraceToken; + this.closeBraceToken = closeBraceToken; + return this; + } + public ClassTreeImpl complete(ModifiersTreeImpl modifiers, SyntaxToken declarationKeyword, IdentifierTree name) { this.modifiers = modifiers; this.declarationKeyword = declarationKeyword; @@ -197,6 +203,7 @@ public ListTree permittedTypes() { return permittedTypes; } + @Nullable @Override public SyntaxToken openBraceToken() { return openBraceToken; @@ -207,6 +214,7 @@ public List members() { return members; } + @Nullable @Override public SyntaxToken closeBraceToken() { return closeBraceToken; @@ -262,9 +270,9 @@ public List children() { addIfNotNull(implementsKeyword), Collections.singletonList(superInterfaces), Collections.singletonList(permittedTypes), - Collections.singletonList(openBraceToken), + addIfNotNull(openBraceToken), members, - Collections.singletonList(closeBraceToken) + addIfNotNull(closeBraceToken) ); } diff --git a/java-frontend/src/main/java/org/sonar/plugins/java/api/tree/Tree.java b/java-frontend/src/main/java/org/sonar/plugins/java/api/tree/Tree.java index 3a20cd8faaa..2e33422b6da 100644 --- a/java-frontend/src/main/java/org/sonar/plugins/java/api/tree/Tree.java +++ b/java-frontend/src/main/java/org/sonar/plugins/java/api/tree/Tree.java @@ -78,6 +78,13 @@ enum Kind implements GrammarRuleKey { */ RECORD(ClassTree.class), + /** + * {@link ClassTree} + * + * @since Java 25 + */ + IMPLICIT_CLASS(ClassTree.class), + /** * {@link EnumConstantTree} * diff --git a/java-frontend/src/test/files/metrics/CompactSource.java b/java-frontend/src/test/files/metrics/CompactSource.java new file mode 100644 index 00000000000..9403aac4e9a --- /dev/null +++ b/java-frontend/src/test/files/metrics/CompactSource.java @@ -0,0 +1,10 @@ +void main() { + int a = 5; + int x = 10; + float b = 3.14f; + System.out.println(a); + System.out.println(x); + System.out.println(b); +} + +int x = 20; diff --git a/java-frontend/src/test/java/org/sonar/java/JavaFilesCacheTest.java b/java-frontend/src/test/java/org/sonar/java/JavaFilesCacheTest.java index 12cac2857bc..74c1d40b8b8 100644 --- a/java-frontend/src/test/java/org/sonar/java/JavaFilesCacheTest.java +++ b/java-frontend/src/test/java/org/sonar/java/JavaFilesCacheTest.java @@ -44,4 +44,14 @@ void resource_file_mapping() { "org/sonar/java/JavaFilesCacheTestFile$A$3"); } + @Test + void compact_source() { + JavaFilesCache javaFilesCache = new JavaFilesCache(); + JavaAstScanner.scanSingleFileForTests(TestUtils.inputFile("src/test/resources/JavaFilesCacheTestFileCompact.java"), new VisitorsBridge(javaFilesCache)); + + Set classNames = javaFilesCache.getClassNames(); + assertThat(classNames) + .hasSize(4) + .contains("$1$1", "$1", "$1$C", "$1$C$D"); + } } diff --git a/java-frontend/src/test/java/org/sonar/java/MeasurerTest.java b/java-frontend/src/test/java/org/sonar/java/MeasurerTest.java index e9a8fc1313f..78332d21c5d 100644 --- a/java-frontend/src/test/java/org/sonar/java/MeasurerTest.java +++ b/java-frontend/src/test/java/org/sonar/java/MeasurerTest.java @@ -77,6 +77,11 @@ void verify_statements_metric() { checkMetric("Statements.java", "statements", 18); } + @Test + void verify_compact_source() { + checkMetric("CompactSource.java", "classes", 1); + } + @Test void verify_ncloc_metric() { checkMetric("LinesOfCode.java", "ncloc", 2); diff --git a/java-frontend/src/test/java/org/sonar/java/ast/visitors/PublicApiCheckerTest.java b/java-frontend/src/test/java/org/sonar/java/ast/visitors/PublicApiCheckerTest.java index 767d16564fb..6f28d681d1b 100644 --- a/java-frontend/src/test/java/org/sonar/java/ast/visitors/PublicApiCheckerTest.java +++ b/java-frontend/src/test/java/org/sonar/java/ast/visitors/PublicApiCheckerTest.java @@ -49,15 +49,15 @@ void private_constructor() throws Exception { @Test void targeted_kinds() { assertThat(PublicApiChecker.classKinds()) - .hasSize(5) - .containsExactlyInAnyOrder(Tree.Kind.ANNOTATION_TYPE, Tree.Kind.ENUM, Tree.Kind.CLASS, Tree.Kind.INTERFACE, Tree.Kind.RECORD); + .hasSize(6) + .containsExactlyInAnyOrder(Tree.Kind.ANNOTATION_TYPE, Tree.Kind.ENUM, Tree.Kind.CLASS, Tree.Kind.INTERFACE, Tree.Kind.RECORD, Tree.Kind.IMPLICIT_CLASS); assertThat(PublicApiChecker.methodKinds()) .hasSize(2) .containsExactlyInAnyOrder(Tree.Kind.CONSTRUCTOR, Tree.Kind.METHOD); assertThat(PublicApiChecker.apiKinds()) - .hasSize(8) + .hasSize(9) .contains(PublicApiChecker.classKinds()) .contains(PublicApiChecker.methodKinds()) .contains(Tree.Kind.VARIABLE); diff --git a/java-frontend/src/test/java/org/sonar/java/model/JParserSemanticTest.java b/java-frontend/src/test/java/org/sonar/java/model/JParserSemanticTest.java index db1d07bcef0..ddfd6acc2be 100644 --- a/java-frontend/src/test/java/org/sonar/java/model/JParserSemanticTest.java +++ b/java-frontend/src/test/java/org/sonar/java/model/JParserSemanticTest.java @@ -2102,4 +2102,40 @@ public class Source { JavaTree.ImportTreeImpl importConst = (JavaTree.ImportTreeImpl) cu.imports().get(1); assertThat(importConst.isModule()).isFalse(); } + + @Test + void compactSource_simple() { + String source = """ + void main() { + } + """; + JavaTree.CompilationUnitTreeImpl cu = test(source); + assertThat(cu.types()).hasSize(1); + ClassTreeImpl clazz = (ClassTreeImpl) cu.types().get(0); + assertThat(clazz).isNotNull(); + assertThat(clazz.kind()).isEqualTo(Tree.Kind.IMPLICIT_CLASS); + assertThat(clazz.simpleName()).isNull(); + assertThat(clazz.openBraceToken()).isNull(); + assertThat(clazz.closeBraceToken()).isNull(); + } + + @Test + void compactSource_complex() { + String source = """ + void main() { + System.out.println("Hello, World!"); + } + int i = 43; + class Helper { + void help() { + System.out.println("Helping..."); + } + } + """; + JavaTree.CompilationUnitTreeImpl cu = test(source); + ClassTreeImpl clazz = (ClassTreeImpl) cu.types().get(0); + assertThat(clazz).isNotNull(); + assertThat(clazz.kind()).isEqualTo(Tree.Kind.IMPLICIT_CLASS); + assertThat(clazz.members()).hasSize(3); + } } diff --git a/java-frontend/src/test/java/org/sonar/plugins/java/api/tree/TreeTest.java b/java-frontend/src/test/java/org/sonar/plugins/java/api/tree/TreeTest.java index fb016687a8c..d5af954cab4 100644 --- a/java-frontend/src/test/java/org/sonar/plugins/java/api/tree/TreeTest.java +++ b/java-frontend/src/test/java/org/sonar/plugins/java/api/tree/TreeTest.java @@ -24,7 +24,7 @@ class TreeTest { @Test void test() { - assertThat(Tree.Kind.values()).hasSize(128); + assertThat(Tree.Kind.values()).hasSize(129); } } diff --git a/java-frontend/src/test/resources/JavaFilesCacheTestFileCompact.java b/java-frontend/src/test/resources/JavaFilesCacheTestFileCompact.java new file mode 100644 index 00000000000..324dc90e815 --- /dev/null +++ b/java-frontend/src/test/resources/JavaFilesCacheTestFileCompact.java @@ -0,0 +1,9 @@ +void main() { + new Object() { + }; +} + +class C { + static class D { + } +} From dd0978967b9a9be1bbbf5c39f1df000c73fe51c2 Mon Sep 17 00:00:00 2001 From: tomasz-tylenda-sonarsource Date: Mon, 2 Feb 2026 15:38:50 +0100 Subject: [PATCH 2/6] Apply suggestions from code review Co-authored-by: Dorian Burihabwa <75226315+dorian-burihabwa-sonarsource@users.noreply.github.com> --- .../src/main/java/checks/IndentationCheck_compactSource.java | 2 +- java-frontend/src/main/java/org/sonar/java/JavaFilesCache.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/java-checks-test-sources/default/src/main/java/checks/IndentationCheck_compactSource.java b/java-checks-test-sources/default/src/main/java/checks/IndentationCheck_compactSource.java index 8dbf6add2cf..086bb6f9517 100644 --- a/java-checks-test-sources/default/src/main/java/checks/IndentationCheck_compactSource.java +++ b/java-checks-test-sources/default/src/main/java/checks/IndentationCheck_compactSource.java @@ -2,7 +2,7 @@ void main() { // Noncompliant System.out.println("Just right."); // Noncompliant - if(true) { + if (true) { System.out.println("Too much."); // Noncompliant } } diff --git a/java-frontend/src/main/java/org/sonar/java/JavaFilesCache.java b/java-frontend/src/main/java/org/sonar/java/JavaFilesCache.java index 31dad657255..371e49270a7 100644 --- a/java-frontend/src/main/java/org/sonar/java/JavaFilesCache.java +++ b/java-frontend/src/main/java/org/sonar/java/JavaFilesCache.java @@ -79,7 +79,7 @@ private String getClassKey(String className) { key = currentPackage + "/" + className; } if ("".equals(className) || (parent.peek() != null && parent.peek().is(Tree.Kind.METHOD))) { - // inner class declared within method or unnamed implicit class in compact source file + // inner class declared within method or implicitly declared class in a compact source file int count = anonymousInnerClassCounter.pop() + 1; String prefix = currentClassKey.isEmpty() ? "" : currentClassKey.peek(); key = prefix + "$" + count + className; From d3a936be06fbba73f923b25599f3abf269708403 Mon Sep 17 00:00:00 2001 From: Tomasz Tylenda Date: Mon, 2 Feb 2026 15:51:10 +0100 Subject: [PATCH 3/6] Use ctor with nullable braces. --- .../java/org/sonar/java/model/JParser.java | 36 +++++++++---------- .../java/model/declaration/ClassTreeImpl.java | 14 +++----- 2 files changed, 22 insertions(+), 28 deletions(-) diff --git a/java-frontend/src/main/java/org/sonar/java/model/JParser.java b/java-frontend/src/main/java/org/sonar/java/model/JParser.java index 1c21b2ef35c..3812d42763c 100644 --- a/java-frontend/src/main/java/org/sonar/java/model/JParser.java +++ b/java-frontend/src/main/java/org/sonar/java/model/JParser.java @@ -777,7 +777,7 @@ private ClassTreeImpl convertUnnamedClassDeclaration(AbstractTypeDeclaration e) processBodyDeclaration((BodyDeclaration) o, members); } - ClassTreeImpl t = new ClassTreeImpl(Tree.Kind.IMPLICIT_CLASS, members); + ClassTreeImpl t = new ClassTreeImpl(Tree.Kind.IMPLICIT_CLASS, null, members, null); t.typeBinding = e.resolveBinding(); declaration(t.typeBinding, t); @@ -835,8 +835,7 @@ private ClassTreeImpl convertTypeDeclaration(AbstractTypeDeclaration e) { private ClassTreeImpl convertTypeDeclaration(TypeDeclaration e, ModifiersTreeImpl modifiers, IdentifierTreeImpl name, InternalSyntaxToken openBraceToken, List members, InternalSyntaxToken closeBraceToken) { InternalSyntaxToken declarationKeyword = firstTokenBefore(e.getName(), e.isInterface() ? TerminalToken.TokenNameinterface : TerminalToken.TokenNameclass); - ClassTreeImpl t = new ClassTreeImpl(e.isInterface() ? Tree.Kind.INTERFACE : Tree.Kind.CLASS, members) - .complete(openBraceToken, closeBraceToken) + ClassTreeImpl t = new ClassTreeImpl(e.isInterface() ? Tree.Kind.INTERFACE : Tree.Kind.CLASS, openBraceToken, members, closeBraceToken) .complete(modifiers, declarationKeyword, name) .completeTypeParameters(convertTypeParameters(e.typeParameters())); @@ -866,16 +865,14 @@ private ClassTreeImpl convertEnumDeclaration(EnumDeclaration e, ModifiersTreeImp members.addAll(0, enumConstants); InternalSyntaxToken declarationKeyword = firstTokenBefore(e.getName(), TerminalToken.TokenNameenum); - return new ClassTreeImpl(Tree.Kind.ENUM, members) - .complete(openBraceToken, closeBraceToken) + return new ClassTreeImpl(Tree.Kind.ENUM, openBraceToken, members, closeBraceToken) .complete(modifiers, declarationKeyword, name); } private ClassTreeImpl convertAnnotationTypeDeclaration(AnnotationTypeDeclaration e, ModifiersTreeImpl modifiers, IdentifierTreeImpl name, InternalSyntaxToken openBraceToken, List members, InternalSyntaxToken closeBraceToken) { InternalSyntaxToken declarationKeyword = firstTokenBefore(e.getName(), TerminalToken.TokenNameinterface); - return new ClassTreeImpl(Tree.Kind.ANNOTATION_TYPE, members) - .complete(openBraceToken, closeBraceToken) + return new ClassTreeImpl(Tree.Kind.ANNOTATION_TYPE, openBraceToken, members, closeBraceToken) .complete(modifiers, declarationKeyword, name) .completeAtToken(firstTokenBefore(e.getName(), TerminalToken.TokenNameAT)); } @@ -888,8 +885,7 @@ private ClassTreeImpl convertRecordDeclaration(RecordDeclaration e, ModifiersTre InternalSyntaxToken closeParen = firstTokenAfter( recordComponents.isEmpty() ? e.getName() : (ASTNode) recordComponents.get(recordComponents.size() - 1), TerminalToken.TokenNameRPAREN); - return new ClassTreeImpl(Tree.Kind.RECORD, members) - .complete(openBraceToken, closeBraceToken) + return new ClassTreeImpl(Tree.Kind.RECORD, openBraceToken, members, closeBraceToken) .complete(modifiers, declarationKeyword, name) .completeTypeParameters(convertTypeParameters(e.typeParameters())) .completeRecordComponents(openParen, convertRecordComponents(e), closeParen); @@ -995,11 +991,12 @@ private EnumConstantTreeImpl processEnumConstantDeclaration(EnumConstantDeclarat for (Object o : e.getAnonymousClassDeclaration().bodyDeclarations()) { processBodyDeclaration((BodyDeclaration) o, members); } - classBody = new ClassTreeImpl(Tree.Kind.CLASS, members) - .complete( - firstTokenIn(e.getAnonymousClassDeclaration(), TerminalToken.TokenNameLBRACE), - lastTokenIn(e.getAnonymousClassDeclaration(), TerminalToken.TokenNameRBRACE) - ); + classBody = new ClassTreeImpl( + Tree.Kind.CLASS, + firstTokenIn(e.getAnonymousClassDeclaration(), TerminalToken.TokenNameLBRACE), + members, + lastTokenIn(e.getAnonymousClassDeclaration(), TerminalToken.TokenNameRBRACE) + ); classBody.typeBinding = e.getAnonymousClassDeclaration().resolveBinding(); declaration(classBody.typeBinding, classBody); @@ -2200,11 +2197,12 @@ private NewClassTreeImpl convertClassInstanceCreation(ClassInstanceCreation e) { for (Object o : e.getAnonymousClassDeclaration().bodyDeclarations()) { processBodyDeclaration((BodyDeclaration) o, members); } - classBody = new ClassTreeImpl(Tree.Kind.CLASS, members). - complete( - firstTokenIn(e.getAnonymousClassDeclaration(), TerminalToken.TokenNameLBRACE), - lastTokenIn(e.getAnonymousClassDeclaration(), TerminalToken.TokenNameRBRACE) - ); + classBody = new ClassTreeImpl( + Tree.Kind.CLASS, + firstTokenIn(e.getAnonymousClassDeclaration(), TerminalToken.TokenNameLBRACE), + members, + lastTokenIn(e.getAnonymousClassDeclaration(), TerminalToken.TokenNameRBRACE) + ); classBody.typeBinding = e.getAnonymousClassDeclaration().resolveBinding(); declaration(classBody.typeBinding, classBody); } diff --git a/java-frontend/src/main/java/org/sonar/java/model/declaration/ClassTreeImpl.java b/java-frontend/src/main/java/org/sonar/java/model/declaration/ClassTreeImpl.java index ee68b697082..2fe299eaa5e 100644 --- a/java-frontend/src/main/java/org/sonar/java/model/declaration/ClassTreeImpl.java +++ b/java-frontend/src/main/java/org/sonar/java/model/declaration/ClassTreeImpl.java @@ -44,10 +44,10 @@ public class ClassTreeImpl extends JavaTree implements ClassTree { private final Kind kind; @Nullable - private SyntaxToken openBraceToken; + private final SyntaxToken openBraceToken; private final List members; @Nullable - private SyntaxToken closeBraceToken; + private final SyntaxToken closeBraceToken; private ModifiersTree modifiers; private SyntaxToken atToken; private SyntaxToken declarationKeyword; @@ -71,21 +71,17 @@ public class ClassTreeImpl extends JavaTree implements ClassTree { @Nullable public ITypeBinding typeBinding; - public ClassTreeImpl(Kind kind, List members) { + public ClassTreeImpl(Kind kind, @Nullable SyntaxToken openBraceToken, List members, @Nullable SyntaxToken closeBraceToken) { this.kind = kind; + this.openBraceToken = openBraceToken; this.members = orderMembers(kind, members); + this.closeBraceToken = closeBraceToken; this.modifiers = ModifiersTreeImpl.emptyModifiers(); this.typeParameters = new TypeParameterListTreeImpl(); this.superInterfaces = QualifiedIdentifierListTreeImpl.emptyList(); this.permittedTypes = QualifiedIdentifierListTreeImpl.emptyList(); } - public ClassTreeImpl complete(SyntaxToken openBraceToken, SyntaxToken closeBraceToken) { - this.openBraceToken = openBraceToken; - this.closeBraceToken = closeBraceToken; - return this; - } - public ClassTreeImpl complete(ModifiersTreeImpl modifiers, SyntaxToken declarationKeyword, IdentifierTree name) { this.modifiers = modifiers; this.declarationKeyword = declarationKeyword; From 6f27a657d866782e44b2b3b888bd1c4ca357beff Mon Sep 17 00:00:00 2001 From: Tomasz Tylenda Date: Mon, 2 Feb 2026 15:58:50 +0100 Subject: [PATCH 4/6] More assertions in compactSource_complex() --- .../test/java/org/sonar/java/model/JParserSemanticTest.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/java-frontend/src/test/java/org/sonar/java/model/JParserSemanticTest.java b/java-frontend/src/test/java/org/sonar/java/model/JParserSemanticTest.java index ddfd6acc2be..febcc6520b3 100644 --- a/java-frontend/src/test/java/org/sonar/java/model/JParserSemanticTest.java +++ b/java-frontend/src/test/java/org/sonar/java/model/JParserSemanticTest.java @@ -2137,5 +2137,8 @@ void help() { assertThat(clazz).isNotNull(); assertThat(clazz.kind()).isEqualTo(Tree.Kind.IMPLICIT_CLASS); assertThat(clazz.members()).hasSize(3); + assertThat(clazz.members().get(0).kind()).isEqualTo(Tree.Kind.METHOD); + assertThat(clazz.members().get(1).kind()).isEqualTo(Tree.Kind.VARIABLE); + assertThat(clazz.members().get(2).kind()).isEqualTo(Tree.Kind.CLASS); } } From 7d074ad6b3451a3fa7d3591de26f6c91a713efff Mon Sep 17 00:00:00 2001 From: Tomasz Tylenda Date: Mon, 2 Feb 2026 16:20:39 +0100 Subject: [PATCH 5/6] empty line --- java-frontend/src/main/java/org/sonar/java/model/JParser.java | 1 - 1 file changed, 1 deletion(-) diff --git a/java-frontend/src/main/java/org/sonar/java/model/JParser.java b/java-frontend/src/main/java/org/sonar/java/model/JParser.java index 3812d42763c..34ffe54e6f8 100644 --- a/java-frontend/src/main/java/org/sonar/java/model/JParser.java +++ b/java-frontend/src/main/java/org/sonar/java/model/JParser.java @@ -997,7 +997,6 @@ private EnumConstantTreeImpl processEnumConstantDeclaration(EnumConstantDeclarat members, lastTokenIn(e.getAnonymousClassDeclaration(), TerminalToken.TokenNameRBRACE) ); - classBody.typeBinding = e.getAnonymousClassDeclaration().resolveBinding(); declaration(classBody.typeBinding, classBody); } From fda73858d992ac4c73049ad8cc2c49981d6a02bd Mon Sep 17 00:00:00 2001 From: Tomasz Tylenda Date: Mon, 2 Feb 2026 17:15:51 +0100 Subject: [PATCH 6/6] Trigger CI