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 * 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 final DetailNode firstNonTightHtmlTag = result.getFirstNonTightHtmlTag(); 314 log(firstNonTightHtmlTag.getLineNumber(), 315 MSG_KEY_UNCLOSED_HTML_TAG, 316 firstNonTightHtmlTag.getText()); 317 } 318 } 319 else { 320 final ParseErrorMessage parseErrorMessage = result.getParseErrorMessage(); 321 log(parseErrorMessage.getLineNumber(), 322 parseErrorMessage.getMessageKey(), 323 parseErrorMessage.getMessageArguments()); 324 } 325 } 326 } 327 328 /** 329 * Getter for block comment in Java language syntax tree. 330 * 331 * @return A block comment in the syntax tree. 332 */ 333 protected DetailAST getBlockCommentAst() { 334 return context.get().blockCommentAst; 335 } 336 337 /** 338 * Processes JavadocAST tree notifying Check. 339 * 340 * @param root 341 * root of JavadocAST tree. 342 */ 343 private void processTree(DetailNode root) { 344 beginJavadocTree(root); 345 walk(root); 346 finishJavadocTree(root); 347 } 348 349 /** 350 * Processes a node calling Check at interested nodes. 351 * 352 * @param root 353 * the root of tree for process 354 */ 355 private void walk(DetailNode root) { 356 DetailNode curNode = root; 357 while (curNode != null) { 358 boolean waitsForProcessing = shouldBeProcessed(curNode); 359 360 if (waitsForProcessing) { 361 visitJavadocToken(curNode); 362 } 363 DetailNode toVisit = curNode.getFirstChild(); 364 while (curNode != null && toVisit == null) { 365 if (waitsForProcessing) { 366 leaveJavadocToken(curNode); 367 } 368 369 toVisit = curNode.getNextSibling(); 370 curNode = curNode.getParent(); 371 if (curNode != null) { 372 waitsForProcessing = shouldBeProcessed(curNode); 373 } 374 } 375 curNode = toVisit; 376 } 377 } 378 379 /** 380 * Checks whether the current node should be processed by the check. 381 * 382 * @param curNode current node. 383 * @return true if the current node should be processed by the check. 384 */ 385 private boolean shouldBeProcessed(DetailNode curNode) { 386 return javadocTokens.contains(curNode.getType()); 387 } 388 389 @Override 390 public void destroy() { 391 super.destroy(); 392 context.remove(); 393 TREE_CACHE.remove(); 394 } 395 396 /** 397 * The file context holder. 398 */ 399 private static final class FileContext { 400 401 /** 402 * Parses content of Javadoc comment as DetailNode tree. 403 */ 404 private final JavadocDetailNodeParser parser = new JavadocDetailNodeParser(); 405 406 /** 407 * DetailAST node of considered Javadoc comment that is just a block comment 408 * in Java language syntax tree. 409 */ 410 private DetailAST blockCommentAst; 411 412 } 413 414}