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.Arrays;
23 import java.util.HashMap;
24 import java.util.HashSet;
25 import java.util.List;
26 import java.util.Locale;
27 import java.util.Map;
28 import java.util.Set;
29 import java.util.stream.Collectors;
30
31 import com.puppycrawl.tools.checkstyle.JavadocDetailNodeParser;
32 import com.puppycrawl.tools.checkstyle.JavadocDetailNodeParser.ParseErrorMessage;
33 import com.puppycrawl.tools.checkstyle.JavadocDetailNodeParser.ParseStatus;
34 import com.puppycrawl.tools.checkstyle.PropertyType;
35 import com.puppycrawl.tools.checkstyle.XdocsPropertyType;
36 import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
37 import com.puppycrawl.tools.checkstyle.api.DetailAST;
38 import com.puppycrawl.tools.checkstyle.api.DetailNode;
39 import com.puppycrawl.tools.checkstyle.api.JavadocCommentsTokenTypes;
40 import com.puppycrawl.tools.checkstyle.api.TokenTypes;
41 import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
42 import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
43
44 /**
45 * Base class for Checks that process Javadoc comments.
46 *
47 * @noinspection NoopMethodInAbstractClass
48 * @noinspectionreason NoopMethodInAbstractClass - we allow each
49 * check to define these methods, as needed. They
50 * should be overridden only by demand in subclasses
51 */
52 public abstract class AbstractJavadocCheck extends AbstractCheck {
53
54 /**
55 * Parse error while rule recognition.
56 */
57 public static final String MSG_JAVADOC_PARSE_RULE_ERROR =
58 JavadocDetailNodeParser.MSG_JAVADOC_PARSE_RULE_ERROR;
59
60 /**
61 * Message key of error message.
62 */
63 public static final String MSG_KEY_UNCLOSED_HTML_TAG =
64 JavadocDetailNodeParser.MSG_UNCLOSED_HTML_TAG;
65
66 /**
67 * Key is the block comment node "lineNo". Value is {@link DetailNode} tree.
68 * Map is stored in {@link ThreadLocal}
69 * to guarantee basic thread safety and avoid shared, mutable state when not necessary.
70 */
71 private static final ThreadLocal<Map<Integer, ParseStatus>> TREE_CACHE =
72 ThreadLocal.withInitial(HashMap::new);
73
74 /**
75 * The file context.
76 *
77 * @noinspection ThreadLocalNotStaticFinal
78 * @noinspectionreason ThreadLocalNotStaticFinal - static context is
79 * problematic for multithreading
80 */
81 private final ThreadLocal<FileContext> context = ThreadLocal.withInitial(FileContext::new);
82
83 /** The javadoc tokens the check is interested in. */
84 @XdocsPropertyType(PropertyType.TOKEN_ARRAY)
85 private final Set<Integer> javadocTokens = new HashSet<>();
86
87 /**
88 * This property determines if a check should log a violation upon encountering javadoc with
89 * non-tight html. The default return value for this method is set to false since checks
90 * generally tend to be fine with non-tight html. It can be set through config file if a check
91 * is to log violation upon encountering non-tight HTML in javadoc.
92 *
93 * @see ParseStatus#isNonTight()
94 * @see <a href="https://checkstyle.org/writingjavadocchecks.html#Tight-HTML_rules">
95 * Tight HTML rules</a>
96 */
97 private boolean violateExecutionOnNonTightHtml;
98
99 /**
100 * Returns the default javadoc token types a check is interested in.
101 *
102 * @return the default javadoc token types
103 * @see JavadocCommentsTokenTypes
104 */
105 public abstract int[] getDefaultJavadocTokens();
106
107 /**
108 * Called to process a Javadoc token.
109 *
110 * @param ast
111 * the token to process
112 */
113 public abstract void visitJavadocToken(DetailNode ast);
114
115 /**
116 * The configurable javadoc token set.
117 * Used to protect Checks against malicious users who specify an
118 * unacceptable javadoc token set in the configuration file.
119 * The default implementation returns the check's default javadoc tokens.
120 *
121 * @return the javadoc token set this check is designed for.
122 * @see JavadocCommentsTokenTypes
123 */
124 public int[] getAcceptableJavadocTokens() {
125 final int[] defaultJavadocTokens = getDefaultJavadocTokens();
126 final int[] copy = new int[defaultJavadocTokens.length];
127 System.arraycopy(defaultJavadocTokens, 0, copy, 0, defaultJavadocTokens.length);
128 return copy;
129 }
130
131 /**
132 * The javadoc tokens that this check must be registered for.
133 *
134 * @return the javadoc token set this must be registered for.
135 * @see JavadocCommentsTokenTypes
136 */
137 public int[] getRequiredJavadocTokens() {
138 return CommonUtil.EMPTY_INT_ARRAY;
139 }
140
141 /**
142 * This method determines if a check should process javadoc containing non-tight html tags.
143 * This method must be overridden in checks extending {@code AbstractJavadocCheck} which
144 * are not supposed to process javadoc containing non-tight html tags.
145 *
146 * @return true if the check should or can process javadoc containing non-tight html tags;
147 * false otherwise
148 * @see ParseStatus#isNonTight()
149 * @see <a href="https://checkstyle.org/writingjavadocchecks.html#Tight-HTML_rules">
150 * Tight HTML rules</a>
151 */
152 public boolean acceptJavadocWithNonTightHtml() {
153 return true;
154 }
155
156 /**
157 * Setter to control when to print violations if the Javadoc being examined by this check
158 * violates the tight html rules defined at
159 * <a href="https://checkstyle.org/writingjavadocchecks.html#Tight-HTML_rules">
160 * Tight-HTML Rules</a>.
161 *
162 * @param shouldReportViolation value to which the field shall be set to
163 * @since 8.3
164 */
165 public void setViolateExecutionOnNonTightHtml(boolean shouldReportViolation) {
166 violateExecutionOnNonTightHtml = shouldReportViolation;
167 }
168
169 /**
170 * Adds a set of tokens the check is interested in.
171 *
172 * @param strRep the string representation of the tokens interested in
173 */
174 public void setJavadocTokens(String... strRep) {
175 for (String str : strRep) {
176 javadocTokens.add(JavadocUtil.getTokenId(str));
177 }
178 }
179
180 @Override
181 public void init() {
182 validateDefaultJavadocTokens();
183 if (javadocTokens.isEmpty()) {
184 javadocTokens.addAll(
185 Arrays.stream(getDefaultJavadocTokens()).boxed()
186 .toList());
187 }
188 else {
189 final int[] acceptableJavadocTokens = getAcceptableJavadocTokens();
190 Arrays.sort(acceptableJavadocTokens);
191 for (Integer javadocTokenId : javadocTokens) {
192 if (Arrays.binarySearch(acceptableJavadocTokens, javadocTokenId) < 0) {
193 final String message = String.format(Locale.ROOT, "Javadoc Token \"%s\" was "
194 + "not found in Acceptable javadoc tokens list in check %s",
195 JavadocUtil.getTokenName(javadocTokenId), getClass().getName());
196 throw new IllegalStateException(message);
197 }
198 }
199 }
200 }
201
202 /**
203 * Validates that check's required javadoc tokens are subset of default javadoc tokens.
204 *
205 * @throws IllegalStateException when validation of default javadoc tokens fails
206 */
207 private void validateDefaultJavadocTokens() {
208 final Set<Integer> defaultTokens = Arrays.stream(getDefaultJavadocTokens())
209 .boxed()
210 .collect(Collectors.toUnmodifiableSet());
211
212 final List<Integer> missingRequiredTokenNames = Arrays.stream(getRequiredJavadocTokens())
213 .boxed()
214 .filter(token -> !defaultTokens.contains(token))
215 .toList();
216
217 if (!missingRequiredTokenNames.isEmpty()) {
218 final String message = String.format(Locale.ROOT,
219 "Javadoc Token \"%s\" from required javadoc "
220 + "tokens was not found in default "
221 + "javadoc tokens list in check %s",
222 missingRequiredTokenNames.stream()
223 .map(String::valueOf)
224 .collect(Collectors.joining(", ")),
225 getClass().getName());
226 throw new IllegalStateException(message);
227 }
228 }
229
230 /**
231 * Called before the starting to process a tree.
232 *
233 * @param rootAst
234 * the root of the tree
235 * @noinspection WeakerAccess
236 * @noinspectionreason WeakerAccess - we avoid 'protected' when possible
237 */
238 public void beginJavadocTree(DetailNode rootAst) {
239 // No code by default, should be overridden only by demand at subclasses
240 }
241
242 /**
243 * Called after finished processing a tree.
244 *
245 * @param rootAst
246 * the root of the tree
247 * @noinspection WeakerAccess
248 * @noinspectionreason WeakerAccess - we avoid 'protected' when possible
249 */
250 public void finishJavadocTree(DetailNode rootAst) {
251 // No code by default, should be overridden only by demand at subclasses
252 }
253
254 /**
255 * Called after all the child nodes have been process.
256 *
257 * @param ast
258 * the token leaving
259 */
260 public void leaveJavadocToken(DetailNode ast) {
261 // No code by default, should be overridden only by demand at subclasses
262 }
263
264 @Override
265 public int[] getDefaultTokens() {
266 return getRequiredTokens();
267 }
268
269 @Override
270 public int[] getAcceptableTokens() {
271 return getRequiredTokens();
272 }
273
274 @Override
275 public int[] getRequiredTokens() {
276 return new int[] {TokenTypes.BLOCK_COMMENT_BEGIN };
277 }
278
279 /**
280 * Defined final because all JavadocChecks require comment nodes.
281 *
282 * @return true
283 */
284 @Override
285 public final boolean isCommentNodesRequired() {
286 return true;
287 }
288
289 @Override
290 public void beginTree(DetailAST rootAST) {
291 TREE_CACHE.get().clear();
292 }
293
294 @Override
295 public void visitToken(DetailAST blockCommentNode) {
296 if (JavadocUtil.isJavadocComment(blockCommentNode)) {
297 // store as field, to share with child Checks
298 context.get().blockCommentAst = blockCommentNode;
299
300 final int treeCacheKey = blockCommentNode.getLineNo();
301
302 final ParseStatus result = TREE_CACHE.get()
303 .computeIfAbsent(treeCacheKey, lineNumber -> {
304 return context.get().parser.parseJavadocComment(blockCommentNode);
305 });
306
307 if (result.getParseErrorMessage() == null) {
308 if (acceptJavadocWithNonTightHtml() || !result.isNonTight()) {
309 processTree(result.getTree());
310 }
311
312 if (violateExecutionOnNonTightHtml && result.isNonTight()) {
313 log(result.getFirstNonTightHtmlTag().getLineNumber(),
314 MSG_KEY_UNCLOSED_HTML_TAG,
315 result.getFirstNonTightHtmlTag().getText());
316 }
317 }
318 else {
319 final ParseErrorMessage parseErrorMessage = result.getParseErrorMessage();
320 log(parseErrorMessage.getLineNumber(),
321 parseErrorMessage.getMessageKey(),
322 parseErrorMessage.getMessageArguments());
323 }
324 }
325 }
326
327 /**
328 * Getter for block comment in Java language syntax tree.
329 *
330 * @return A block comment in the syntax tree.
331 */
332 protected DetailAST getBlockCommentAst() {
333 return context.get().blockCommentAst;
334 }
335
336 /**
337 * Processes JavadocAST tree notifying Check.
338 *
339 * @param root
340 * root of JavadocAST tree.
341 */
342 private void processTree(DetailNode root) {
343 beginJavadocTree(root);
344 walk(root);
345 finishJavadocTree(root);
346 }
347
348 /**
349 * Processes a node calling Check at interested nodes.
350 *
351 * @param root
352 * the root of tree for process
353 */
354 private void walk(DetailNode root) {
355 DetailNode curNode = root;
356 while (curNode != null) {
357 boolean waitsForProcessing = shouldBeProcessed(curNode);
358
359 if (waitsForProcessing) {
360 visitJavadocToken(curNode);
361 }
362 DetailNode toVisit = curNode.getFirstChild();
363 while (curNode != null && toVisit == null) {
364 if (waitsForProcessing) {
365 leaveJavadocToken(curNode);
366 }
367
368 toVisit = curNode.getNextSibling();
369 curNode = curNode.getParent();
370 if (curNode != null) {
371 waitsForProcessing = shouldBeProcessed(curNode);
372 }
373 }
374 curNode = toVisit;
375 }
376 }
377
378 /**
379 * Checks whether the current node should be processed by the check.
380 *
381 * @param curNode current node.
382 * @return true if the current node should be processed by the check.
383 */
384 private boolean shouldBeProcessed(DetailNode curNode) {
385 return javadocTokens.contains(curNode.getType());
386 }
387
388 @Override
389 public void destroy() {
390 super.destroy();
391 context.remove();
392 TREE_CACHE.remove();
393 }
394
395 /**
396 * The file context holder.
397 */
398 private static final class FileContext {
399
400 /**
401 * Parses content of Javadoc comment as DetailNode tree.
402 */
403 private final JavadocDetailNodeParser parser = new JavadocDetailNodeParser();
404
405 /**
406 * DetailAST node of considered Javadoc comment that is just a block comment
407 * in Java language syntax tree.
408 */
409 private DetailAST blockCommentAst;
410
411 }
412
413 }