diff --git a/java-checks-test-sources/default/src/test/java/checks/tests/TestNGJavadocTagsCheckSample.java b/java-checks-test-sources/default/src/test/java/checks/tests/TestNGJavadocTagsCheckSample.java new file mode 100644 index 00000000000..5109230f5f5 --- /dev/null +++ b/java-checks-test-sources/default/src/test/java/checks/tests/TestNGJavadocTagsCheckSample.java @@ -0,0 +1,216 @@ +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.}} +// ^^^^^^^^ + } + + /** + * @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[][] {}; + } + + /** + * @factory + */ + public Object[] createInstances() { // Noncompliant {{Replace this "@factory" Javadoc tag with the TestNG "@Factory" annotation.}} +// ^^^^^^^^^^^^^^^ + return new Object[] {}; + } + + /** + * @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.}} [[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.}} +// ^^^^^^^^^^^^^ + } + + /** + * 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 + } + + /** + * How to use annotations: + * {@code + * @Test + * void example() {} + * } + */ + public void withMultilineCodeTag() { // compliant + } + + // --- 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 + } + + /** + * @listeners - Type-only annotation, not applicable to methods + */ + public void typeOnlyTag() { // compliant + } + + /** + * @parameters - Type-only annotation, not applicable to methods + */ + public void anotherTypeOnlyTag() { // 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..598539c7466 --- /dev/null +++ b/java-checks/src/main/java/org/sonar/java/checks/tests/TestNGJavadocTagsCheck.java @@ -0,0 +1,108 @@ +/* + * 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.MethodTree; +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("(?:^|/\\*\\*)[\\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 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") + .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((MethodTree) tree, trivia)); + } + + private void checkJavadoc(MethodTree tree, SyntaxTrivia trivia) { + String commentText = trivia.comment(); + String textWithoutCodeBlocks = removeCodeBlocks(commentText); + + Matcher matcher = BLOCK_TAG_PATTERN.matcher(textWithoutCodeBlocks); + + 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, annotation), + tree.simpleName())); + } + } + } + + if (firstTagOriginal != null) { + String message = String.format("Replace this \"@%s\" Javadoc tag with the TestNG \"%s\" annotation.", firstTagOriginal, firstAnnotation); + 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/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..82c0caf5c6d --- /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.testCodeSourcesPath; + +class TestNGJavadocTagsCheckTest { + + @Test + void test() { + CheckVerifier.newVerifier() + .onFile(testCodeSourcesPath("checks/tests/TestNGJavadocTagsCheckSample.java")) + .withCheck(new TestNGJavadocTagsCheck()) + .verifyIssues(); + } + + @Test + void test_without_semantic() { + CheckVerifier.newVerifier() + .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.html b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9387.html new file mode 100644 index 00000000000..5af4ed4a31a --- /dev/null +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9387.html @@ -0,0 +1,74 @@ +

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
  • +
  • /** @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.

+

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..9b8976ad8db --- /dev/null +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9387.json @@ -0,0 +1,25 @@ +{ + "title": "TestNG Javadoc tags should be converted to annotations", + "type": "CODE_SMELL", + "status": "ready", + "remediation": { + "func": "Constant\/Issue", + "constantCost": "5 min" + }, + "tags": [ + "deprecated", + "convention", + "tests" + ], + "defaultSeverity": "Major", + "ruleSpecification": "RSPEC-9387", + "sqKey": "S9387", + "scope": "Tests", + "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