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