From c3e66fefbd82b726bc810f5f8cb89d9054939e67 Mon Sep 17 00:00:00 2001 From: Romain Brenguier Date: Fri, 11 Sep 2026 16:59:06 +0200 Subject: [PATCH 1/9] SONARJAVA-6944: Implement rule S9387 TestNG Javadoc tags should be converted to annotations Detect TestNG-specific Javadoc tags (@test, @beforeMethod, @afterMethod, etc.) used as configuration markers instead of proper TestNG annotations (@Test, @BeforeMethod, @AfterMethod, etc.). Case-insensitive matching with support for all 15 TestNG tags and secondary locations for multiple tags per method. Co-Authored-By: Claude Opus 4.6 --- .../tests/TestNGJavadocTagsCheckSample.java | 167 ++++++++++++++++++ .../checks/tests/TestNGJavadocTagsCheck.java | 101 +++++++++++ .../tests/TestNGJavadocTagsCheckTest.java | 42 +++++ .../org/sonar/l10n/java/rules/java/S9387.html | 69 ++++++++ .../org/sonar/l10n/java/rules/java/S9387.json | 24 +++ .../main/resources/profiles/Sonar_way/S9387 | 0 6 files changed, 403 insertions(+) create mode 100644 java-checks-test-sources/default/src/main/java/checks/tests/TestNGJavadocTagsCheckSample.java create mode 100644 java-checks/src/main/java/org/sonar/java/checks/tests/TestNGJavadocTagsCheck.java create mode 100644 java-checks/src/test/java/org/sonar/java/checks/tests/TestNGJavadocTagsCheckTest.java create mode 100644 sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9387.html create mode 100644 sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9387.json create mode 100644 sonar-java-plugin/src/main/resources/profiles/Sonar_way/S9387 diff --git a/java-checks-test-sources/default/src/main/java/checks/tests/TestNGJavadocTagsCheckSample.java b/java-checks-test-sources/default/src/main/java/checks/tests/TestNGJavadocTagsCheckSample.java new file mode 100644 index 00000000000..a03a655ec8d --- /dev/null +++ b/java-checks-test-sources/default/src/main/java/checks/tests/TestNGJavadocTagsCheckSample.java @@ -0,0 +1,167 @@ +package checks.tests; + +class TestNGJavadocTagsCheckSample { + + /** + * @test + */ + public void shouldValidateInput() { // Noncompliant {{Replace this "@test" Javadoc tag with the TestNG "@Test" annotation.}} + } + + /** + * @beforeMethod + */ + public void setUp() { // Noncompliant {{Replace this "@beforeMethod" Javadoc tag with the TestNG "@BeforeMethod" annotation.}} + } + + /** + * @afterMethod + */ + public void tearDown() { // Noncompliant {{Replace this "@afterMethod" Javadoc tag with the TestNG "@AfterMethod" annotation.}} + } + + /** + * @beforeClass + */ + public void classSetUp() { // Noncompliant {{Replace this "@beforeClass" Javadoc tag with the TestNG "@BeforeClass" annotation.}} + } + + /** + * @afterClass + */ + public void classTearDown() { // Noncompliant {{Replace this "@afterClass" Javadoc tag with the TestNG "@AfterClass" annotation.}} + } + + /** + * @dataProvider + */ + public Object[][] provideData() { // Noncompliant {{Replace this "@dataProvider" Javadoc tag with the TestNG "@DataProvider" annotation.}} + return new Object[][] {}; + } + + /** + * @factory + */ + public Object[] createInstances() { // Noncompliant {{Replace this "@factory" Javadoc tag with the TestNG "@Factory" annotation.}} + return new Object[] {}; + } + + /** + * @beforeSuite + */ + public void suiteSetUp() { // Noncompliant {{Replace this "@beforeSuite" Javadoc tag with the TestNG "@BeforeSuite" annotation.}} + } + + /** + * @afterSuite + */ + public void suiteTearDown() { // Noncompliant {{Replace this "@afterSuite" Javadoc tag with the TestNG "@AfterSuite" annotation.}} + } + + /** + * @beforeTest + */ + public void testSetUp() { // Noncompliant {{Replace this "@beforeTest" Javadoc tag with the TestNG "@BeforeTest" annotation.}} + } + + /** + * @afterTest + */ + public void testTearDown() { // Noncompliant {{Replace this "@afterTest" Javadoc tag with the TestNG "@AfterTest" annotation.}} + } + + /** + * @beforeGroups + */ + public void groupSetUp() { // Noncompliant {{Replace this "@beforeGroups" Javadoc tag with the TestNG "@BeforeGroups" annotation.}} + } + + /** + * @afterGroups + */ + public void groupTearDown() { // Noncompliant {{Replace this "@afterGroups" Javadoc tag with the TestNG "@AfterGroups" annotation.}} + } + + /** + * @parameters + */ + public void parameterized() { // Noncompliant {{Replace this "@parameters" Javadoc tag with the TestNG "@Parameters" annotation.}} + } + + /** + * @listeners + */ + public void withListeners() { // Noncompliant {{Replace this "@listeners" Javadoc tag with the TestNG "@Listeners" annotation.}} + } + + /** + * @Test + */ + public void caseInsensitiveUpperCase() { // Noncompliant {{Replace this "@Test" Javadoc tag with the TestNG "@Test" annotation.}} + } + + /** + * @TEST + */ + public void caseInsensitiveAllCaps() { // Noncompliant {{Replace this "@TEST" Javadoc tag with the TestNG "@Test" annotation.}} + } + + /** + * Validates user input. + * @param input the input string + * @test + */ + public void mixedWithStandardTags(String input) { // Noncompliant {{Replace this "@test" Javadoc tag with the TestNG "@Test" annotation.}} + } + + /** + * Sets up resources. + * @test + * @beforeMethod + */ + public void multipleTestNGTags() { // Noncompliant {{Replace this "@test" Javadoc tag with the TestNG "@Test" annotation.}} + } + + // --- Compliant cases --- + + /** + * @param input the input string + * @return the result + * @throws IllegalArgumentException if input is invalid + */ + public String standardJavadocTags(String input) { // compliant + return input; + } + + /** + * @see String + * @since 1.0 + * @deprecated Use another method + */ + public void otherStandardTags() { // compliant + } + + public void noJavadoc() { // compliant + } + + /* @test - not a Javadoc comment */ + public void blockComment() { // compliant + } + + // @test - not a Javadoc comment + public void lineComment() { // compliant + } + + /** + * Processes input data. + */ + public void javadocWithoutTags() { // compliant + } + + /** + * @author John Doe + * @version 1.0 + */ + public void authorAndVersionTags() { // compliant + } +} diff --git a/java-checks/src/main/java/org/sonar/java/checks/tests/TestNGJavadocTagsCheck.java b/java-checks/src/main/java/org/sonar/java/checks/tests/TestNGJavadocTagsCheck.java new file mode 100644 index 00000000000..864e4e6f35a --- /dev/null +++ b/java-checks/src/main/java/org/sonar/java/checks/tests/TestNGJavadocTagsCheck.java @@ -0,0 +1,101 @@ +/* + * 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 java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.sonar.check.Rule; +import org.sonar.plugins.java.api.IssuableSubscriptionVisitor; +import org.sonar.plugins.java.api.JavaFileScannerContext; +import org.sonar.plugins.java.api.tree.SyntaxTrivia; +import org.sonar.plugins.java.api.tree.Tree; +import org.sonarsource.analyzer.commons.collections.MapBuilder; + +@Rule(key = "S9387") +public class TestNGJavadocTagsCheck extends IssuableSubscriptionVisitor { + + private static final Pattern BLOCK_TAG_PATTERN = Pattern.compile("^\\s*\\*?\\s*@(\\w+)", Pattern.MULTILINE); + + private static final Map TESTNG_TAGS_TO_ANNOTATIONS = MapBuilder.newMap() + .put("test", "@Test") + .put("beforemethod", "@BeforeMethod") + .put("aftermethod", "@AfterMethod") + .put("beforeclass", "@BeforeClass") + .put("afterclass", "@AfterClass") + .put("beforesuite", "@BeforeSuite") + .put("aftersuite", "@AfterSuite") + .put("beforetest", "@BeforeTest") + .put("aftertest", "@AfterTest") + .put("beforegroups", "@BeforeGroups") + .put("aftergroups", "@AfterGroups") + .put("dataprovider", "@DataProvider") + .put("factory", "@Factory") + .put("parameters", "@Parameters") + .put("listeners", "@Listeners") + .build(); + + @Override + public List nodesToVisit() { + return Arrays.asList(Tree.Kind.METHOD, Tree.Kind.CONSTRUCTOR); + } + + @Override + public void visitNode(Tree tree) { + tree.firstToken().trivias().stream() + .filter(trivia -> trivia.isComment(SyntaxTrivia.CommentKind.JAVADOC)) + .forEach(trivia -> checkJavadoc(tree, trivia)); + } + + private void checkJavadoc(Tree tree, SyntaxTrivia trivia) { + String commentText = trivia.comment(); + Matcher matcher = BLOCK_TAG_PATTERN.matcher(commentText); + + String firstTagOriginal = null; + String firstAnnotation = null; + List secondaryLocations = new ArrayList<>(); + + while (matcher.find()) { + String tagName = matcher.group(1); + String annotation = TESTNG_TAGS_TO_ANNOTATIONS.get(tagName.toLowerCase(Locale.ROOT)); + if (annotation != null) { + if (firstTagOriginal == null) { + firstTagOriginal = tagName; + firstAnnotation = annotation; + } else { + secondaryLocations.add(new JavaFileScannerContext.Location( + String.format("Also replace \"@%s\" with the TestNG \"%s\" annotation.", + tagName, TESTNG_TAGS_TO_ANNOTATIONS.get(tagName.toLowerCase(Locale.ROOT))), + tree)); + } + } + } + + if (firstTagOriginal != null) { + String message = String.format("Replace this \"@%s\" Javadoc tag with the TestNG \"%s\" annotation.", firstTagOriginal, firstAnnotation); + if (secondaryLocations.isEmpty()) { + reportIssue(tree, message); + } else { + reportIssue(tree, message, secondaryLocations, null); + } + } + } +} diff --git a/java-checks/src/test/java/org/sonar/java/checks/tests/TestNGJavadocTagsCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/tests/TestNGJavadocTagsCheckTest.java new file mode 100644 index 00000000000..4f700579dd2 --- /dev/null +++ b/java-checks/src/test/java/org/sonar/java/checks/tests/TestNGJavadocTagsCheckTest.java @@ -0,0 +1,42 @@ +/* + * 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.mainCodeSourcesPath; + +class TestNGJavadocTagsCheckTest { + + @Test + void test() { + CheckVerifier.newVerifier() + .onFile(mainCodeSourcesPath("checks/tests/TestNGJavadocTagsCheckSample.java")) + .withCheck(new TestNGJavadocTagsCheck()) + .verifyIssues(); + } + + @Test + void test_without_semantic() { + CheckVerifier.newVerifier() + .onFile(mainCodeSourcesPath("checks/tests/TestNGJavadocTagsCheckSample.java")) + .withCheck(new TestNGJavadocTagsCheck()) + .withoutSemantic() + .verifyIssues(); + } +} diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9387.html b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9387.html new file mode 100644 index 00000000000..1ab5f850619 --- /dev/null +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9387.html @@ -0,0 +1,69 @@ +

An issue is raised when a test method uses documentation comment markers (like comment-based tags) instead of proper metadata attributes or +decorators that the testing framework requires to identify and execute tests.

+

In Java TestNG specifically, this means using Javadoc-style tags like @test or @beforeMethod in comments instead of the +proper Java annotations @Test and @BeforeMethod.

+

Why is this an issue?

+

Testing frameworks initially supported documentation comment tags for configuring test methods. For example, developers could write special markers +in documentation comments to designate methods as tests. While this approach worked in older framework versions, it has significant drawbacks compared +to modern declarative configuration mechanisms.

+

Documentation comment tags are simply strings embedded in comments. The compiler doesn’t check them, and development environments cannot validate +them during development. This means typos or incorrect usage won’t be caught until runtime, making bugs harder to find.

+

Modern testing frameworks use first-class language constructs that declare test configuration directly in code. These are part of the language +itself, so the compiler validates them at compile time. This catches errors early and makes the code more reliable.

+

Language-native declarations also integrate better with development tools. IDEs can provide autocomplete, navigation, and refactoring support. When +you rename a test class or reorganize code, these declarations are automatically updated, while comment-based strings remain unchanged.

+

Using documentation comment tags in new code creates technical debt. The code appears outdated and may confuse developers who are familiar with +standard framework practices. It also makes the codebase inconsistent if some tests use language-native declarations while others use comment +tags.

+

In Java, TestNG uses annotations like @Test, @BeforeMethod, and @AfterMethod instead of the older +Javadoc-style tags such as /** @test */.

+

What is the potential impact?

+

Using documentation comments for metadata instead of structured metadata mechanisms reduces code maintainability and increases the risk of +errors:

+
    +
  • Delayed error detection: Typos or configuration mistakes in comment-based metadata are only discovered at runtime, potentially + during test execution in production environments.
  • +
  • Harder refactoring: When restructuring code, development tools cannot automatically update string values embedded in comments, + leading to broken test configurations.
  • +
  • Reduced discoverability: New team members may not recognize comment-based test configuration, making the codebase harder to + understand.
  • +
  • Limited tooling support: Build tools and test runners may have reduced functionality with comment-based configuration compared + to structured metadata mechanisms.
  • +
+

How to fix it

+

Replace Javadoc tags with the corresponding TestNG annotations. The most common conversions are:

+
    +
  • /** @test */ becomes @Test
  • +
  • /** @beforeMethod */ becomes @BeforeMethod
  • +
  • /** @afterMethod */ becomes @AfterMethod
  • +
  • /** @beforeClass */ becomes @BeforeClass
  • +
  • /** @afterClass */ becomes @AfterClass
  • +
+

Import the annotation classes from org.testng.annotations package. Remove the Javadoc comment or keep it for documentation purposes +without the tag.

+

Code examples

+

Noncompliant code example

+
+/**
+ * @test
+ */
+public void shouldValidateInput() { // Noncompliant
+    // test code
+}
+
+

Compliant solution

+
+import org.testng.annotations.Test;
+
+@Test
+public void shouldValidateInput() {
+    // test code
+}
+
+

Resources

+

Documentation

+ + diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9387.json b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9387.json new file mode 100644 index 00000000000..89f8e8f87ec --- /dev/null +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9387.json @@ -0,0 +1,24 @@ +{ + "title": "TestNG Javadoc tags should be converted to annotations", + "type": "CODE_SMELL", + "status": "ready", + "remediation": { + "func": "Constant\/Issue", + "constantCost": "5 min" + }, + "tags": [ + "deprecated", + "convention" + ], + "defaultSeverity": "Major", + "ruleSpecification": "RSPEC-9387", + "sqKey": "S9387", + "scope": "All", + "quickfix": "unknown", + "code": { + "impacts": { + "MAINTAINABILITY": "MEDIUM" + }, + "attribute": "CONVENTIONAL" + } +} diff --git a/sonar-java-plugin/src/main/resources/profiles/Sonar_way/S9387 b/sonar-java-plugin/src/main/resources/profiles/Sonar_way/S9387 new file mode 100644 index 00000000000..e69de29bb2d From dd523e0fcf505fb010a82f5946dfe2713d34e63d Mon Sep 17 00:00:00 2001 From: Romain Brenguier Date: Mon, 14 Sep 2026 10:24:45 +0200 Subject: [PATCH 2/9] SONARJAVA-6944: Fix rule S9387 TestNG Javadoc tags detection Addresses code review feedback: 1. Single-line Javadoc `/** @test */` is now detected by updating BLOCK_TAG_PATTERN to match tags following the `/**` opener 2. False positives from annotation examples in `
` and `{@code}` blocks are eliminated by removing those regions before pattern matching
3. Issue and secondary locations now report precisely on the method name using `simpleName()` and avoid redundant operations
4. Type-only tags (`@beforeClass`, `@afterClass`, `@parameters`, `@listeners`) removed from method-level checks since they cannot be applied to methods

Co-Authored-By: Claude Opus 4.6 
---
 .../tests/TestNGJavadocTagsCheckSample.java   | 88 ++++++++++++-------
 .../checks/tests/TestNGJavadocTagsCheck.java  | 33 ++++---
 .../org/sonar/l10n/java/rules/java/S9387.html | 15 ++--
 3 files changed, 87 insertions(+), 49 deletions(-)

diff --git a/java-checks-test-sources/default/src/main/java/checks/tests/TestNGJavadocTagsCheckSample.java b/java-checks-test-sources/default/src/main/java/checks/tests/TestNGJavadocTagsCheckSample.java
index a03a655ec8d..a651b0ec645 100644
--- a/java-checks-test-sources/default/src/main/java/checks/tests/TestNGJavadocTagsCheckSample.java
+++ b/java-checks-test-sources/default/src/main/java/checks/tests/TestNGJavadocTagsCheckSample.java
@@ -20,32 +20,6 @@ public void setUp() { // Noncompliant {{Replace this "@beforeMethod" Javadoc tag
   public void tearDown() { // Noncompliant {{Replace this "@afterMethod" Javadoc tag with the TestNG "@AfterMethod" annotation.}}
   }
 
-  /**
-   * @beforeClass
-   */
-  public void classSetUp() { // Noncompliant {{Replace this "@beforeClass" Javadoc tag with the TestNG "@BeforeClass" annotation.}}
-  }
-
-  /**
-   * @afterClass
-   */
-  public void classTearDown() { // Noncompliant {{Replace this "@afterClass" Javadoc tag with the TestNG "@AfterClass" annotation.}}
-  }
-
-  /**
-   * @dataProvider
-   */
-  public Object[][] provideData() { // Noncompliant {{Replace this "@dataProvider" Javadoc tag with the TestNG "@DataProvider" annotation.}}
-    return new Object[][] {};
-  }
-
-  /**
-   * @factory
-   */
-  public Object[] createInstances() { // Noncompliant {{Replace this "@factory" Javadoc tag with the TestNG "@Factory" annotation.}}
-    return new Object[] {};
-  }
-
   /**
    * @beforeSuite
    */
@@ -83,15 +57,17 @@ public void groupTearDown() { // Noncompliant {{Replace this "@afterGroups" Java
   }
 
   /**
-   * @parameters
+   * @dataProvider
    */
-  public void parameterized() { // Noncompliant {{Replace this "@parameters" Javadoc tag with the TestNG "@Parameters" annotation.}}
+  public Object[][] provideData() { // Noncompliant {{Replace this "@dataProvider" Javadoc tag with the TestNG "@DataProvider" annotation.}}
+    return new Object[][] {};
   }
 
   /**
-   * @listeners
+   * @factory
    */
-  public void withListeners() { // Noncompliant {{Replace this "@listeners" Javadoc tag with the TestNG "@Listeners" annotation.}}
+  public Object[] createInstances() { // Noncompliant {{Replace this "@factory" Javadoc tag with the TestNG "@Factory" annotation.}}
+    return new Object[] {};
   }
 
   /**
@@ -122,6 +98,34 @@ public void mixedWithStandardTags(String input) { // Noncompliant {{Replace this
   public void multipleTestNGTags() { // Noncompliant {{Replace this "@test" Javadoc tag with the TestNG "@Test" annotation.}}
   }
 
+  /** @test */
+  public void singleLineJavadoc() { // Noncompliant {{Replace this "@test" Javadoc tag with the TestNG "@Test" annotation.}}
+  }
+
+  /**
+   * Example of TestNG usage:
+   * 
+   * @Test
+   * public void exampleTest() {}
+   * 
+ */ + public void withPreBlock() { // compliant + } + + /** + * How to use annotations: + * {@code @Test public void test() {}} + */ + public void withCodeTag() { // compliant + } + + /** + * @test + * @beforeMethod + */ + public void multipleTagsSecondaryLocation() { // Noncompliant {{Replace this "@test" Javadoc tag with the TestNG "@Test" annotation.}} + } + // --- Compliant cases --- /** @@ -164,4 +168,28 @@ public void javadocWithoutTags() { // compliant */ public void authorAndVersionTags() { // compliant } + + /** + * @listeners - Type-only annotation, not applicable to methods + */ + public void typeOnlyTag() { // compliant + } + + /** + * @parameters - Type-only annotation, not applicable to methods + */ + public void anotherTypeOnlyTag() { // compliant + } + + /** + * @beforeClass - Type-only annotation, not applicable to methods + */ + public void beforeClassTag() { // compliant + } + + /** + * @afterClass - Type-only annotation, not applicable to methods + */ + public void afterClassTag() { // compliant + } } diff --git a/java-checks/src/main/java/org/sonar/java/checks/tests/TestNGJavadocTagsCheck.java b/java-checks/src/main/java/org/sonar/java/checks/tests/TestNGJavadocTagsCheck.java index 864e4e6f35a..277270af66c 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/tests/TestNGJavadocTagsCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/tests/TestNGJavadocTagsCheck.java @@ -26,6 +26,7 @@ import org.sonar.check.Rule; import org.sonar.plugins.java.api.IssuableSubscriptionVisitor; import org.sonar.plugins.java.api.JavaFileScannerContext; +import org.sonar.plugins.java.api.tree.MethodTree; import org.sonar.plugins.java.api.tree.SyntaxTrivia; import org.sonar.plugins.java.api.tree.Tree; import org.sonarsource.analyzer.commons.collections.MapBuilder; @@ -33,14 +34,14 @@ @Rule(key = "S9387") public class TestNGJavadocTagsCheck extends IssuableSubscriptionVisitor { - private static final Pattern BLOCK_TAG_PATTERN = Pattern.compile("^\\s*\\*?\\s*@(\\w+)", Pattern.MULTILINE); + private static final Pattern BLOCK_TAG_PATTERN = Pattern.compile("(?:^|/\\*\\*)\\s*\\*?\\s*@(\\w+)", Pattern.MULTILINE); + private static final Pattern PRE_BLOCK_PATTERN = Pattern.compile("
.*?
", Pattern.DOTALL); + private static final Pattern CODE_TAG_PATTERN = Pattern.compile("\\{@code\\s+[^}]*\\}"); private static final Map TESTNG_TAGS_TO_ANNOTATIONS = MapBuilder.newMap() .put("test", "@Test") .put("beforemethod", "@BeforeMethod") .put("aftermethod", "@AfterMethod") - .put("beforeclass", "@BeforeClass") - .put("afterclass", "@AfterClass") .put("beforesuite", "@BeforeSuite") .put("aftersuite", "@AfterSuite") .put("beforetest", "@BeforeTest") @@ -49,8 +50,6 @@ public class TestNGJavadocTagsCheck extends IssuableSubscriptionVisitor { .put("aftergroups", "@AfterGroups") .put("dataprovider", "@DataProvider") .put("factory", "@Factory") - .put("parameters", "@Parameters") - .put("listeners", "@Listeners") .build(); @Override @@ -62,12 +61,14 @@ public List nodesToVisit() { public void visitNode(Tree tree) { tree.firstToken().trivias().stream() .filter(trivia -> trivia.isComment(SyntaxTrivia.CommentKind.JAVADOC)) - .forEach(trivia -> checkJavadoc(tree, trivia)); + .forEach(trivia -> checkJavadoc((MethodTree) tree, trivia)); } - private void checkJavadoc(Tree tree, SyntaxTrivia trivia) { + private void checkJavadoc(MethodTree tree, SyntaxTrivia trivia) { String commentText = trivia.comment(); - Matcher matcher = BLOCK_TAG_PATTERN.matcher(commentText); + String textWithoutCodeBlocks = removeCodeBlocks(commentText); + + Matcher matcher = BLOCK_TAG_PATTERN.matcher(textWithoutCodeBlocks); String firstTagOriginal = null; String firstAnnotation = null; @@ -76,6 +77,7 @@ private void checkJavadoc(Tree tree, SyntaxTrivia trivia) { while (matcher.find()) { String tagName = matcher.group(1); String annotation = TESTNG_TAGS_TO_ANNOTATIONS.get(tagName.toLowerCase(Locale.ROOT)); + if (annotation != null) { if (firstTagOriginal == null) { firstTagOriginal = tagName; @@ -83,7 +85,7 @@ private void checkJavadoc(Tree tree, SyntaxTrivia trivia) { } else { secondaryLocations.add(new JavaFileScannerContext.Location( String.format("Also replace \"@%s\" with the TestNG \"%s\" annotation.", - tagName, TESTNG_TAGS_TO_ANNOTATIONS.get(tagName.toLowerCase(Locale.ROOT))), + tagName, annotation), tree)); } } @@ -91,11 +93,14 @@ private void checkJavadoc(Tree tree, SyntaxTrivia trivia) { if (firstTagOriginal != null) { String message = String.format("Replace this \"@%s\" Javadoc tag with the TestNG \"%s\" annotation.", firstTagOriginal, firstAnnotation); - if (secondaryLocations.isEmpty()) { - reportIssue(tree, message); - } else { - reportIssue(tree, message, secondaryLocations, null); - } + reportIssue(tree.simpleName(), message, secondaryLocations, null); } } + + private static String removeCodeBlocks(String javadoc) { + String result = javadoc; + result = PRE_BLOCK_PATTERN.matcher(result).replaceAll(m -> " ".repeat(m.group().length())); + result = CODE_TAG_PATTERN.matcher(result).replaceAll(m -> " ".repeat(m.group().length())); + return result; + } } diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9387.html b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9387.html index 1ab5f850619..5af4ed4a31a 100644 --- a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9387.html +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9387.html @@ -6,8 +6,8 @@

Why is this an issue?

Testing frameworks initially supported documentation comment tags for configuring test methods. For example, developers could write special markers in documentation comments to designate methods as tests. While this approach worked in older framework versions, it has significant drawbacks compared to modern declarative configuration mechanisms.

-

Documentation comment tags are simply strings embedded in comments. The compiler doesn’t check them, and development environments cannot validate -them during development. This means typos or incorrect usage won’t be caught until runtime, making bugs harder to find.

+

Documentation comment tags are simply strings embedded in comments. The compiler doesn't check them, and development environments cannot validate +them during development. This means typos or incorrect usage won't be caught until runtime, making bugs harder to find.

Modern testing frameworks use first-class language constructs that declare test configuration directly in code. These are part of the language itself, so the compiler validates them at compile time. This catches errors early and makes the code more reliable.

Language-native declarations also integrate better with development tools. IDEs can provide autocomplete, navigation, and refactoring support. When @@ -36,8 +36,14 @@

How to fix it

  • /** @test */ becomes @Test
  • /** @beforeMethod */ becomes @BeforeMethod
  • /** @afterMethod */ becomes @AfterMethod
  • -
  • /** @beforeClass */ becomes @BeforeClass
  • -
  • /** @afterClass */ becomes @AfterClass
  • +
  • /** @beforeSuite */ becomes @BeforeSuite
  • +
  • /** @afterSuite */ becomes @AfterSuite
  • +
  • /** @beforeTest */ becomes @BeforeTest
  • +
  • /** @afterTest */ becomes @AfterTest
  • +
  • /** @beforeGroups */ becomes @BeforeGroups
  • +
  • /** @afterGroups */ becomes @AfterGroups
  • +
  • /** @dataProvider */ becomes @DataProvider
  • +
  • /** @factory */ becomes @Factory
  • Import the annotation classes from org.testng.annotations package. Remove the Javadoc comment or keep it for documentation purposes without the tag.

    @@ -66,4 +72,3 @@

    Documentation

  • TestNG Documentation - Annotations
  • TestNG Documentation - TestNG Test Methods
  • - From 62d2f1c94869803cb86dcd2ee35c066d3c14b242 Mon Sep 17 00:00:00 2001 From: Romain Brenguier Date: Mon, 14 Sep 2026 10:28:39 +0200 Subject: [PATCH 3/9] SONARJAVA-6944: Restore @BeforeClass/@AfterClass tags and remove duplicate test @BeforeClass and @AfterClass are method-level annotations in TestNG (@Target(METHOD)), not type-only. The previous commit incorrectly removed them from the tag map. Co-Authored-By: Claude Opus 4.6 --- .../tests/TestNGJavadocTagsCheckSample.java | 15 ++++----------- .../java/checks/tests/TestNGJavadocTagsCheck.java | 2 ++ .../org/sonar/l10n/java/rules/java/S9387.html | 2 ++ 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/java-checks-test-sources/default/src/main/java/checks/tests/TestNGJavadocTagsCheckSample.java b/java-checks-test-sources/default/src/main/java/checks/tests/TestNGJavadocTagsCheckSample.java index a651b0ec645..9324a80637e 100644 --- a/java-checks-test-sources/default/src/main/java/checks/tests/TestNGJavadocTagsCheckSample.java +++ b/java-checks-test-sources/default/src/main/java/checks/tests/TestNGJavadocTagsCheckSample.java @@ -119,13 +119,6 @@ public void withPreBlock() { // compliant public void withCodeTag() { // compliant } - /** - * @test - * @beforeMethod - */ - public void multipleTagsSecondaryLocation() { // Noncompliant {{Replace this "@test" Javadoc tag with the TestNG "@Test" annotation.}} - } - // --- Compliant cases --- /** @@ -182,14 +175,14 @@ public void anotherTypeOnlyTag() { // compliant } /** - * @beforeClass - Type-only annotation, not applicable to methods + * @beforeClass */ - public void beforeClassTag() { // compliant + public void classSetUp() { // Noncompliant {{Replace this "@beforeClass" Javadoc tag with the TestNG "@BeforeClass" annotation.}} } /** - * @afterClass - Type-only annotation, not applicable to methods + * @afterClass */ - public void afterClassTag() { // compliant + public void classTearDown() { // Noncompliant {{Replace this "@afterClass" Javadoc tag with the TestNG "@AfterClass" annotation.}} } } diff --git a/java-checks/src/main/java/org/sonar/java/checks/tests/TestNGJavadocTagsCheck.java b/java-checks/src/main/java/org/sonar/java/checks/tests/TestNGJavadocTagsCheck.java index 277270af66c..e6b9c1b2ce3 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/tests/TestNGJavadocTagsCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/tests/TestNGJavadocTagsCheck.java @@ -42,6 +42,8 @@ public class TestNGJavadocTagsCheck extends IssuableSubscriptionVisitor { .put("test", "@Test") .put("beforemethod", "@BeforeMethod") .put("aftermethod", "@AfterMethod") + .put("beforeclass", "@BeforeClass") + .put("afterclass", "@AfterClass") .put("beforesuite", "@BeforeSuite") .put("aftersuite", "@AfterSuite") .put("beforetest", "@BeforeTest") diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9387.html b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9387.html index 5af4ed4a31a..377b0544600 100644 --- a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9387.html +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9387.html @@ -36,6 +36,8 @@

    How to fix it

  • /** @test */ becomes @Test
  • /** @beforeMethod */ becomes @BeforeMethod
  • /** @afterMethod */ becomes @AfterMethod
  • +
  • /** @beforeClass */ becomes @BeforeClass
  • +
  • /** @afterClass */ becomes @AfterClass
  • /** @beforeSuite */ becomes @BeforeSuite
  • /** @afterSuite */ becomes @AfterSuite
  • /** @beforeTest */ becomes @BeforeTest
  • From 26754149d293ddd58a8f4831b13c94da7011c252 Mon Sep 17 00:00:00 2001 From: Romain Brenguier Date: Mon, 14 Sep 2026 10:52:40 +0200 Subject: [PATCH 4/9] SONARJAVA-6944: Address review comments on S9387 - Revert manually added @beforeClass/@afterClass entries in S9387.html (auto-generated from RSPEC, should not be hand-edited) - Move @beforeClass/@afterClass noncompliant test cases above the "Compliant cases" section for clarity - Add secondary location assertion to multipleTestNGTags test case Co-Authored-By: Claude Opus 4.6 --- .../tests/TestNGJavadocTagsCheckSample.java | 26 +++++++++---------- .../org/sonar/l10n/java/rules/java/S9387.html | 2 -- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/java-checks-test-sources/default/src/main/java/checks/tests/TestNGJavadocTagsCheckSample.java b/java-checks-test-sources/default/src/main/java/checks/tests/TestNGJavadocTagsCheckSample.java index 9324a80637e..ec865891e6c 100644 --- a/java-checks-test-sources/default/src/main/java/checks/tests/TestNGJavadocTagsCheckSample.java +++ b/java-checks-test-sources/default/src/main/java/checks/tests/TestNGJavadocTagsCheckSample.java @@ -95,13 +95,25 @@ public void mixedWithStandardTags(String input) { // Noncompliant {{Replace this * @test * @beforeMethod */ - public void multipleTestNGTags() { // Noncompliant {{Replace this "@test" Javadoc tag with the TestNG "@Test" annotation.}} + public void multipleTestNGTags() { // Noncompliant {{Replace this "@test" Javadoc tag with the TestNG "@Test" annotation.}} [[secondary=98]] } /** @test */ public void singleLineJavadoc() { // Noncompliant {{Replace this "@test" Javadoc tag with the TestNG "@Test" annotation.}} } + /** + * @beforeClass + */ + public void classSetUp() { // Noncompliant {{Replace this "@beforeClass" Javadoc tag with the TestNG "@BeforeClass" annotation.}} + } + + /** + * @afterClass + */ + public void classTearDown() { // Noncompliant {{Replace this "@afterClass" Javadoc tag with the TestNG "@AfterClass" annotation.}} + } + /** * Example of TestNG usage: *
    @@ -173,16 +185,4 @@ public void typeOnlyTag() { // compliant
        */
       public void anotherTypeOnlyTag() { // compliant
       }
    -
    -  /**
    -   * @beforeClass
    -   */
    -  public void classSetUp() { // Noncompliant {{Replace this "@beforeClass" Javadoc tag with the TestNG "@BeforeClass" annotation.}}
    -  }
    -
    -  /**
    -   * @afterClass
    -   */
    -  public void classTearDown() { // Noncompliant {{Replace this "@afterClass" Javadoc tag with the TestNG "@AfterClass" annotation.}}
    -  }
     }
    diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9387.html b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9387.html
    index 377b0544600..5af4ed4a31a 100644
    --- a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9387.html
    +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9387.html
    @@ -36,8 +36,6 @@ 

    How to fix it

  • /** @test */ becomes @Test
  • /** @beforeMethod */ becomes @BeforeMethod
  • /** @afterMethod */ becomes @AfterMethod
  • -
  • /** @beforeClass */ becomes @BeforeClass
  • -
  • /** @afterClass */ becomes @AfterClass
  • /** @beforeSuite */ becomes @BeforeSuite
  • /** @afterSuite */ becomes @AfterSuite
  • /** @beforeTest */ becomes @BeforeTest
  • From 2c1b2c9210cf954b6c94df100ce9a2bfd8f075fb Mon Sep 17 00:00:00 2001 From: Romain Brenguier Date: Mon, 14 Sep 2026 11:09:13 +0200 Subject: [PATCH 5/9] SONARJAVA-6944: Fix regex backtracking and secondary location in S9387 Replace \s with [\t ] in regex patterns to avoid super-linear backtracking (SonarQube S8786). Use tree.simpleName() for secondary locations to match the primary issue location scope. Co-Authored-By: Claude Opus 4.6 --- .../org/sonar/java/checks/tests/TestNGJavadocTagsCheck.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/java-checks/src/main/java/org/sonar/java/checks/tests/TestNGJavadocTagsCheck.java b/java-checks/src/main/java/org/sonar/java/checks/tests/TestNGJavadocTagsCheck.java index e6b9c1b2ce3..3ca9448f04d 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/tests/TestNGJavadocTagsCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/tests/TestNGJavadocTagsCheck.java @@ -34,9 +34,9 @@ @Rule(key = "S9387") public class TestNGJavadocTagsCheck extends IssuableSubscriptionVisitor { - private static final Pattern BLOCK_TAG_PATTERN = Pattern.compile("(?:^|/\\*\\*)\\s*\\*?\\s*@(\\w+)", Pattern.MULTILINE); + private static final Pattern BLOCK_TAG_PATTERN = Pattern.compile("(?:^|/\\*\\*)[\\t ]*\\*?[\\t ]*@(\\w+)", Pattern.MULTILINE); private static final Pattern PRE_BLOCK_PATTERN = Pattern.compile("
    .*?
    ", Pattern.DOTALL); - private static final Pattern CODE_TAG_PATTERN = Pattern.compile("\\{@code\\s+[^}]*\\}"); + private static final Pattern CODE_TAG_PATTERN = Pattern.compile("\\{@code[\\t ]+[^}]*\\}"); private static final Map TESTNG_TAGS_TO_ANNOTATIONS = MapBuilder.newMap() .put("test", "@Test") @@ -88,7 +88,7 @@ private void checkJavadoc(MethodTree tree, SyntaxTrivia trivia) { secondaryLocations.add(new JavaFileScannerContext.Location( String.format("Also replace \"@%s\" with the TestNG \"%s\" annotation.", tagName, annotation), - tree)); + tree.simpleName())); } } } From 506c25bdc45da46ce526ffb74ff411dfc1c9c57c Mon Sep 17 00:00:00 2001 From: Romain Brenguier Date: Mon, 14 Sep 2026 11:31:03 +0200 Subject: [PATCH 6/9] SONARJAVA-6944: Use possessive quantifiers to fix regex backtracking in S9387 Co-Authored-By: Claude Opus 4.6 --- .../org/sonar/java/checks/tests/TestNGJavadocTagsCheck.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/java-checks/src/main/java/org/sonar/java/checks/tests/TestNGJavadocTagsCheck.java b/java-checks/src/main/java/org/sonar/java/checks/tests/TestNGJavadocTagsCheck.java index 3ca9448f04d..e713c20c726 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/tests/TestNGJavadocTagsCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/tests/TestNGJavadocTagsCheck.java @@ -34,9 +34,9 @@ @Rule(key = "S9387") public class TestNGJavadocTagsCheck extends IssuableSubscriptionVisitor { - private static final Pattern BLOCK_TAG_PATTERN = Pattern.compile("(?:^|/\\*\\*)[\\t ]*\\*?[\\t ]*@(\\w+)", Pattern.MULTILINE); + private static final Pattern BLOCK_TAG_PATTERN = Pattern.compile("(?:^|/\\*\\*)[\\t ]*+\\*?[\\t ]*+@(\\w+)", Pattern.MULTILINE); private static final Pattern PRE_BLOCK_PATTERN = Pattern.compile("
    .*?
    ", Pattern.DOTALL); - private static final Pattern CODE_TAG_PATTERN = Pattern.compile("\\{@code[\\t ]+[^}]*\\}"); + private static final Pattern CODE_TAG_PATTERN = Pattern.compile("\\{@code[\\t ][^}]*+\\}"); private static final Map TESTNG_TAGS_TO_ANNOTATIONS = MapBuilder.newMap() .put("test", "@Test") From b5b631026b015f38ef3f5b25b35a13c520da3a06 Mon Sep 17 00:00:00 2001 From: Romain Brenguier Date: Mon, 14 Sep 2026 15:26:03 +0200 Subject: [PATCH 7/9] SONARJAVA-6944: Add precise location markers to S9387 test sample Co-Authored-By: Claude Opus 4.6 --- .../tests/TestNGJavadocTagsCheckSample.java | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/java-checks-test-sources/default/src/main/java/checks/tests/TestNGJavadocTagsCheckSample.java b/java-checks-test-sources/default/src/main/java/checks/tests/TestNGJavadocTagsCheckSample.java index ec865891e6c..5bdb449d288 100644 --- a/java-checks-test-sources/default/src/main/java/checks/tests/TestNGJavadocTagsCheckSample.java +++ b/java-checks-test-sources/default/src/main/java/checks/tests/TestNGJavadocTagsCheckSample.java @@ -6,60 +6,70 @@ class TestNGJavadocTagsCheckSample { * @test */ public void shouldValidateInput() { // Noncompliant {{Replace this "@test" Javadoc tag with the TestNG "@Test" annotation.}} +// ^^^^^^^^^^^^^^^^^^^ } /** * @beforeMethod */ public void setUp() { // Noncompliant {{Replace this "@beforeMethod" Javadoc tag with the TestNG "@BeforeMethod" annotation.}} +// ^^^^^ } /** * @afterMethod */ public void tearDown() { // Noncompliant {{Replace this "@afterMethod" Javadoc tag with the TestNG "@AfterMethod" annotation.}} +// ^^^^^^^^ } /** * @beforeSuite */ public void suiteSetUp() { // Noncompliant {{Replace this "@beforeSuite" Javadoc tag with the TestNG "@BeforeSuite" annotation.}} +// ^^^^^^^^^^ } /** * @afterSuite */ public void suiteTearDown() { // Noncompliant {{Replace this "@afterSuite" Javadoc tag with the TestNG "@AfterSuite" annotation.}} +// ^^^^^^^^^^^^^ } /** * @beforeTest */ public void testSetUp() { // Noncompliant {{Replace this "@beforeTest" Javadoc tag with the TestNG "@BeforeTest" annotation.}} +// ^^^^^^^^^ } /** * @afterTest */ public void testTearDown() { // Noncompliant {{Replace this "@afterTest" Javadoc tag with the TestNG "@AfterTest" annotation.}} +// ^^^^^^^^^^^^ } /** * @beforeGroups */ public void groupSetUp() { // Noncompliant {{Replace this "@beforeGroups" Javadoc tag with the TestNG "@BeforeGroups" annotation.}} +// ^^^^^^^^^^ } /** * @afterGroups */ public void groupTearDown() { // Noncompliant {{Replace this "@afterGroups" Javadoc tag with the TestNG "@AfterGroups" annotation.}} +// ^^^^^^^^^^^^^ } /** * @dataProvider */ public Object[][] provideData() { // Noncompliant {{Replace this "@dataProvider" Javadoc tag with the TestNG "@DataProvider" annotation.}} +// ^^^^^^^^^^^ return new Object[][] {}; } @@ -67,6 +77,7 @@ public Object[][] provideData() { // Noncompliant {{Replace this "@dataProvider" * @factory */ public Object[] createInstances() { // Noncompliant {{Replace this "@factory" Javadoc tag with the TestNG "@Factory" annotation.}} +// ^^^^^^^^^^^^^^^ return new Object[] {}; } @@ -74,12 +85,14 @@ public Object[] createInstances() { // Noncompliant {{Replace this "@factory" Ja * @Test */ public void caseInsensitiveUpperCase() { // Noncompliant {{Replace this "@Test" Javadoc tag with the TestNG "@Test" annotation.}} +// ^^^^^^^^^^^^^^^^^^^^^^^^ } /** * @TEST */ public void caseInsensitiveAllCaps() { // Noncompliant {{Replace this "@TEST" Javadoc tag with the TestNG "@Test" annotation.}} +// ^^^^^^^^^^^^^^^^^^^^^^ } /** @@ -88,6 +101,7 @@ public void caseInsensitiveAllCaps() { // Noncompliant {{Replace this "@TEST" Ja * @test */ public void mixedWithStandardTags(String input) { // Noncompliant {{Replace this "@test" Javadoc tag with the TestNG "@Test" annotation.}} +// ^^^^^^^^^^^^^^^^^^^^^ } /** @@ -95,23 +109,27 @@ public void mixedWithStandardTags(String input) { // Noncompliant {{Replace this * @test * @beforeMethod */ - public void multipleTestNGTags() { // Noncompliant {{Replace this "@test" Javadoc tag with the TestNG "@Test" annotation.}} [[secondary=98]] + public void multipleTestNGTags() { // Noncompliant {{Replace this "@test" Javadoc tag with the TestNG "@Test" annotation.}} [[secondary=112]] +// ^^^^^^^^^^^^^^^^^^ } /** @test */ public void singleLineJavadoc() { // Noncompliant {{Replace this "@test" Javadoc tag with the TestNG "@Test" annotation.}} +// ^^^^^^^^^^^^^^^^^ } /** * @beforeClass */ public void classSetUp() { // Noncompliant {{Replace this "@beforeClass" Javadoc tag with the TestNG "@BeforeClass" annotation.}} +// ^^^^^^^^^^ } /** * @afterClass */ public void classTearDown() { // Noncompliant {{Replace this "@afterClass" Javadoc tag with the TestNG "@AfterClass" annotation.}} +// ^^^^^^^^^^^^^ } /** From d5d489102e3aa6c088017298e3636b305e99a543 Mon Sep 17 00:00:00 2001 From: Romain Brenguier Date: Tue, 15 Sep 2026 12:14:56 +0200 Subject: [PATCH 8/9] SONARJAVA-6944: Address PR review comments on S9387 - Switch rule scope from "All" to "Tests" to match sibling test rules - Fix multiline {@code} blocks not being masked (support newlines after {@code) - Add regression test for multiline {@code} block Co-Authored-By: Claude Opus 4.6 --- .../checks/tests/TestNGJavadocTagsCheckSample.java | 10 ++++++++++ .../java/checks/tests/TestNGJavadocTagsCheck.java | 2 +- .../org/sonar/l10n/java/rules/java/S9387.json | 2 +- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/java-checks-test-sources/default/src/main/java/checks/tests/TestNGJavadocTagsCheckSample.java b/java-checks-test-sources/default/src/main/java/checks/tests/TestNGJavadocTagsCheckSample.java index 5bdb449d288..5109230f5f5 100644 --- a/java-checks-test-sources/default/src/main/java/checks/tests/TestNGJavadocTagsCheckSample.java +++ b/java-checks-test-sources/default/src/main/java/checks/tests/TestNGJavadocTagsCheckSample.java @@ -149,6 +149,16 @@ public void withPreBlock() { // compliant public void withCodeTag() { // compliant } + /** + * How to use annotations: + * {@code + * @Test + * void example() {} + * } + */ + public void withMultilineCodeTag() { // compliant + } + // --- Compliant cases --- /** diff --git a/java-checks/src/main/java/org/sonar/java/checks/tests/TestNGJavadocTagsCheck.java b/java-checks/src/main/java/org/sonar/java/checks/tests/TestNGJavadocTagsCheck.java index e713c20c726..6dbb0401369 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/tests/TestNGJavadocTagsCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/tests/TestNGJavadocTagsCheck.java @@ -36,7 +36,7 @@ public class TestNGJavadocTagsCheck extends IssuableSubscriptionVisitor { private static final Pattern BLOCK_TAG_PATTERN = Pattern.compile("(?:^|/\\*\\*)[\\t ]*+\\*?[\\t ]*+@(\\w+)", Pattern.MULTILINE); private static final Pattern PRE_BLOCK_PATTERN = Pattern.compile("
    .*?
    ", Pattern.DOTALL); - private static final Pattern CODE_TAG_PATTERN = Pattern.compile("\\{@code[\\t ][^}]*+\\}"); + private static final Pattern CODE_TAG_PATTERN = Pattern.compile("\\{@code[\\s][^}]*+\\}", Pattern.DOTALL); private static final Map TESTNG_TAGS_TO_ANNOTATIONS = MapBuilder.newMap() .put("test", "@Test") diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9387.json b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9387.json index 89f8e8f87ec..cdee8d59db1 100644 --- a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9387.json +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9387.json @@ -13,7 +13,7 @@ "defaultSeverity": "Major", "ruleSpecification": "RSPEC-9387", "sqKey": "S9387", - "scope": "All", + "scope": "Tests", "quickfix": "unknown", "code": { "impacts": { From b0abab33486c706825642c254b312fddffd7b723 Mon Sep 17 00:00:00 2001 From: Romain Brenguier Date: Thu, 17 Sep 2026 13:38:17 +0200 Subject: [PATCH 9/9] SONARJAVA-6944: Address remaining PR review comments on S9387 Add missing 'tests' tag for Tests-scoped rule, move test sample to test sources path, and remove ineffective DOTALL flag from regex. Co-Authored-By: Claude Opus 4.6 --- .../java/checks/tests/TestNGJavadocTagsCheckSample.java | 0 .../org/sonar/java/checks/tests/TestNGJavadocTagsCheck.java | 2 +- .../sonar/java/checks/tests/TestNGJavadocTagsCheckTest.java | 6 +++--- .../resources/org/sonar/l10n/java/rules/java/S9387.json | 3 ++- 4 files changed, 6 insertions(+), 5 deletions(-) rename java-checks-test-sources/default/src/{main => test}/java/checks/tests/TestNGJavadocTagsCheckSample.java (100%) diff --git a/java-checks-test-sources/default/src/main/java/checks/tests/TestNGJavadocTagsCheckSample.java b/java-checks-test-sources/default/src/test/java/checks/tests/TestNGJavadocTagsCheckSample.java similarity index 100% rename from java-checks-test-sources/default/src/main/java/checks/tests/TestNGJavadocTagsCheckSample.java rename to java-checks-test-sources/default/src/test/java/checks/tests/TestNGJavadocTagsCheckSample.java diff --git a/java-checks/src/main/java/org/sonar/java/checks/tests/TestNGJavadocTagsCheck.java b/java-checks/src/main/java/org/sonar/java/checks/tests/TestNGJavadocTagsCheck.java index 6dbb0401369..598539c7466 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/tests/TestNGJavadocTagsCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/tests/TestNGJavadocTagsCheck.java @@ -36,7 +36,7 @@ public class TestNGJavadocTagsCheck extends IssuableSubscriptionVisitor { private static final Pattern BLOCK_TAG_PATTERN = Pattern.compile("(?:^|/\\*\\*)[\\t ]*+\\*?[\\t ]*+@(\\w+)", Pattern.MULTILINE); private static final Pattern PRE_BLOCK_PATTERN = Pattern.compile("
    .*?
    ", Pattern.DOTALL); - private static final Pattern CODE_TAG_PATTERN = Pattern.compile("\\{@code[\\s][^}]*+\\}", Pattern.DOTALL); + private static final Pattern CODE_TAG_PATTERN = Pattern.compile("\\{@code\\s[^}]*+\\}"); private static final Map TESTNG_TAGS_TO_ANNOTATIONS = MapBuilder.newMap() .put("test", "@Test") diff --git a/java-checks/src/test/java/org/sonar/java/checks/tests/TestNGJavadocTagsCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/tests/TestNGJavadocTagsCheckTest.java index 4f700579dd2..82c0caf5c6d 100644 --- a/java-checks/src/test/java/org/sonar/java/checks/tests/TestNGJavadocTagsCheckTest.java +++ b/java-checks/src/test/java/org/sonar/java/checks/tests/TestNGJavadocTagsCheckTest.java @@ -19,14 +19,14 @@ import org.junit.jupiter.api.Test; import org.sonar.java.checks.verifier.CheckVerifier; -import static org.sonar.java.checks.verifier.TestUtils.mainCodeSourcesPath; +import static org.sonar.java.checks.verifier.TestUtils.testCodeSourcesPath; class TestNGJavadocTagsCheckTest { @Test void test() { CheckVerifier.newVerifier() - .onFile(mainCodeSourcesPath("checks/tests/TestNGJavadocTagsCheckSample.java")) + .onFile(testCodeSourcesPath("checks/tests/TestNGJavadocTagsCheckSample.java")) .withCheck(new TestNGJavadocTagsCheck()) .verifyIssues(); } @@ -34,7 +34,7 @@ void test() { @Test void test_without_semantic() { CheckVerifier.newVerifier() - .onFile(mainCodeSourcesPath("checks/tests/TestNGJavadocTagsCheckSample.java")) + .onFile(testCodeSourcesPath("checks/tests/TestNGJavadocTagsCheckSample.java")) .withCheck(new TestNGJavadocTagsCheck()) .withoutSemantic() .verifyIssues(); diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9387.json b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9387.json index cdee8d59db1..9b8976ad8db 100644 --- a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9387.json +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9387.json @@ -8,7 +8,8 @@ }, "tags": [ "deprecated", - "convention" + "convention", + "tests" ], "defaultSeverity": "Major", "ruleSpecification": "RSPEC-9387",