001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2026 the original author or authors. 004// 005// This library is free software; you can redistribute it and/or 006// modify it under the terms of the GNU Lesser General Public 007// License as published by the Free Software Foundation; either 008// version 2.1 of the License, or (at your option) any later version. 009// 010// This library is distributed in the hope that it will be useful, 011// but WITHOUT ANY WARRANTY; without even the implied warranty of 012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 013// Lesser General Public License for more details. 014// 015// You should have received a copy of the GNU Lesser General Public 016// License along with this library; if not, write to the Free Software 017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 018/////////////////////////////////////////////////////////////////////////////////////////////// 019 020package com.puppycrawl.tools.checkstyle.checks.javadoc; 021 022import java.util.Arrays; 023import java.util.HashMap; 024import java.util.HashSet; 025import java.util.List; 026import java.util.Locale; 027import java.util.Map; 028import java.util.Set; 029import java.util.stream.Collectors; 030 031import com.puppycrawl.tools.checkstyle.JavadocDetailNodeParser; 032import com.puppycrawl.tools.checkstyle.JavadocDetailNodeParser.ParseErrorMessage; 033import com.puppycrawl.tools.checkstyle.JavadocDetailNodeParser.ParseStatus; 034import com.puppycrawl.tools.checkstyle.PropertyType; 035import com.puppycrawl.tools.checkstyle.XdocsPropertyType; 036import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 037import com.puppycrawl.tools.checkstyle.api.DetailAST; 038import com.puppycrawl.tools.checkstyle.api.DetailNode; 039import com.puppycrawl.tools.checkstyle.api.JavadocCommentsTokenTypes; 040import com.puppycrawl.tools.checkstyle.api.TokenTypes; 041import com.puppycrawl.tools.checkstyle.utils.CommonUtil; 042import com.puppycrawl.tools.checkstyle.utils.JavadocUtil; 043 044/** 045 * Base class for Checks that process Javadoc comments. 046 * 047 * @noinspection NoopMethodInAbstractClass 048 * @noinspectionreason NoopMethodInAbstractClass - we allow each 049 * check to define these methods, as needed. They 050 * should be overridden only by demand in subclasses 051 */ 052public abstract class AbstractJavadocCheck extends AbstractCheck { 053 054 /** 055 * Parse error while rule recognition. 056 */ 057 public static final String MSG_JAVADOC_PARSE_RULE_ERROR = 058 JavadocDetailNodeParser.MSG_JAVADOC_PARSE_RULE_ERROR; 059 060 /** 061 * Message key of error message. 062 */ 063 public static final String MSG_KEY_UNCLOSED_HTML_TAG = 064 JavadocDetailNodeParser.MSG_UNCLOSED_HTML_TAG; 065 066 /** 067 * Key is the block comment node "lineNo". Value is {@link DetailNode} tree. 068 * Map is stored in {@link ThreadLocal} 069 * to guarantee basic thread safety and avoid shared, mutable state when not necessary. 070 */ 071 private static final ThreadLocal<Map<Integer, ParseStatus>> TREE_CACHE = 072 ThreadLocal.withInitial(HashMap::new); 073 074 /** 075 * The file context. 076 * 077 * @noinspection ThreadLocalNotStaticFinal 078 * @noinspectionreason ThreadLocalNotStaticFinal - static context is 079 * problematic for multithreading 080 */ 081 private final ThreadLocal<FileContext> context = ThreadLocal.withInitial(FileContext::new); 082 083 /** The javadoc tokens the check is interested in. */ 084 @XdocsPropertyType(PropertyType.TOKEN_ARRAY) 085 private final Set<Integer> javadocTokens = new HashSet<>(); 086 087 /** 088 * This property determines if a check should log a violation upon encountering javadoc with 089 * non-tight html. The default return value for this method is set to false since checks 090 * generally tend to be fine with non-tight html. It can be set through config file if a check 091 * is to log violation upon encountering non-tight HTML in javadoc. 092 * 093 * @see ParseStatus#isNonTight() 094 * @see <a href="https://checkstyle.org/writingjavadocchecks.html#Tight-HTML_rules"> 095 * Tight HTML rules</a> 096 */ 097 private boolean violateExecutionOnNonTightHtml; 098 099 /** 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}