Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
package checks.tests;

class TestNGJavadocTagsCheckSample {
Comment thread
gitar-bot[bot] marked this conversation as resolved.

/**
* @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]]
// ^^^^^^^^^^^^^^^^^^
}
Comment thread
gitar-bot[bot] marked this conversation as resolved.

/** @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:
* <pre>
* @Test
* public void exampleTest() {}
* </pre>
*/
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 ---
Comment thread
gitar-bot[bot] marked this conversation as resolved.

/**
* @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
}
}
Original file line number Diff line number Diff line change
@@ -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("<pre>.*?</pre>", Pattern.DOTALL);
private static final Pattern CODE_TAG_PATTERN = Pattern.compile("\\{@code\\s[^}]*+\\}");

private static final Map<String, String> TESTNG_TAGS_TO_ANNOTATIONS = MapBuilder.<String, String>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<Tree.Kind> 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));
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Java 23 Markdown documentation comments are represented as SyntaxTrivia.CommentKind.MARKDOWN, so /// @test is currently skipped even though it is still a documentation-based declaration. If the rule is intended to cover all Java documentation comments, could we inspect MARKDOWN trivia too, as the existing documentation checks do?

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<JavaFileScannerContext.Location> secondaryLocations = new ArrayList<>();

while (matcher.find()) {
String tagName = matcher.group(1);
String annotation = TESTNG_TAGS_TO_ANNOTATIONS.get(tagName.toLowerCase(Locale.ROOT));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lowercasing makes annotation names indistinguishable from the legacy case-sensitive tags. For example, documentation containing @Test is the annotation used by TestNG is treated as obsolete @test. Could we match the documented legacy spellings exactly (@test, @beforeMethod, etc.) and keep uppercase annotation names compliant?


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;
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
Loading
Loading