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..086bb6f9517 --- /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..371e49270a7 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 implicitly declared class in a 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..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 @@ -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, null, members, null); + + 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); @@ -932,7 +952,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()); @@ -1021,7 +1041,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: 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..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 @@ -43,8 +43,10 @@ public class ClassTreeImpl extends JavaTree implements ClassTree { private final Kind kind; + @Nullable private final SyntaxToken openBraceToken; private final List members; + @Nullable private final SyntaxToken closeBraceToken; private ModifiersTree modifiers; private SyntaxToken atToken; @@ -69,7 +71,7 @@ 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, @Nullable SyntaxToken openBraceToken, List members, @Nullable SyntaxToken closeBraceToken) { this.kind = kind; this.openBraceToken = openBraceToken; this.members = orderMembers(kind, members); @@ -197,6 +199,7 @@ public ListTree permittedTypes() { return permittedTypes; } + @Nullable @Override public SyntaxToken openBraceToken() { return openBraceToken; @@ -207,6 +210,7 @@ public List members() { return members; } + @Nullable @Override public SyntaxToken closeBraceToken() { return closeBraceToken; @@ -262,9 +266,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..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 @@ -2102,4 +2102,43 @@ 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); + 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); + } } 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 { + } +}