From 091bdc8f60653f669f5d98c921c842a4b5c1063c Mon Sep 17 00:00:00 2001 From: nathsou Date: Wed, 19 Aug 2026 11:54:35 +0200 Subject: [PATCH 1/8] Implement S9355 Flag comments that contain Javadoc or HTML tags but are not started with /**, so they do not document the following declaration. --- .../java/checks/AlmostJavadocCheckSample.java | 129 ++++++++++++++++++ .../sonar/java/checks/AlmostJavadocCheck.java | 120 ++++++++++++++++ .../java/checks/AlmostJavadocCheckTest.java | 50 +++++++ .../org/sonar/l10n/java/rules/java/S9355.html | 90 ++++++++++++ .../org/sonar/l10n/java/rules/java/S9355.json | 24 ++++ .../main/resources/profiles/Sonar_way/S9355 | 0 6 files changed, 413 insertions(+) create mode 100644 java-checks-test-sources/default/src/main/java/checks/AlmostJavadocCheckSample.java create mode 100644 java-checks/src/main/java/org/sonar/java/checks/AlmostJavadocCheck.java create mode 100644 java-checks/src/test/java/org/sonar/java/checks/AlmostJavadocCheckTest.java create mode 100644 sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9355.html create mode 100644 sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9355.json create mode 100644 sonar-java-plugin/src/main/resources/profiles/Sonar_way/S9355 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..f4a06d7b2f2 --- /dev/null +++ b/java-checks-test-sources/default/src/main/java/checks/AlmostJavadocCheckSample.java @@ -0,0 +1,129 @@ +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 + /* {@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 { + } + } + + static class Entity { + } +} 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..fef2ca42b30 --- /dev/null +++ b/java-checks/src/main/java/org/sonar/java/checks/AlmostJavadocCheck.java @@ -0,0 +1,120 @@ +/* + * 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.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.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; + +@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 HAS_TAG = Pattern.compile( + "" + + "|(? 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 (isAlmostJavadoc(trivia)) { + reportAlmostJavadoc(trivia); + } + } + } + + private static boolean isDocumentableDeclaration(Tree tree) { + if (tree.is(Tree.Kind.VARIABLE)) { + Tree parent = tree.parent(); + return parent != null && parent.is(PublicApiChecker.classKinds()); + } + return true; + } + + private static boolean isAlmostJavadoc(SyntaxTrivia trivia) { + if (trivia.isComment(CommentKind.BLOCK)) { + return HAS_TAG.matcher(trivia.comment()).find(); + } + return trivia.isComment(CommentKind.LINE) + && trivia.comment().endsWith("*/") + && HAS_TAG.matcher(trivia.comment()).find(); + } + + 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..00434a0926b --- /dev/null +++ b/java-checks/src/test/java/org/sonar/java/checks/AlmostJavadocCheckTest.java @@ -0,0 +1,50 @@ +/* + * 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(); + } +} 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..b6f375f31df --- /dev/null +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9355.html @@ -0,0 +1,90 @@ +

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:S2920} - Javadoc tags should not be used in non-Javadoc comments
  • +
  • {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..9ff54f9fdc7 --- /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": "targeted", + "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 From d717a1e8ad7f3b55e164101dccee7f07eef3f9a6 Mon Sep 17 00:00:00 2001 From: nathsou Date: Wed, 19 Aug 2026 12:08:57 +0200 Subject: [PATCH 2/8] Fix review findings for S9355 Deduplicate multi-declarator fields, ignore trailing comments and local classes, and mark the implemented quick fix as covered. --- .../java/checks/AlmostJavadocCheckSample.java | 20 +++++++ .../sonar/java/checks/AlmostJavadocCheck.java | 52 +++++++++++++++++-- .../org/sonar/l10n/java/rules/java/S9355.json | 2 +- 3 files changed, 68 insertions(+), 6 deletions(-) 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 index f4a06d7b2f2..bcfd3ab51c3 100644 --- a/java-checks-test-sources/default/src/main/java/checks/AlmostJavadocCheckSample.java +++ b/java-checks-test-sources/default/src/main/java/checks/AlmostJavadocCheckSample.java @@ -47,6 +47,10 @@ interface Repository { /** Loads the entity. {@link Entity} */ Entity documentedLoad(String id); + + // Noncompliant@+1 + // Loads with trailing space. {@link Entity} */ + Entity loadWithTrailingSpace(String id); } // Noncompliant@+1 @@ -124,6 +128,22 @@ record Point(int x, int y) { } } + // 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 { } } 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 index fef2ca42b30..9d17fa49ef9 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/AlmostJavadocCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/AlmostJavadocCheck.java @@ -28,10 +28,13 @@ 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 { @@ -64,27 +67,66 @@ public void visitNode(Tree tree) { return; } for (SyntaxTrivia trivia : trivias) { - if (isAlmostJavadoc(trivia)) { + if (!belongsToPreviousMember(tree, trivia) && isAlmostJavadoc(trivia)) { reportAlmostJavadoc(trivia); } } } private static boolean isDocumentableDeclaration(Tree tree) { + if (isInsideMethodOrConstructor(tree)) { + return false; + } if (tree.is(Tree.Kind.VARIABLE)) { Tree parent = tree.parent(); - return parent != null && parent.is(PublicApiChecker.classKinds()); + return parent != null + && parent.is(PublicApiChecker.classKinds()) + && QuickFixHelper.previousVariable((VariableTree) tree).isEmpty(); } return true; } + 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 HAS_TAG.matcher(trivia.comment()).find(); + return HAS_TAG.matcher(text).find(); } return trivia.isComment(CommentKind.LINE) - && trivia.comment().endsWith("*/") - && HAS_TAG.matcher(trivia.comment()).find(); + && text.endsWith("*/") + && HAS_TAG.matcher(text).find(); } private void reportAlmostJavadoc(SyntaxTrivia trivia) { 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 index 9ff54f9fdc7..101f32dafb5 100644 --- 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 @@ -14,7 +14,7 @@ "ruleSpecification": "RSPEC-9355", "sqKey": "S9355", "scope": "All", - "quickfix": "targeted", + "quickfix": "covered", "code": { "impacts": { "MAINTAINABILITY": "MEDIUM" From ad0313a137a1201561d7b2e5f6573ad8bd1c63d3 Mon Sep 17 00:00:00 2001 From: nathsou Date: Wed, 19 Aug 2026 12:23:36 +0200 Subject: [PATCH 3/8] fix-ci: handle implicit-class fields in S9355 Sanity Test crashed because QuickFixHelper.previousVariable does not support IMPLICIT_CLASS parents on compact source files. Walk class members locally instead, skip the unnamed class itself, and cover the case with a compact-source sample. --- .../AlmostJavadocCheck_compactSource.java | 20 +++++++++++++++++++ .../sonar/java/checks/AlmostJavadocCheck.java | 17 +++++++++++----- .../java/checks/AlmostJavadocCheckTest.java | 9 +++++++++ 3 files changed, 41 insertions(+), 5 deletions(-) create mode 100644 java-checks-test-sources/default/src/main/java/checks/AlmostJavadocCheck_compactSource.java 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 index 9d17fa49ef9..a8e73fb710d 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/AlmostJavadocCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/AlmostJavadocCheck.java @@ -74,18 +74,25 @@ public void visitNode(Tree tree) { } private static boolean isDocumentableDeclaration(Tree tree) { - if (isInsideMethodOrConstructor(tree)) { + if (tree.is(Tree.Kind.IMPLICIT_CLASS) || isInsideMethodOrConstructor(tree)) { return false; } if (tree.is(Tree.Kind.VARIABLE)) { - Tree parent = tree.parent(); - return parent != null - && parent.is(PublicApiChecker.classKinds()) - && QuickFixHelper.previousVariable((VariableTree) tree).isEmpty(); + 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)) { 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 index 00434a0926b..69af7be1add 100644 --- a/java-checks/src/test/java/org/sonar/java/checks/AlmostJavadocCheckTest.java +++ b/java-checks/src/test/java/org/sonar/java/checks/AlmostJavadocCheckTest.java @@ -47,4 +47,13 @@ void test_without_semantic() { .withoutSemantic() .verifyIssues(); } + + @Test + void compact_source() { + CheckVerifier.newVerifier() + .onFile(mainCodeSourcesPath("checks/AlmostJavadocCheck_compactSource.java")) + .withCheck(new AlmostJavadocCheck()) + .withJavaVersion(25) + .verifyIssues(); + } } From 6d9a717201c8f83b96d321e2b81e465224a6333e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 19 Aug 2026 12:24:41 +0200 Subject: [PATCH 4/8] Update ruling results for PR #5960 (#5962) Co-authored-by: github-actions[bot] --- .../resources/eclipse-jetty/java-S9355.json | 30 +++++++++++++++++++ .../src/test/resources/guava/java-S9355.json | 15 ++++++++++ 2 files changed, 45 insertions(+) create mode 100644 its/ruling/src/test/resources/eclipse-jetty/java-S9355.json create mode 100644 its/ruling/src/test/resources/guava/java-S9355.json 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 +] +} From 0d498dbc09ed3bc918a6ec5c682f38d7c24177ea Mon Sep 17 00:00:00 2001 From: nathsou Date: Wed, 19 Aug 2026 12:25:57 +0200 Subject: [PATCH 5/8] fix-ci: cover remaining S9355 comment forms Exercise the // /** quick fix and a second top-level type so compilation-unit siblings and the last convert-to-Javadoc branch are covered by tests. --- .../src/main/java/checks/AlmostJavadocCheckSample.java | 7 +++++++ .../org/sonar/java/checks/AlmostJavadocCheckTest.java | 10 ++++++++++ 2 files changed, 17 insertions(+) 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 index bcfd3ab51c3..32695186b26 100644 --- a/java-checks-test-sources/default/src/main/java/checks/AlmostJavadocCheckSample.java +++ b/java-checks-test-sources/default/src/main/java/checks/AlmostJavadocCheckSample.java @@ -147,3 +147,10 @@ class Local { 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/src/test/java/org/sonar/java/checks/AlmostJavadocCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/AlmostJavadocCheckTest.java index 69af7be1add..52e41cda2d0 100644 --- a/java-checks/src/test/java/org/sonar/java/checks/AlmostJavadocCheckTest.java +++ b/java-checks/src/test/java/org/sonar/java/checks/AlmostJavadocCheckTest.java @@ -56,4 +56,14 @@ void compact_source() { .withJavaVersion(25) .verifyIssues(); } + + @Test + void compact_source_without_semantic() { + CheckVerifier.newVerifier() + .onFile(mainCodeSourcesPath("checks/AlmostJavadocCheck_compactSource.java")) + .withCheck(new AlmostJavadocCheck()) + .withJavaVersion(25) + .withoutSemantic() + .verifyIssues(); + } } From 01fbfc9da3a33cc325eadd0b63d7b2117e4cefff Mon Sep 17 00:00:00 2001 From: nathsou Date: Wed, 19 Aug 2026 12:34:54 +0200 Subject: [PATCH 6/8] fix-ci: add S9355 ruling expected file for eclipse-jetty-similar-to-main The first auto-generated ruling update covered eclipse-jetty and guava only. The similar-to-main project reports the same HttpChannel/Request/ContextHandler issues and needs its own expected dump. --- .../java-S9355.json | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 its/ruling/src/test/resources/eclipse-jetty-similar-to-main/java-S9355.json 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 +] +} From 4ab5e1b54851295c7deabaf877dd0feaf483cb4e Mon Sep 17 00:00:00 2001 From: nathsou Date: Wed, 19 Aug 2026 12:49:14 +0200 Subject: [PATCH 7/8] fix-ci: split S9355 tag matching to satisfy S5843 The combined HTML/Javadoc regex exceeded the allowed complexity of 20. Match closing HTML tags and @tags separately against a known tag set. --- .../sonar/java/checks/AlmostJavadocCheck.java | 29 +++++++++++++++---- 1 file changed, 23 insertions(+), 6 deletions(-) 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 index a8e73fb710d..dbb292499df 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/AlmostJavadocCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/AlmostJavadocCheck.java @@ -19,6 +19,8 @@ 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; @@ -41,10 +43,12 @@ 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 HAS_TAG = 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() { @@ -129,11 +133,24 @@ private static Tree previousSibling(Tree tree) { private static boolean isAlmostJavadoc(SyntaxTrivia trivia) { String text = trivia.comment().stripTrailing(); if (trivia.isComment(CommentKind.BLOCK)) { - return HAS_TAG.matcher(text).find(); + return hasTag(text); } return trivia.isComment(CommentKind.LINE) && text.endsWith("*/") - && HAS_TAG.matcher(text).find(); + && 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) { From 4b1b9802a8e96fb6d4f5b50d895aa972830a87e0 Mon Sep 17 00:00:00 2001 From: nathsou Date: Wed, 19 Aug 2026 16:36:24 +0200 Subject: [PATCH 8/8] Regenerate S9355 description without S2920 reference S2920 is not implemented in the Java analyzer, so the generated {rule:java:S2920} link did not resolve for users. --- .../src/main/resources/org/sonar/l10n/java/rules/java/S9355.html | 1 - 1 file changed, 1 deletion(-) 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 index b6f375f31df..466fe27486e 100644 --- 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 @@ -84,7 +84,6 @@

Documentation

Related rules

  • {rule:java:S1176} - Public types, methods and fields (API) should be documented with Javadoc
  • -
  • {rule:java:S2920} - Javadoc tags should not be used in non-Javadoc comments
  • {rule:java:S8491} - Dangling Javadoc comments should be removed