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