001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2023 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.Locale; 026import java.util.Map; 027import java.util.Set; 028import java.util.stream.Collectors; 029 030import com.puppycrawl.tools.checkstyle.JavadocDetailNodeParser; 031import com.puppycrawl.tools.checkstyle.JavadocDetailNodeParser.ParseErrorMessage; 032import com.puppycrawl.tools.checkstyle.JavadocDetailNodeParser.ParseStatus; 033import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 034import com.puppycrawl.tools.checkstyle.api.DetailAST; 035import com.puppycrawl.tools.checkstyle.api.DetailNode; 036import com.puppycrawl.tools.checkstyle.api.JavadocTokenTypes; 037import com.puppycrawl.tools.checkstyle.api.LineColumn; 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 * Key is "line:column". Value is {@link DetailNode} tree. Map is stored in {@link ThreadLocal} 076 * to guarantee basic thread safety and avoid shared, mutable state when not necessary. 077 */ 078 private static final ThreadLocal<Map<LineColumn, ParseStatus>> TREE_CACHE = 079 ThreadLocal.withInitial(HashMap::new); 080 081 /** 082 * The file context. 083 * 084 * @noinspection ThreadLocalNotStaticFinal 085 * @noinspectionreason ThreadLocalNotStaticFinal - static context is 086 * problematic for multithreading 087 */ 088 private final ThreadLocal<FileContext> context = ThreadLocal.withInitial(FileContext::new); 089 090 /** The javadoc tokens the check is interested in. */ 091 private final Set<Integer> javadocTokens = new HashSet<>(); 092 093 /** 094 * This property determines if a check should log a violation upon encountering javadoc with 095 * non-tight html. The default return value for this method is set to false since checks 096 * generally tend to be fine with non-tight html. It can be set through config file if a check 097 * is to log violation upon encountering non-tight HTML in javadoc. 098 * 099 * @see ParseStatus#isNonTight() 100 * @see <a href="https://checkstyle.org/writingjavadocchecks.html#Tight-HTML_rules"> 101 * Tight HTML rules</a> 102 */ 103 private boolean violateExecutionOnNonTightHtml; 104 105 /** 106 * Returns the default javadoc token types a check is interested in. 107 * 108 * @return the default javadoc token types 109 * @see JavadocTokenTypes 110 */ 111 public abstract int[] getDefaultJavadocTokens(); 112 113 /** 114 * Called to process a Javadoc token. 115 * 116 * @param ast 117 * the token to process 118 */ 119 public abstract void visitJavadocToken(DetailNode ast); 120 121 /** 122 * The configurable javadoc token set. 123 * Used to protect Checks against malicious users who specify an 124 * unacceptable javadoc token set in the configuration file. 125 * The default implementation returns the check's default javadoc tokens. 126 * 127 * @return the javadoc token set this check is designed for. 128 * @see JavadocTokenTypes 129 */ 130 public int[] getAcceptableJavadocTokens() { 131 final int[] defaultJavadocTokens = getDefaultJavadocTokens(); 132 final int[] copy = new int[defaultJavadocTokens.length]; 133 System.arraycopy(defaultJavadocTokens, 0, copy, 0, defaultJavadocTokens.length); 134 return copy; 135 } 136 137 /** 138 * The javadoc tokens that this check must be registered for. 139 * 140 * @return the javadoc token set this must be registered for. 141 * @see JavadocTokenTypes 142 */ 143 public int[] getRequiredJavadocTokens() { 144 return CommonUtil.EMPTY_INT_ARRAY; 145 } 146 147 /** 148 * This method determines if a check should process javadoc containing non-tight html tags. 149 * This method must be overridden in checks extending {@code AbstractJavadocCheck} which 150 * are not supposed to process javadoc containing non-tight html tags. 151 * 152 * @return true if the check should or can process javadoc containing non-tight html tags; 153 * false otherwise 154 * @see ParseStatus#isNonTight() 155 * @see <a href="https://checkstyle.org/writingjavadocchecks.html#Tight-HTML_rules"> 156 * Tight HTML rules</a> 157 */ 158 public boolean acceptJavadocWithNonTightHtml() { 159 return true; 160 } 161 162 /** 163 * Setter to control when to print violations if the Javadoc being examined by this check 164 * violates the tight html rules defined at 165 * <a href="https://checkstyle.org/writingjavadocchecks.html#Tight-HTML_rules"> 166 * Tight-HTML Rules</a>. 167 * 168 * @param shouldReportViolation value to which the field shall be set to 169 */ 170 public final void setViolateExecutionOnNonTightHtml(boolean shouldReportViolation) { 171 violateExecutionOnNonTightHtml = shouldReportViolation; 172 } 173 174 /** 175 * Adds a set of tokens the check is interested in. 176 * 177 * @param strRep the string representation of the tokens interested in 178 */ 179 public final void setJavadocTokens(String... strRep) { 180 for (String str : strRep) { 181 javadocTokens.add(JavadocUtil.getTokenId(str)); 182 } 183 } 184 185 @Override 186 public void init() { 187 validateDefaultJavadocTokens(); 188 if (javadocTokens.isEmpty()) { 189 javadocTokens.addAll( 190 Arrays.stream(getDefaultJavadocTokens()).boxed().collect(Collectors.toList())); 191 } 192 else { 193 final int[] acceptableJavadocTokens = getAcceptableJavadocTokens(); 194 Arrays.sort(acceptableJavadocTokens); 195 for (Integer javadocTokenId : javadocTokens) { 196 if (Arrays.binarySearch(acceptableJavadocTokens, javadocTokenId) < 0) { 197 final String message = String.format(Locale.ROOT, "Javadoc Token \"%s\" was " 198 + "not found in Acceptable javadoc tokens list in check %s", 199 JavadocUtil.getTokenName(javadocTokenId), getClass().getName()); 200 throw new IllegalStateException(message); 201 } 202 } 203 } 204 } 205 206 /** 207 * Validates that check's required javadoc tokens are subset of default javadoc tokens. 208 * 209 * @throws IllegalStateException when validation of default javadoc tokens fails 210 */ 211 private void validateDefaultJavadocTokens() { 212 if (getRequiredJavadocTokens().length != 0) { 213 final int[] defaultJavadocTokens = getDefaultJavadocTokens(); 214 Arrays.sort(defaultJavadocTokens); 215 for (final int javadocToken : getRequiredJavadocTokens()) { 216 if (Arrays.binarySearch(defaultJavadocTokens, javadocToken) < 0) { 217 final String message = String.format(Locale.ROOT, 218 "Javadoc Token \"%s\" from required javadoc " 219 + "tokens was not found in default " 220 + "javadoc tokens list in check %s", 221 javadocToken, getClass().getName()); 222 throw new IllegalStateException(message); 223 } 224 } 225 } 226 } 227 228 /** 229 * Called before the starting to process a tree. 230 * 231 * @param rootAst 232 * the root of the tree 233 * @noinspection WeakerAccess 234 * @noinspectionreason WeakerAccess - we avoid 'protected' when possible 235 */ 236 public void beginJavadocTree(DetailNode rootAst) { 237 // No code by default, should be overridden only by demand at subclasses 238 } 239 240 /** 241 * Called after finished processing a tree. 242 * 243 * @param rootAst 244 * the root of the tree 245 * @noinspection WeakerAccess 246 * @noinspectionreason WeakerAccess - we avoid 'protected' when possible 247 */ 248 public void finishJavadocTree(DetailNode rootAst) { 249 // No code by default, should be overridden only by demand at subclasses 250 } 251 252 /** 253 * Called after all the child nodes have been process. 254 * 255 * @param ast 256 * the token leaving 257 */ 258 public void leaveJavadocToken(DetailNode ast) { 259 // No code by default, should be overridden only by demand at subclasses 260 } 261 262 /** 263 * Defined final to not allow JavadocChecks to change default tokens. 264 * 265 * @return default tokens 266 */ 267 @Override 268 public final int[] getDefaultTokens() { 269 return getRequiredTokens(); 270 } 271 272 @Override 273 public final int[] getAcceptableTokens() { 274 return getRequiredTokens(); 275 } 276 277 @Override 278 public final int[] getRequiredTokens() { 279 return new int[] {TokenTypes.BLOCK_COMMENT_BEGIN }; 280 } 281 282 /** 283 * Defined final because all JavadocChecks require comment nodes. 284 * 285 * @return true 286 */ 287 @Override 288 public final boolean isCommentNodesRequired() { 289 return true; 290 } 291 292 @Override 293 public final void beginTree(DetailAST rootAST) { 294 TREE_CACHE.get().clear(); 295 } 296 297 @Override 298 public final void finishTree(DetailAST rootAST) { 299 // No code, prevent override in subclasses 300 } 301 302 @Override 303 public final void visitToken(DetailAST blockCommentNode) { 304 if (JavadocUtil.isJavadocComment(blockCommentNode)) { 305 // store as field, to share with child Checks 306 context.get().blockCommentAst = blockCommentNode; 307 308 final LineColumn treeCacheKey = new LineColumn(blockCommentNode.getLineNo(), 309 blockCommentNode.getColumnNo()); 310 311 final ParseStatus result = TREE_CACHE.get().computeIfAbsent(treeCacheKey, key -> { 312 return context.get().parser.parseJavadocAsDetailNode(blockCommentNode); 313 }); 314 315 if (result.getParseErrorMessage() == null) { 316 if (acceptJavadocWithNonTightHtml() || !result.isNonTight()) { 317 processTree(result.getTree()); 318 } 319 320 if (violateExecutionOnNonTightHtml && result.isNonTight()) { 321 log(result.getFirstNonTightHtmlTag().getLine(), 322 JavadocDetailNodeParser.MSG_UNCLOSED_HTML_TAG, 323 result.getFirstNonTightHtmlTag().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 = JavadocUtil.getFirstChild(curNode); 371 while (curNode != null && toVisit == null) { 372 if (waitsForProcessing) { 373 leaveJavadocToken(curNode); 374 } 375 376 toVisit = JavadocUtil.getNextSibling(curNode); 377 if (toVisit == null) { 378 curNode = curNode.getParent(); 379 if (curNode != null) { 380 waitsForProcessing = shouldBeProcessed(curNode); 381 } 382 } 383 } 384 curNode = toVisit; 385 } 386 } 387 388 /** 389 * Checks whether the current node should be processed by the check. 390 * 391 * @param curNode current node. 392 * @return true if the current node should be processed by the check. 393 */ 394 private boolean shouldBeProcessed(DetailNode curNode) { 395 return javadocTokens.contains(curNode.getType()); 396 } 397 398 @Override 399 public void destroy() { 400 super.destroy(); 401 context.remove(); 402 TREE_CACHE.remove(); 403 } 404 405 /** 406 * The file context holder. 407 */ 408 private static class FileContext { 409 410 /** 411 * Parses content of Javadoc comment as DetailNode tree. 412 */ 413 private final JavadocDetailNodeParser parser = new JavadocDetailNodeParser(); 414 415 /** 416 * DetailAST node of considered Javadoc comment that is just a block comment 417 * in Java language syntax tree. 418 */ 419 private DetailAST blockCommentAst; 420 421 } 422 423}