From 552e12edf7927860e5af7b8fcd5600c422b112b4 Mon Sep 17 00:00:00 2001 From: Tomasz Tylenda Date: Wed, 27 May 2026 16:39:29 +0200 Subject: [PATCH 1/3] SONARJAVA-6405 Only one @BeforeEach/@AfterEach should be presen: prototype --- .../OneBeforeEachAfterEachCheckSample.java | 25 +++++++++ .../tests/OneBeforeEachAfterEachCheck.java | 55 +++++++++++++++++++ .../OneBeforeEachAfterEachCheckTest.java | 32 +++++++++++ 3 files changed, 112 insertions(+) create mode 100644 java-checks-test-sources/default/src/test/java/checks/tests/OneBeforeEachAfterEachCheckSample.java create mode 100644 java-checks/src/main/java/org/sonar/java/checks/tests/OneBeforeEachAfterEachCheck.java create mode 100644 java-checks/src/test/java/org/sonar/java/checks/tests/OneBeforeEachAfterEachCheckTest.java diff --git a/java-checks-test-sources/default/src/test/java/checks/tests/OneBeforeEachAfterEachCheckSample.java b/java-checks-test-sources/default/src/test/java/checks/tests/OneBeforeEachAfterEachCheckSample.java new file mode 100644 index 00000000000..dbe46de4e57 --- /dev/null +++ b/java-checks-test-sources/default/src/test/java/checks/tests/OneBeforeEachAfterEachCheckSample.java @@ -0,0 +1,25 @@ +package checks.tests; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.fail; + +class OneBeforeEachAfterEachCheckSample { // Noncompliant {{Only one methods should be annotated @BeforeEach.}} + + @BeforeEach + void setUp1() { + // pass + } + + @BeforeEach + void setUp2() { + // pass + } + + + @Test + void decoy() { + fail("not important"); + } +} diff --git a/java-checks/src/main/java/org/sonar/java/checks/tests/OneBeforeEachAfterEachCheck.java b/java-checks/src/main/java/org/sonar/java/checks/tests/OneBeforeEachAfterEachCheck.java new file mode 100644 index 00000000000..d5b120e84ea --- /dev/null +++ b/java-checks/src/main/java/org/sonar/java/checks/tests/OneBeforeEachAfterEachCheck.java @@ -0,0 +1,55 @@ +/* + * SonarQube Java + * Copyright (C) SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * You can redistribute and/or modify this program under the terms of + * the Sonar Source-Available License Version 1, as published by SonarSource Sàrl. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ +package org.sonar.java.checks.tests; + +import org.sonar.check.Rule; +import org.sonar.plugins.java.api.IssuableSubscriptionVisitor; +import org.sonar.plugins.java.api.tree.ClassTree; +import org.sonar.plugins.java.api.tree.IdentifierTree; +import org.sonar.plugins.java.api.tree.MethodTree; +import org.sonar.plugins.java.api.tree.Tree; + +import java.util.List; + +// FIXME: get a rule id +@Rule(key = "S3360") +public class OneBeforeEachAfterEachCheck extends IssuableSubscriptionVisitor { + + private static final String BEFORE_EACH = "org.junit.jupiter.api.BeforeEach"; + + @Override + public List nodesToVisit() { + return List.of(Tree.Kind.CLASS); + } + + @Override + public void visitNode(Tree tree) { + ClassTree classTree = (ClassTree) tree; + IdentifierTree className = classTree.simpleName(); + if (className == null) { + return; + } + long beforeEachCount = classTree.members().stream() + .filter(member -> member.is(Tree.Kind.METHOD)) + .map(MethodTree.class::cast) + .filter(method -> method.symbol().metadata().isAnnotatedWith(BEFORE_EACH)) + .count(); + if (beforeEachCount > 1) { + reportIssue(className, "Only one methods should be annotated @BeforeEach."); + } + } +} diff --git a/java-checks/src/test/java/org/sonar/java/checks/tests/OneBeforeEachAfterEachCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/tests/OneBeforeEachAfterEachCheckTest.java new file mode 100644 index 00000000000..3453e100fc5 --- /dev/null +++ b/java-checks/src/test/java/org/sonar/java/checks/tests/OneBeforeEachAfterEachCheckTest.java @@ -0,0 +1,32 @@ +/* + * SonarQube Java + * Copyright (C) SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * You can redistribute and/or modify this program under the terms of + * the Sonar Source-Available License Version 1, as published by SonarSource Sàrl. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ +package org.sonar.java.checks.tests; + +import org.junit.jupiter.api.Test; +import org.sonar.java.checks.verifier.CheckVerifier; + +import static org.sonar.java.checks.verifier.TestUtils.testCodeSourcesPath; + +class OneBeforeEachAfterEachCheckTest { + @Test + void test() { + CheckVerifier.newVerifier() + .onFile(testCodeSourcesPath("checks/tests/OneBeforeEachAfterEachCheckSample.java")) + .withCheck(new OneBeforeEachAfterEachCheck()) + .verifyIssues(); + } +} From 7be21cfdf8a69b85e6849c50b3df58cce036644c Mon Sep 17 00:00:00 2001 From: Tomasz Tylenda Date: Thu, 28 May 2026 16:13:51 +0200 Subject: [PATCH 2/3] Also @Before --- .../tests/OneBeforeEachAfterEachCheckSample.java | 2 +- .../tests/OneBeforeEachAfterEachCheck.java | 16 +++++++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/java-checks-test-sources/default/src/test/java/checks/tests/OneBeforeEachAfterEachCheckSample.java b/java-checks-test-sources/default/src/test/java/checks/tests/OneBeforeEachAfterEachCheckSample.java index dbe46de4e57..fcf32a566b3 100644 --- a/java-checks-test-sources/default/src/test/java/checks/tests/OneBeforeEachAfterEachCheckSample.java +++ b/java-checks-test-sources/default/src/test/java/checks/tests/OneBeforeEachAfterEachCheckSample.java @@ -5,7 +5,7 @@ import static org.junit.jupiter.api.Assertions.fail; -class OneBeforeEachAfterEachCheckSample { // Noncompliant {{Only one methods should be annotated @BeforeEach.}} +class OneBeforeEachAfterEachCheckSample { // Noncompliant {{Only one method should be annotated @Before(Each).}} @BeforeEach void setUp1() { diff --git a/java-checks/src/main/java/org/sonar/java/checks/tests/OneBeforeEachAfterEachCheck.java b/java-checks/src/main/java/org/sonar/java/checks/tests/OneBeforeEachAfterEachCheck.java index d5b120e84ea..f73f1215d80 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/tests/OneBeforeEachAfterEachCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/tests/OneBeforeEachAfterEachCheck.java @@ -30,6 +30,7 @@ public class OneBeforeEachAfterEachCheck extends IssuableSubscriptionVisitor { private static final String BEFORE_EACH = "org.junit.jupiter.api.BeforeEach"; + private static final String BEFORE = "org.junit.Before"; @Override public List nodesToVisit() { @@ -43,13 +44,18 @@ public void visitNode(Tree tree) { if (className == null) { return; } - long beforeEachCount = classTree.members().stream() + int beforeCount = countAnnotations(BEFORE, classTree); + int beforeEachCount = countAnnotations(BEFORE_EACH, classTree); + if (beforeCount > 1 || beforeEachCount > 1) { + reportIssue(className, "Only one method should be annotated @Before(Each)."); + } + } + + private int countAnnotations(String annotation, ClassTree classTree) { + return (int) classTree.members().stream() .filter(member -> member.is(Tree.Kind.METHOD)) .map(MethodTree.class::cast) - .filter(method -> method.symbol().metadata().isAnnotatedWith(BEFORE_EACH)) + .filter(method -> method.symbol().metadata().isAnnotatedWith(annotation)) .count(); - if (beforeEachCount > 1) { - reportIssue(className, "Only one methods should be annotated @BeforeEach."); - } } } From 156e7d965be94a71e84784b4e2566f5e29f31d53 Mon Sep 17 00:00:00 2001 From: Tomasz Tylenda Date: Thu, 28 May 2026 16:23:30 +0200 Subject: [PATCH 3/3] Temp metadata --- .../org/sonar/l10n/java/rules/java/S3360.html | 19 +++++++++++++++ .../org/sonar/l10n/java/rules/java/S3360.json | 23 +++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S3360.html create mode 100644 sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S3360.json diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S3360.html b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S3360.html new file mode 100644 index 00000000000..f46bc31b711 --- /dev/null +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S3360.html @@ -0,0 +1,19 @@ +

Why is this an issue?

+

By default, the Maven Surefire plugin only executes test classes with names that end in "Test" or "TestCase". Name your class "TestClassX.java", +for instance, and it will be skipped.

+

This rule raises an issue for each test class with a name not ending in "Test" or "TestCase".

+

Noncompliant code example

+
+public class TestClassX {  // Noncompliant
+  @Test
+  public void testDoTheThing() {
+    //...
+
+

Compliant solution

+
+public class ClassXTest {
+  @Test
+  public void testDoTheThing() {
+    //...
+
+ diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S3360.json b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S3360.json new file mode 100644 index 00000000000..9c68894d883 --- /dev/null +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S3360.json @@ -0,0 +1,23 @@ +{ + "title": "Test class names should end with \"Test\" or \"TestCase\"", + "type": "CODE_SMELL", + "code": { + "impacts": { + "MAINTAINABILITY": "BLOCKER" + }, + "attribute": "IDENTIFIABLE" + }, + "status": "ready", + "remediation": { + "func": "Constant\/Issue", + "constantCost": "2min" + }, + "tags": [ + "tests" + ], + "defaultSeverity": "Blocker", + "ruleSpecification": "RSPEC-3360", + "sqKey": "S3360", + "scope": "Main", + "quickfix": "unknown" +}