diff --git a/its/ruling/src/test/resources/eclipse-jetty-similar-to-main/java-S9355.json b/its/ruling/src/test/resources/eclipse-jetty-similar-to-main/java-S9355.json new file mode 100644 index 00000000000..5712b28d32e --- /dev/null +++ b/its/ruling/src/test/resources/eclipse-jetty-similar-to-main/java-S9355.json @@ -0,0 +1,23 @@ +{ +"org.eclipse.jetty:jetty-project:jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java": [ +56, +75, +105, +118 +], +"org.eclipse.jetty:jetty-project:jetty-server/src/main/java/org/eclipse/jetty/server/Request.java": [ +1115, +1994, +2002 +], +"org.eclipse.jetty:jetty-project:jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java": [ +596, +609, +626, +1174, +1527 +], +"org.eclipse.jetty:jetty-project:jetty-server/src/main/java/org/eclipse/jetty/server/handler/ScopedHandler.java": [ +204 +] +} diff --git a/its/ruling/src/test/resources/eclipse-jetty/java-S9355.json b/its/ruling/src/test/resources/eclipse-jetty/java-S9355.json new file mode 100644 index 00000000000..e7038766ea2 --- /dev/null +++ b/its/ruling/src/test/resources/eclipse-jetty/java-S9355.json @@ -0,0 +1,30 @@ +{ +"org.eclipse.jetty:jetty-project:jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java": [ +56, +75, +105, +118 +], +"org.eclipse.jetty:jetty-project:jetty-server/src/main/java/org/eclipse/jetty/server/Request.java": [ +1115, +1994, +2002 +], +"org.eclipse.jetty:jetty-project:jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java": [ +596, +609, +626, +1174, +1527 +], +"org.eclipse.jetty:jetty-project:jetty-server/src/main/java/org/eclipse/jetty/server/handler/ScopedHandler.java": [ +204 +], +"org.eclipse.jetty:jetty-project:jetty-util/src/main/java/org/eclipse/jetty/util/IteratingCallback.java": [ +420 +], +"org.eclipse.jetty:jetty-project:jetty-util/src/main/java/org/eclipse/jetty/util/ssl/X509.java": [ +41, +46 +] +} diff --git a/its/ruling/src/test/resources/guava/java-S9355.json b/its/ruling/src/test/resources/guava/java-S9355.json new file mode 100644 index 00000000000..cd9a7c2e35c --- /dev/null +++ b/its/ruling/src/test/resources/guava/java-S9355.json @@ -0,0 +1,15 @@ +{ +"com.google.guava:guava:src/com/google/common/collect/ImmutableEnumSet.java": [ +46 +], +"com.google.guava:guava:src/com/google/common/collect/LinkedListMultimap.java": [ +105 +], +"com.google.guava:guava:src/com/google/common/net/PercentEscaper.java": [ +136, +153 +], +"com.google.guava:guava:src/com/google/common/util/concurrent/AbstractIdleService.java": [ +39 +] +} diff --git a/java-checks-test-sources/default/src/main/java/checks/AlmostJavadocCheckSample.java b/java-checks-test-sources/default/src/main/java/checks/AlmostJavadocCheckSample.java new file mode 100644 index 00000000000..32695186b26 --- /dev/null +++ b/java-checks-test-sources/default/src/main/java/checks/AlmostJavadocCheckSample.java @@ -0,0 +1,156 @@ +package checks; + +class AlmostJavadocCheckSample { + + String name; + + // Noncompliant@+1 [[quickfixes=qf1]] + /* + * Computes the factorial of a positive integer. + * @param n the number to compute factorial for + * @return the factorial of n + */ + // fix@qf1 {{Convert to Javadoc comment}} + // edit@qf1 [[sc=4;ec=4]] {{*}} + public long factorial(int n) { + return (n <= 1) ? 1L : n * factorial(n - 1); + } + + /** + * Computes the factorial of a positive integer. + * @param n the number to compute factorial for + * @return the factorial of n + */ + public long documentedFactorial(int n) { + return (n <= 1) ? 1L : n * documentedFactorial(n - 1); + } + + // Noncompliant@+1 [[quickfixes=qf2]] + /* Returns the display name as String. */ + // fix@qf2 {{Convert to Javadoc comment}} + // edit@qf2 [[sc=4;ec=4]] {{*}} + public String displayName() { + return name; + } + + /** Returns the display name as String. */ + public String documentedDisplayName() { + return name; + } + + interface Repository { + // Noncompliant@+1 [[quickfixes=qf3]] + // Loads the entity. {@link Entity} */ + // fix@qf3 {{Convert to Javadoc comment}} + // edit@qf3 [[sc=5;ec=7]] {{/**}} + Entity load(String id); + + /** Loads the entity. {@link Entity} */ + Entity documentedLoad(String id); + + // Noncompliant@+1 + // Loads with trailing space. {@link Entity} */ + Entity loadWithTrailingSpace(String id); + } + + // Noncompliant@+1 + /* {@link AlmostJavadocCheckSample} */ + static class Nested {} + + /** {@link AlmostJavadocCheckSample} */ + static class DocumentedNested {} + + // Noncompliant@+1 + /* @since 1.0 */ + int version; + + /** @since 1.0 */ + int documentedVersion; + + enum Kind { + // Noncompliant@+1 + /* Foo bar. */ + FOO, + /** Foo bar. */ + BAR + } + + /* Regular commentary without tags. */ + void undocumentedOnPurpose() { + } + + // Regular line comment with {@link tags} is not almost-Javadoc + void lineCommentWithoutTerminator() { + } + + /* returns 0 on success */ + int noTagBecauseReturnIsAWord() { + return 0; + } + + /* support@param.org is an email, not a Javadoc tag */ + void emailLooksLikeTag() { + } + + /* @Override is a Java annotation mentioned in a comment */ + void annotationMention() { + } + + /* List uses generics, not HTML */ + void genericsAreNotHtml() { + voidWithLocalComment(); + } + + void voidWithLocalComment() { + /* @param local is not attached to a documentable declaration */ + int local = 1; + } + + /** Valid Javadoc. */ + /* Extra note with {@link tags}. */ + void alreadyHasJavadoc() { + } + + /// Markdown documentation with {@link tags}. + /* Extra note with @param. */ + void alreadyHasMarkdown() { + } + + @Override + public String toString() { + return name; + } + + record Point(int x, int y) { + // Noncompliant@+1 + /* @param x the x coordinate */ + Point { + } + } + + // Noncompliant@+1 + /* @since 1.0 */ + int a, b, c; + + int trailingField; // trailing note: see @param x */ + void methodAfterTrailingComment(int x) { + } + + void localClassIsNotDocumentable() { + /* @param q */ + class Local { + /* @return r */ + int r; + } + } + + static class Entity { + } +} + +// Noncompliant@+1 [[quickfixes=qf4]] +// /** Extra documentation. {@link AlmostJavadocCheckSample.Entity} */ +// fix@qf4 {{Convert to Javadoc comment}} +// edit@qf4 [[sc=1;ec=3]] {{}} +class AlmostJavadocCheckSampleSecondType { +} diff --git a/java-checks-test-sources/default/src/main/java/checks/AlmostJavadocCheck_compactSource.java b/java-checks-test-sources/default/src/main/java/checks/AlmostJavadocCheck_compactSource.java new file mode 100644 index 00000000000..30a7986143f --- /dev/null +++ b/java-checks-test-sources/default/src/main/java/checks/AlmostJavadocCheck_compactSource.java @@ -0,0 +1,20 @@ +void main() { + System.out.println("compact source"); +} + +// Noncompliant@+1 +/* @since 1.0 */ +int version; + +/** @since 1.0 */ +int documentedVersion; + +// Noncompliant@+1 +/* @param n unused */ +int a, b, c; + +// Noncompliant@+1 +/* Returns ok. */ +String status() { + return "ok"; +} diff --git a/java-checks/src/main/java/org/sonar/java/checks/AlmostJavadocCheck.java b/java-checks/src/main/java/org/sonar/java/checks/AlmostJavadocCheck.java new file mode 100644 index 00000000000..dbb292499df --- /dev/null +++ b/java-checks/src/main/java/org/sonar/java/checks/AlmostJavadocCheck.java @@ -0,0 +1,186 @@ +/* + * 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; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.sonar.check.Rule; +import org.sonar.java.ast.visitors.PublicApiChecker; +import org.sonar.java.checks.helpers.QuickFixHelper; +import org.sonar.java.reporting.AnalyzerMessage; +import org.sonar.java.reporting.JavaQuickFix; +import org.sonar.java.reporting.JavaTextEdit; +import org.sonar.plugins.java.api.IssuableSubscriptionVisitor; +import org.sonar.plugins.java.api.location.Position; +import org.sonar.plugins.java.api.tree.ClassTree; +import org.sonar.plugins.java.api.tree.CompilationUnitTree; +import org.sonar.plugins.java.api.tree.SyntaxToken; +import org.sonar.plugins.java.api.tree.SyntaxTrivia; +import org.sonar.plugins.java.api.tree.SyntaxTrivia.CommentKind; +import org.sonar.plugins.java.api.tree.Tree; +import org.sonar.plugins.java.api.tree.VariableTree; + +@Rule(key = "S9355") +public class AlmostJavadocCheck extends IssuableSubscriptionVisitor { + + static final String MESSAGE = "This comment contains Javadoc or HTML tags, but isn't started with a double asterisk (/**); is it meant to be Javadoc?"; + + private static final Pattern CLOSING_HTML = Pattern.compile(""); + private static final Pattern AT_TAG = Pattern.compile("(? JAVADOC_TAGS = Set.of( + "@author", "@code", "@deprecated", "@docRoot", "@exception", "@inheritDoc", + "@link", "@linkplain", "@literal", "@param", "@return", "@see", "@serial", + "@serialData", "@serialField", "@since", "@snippet", "@throws", "@value", "@version"); + + @Override + public List nodesToVisit() { + List kinds = new ArrayList<>(Arrays.asList(PublicApiChecker.apiKinds())); + kinds.add(Tree.Kind.ENUM_CONSTANT); + return kinds; + } + + @Override + public void visitNode(Tree tree) { + if (!isDocumentableDeclaration(tree)) { + return; + } + SyntaxToken firstToken = tree.firstToken(); + if (firstToken == null) { + return; + } + List trivias = firstToken.trivias(); + if (trivias.stream().anyMatch(trivia -> trivia.isComment(CommentKind.JAVADOC, CommentKind.MARKDOWN))) { + return; + } + for (SyntaxTrivia trivia : trivias) { + if (!belongsToPreviousMember(tree, trivia) && isAlmostJavadoc(trivia)) { + reportAlmostJavadoc(trivia); + } + } + } + + private static boolean isDocumentableDeclaration(Tree tree) { + if (tree.is(Tree.Kind.IMPLICIT_CLASS) || isInsideMethodOrConstructor(tree)) { + return false; + } + if (tree.is(Tree.Kind.VARIABLE)) { + return tree.parent() instanceof ClassTree classTree && isFirstDeclarator(classTree, (VariableTree) tree); + } + return true; + } + + private static boolean isFirstDeclarator(ClassTree classTree, VariableTree variable) { + List members = classTree.members(); + int index = members.indexOf(variable); + if (index <= 0) { + return true; + } + Tree preceding = members.get(index - 1); + return !(preceding.is(Tree.Kind.VARIABLE) && preceding.firstToken().equals(variable.firstToken())); + } + + private static boolean isInsideMethodOrConstructor(Tree tree) { + for (Tree current = tree.parent(); current != null; current = current.parent()) { + if (current.is(Tree.Kind.METHOD, Tree.Kind.CONSTRUCTOR, Tree.Kind.LAMBDA_EXPRESSION)) { + return true; + } + } + return false; + } + + private static boolean belongsToPreviousMember(Tree tree, SyntaxTrivia trivia) { + Tree previous = previousSibling(tree); + if (previous == null) { + return false; + } + SyntaxToken previousEnd = previous.lastToken(); + return previousEnd != null && trivia.range().start().line() == previousEnd.range().end().line(); + } + + private static Tree previousSibling(Tree tree) { + Tree parent = tree.parent(); + if (parent instanceof ClassTree classTree) { + List members = classTree.members(); + int index = members.indexOf(tree); + return index > 0 ? members.get(index - 1) : null; + } + if (parent instanceof CompilationUnitTree compilationUnit) { + List types = compilationUnit.types(); + int index = types.indexOf(tree); + return index > 0 ? types.get(index - 1) : null; + } + return null; + } + + private static boolean isAlmostJavadoc(SyntaxTrivia trivia) { + String text = trivia.comment().stripTrailing(); + if (trivia.isComment(CommentKind.BLOCK)) { + return hasTag(text); + } + return trivia.isComment(CommentKind.LINE) + && text.endsWith("*/") + && hasTag(text); + } + + private static boolean hasTag(String text) { + if (CLOSING_HTML.matcher(text).find()) { + return true; + } + Matcher matcher = AT_TAG.matcher(text); + while (matcher.find()) { + if (JAVADOC_TAGS.contains(matcher.group())) { + return true; + } + } + return false; + } + + private void reportAlmostJavadoc(SyntaxTrivia trivia) { + Position start = trivia.range().start(); + Position end = trivia.range().end(); + QuickFixHelper.newIssue(context) + .forRule(this) + .onRange(start.line(), start.columnOffset(), end.line(), end.columnOffset()) + .withMessage(MESSAGE) + .withQuickFix(() -> convertToJavadoc(trivia)) + .report(); + } + + private static JavaQuickFix convertToJavadoc(SyntaxTrivia trivia) { + Position start = trivia.range().start(); + String text = trivia.comment(); + JavaTextEdit edit; + if (trivia.isComment(CommentKind.LINE) && text.startsWith("// /**")) { + edit = JavaTextEdit.replaceTextSpan(firstCharacters(start, 2), ""); + } else if (trivia.isComment(CommentKind.BLOCK)) { + edit = JavaTextEdit.insertAtPosition(start.line(), start.columnOffset() + 1, "*"); + } else { + edit = JavaTextEdit.replaceTextSpan(firstCharacters(start, 2), "/**"); + } + return JavaQuickFix.newQuickFix("Convert to Javadoc comment") + .addTextEdit(edit) + .build(); + } + + private static AnalyzerMessage.TextSpan firstCharacters(Position start, int length) { + return new AnalyzerMessage.TextSpan(start.line(), start.columnOffset(), start.line(), start.columnOffset() + length); + } +} diff --git a/java-checks/src/test/java/org/sonar/java/checks/AlmostJavadocCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/AlmostJavadocCheckTest.java new file mode 100644 index 00000000000..52e41cda2d0 --- /dev/null +++ b/java-checks/src/test/java/org/sonar/java/checks/AlmostJavadocCheckTest.java @@ -0,0 +1,69 @@ +/* + * 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; + +import org.junit.jupiter.api.Test; +import org.sonar.java.checks.verifier.CheckVerifier; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.sonar.java.checks.verifier.TestUtils.mainCodeSourcesPath; + +class AlmostJavadocCheckTest { + + @Test + void issue_message() { + assertThat(AlmostJavadocCheck.MESSAGE) + .isEqualTo("This comment contains Javadoc or HTML tags, but isn't started with a double asterisk (/**); is it meant to be Javadoc?"); + } + + + @Test + void test() { + CheckVerifier.newVerifier() + .onFile(mainCodeSourcesPath("checks/AlmostJavadocCheckSample.java")) + .withCheck(new AlmostJavadocCheck()) + .verifyIssues(); + } + + @Test + void test_without_semantic() { + CheckVerifier.newVerifier() + .onFile(mainCodeSourcesPath("checks/AlmostJavadocCheckSample.java")) + .withCheck(new AlmostJavadocCheck()) + .withoutSemantic() + .verifyIssues(); + } + + @Test + void compact_source() { + CheckVerifier.newVerifier() + .onFile(mainCodeSourcesPath("checks/AlmostJavadocCheck_compactSource.java")) + .withCheck(new AlmostJavadocCheck()) + .withJavaVersion(25) + .verifyIssues(); + } + + @Test + void compact_source_without_semantic() { + CheckVerifier.newVerifier() + .onFile(mainCodeSourcesPath("checks/AlmostJavadocCheck_compactSource.java")) + .withCheck(new AlmostJavadocCheck()) + .withJavaVersion(25) + .withoutSemantic() + .verifyIssues(); + } +} diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9355.html b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9355.html new file mode 100644 index 00000000000..466fe27486e --- /dev/null +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9355.html @@ -0,0 +1,89 @@ +

A documentation comment must start with /**. A regular comment that contains Javadoc tags or HTML documentation tags, and that sits +immediately above a declaration, is ignored by the Javadoc tool, so the following declaration stays undocumented.

+

Why is this an issue?

+

The Javadoc tool recognizes documentation comments that start with /** and that are placed immediately before a class, interface, +constructor, method, field, or enum constant. A comment that starts with /* or // in that position is ordinary +commentary.

+

When such a comment contains Javadoc tags such as @param, @return, or {@link}, or HTML documentation tags +such as </code> or </em>, it is almost always intended to be Javadoc. Because it is not a documentation comment, +those tags never attach to the following declaration. Readers and generated API docs then miss the contract that the author already wrote.

+

Start the comment with /** so the tags document the following declaration.

+

Exceptions

+

This rule does not raise an issue when:

+
    +
  • The comment does not contain a Javadoc tag or one of the HTML documentation tags </em>, </b>, + </a>, </strong>, </i>, </pre>, or </code>.
  • +
  • The comment does not immediately precede a documentable declaration.
  • +
  • The declaration already has a documentation comment.
  • +
+

Code examples

+

Noncompliant code example

+
+public class MathUtils {
+  /* // Noncompliant: not a documentation comment, so these tags are ignored
+   * Computes the factorial of a positive integer.
+   * @param n the number to compute factorial for
+   * @return the factorial of n
+   */
+  public long factorial(int n) {
+    return (n <= 1) ? 1 : n * factorial(n - 1);
+  }
+}
+
+

Compliant solution

+
+public class MathUtils {
+  /**
+   * Computes the factorial of a positive integer.
+   * @param n the number to compute factorial for
+   * @return the factorial of n
+   */
+  public long factorial(int n) {
+    return (n <= 1) ? 1 : n * factorial(n - 1);
+  }
+}
+
+

Noncompliant code example

+
+public class Names {
+  /* Returns the display name as <code>String</code>. */ // Noncompliant: HTML documentation tags in a regular block comment
+  public String displayName() {
+    return name;
+  }
+}
+
+

Compliant solution

+
+public class Names {
+  /** Returns the display name as <code>String</code>. */
+  public String displayName() {
+    return name;
+  }
+}
+
+

Noncompliant code example

+
+interface Repository {
+  // Loads the entity. {@link Entity} */ // Noncompliant: line comment is not started with /**
+  Entity load(String id);
+}
+
+

Compliant solution

+
+interface Repository {
+  /** Loads the entity. {@link Entity} */
+  Entity load(String id);
+}
+
+

Resources

+

Documentation

+ +

Related rules

+
    +
  • {rule:java:S1176} - Public types, methods and fields (API) should be documented with Javadoc
  • +
  • {rule:java:S8491} - Dangling Javadoc comments should be removed
  • +
+ diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9355.json b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9355.json new file mode 100644 index 00000000000..101f32dafb5 --- /dev/null +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9355.json @@ -0,0 +1,24 @@ +{ + "title": "Comments containing Javadoc or HTML tags should use Javadoc syntax", + "type": "CODE_SMELL", + "status": "ready", + "remediation": { + "func": "Constant\/Issue", + "constantCost": "5min" + }, + "tags": [ + "javadoc", + "confusing" + ], + "defaultSeverity": "Major", + "ruleSpecification": "RSPEC-9355", + "sqKey": "S9355", + "scope": "All", + "quickfix": "covered", + "code": { + "impacts": { + "MAINTAINABILITY": "MEDIUM" + }, + "attribute": "CONVENTIONAL" + } +} diff --git a/sonar-java-plugin/src/main/resources/profiles/Sonar_way/S9355 b/sonar-java-plugin/src/main/resources/profiles/Sonar_way/S9355 new file mode 100644 index 00000000000..e69de29bb2d