1 ///////////////////////////////////////////////////////////////////////////////////////////////
2 // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3 // Copyright (C) 2001-2026 the original author or authors.
4 //
5 // This library is free software; you can redistribute it and/or
6 // modify it under the terms of the GNU Lesser General Public
7 // License as published by the Free Software Foundation; either
8 // version 2.1 of the License, or (at your option) any later version.
9 //
10 // This library is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 // Lesser General Public License for more details.
14 //
15 // You should have received a copy of the GNU Lesser General Public
16 // License along with this library; if not, write to the Free Software
17 // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18 ///////////////////////////////////////////////////////////////////////////////////////////////
19
20 package com.puppycrawl.tools.checkstyle.checks.javadoc;
21
22 import java.util.ArrayDeque;
23 import java.util.ArrayList;
24 import java.util.Deque;
25 import java.util.List;
26
27 import javax.annotation.Nullable;
28
29 import com.puppycrawl.tools.checkstyle.StatelessCheck;
30 import com.puppycrawl.tools.checkstyle.api.DetailNode;
31 import com.puppycrawl.tools.checkstyle.api.JavadocCommentsTokenTypes;
32 import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
33
34 /**
35 * <div>
36 * Checks that Javadoc inline tags {@code {@code ...}} and {@code {@snippet ...}}
37 * are preferred over HTML tags {@code <code>} and {@code <pre>}.
38 * </div>
39 *
40 * <p>
41 * This check enforces using either {@code {@code ...}} or {@code {@snippet ...}} inline tags
42 * instead of single-line {@code <code>} and {@code <pre>} HTML tags, and using
43 * {@code {@snippet ...}} inline tags instead of multi-line {@code <code>} and {@code <pre>}
44 * HTML tags.
45 * </p>
46 *
47 * <p>
48 * Per <a href="https://cr.openjdk.org/~alundblad/styleguide/index-v6.html">
49 * OpenJDK Style Guidelines v6</a>,
50 * Javadoc inline tags should be preferred over their HTML equivalents.
51 * </p>
52 *
53 * <p>
54 * To suppress violation for snippet inline tag:
55 * </p>
56 *
57 * <div class="wrapper"><pre class="prettyprint"><code class="language-xml">
58 * <module name="SuppressionSingleFilter">
59 * <property name="checks" value="PreferCodeOrSnippetJavadocInlineTag"/>
60 * <property name="files" value="file-name"/>
61 * <property name="message" value="Use snippet inline tag instead of.*"/>
62 * </module>
63 * </code></pre></div>
64 *
65 * <b>Not Flagged :</b>
66 * <ul>
67 * <li>Tags which have unbalanced curly braces</li>
68 * <li>Tags which have content that starts with star.</li>
69 * <li>Tags which are inside other tags</li>
70 * </ul>
71 *
72 * @since 14.1.0
73 */
74 @StatelessCheck
75 public class PreferCodeOrSnippetJavadocInlineTagCheck extends AbstractJavadocCheck {
76
77 /**
78 * A key is pointing to the warning message text in "messages.properties"
79 * file.
80 */
81 public static final String MSG_KEY_SINGLE_LINE = "prefer.code.javadoc.singleline.tag";
82
83 /**
84 * A key is pointing to the warning message text in "messages.properties"
85 * file.
86 */
87 public static final String MSG_KEY_MULTI_LINE = "prefer.code.javadoc.multiline.tag";
88
89 /**
90 * Creates a new {@code PreferCodeOrSnippetJavadocInlineTagCheck} instance.
91 */
92 public PreferCodeOrSnippetJavadocInlineTagCheck() {
93 // no code by default
94 }
95
96 @Override
97 public int[] getRequiredJavadocTokens() {
98 return new int[] {
99 JavadocCommentsTokenTypes.HTML_ELEMENT,
100 };
101 }
102
103 @Override
104 public int[] getDefaultJavadocTokens() {
105 return getRequiredJavadocTokens();
106 }
107
108 @Override
109 public void visitJavadocToken(DetailNode node) {
110 if (isCodeOrPreTag(node) && isCompleteTag(node) && !isNested(node)) {
111 final List<DetailNode> textNodes = collectTextNodes(node);
112
113 if (isSingleLineTag(node) && containsBalancedBraces(textNodes)) {
114 log(node, MSG_KEY_SINGLE_LINE, getHtmlTagName(node));
115 }
116 else if (isConvertableInInlineTag(textNodes)) {
117 log(node, MSG_KEY_MULTI_LINE, getHtmlTagName(node));
118 }
119 }
120 }
121
122 /**
123 * Checks if the tag is code or pre tag.
124 *
125 * @param node the node to check
126 * @return {@code true} if the tag is code or pre tag, {@code false} otherwise
127 */
128 private static boolean isCodeOrPreTag(DetailNode node) {
129 final String tagName = getHtmlTagName(node);
130 return "code".equals(tagName) || "pre".equals(tagName);
131 }
132
133 /**
134 * Checks if the tag is nested inside any other code or pre tag.
135 *
136 * @param node the node to check
137 * @return {@code true} if the tag is nested, {@code false} otherwise
138 */
139 private static boolean isNested(DetailNode node) {
140 boolean result = false;
141 DetailNode parent = node.getParent();
142 while (parent != null) {
143 if (isCodeOrPreTag(parent)) {
144 result = true;
145 break;
146 }
147 parent = parent.getParent();
148 }
149 return result;
150 }
151
152 /**
153 * Checks if the tag is convertable in inline tag.
154 *
155 * @param listOfTextNodes the list of text nodes to check.
156 * @return {@code true} if the tag is convertable in inline tag, {@code false} otherwise.
157 */
158 public static boolean isConvertableInInlineTag(Iterable<DetailNode> listOfTextNodes) {
159 return containsBalancedBraces(listOfTextNodes) && !isStartWithStar(listOfTextNodes);
160 }
161
162 /**
163 * Checks if the text list contains balanced braces.
164 *
165 * @param listOfTextNodes the list of text nodes to check
166 * @return {@code true} if the text list contains balanced braces, {@code false} otherwise
167 */
168 public static boolean containsBalancedBraces(Iterable<DetailNode> listOfTextNodes) {
169 int braceCount = 0;
170 for (DetailNode node: listOfTextNodes) {
171 final String text = node.getText();
172 for (int idx = 0; idx < text.length(); idx++) {
173 final char letter = text.charAt(idx);
174 if (letter == '{') {
175 braceCount++;
176 }
177 else if (letter == '}') {
178 braceCount--;
179 }
180 if (braceCount < 0) {
181 break;
182 }
183 }
184 }
185 return braceCount == 0;
186 }
187
188 /**
189 * Checks if the first element of the text list starts with a star.
190 *
191 * @param listOfTextNodes the list of text nodes to check
192 * @return {@code true} if the first element of the text list does not start
193 * with a star, {@code false} otherwise
194 */
195 private static boolean isStartWithStar(Iterable<DetailNode> listOfTextNodes) {
196 boolean result = false;
197 for (DetailNode node : listOfTextNodes) {
198 final String text = node.getText().trim();
199 if (text.startsWith("*")) {
200 result = true;
201 break;
202 }
203 }
204 return result;
205 }
206
207 /**
208 * Collects all text nodes contained within the specified node, including
209 * text nested inside HTML elements and inline tags.
210 *
211 * @param node the root {@code DetailNode} to extract text nodes from
212 * @return a list of text {@code DetailNode} instances
213 */
214 public static List<DetailNode> collectTextNodes(DetailNode node) {
215 final DetailNode rootNode = JavadocUtil.findFirstToken(node,
216 JavadocCommentsTokenTypes.HTML_CONTENT);
217 final List<DetailNode> textNodes = new ArrayList<>();
218 final List<DetailNode> inlineTags = new ArrayList<>();
219 final Deque<DetailNode> htmlElements = new ArrayDeque<>();
220
221 if (rootNode != null) {
222 textNodes.addAll(JavadocUtil.getAllNodesOfType(rootNode,
223 JavadocCommentsTokenTypes.TEXT));
224 inlineTags.addAll(JavadocUtil.getAllNodesOfType(
225 rootNode, JavadocCommentsTokenTypes.JAVADOC_INLINE_TAG));
226 htmlElements.addAll(JavadocUtil.getAllNodesOfType(
227 rootNode, JavadocCommentsTokenTypes.HTML_ELEMENT));
228 }
229
230 while (!htmlElements.isEmpty()) {
231 final DetailNode currentHtmlElement = htmlElements.pop();
232 final DetailNode currentHtmlContent = JavadocUtil.findFirstToken(currentHtmlElement,
233 JavadocCommentsTokenTypes.HTML_CONTENT);
234
235 if (currentHtmlContent != null) {
236 textNodes.addAll(JavadocUtil.getAllNodesOfType(
237 currentHtmlContent, JavadocCommentsTokenTypes.TEXT));
238 inlineTags.addAll(JavadocUtil.getAllNodesOfType(
239 currentHtmlContent, JavadocCommentsTokenTypes.JAVADOC_INLINE_TAG));
240 htmlElements.addAll(JavadocUtil.getAllNodesOfType(
241 currentHtmlContent, JavadocCommentsTokenTypes.HTML_ELEMENT));
242 }
243 }
244
245 textNodes.addAll(getTextNodesFromInlineTags(inlineTags));
246 return textNodes;
247 }
248
249 /**
250 * Extracts text nodes from child nodes of the provided list of inline tags.
251 *
252 * @param inlineTags the list of inline tag {@code DetailNode}s
253 * @return a list of text {@code DetailNode} instances found inside the inline tags
254 */
255 private static List<DetailNode> getTextNodesFromInlineTags(Iterable<DetailNode> inlineTags) {
256 final List<DetailNode> textNodes = new ArrayList<>();
257 for (DetailNode inlineTag : inlineTags) {
258 textNodes.addAll(JavadocUtil.getAllNodesOfType(
259 inlineTag.getFirstChild(), JavadocCommentsTokenTypes.TEXT));
260 }
261 return textNodes;
262 }
263
264 /**
265 * Checks if the tag is single-line.
266 *
267 * @param node the node to check
268 * @return {@code true} if the tag is single-line, {@code false} otherwise
269 */
270 private static boolean isSingleLineTag(DetailNode node) {
271 final DetailNode endOfTag =
272 JavadocUtil.findFirstToken(node, JavadocCommentsTokenTypes.HTML_TAG_END);
273 return node.getLineNumber() == endOfTag.getLineNumber();
274 }
275
276 /**
277 * Checks if the tag is complete.
278 *
279 * @param node the node to check
280 * @return {@code true} if the tag is complete, {@code false} otherwise
281 */
282 private static boolean isCompleteTag(DetailNode node) {
283 final DetailNode endOfTag =
284 JavadocUtil.findFirstToken(node, JavadocCommentsTokenTypes.HTML_TAG_END);
285 return endOfTag != null;
286 }
287
288 /**
289 * Gets the tag name from an HTML_ELEMENT node.
290 *
291 * @param htmlElement the HTML_ELEMENT node
292 * @return the tag name (e.g., "code", "pre")
293 */
294 @Nullable
295 private static String getHtmlTagName(DetailNode htmlElement) {
296 String result = null;
297 final DetailNode htmlTagStart = JavadocUtil.findFirstToken(
298 htmlElement, JavadocCommentsTokenTypes.HTML_TAG_START);
299 if (htmlTagStart != null) {
300 final DetailNode tagName = JavadocUtil.findFirstToken(
301 htmlTagStart, JavadocCommentsTokenTypes.TAG_NAME);
302 result = tagName.getText();
303 }
304 return result;
305 }
306
307 }