Skip to content

Commit c01d9ba

Browse files
SONARJAVA-5501 Implement check for syntax in Markdown comments S7474 (#5152)
1 parent e5d1375 commit c01d9ba

12 files changed

Lines changed: 558 additions & 1 deletion

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"ruleKey": "S7474",
3+
"hasTruePositives": true,
4+
"falseNegatives": 0,
5+
"falsePositives": 0
6+
}
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
package checks;
2+
// Noncompliant@+3 {{replace HTML syntax with Markdown syntax in javadoc}}
3+
4+
/// The javadoc in this class uses markdown comment (3 slashes) but uses some HTML tags.
5+
/// It makes some text appear in <b>bold</b>.
6+
// ^^^
7+
public class MarkdownJavadocSyntaxCheckSample {
8+
9+
// Noncompliant@+2
10+
11+
/// Some text appears in <i>italic</i>.
12+
// ^^^
13+
public void withItalic() {
14+
// Empty
15+
}
16+
17+
/// Some text appears in _italic_.
18+
public void withItalicMarkdown() { // Compliant
19+
// Empty
20+
}
21+
22+
// Noncompliant@+2
23+
24+
/// Separate <p> paragraphs are created by simply using the {@code <p>} tag.
25+
// ^^^
26+
public void withParagraph() {
27+
// Empty
28+
}
29+
30+
/// Separate
31+
///
32+
/// paragraphs are created by simply using the `<p>` tag.
33+
public void withParagraphMarkdown() { // Compliant
34+
// Empty
35+
}
36+
37+
// Noncompliant@+1
38+
/// For inline code snippets, it uses the {@code <code>} tag.
39+
// ^^^^^^^^^^^^^^
40+
public void withCode() {
41+
// Empty
42+
}
43+
44+
// Noncompliant@+2
45+
/// For larger blocks of code, the `<pre>` tag is used:
46+
/// <pre>
47+
/// public class Example {
48+
/// public static void main(String[] args) {
49+
/// System.out.println("Hello, Javadoc!");
50+
/// }
51+
/// }
52+
/// </pre>
53+
public void withPreBlock() {
54+
// Empty
55+
}
56+
57+
/// For larger blocks of code, triple quotes should be used:
58+
/// ```
59+
/// public class Example {
60+
/// public static void main(String[] args){
61+
/// System.out.println("Hello, <b>Javadoc</b>!");
62+
///}
63+
///}
64+
///```
65+
public void withMarkdownBlock() { // Compliant
66+
// Empty
67+
}
68+
69+
// Noncompliant@+1
70+
/// {@link String#length()} links to the {@link java.lang.String#length()} method.
71+
// ^^^^^^^^^^^^^^^^^^^^^^^
72+
public void withLink() {
73+
// Empty
74+
}
75+
76+
/// [String#length()] links to the [java.lang.String#length()] method.
77+
public void withLinkMarkdown() { // Compliant
78+
}
79+
80+
// Noncompliant@+2
81+
/// Here is a list:
82+
/// <ul>
83+
/// <li> Red </li>
84+
/// <li> Blue </li>
85+
/// <li> Green </li>
86+
/// </ul>
87+
public void withList() {
88+
// Empty
89+
}
90+
91+
/// Here is a list:
92+
/// * Red
93+
/// * Blue
94+
/// * Green
95+
public void withListMarkdown() { // Compliant
96+
// Empty
97+
}
98+
99+
// Noncompliant@+2
100+
/// An ordered list:
101+
/// <ol>
102+
/// <li>one
103+
/// <li>two
104+
/// </ol>
105+
public int withOrderedList() {
106+
return 0;
107+
}
108+
109+
/// An ordered list:
110+
/// 1. one
111+
/// 1. two
112+
public int withOrderedListMarkdown() {
113+
return 0;
114+
}
115+
116+
// Noncompliant@+2
117+
/// Here is a table:
118+
/// <table>
119+
/// <tr>
120+
/// <th>Header 1</th>
121+
/// <th>Header 2</th>
122+
/// </tr>
123+
/// <tr>
124+
/// <td>Row 1, Col 1</td>
125+
/// <td>Row 1, Col 2</td>
126+
/// </tr>
127+
/// <tr>
128+
/// <td>Row 2, Col 1</td>
129+
/// <td>Row 2, Col 2</td>
130+
/// </tr>
131+
/// </table>
132+
public void withTable() {
133+
// Empty
134+
}
135+
136+
/// Here is a table:
137+
///
138+
/// | Header 1 | Header 2 |
139+
/// |--------------|--------------|
140+
/// | Row 1, Col 1 | Row 1, Col 2 |
141+
/// | Row 2, Col 1 | Row 2, Col 2 |
142+
public void withTableMarkdown() { // Compliant
143+
// Empty
144+
}
145+
146+
public void danglingComment() {
147+
// Compliant, because this is not placed where it would be considered by JavaDocs.
148+
149+
/// Some text in <i>italic</i>
150+
}
151+
}
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
/*
2+
* SonarQube Java
3+
* Copyright (C) 2012-2025 SonarSource SA
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12+
* See the Sonar Source-Available License for more details.
13+
*
14+
* You should have received a copy of the Sonar Source-Available License
15+
* along with this program; if not, see https://sonarsource.com/license/ssal/
16+
*/
17+
package org.sonar.java.checks;
18+
19+
import java.util.Arrays;
20+
import java.util.List;
21+
import java.util.Optional;
22+
import java.util.regex.Matcher;
23+
import java.util.regex.Pattern;
24+
import org.sonar.check.Rule;
25+
import org.sonar.java.annotations.VisibleForTesting;
26+
import org.sonar.java.ast.visitors.PublicApiChecker;
27+
import org.sonar.java.model.DefaultModuleScannerContext;
28+
import org.sonar.java.model.LineColumnConverter;
29+
import org.sonar.java.reporting.AnalyzerMessage;
30+
import org.sonar.plugins.java.api.IssuableSubscriptionVisitor;
31+
import org.sonar.plugins.java.api.location.Position;
32+
import org.sonar.plugins.java.api.tree.SyntaxTrivia;
33+
import org.sonar.plugins.java.api.tree.Tree;
34+
35+
@Rule(key = "S7474")
36+
public class MarkdownJavadocSyntaxCheck extends IssuableSubscriptionVisitor {
37+
38+
/**
39+
* Pattern to find Javadoc or HTML tags that can be replaced with Markdown.
40+
*/
41+
@VisibleForTesting
42+
static final Pattern NON_MARKDOWN_JAVADOC_PATTERN = Pattern.compile("<b>|<i>|<p>|<pre>|<ul>|<ol>|<table>|\\{@code |\\{@link ");
43+
44+
private static final Pattern TRIPLE_QUOTE = Pattern.compile("```");
45+
private static final String MESSAGE = "replace HTML syntax with Markdown syntax in javadoc";
46+
47+
@Override
48+
public List<Tree.Kind> nodesToVisit() {
49+
return Arrays.asList(PublicApiChecker.apiKinds());
50+
}
51+
52+
@Override
53+
public void visitNode(Tree tree) {
54+
List<SyntaxTrivia> markdownJavadoc =
55+
Optional.ofNullable(tree.firstToken())
56+
.stream()
57+
.flatMap(token -> token.trivias().stream())
58+
.filter(trivia -> trivia.isComment(SyntaxTrivia.CommentKind.MARKDOWN))
59+
.toList();
60+
61+
for (SyntaxTrivia trivia : markdownJavadoc) {
62+
String withoutQuotedCode = replaceQuotedCodeWithBlanks(trivia.comment());
63+
Matcher matcher = NON_MARKDOWN_JAVADOC_PATTERN.matcher(withoutQuotedCode);
64+
LineColumnConverter lineColumnConverter = new LineColumnConverter(withoutQuotedCode);
65+
if (matcher.find()) {
66+
Position startPosition = lineColumnConverter.toPosition(matcher.start());
67+
int endIndex = endIndexOfTag(matcher, withoutQuotedCode);
68+
Position endPosition = lineColumnConverter.toPosition(endIndex);
69+
reportNonMarkdownSyntax(trivia, startPosition, endPosition);
70+
}
71+
}
72+
}
73+
74+
@VisibleForTesting
75+
static int endIndexOfTag(Matcher matcher, String comment) {
76+
if (!matcher.group().startsWith("{")) {
77+
return matcher.end();
78+
}
79+
int index = indexOfClosingBracket(comment, matcher.end());
80+
if (index == -1) {
81+
return comment.length();
82+
}
83+
return index + 1;
84+
}
85+
86+
private static int indexOfClosingBracket(String comment, int fromIndex) {
87+
int unclosedBrackets = 1;
88+
for (int i = fromIndex; i < comment.length(); i++) {
89+
if (comment.charAt(i) == '{') {
90+
unclosedBrackets++;
91+
} else if (comment.charAt(i) == '}') {
92+
unclosedBrackets--;
93+
}
94+
if (unclosedBrackets == 0) {
95+
return i;
96+
}
97+
}
98+
return -1;
99+
}
100+
101+
void reportNonMarkdownSyntax(SyntaxTrivia trivia, Position start, Position end) {
102+
Position triviaPosition = trivia.range().start();
103+
Position absoluteStart = start.relativeTo(triviaPosition);
104+
Position absoluteEnd = end.relativeTo(triviaPosition);
105+
var textSpan = new AnalyzerMessage.TextSpan(
106+
absoluteStart.line(), absoluteStart.columnOffset(),
107+
absoluteEnd.line(), absoluteEnd.columnOffset());
108+
((DefaultModuleScannerContext) this.context).reportIssue(
109+
new AnalyzerMessage(this, context.getInputFile(), textSpan, MESSAGE, 0));
110+
}
111+
112+
/**
113+
* Return a new string, where parts of the text that are between backquotes ({@code `}) and triple backquote ({@code ```}),
114+
* are replaced with blank spaces, while preserving the number of characters and line numbers.
115+
*/
116+
@VisibleForTesting
117+
static String replaceQuotedCodeWithBlanks(String javadoc) {
118+
StringBuilder result = new StringBuilder();
119+
int currentPosition = 0;
120+
while (currentPosition != -1) {
121+
int nextQuote = javadoc.indexOf("`", currentPosition);
122+
if (nextQuote != -1) {
123+
result.append(javadoc, currentPosition, nextQuote);
124+
currentPosition = findEndOfMarkdownQuote(javadoc, nextQuote);
125+
int endOfQuote = currentPosition == -1 ? javadoc.length() : currentPosition;
126+
// Replace all printable characters by spaces, so that they can't be interpreted as tags.
127+
// Don't replace non-printable characters, as this could interfere with line counting.
128+
result.append(javadoc.substring(nextQuote, endOfQuote)
129+
.replaceAll("\\p{Print}", " "));
130+
} else {
131+
result.append(javadoc, currentPosition, javadoc.length());
132+
currentPosition = -1;
133+
}
134+
}
135+
return result.toString();
136+
}
137+
138+
@VisibleForTesting
139+
static int findEndOfMarkdownQuote(String javadoc, int startPosition) {
140+
Matcher matcher = TRIPLE_QUOTE.matcher(javadoc);
141+
matcher.region(startPosition, javadoc.length());
142+
if (matcher.lookingAt()) {
143+
boolean closingQuotesFound = matcher.find(startPosition + 3);
144+
return !closingQuotesFound ? -1 : matcher.end();
145+
} else {
146+
int closingQuotePosition = javadoc.indexOf("`", startPosition + 1);
147+
return closingQuotePosition == -1 ? -1 : (closingQuotePosition + 1);
148+
}
149+
}
150+
}

0 commit comments

Comments
 (0)