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 * Creates a new {@code AbstractJavadocCheck} instance.
101 */
102 protected AbstractJavadocCheck() {
103 // no code by default
104 }
105
106 /**
107 * Returns the default javadoc token types a check is interested in.
108 *
109 * @return the default javadoc token types
110 * @see JavadocCommentsTokenTypes
111 */
112 public abstract int[] getDefaultJavadocTokens();
113
114 /**
115 * Called to process a Javadoc token.
116 *
117 * @param ast
118 * the token to process
119 */
120 public abstract void visitJavadocToken(DetailNode ast);
121
122 /**
123 * The configurable javadoc token set.
124 * Used to protect Checks against malicious users who specify an
125 * unacceptable javadoc token set in the configuration file.
126 * The default implementation returns the check's default javadoc tokens.
127 *
128 * @return the javadoc token set this check is designed for.
129 * @see JavadocCommentsTokenTypes
130 */
131 public int[] getAcceptableJavadocTokens() {
132 final int[] defaultJavadocTokens = getDefaultJavadocTokens();
133 final int[] copy = new int[defaultJavadocTokens.length];
134 System.arraycopy(defaultJavadocTokens, 0, copy, 0, defaultJavadocTokens.length);
135 return copy;
136 }
137
138 /**
139 * The javadoc tokens that this check must be registered for.
140 *
141 * @return the javadoc token set this must be registered for.
142 * @see JavadocCommentsTokenTypes
143 */
144 public int[] getRequiredJavadocTokens() {
145 return CommonUtil.EMPTY_INT_ARRAY;
146 }
147
148 /**
149 * This method determines if a check should process javadoc containing non-tight html tags.
150 * This method must be overridden in checks extending {@code AbstractJavadocCheck} which
151 * are not supposed to process javadoc containing non-tight html tags.
152 *
153 * @return true if the check should or can process javadoc containing non-tight html tags;
154 * false otherwise
155 * @see ParseStatus#isNonTight()
156 * @see <a href="https://checkstyle.org/writingjavadocchecks.html#Tight-HTML_rules">
157 * Tight HTML rules</a>
158 */
159 public boolean acceptJavadocWithNonTightHtml() {
160 return true;
161 }
162
163 /**
164 * Setter to control when to print violations if the Javadoc being examined by this check
165 * violates the tight html rules defined at
166 * <a href="https://checkstyle.org/writingjavadocchecks.html#Tight-HTML_rules">
167 * Tight-HTML Rules</a>.
168 *
169 * @param shouldReportViolation value to which the field shall be set to
170 * @since 8.3
171 */
172 public void setViolateExecutionOnNonTightHtml(boolean shouldReportViolation) {
173 violateExecutionOnNonTightHtml = shouldReportViolation;
174 }
175
176 /**
177 * Adds a set of tokens the check is interested in.
178 *
179 * @param strRep the string representation of the tokens interested in
180 */
181 public void setJavadocTokens(String... strRep) {
182 for (String str : strRep) {
183 javadocTokens.add(JavadocUtil.getTokenId(str));
184 }
185 }
186
187 @Override
188 public void init() {
189 validateDefaultJavadocTokens();
190 if (javadocTokens.isEmpty()) {
191 javadocTokens.addAll(
192 Arrays.stream(getDefaultJavadocTokens()).boxed()
193 .toList());
194 }
195 else {
196 final int[] acceptableJavadocTokens = getAcceptableJavadocTokens();
197 Arrays.sort(acceptableJavadocTokens);
198 for (Integer javadocTokenId : javadocTokens) {
199 if (Arrays.binarySearch(acceptableJavadocTokens, javadocTokenId) < 0) {
200 final String message = String.format(Locale.ROOT, "Javadoc Token \"%s\" was "
201 + "not found in Acceptable javadoc tokens list in check %s",
202 JavadocUtil.getTokenName(javadocTokenId), getClass().getName());
203 throw new IllegalStateException(message);
204 }
205 }
206 }
207 }
208
209 /**
210 * Validates that check's required javadoc tokens are subset of default javadoc tokens.
211 *
212 * @throws IllegalStateException when validation of default javadoc tokens fails
213 */
214 private void validateDefaultJavadocTokens() {
215 final Set<Integer> defaultTokens = Arrays.stream(getDefaultJavadocTokens())
216 .boxed()
217 .collect(Collectors.toUnmodifiableSet());
218
219 final List<Integer> missingRequiredTokenNames = Arrays.stream(getRequiredJavadocTokens())
220 .boxed()
221 .filter(token -> !defaultTokens.contains(token))
222 .toList();
223
224 if (!missingRequiredTokenNames.isEmpty()) {
225 final String message = String.format(Locale.ROOT,
226 "Javadoc Token \"%s\" from required javadoc "
227 + "tokens was not found in default "
228 + "javadoc tokens list in check %s",
229 missingRequiredTokenNames.stream()
230 .map(String::valueOf)
231 .collect(Collectors.joining(", ")),
232 getClass().getName());
233 throw new IllegalStateException(message);
234 }
235 }
236
237 /**
238 * Called before the starting to process a tree.
239 *
240 * @param rootAst
241 * the root of the tree
242 * @noinspection WeakerAccess
243 * @noinspectionreason WeakerAccess - we avoid 'protected' when possible
244 */
245 public void beginJavadocTree(DetailNode rootAst) {
246 // No code by default, should be overridden only by demand at subclasses
247 }
248
249 /**
250 * Called after finished processing a tree.
251 *
252 * @param rootAst
253 * the root of the tree
254 * @noinspection WeakerAccess
255 * @noinspectionreason WeakerAccess - we avoid 'protected' when possible
256 */
257 public void finishJavadocTree(DetailNode rootAst) {
258 // No code by default, should be overridden only by demand at subclasses
259 }
260
261 /**
262 * Called after all the child nodes have been process.
263 *
264 * @param ast
265 * the token leaving
266 */
267 public void leaveJavadocToken(DetailNode ast) {
268 // No code by default, should be overridden only by demand at subclasses
269 }
270
271 @Override
272 public int[] getDefaultTokens() {
273 return getRequiredTokens();
274 }
275
276 @Override
277 public int[] getAcceptableTokens() {
278 return getRequiredTokens();
279 }
280
281 @Override
282 public int[] getRequiredTokens() {
283 return new int[] {TokenTypes.BLOCK_COMMENT_BEGIN };
284 }
285
286 /**
287 * Defined final because all JavadocChecks require comment nodes.
288 *
289 * @return true
290 */
291 @Override
292 public final boolean isCommentNodesRequired() {
293 return true;
294 }
295
296 @Override
297 public void beginTree(DetailAST rootAST) {
298 TREE_CACHE.get().clear();
299 }
300
301 @Override
302 public void visitToken(DetailAST blockCommentNode) {
303 if (JavadocUtil.isJavadocComment(blockCommentNode)) {
304 // store as field, to share with child Checks
305 context.get().blockCommentAst = blockCommentNode;
306
307 final int treeCacheKey = blockCommentNode.getLineNo();
308
309 final ParseStatus result = TREE_CACHE.get()
310 .computeIfAbsent(treeCacheKey, lineNumber -> {
311 return context.get().parser.parseJavadocComment(blockCommentNode);
312 });
313
314 if (result.getParseErrorMessage() == null) {
315 if (acceptJavadocWithNonTightHtml() || !result.isNonTight()) {
316 processTree(result.getTree());
317 }
318
319 if (violateExecutionOnNonTightHtml && result.isNonTight()) {
320 final DetailNode firstNonTightHtmlTag = result.getFirstNonTightHtmlTag();
321 log(firstNonTightHtmlTag.getLineNumber(),
322 MSG_KEY_UNCLOSED_HTML_TAG,
323 firstNonTightHtmlTag.getText());
324 }
325 }
326 else {
327 final ParseErrorMessage parseErrorMessage = result.getParseErrorMessage();
328 log(parseErrorMessage.getLineNumber(),
329 parseErrorMessage.getMessageKey(),
330 parseErrorMessage.getMessageArguments());
331 }
332 }
333 }
334
335 /**
336 * Getter for block comment in Java language syntax tree.
337 *
338 * @return A block comment in the syntax tree.
339 */
340 protected DetailAST getBlockCommentAst() {
341 return context.get().blockCommentAst;
342 }
343
344 /**
345 * Processes JavadocAST tree notifying Check.
346 *
347 * @param root
348 * root of JavadocAST tree.
349 */
350 private void processTree(DetailNode root) {
351 beginJavadocTree(root);
352 walk(root);
353 finishJavadocTree(root);
354 }
355
356 /**
357 * Processes a node calling Check at interested nodes.
358 *
359 * @param root
360 * the root of tree for process
361 */
362 private void walk(DetailNode root) {
363 DetailNode curNode = root;
364 while (curNode != null) {
365 boolean waitsForProcessing = shouldBeProcessed(curNode);
366
367 if (waitsForProcessing) {
368 visitJavadocToken(curNode);
369 }
370 DetailNode toVisit = curNode.getFirstChild();
371 while (curNode != null && toVisit == null) {
372 if (waitsForProcessing) {
373 leaveJavadocToken(curNode);
374 }
375
376 toVisit = curNode.getNextSibling();
377 curNode = curNode.getParent();
378 if (curNode != null) {
379 waitsForProcessing = shouldBeProcessed(curNode);
380 }
381 }
382 curNode = toVisit;
383 }
384 }
385
386 /**
387 * Checks whether the current node should be processed by the check.
388 *
389 * @param curNode current node.
390 * @return true if the current node should be processed by the check.
391 */
392 private boolean shouldBeProcessed(DetailNode curNode) {
393 return javadocTokens.contains(curNode.getType());
394 }
395
396 @Override
397 public void destroy() {
398 super.destroy();
399 context.remove();
400 TREE_CACHE.remove();
401 }
402
403 /**
404 * Logs a message against a DetailNode.
405 * This is a wrapper method to log violations using a DetailNode
406 * instead of manually specifying line and column numbers.
407 *
408 * @param node the DetailNode that has the violation
409 * @param key the message key from the check messages
410 * @param args the arguments to the message
411 */
412 protected final void log(DetailNode node, String key, Object... args) {
413 log(node.getLineNumber(), node.getColumnNumber(), key, args);
414 }
415
416 /**
417 * The file context holder.
418 */
419 private static final class FileContext {
420
421 /**
422 * Parses content of Javadoc comment as DetailNode tree.
423 */
424 private final JavadocDetailNodeParser parser = new JavadocDetailNodeParser();
425
426 /**
427 * DetailAST node of considered Javadoc comment that is just a block comment
428 * in Java language syntax tree.
429 */
430 private DetailAST blockCommentAst;
431
432 /**
433 * Creates a new {@code FileContext} instance.
434 */
435 private FileContext() {
436 // no code by default
437 }
438 }
439
440 }