001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2021 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.TokenTypes; 038import com.puppycrawl.tools.checkstyle.utils.CommonUtil; 039import com.puppycrawl.tools.checkstyle.utils.JavadocUtil; 040 041/** 042 * Base class for Checks that process Javadoc comments. 043 * 044 * @noinspection NoopMethodInAbstractClass 045 */ 046public abstract class AbstractJavadocCheck extends AbstractCheck { 047 048 /** 049 * Message key of error message. Missed close HTML tag breaks structure 050 * of parse tree, so parser stops parsing and generates such error 051 * message. This case is special because parser prints error like 052 * {@code "no viable alternative at input 'b \n *\n'"} and it is not 053 * clear that error is about missed close HTML tag. 054 */ 055 public static final String MSG_JAVADOC_MISSED_HTML_CLOSE = 056 JavadocDetailNodeParser.MSG_JAVADOC_MISSED_HTML_CLOSE; 057 058 /** 059 * Message key of error message. 060 */ 061 public static final String MSG_JAVADOC_WRONG_SINGLETON_TAG = 062 JavadocDetailNodeParser.MSG_JAVADOC_WRONG_SINGLETON_TAG; 063 064 /** 065 * Parse error while rule recognition. 066 */ 067 public static final String MSG_JAVADOC_PARSE_RULE_ERROR = 068 JavadocDetailNodeParser.MSG_JAVADOC_PARSE_RULE_ERROR; 069 070 /** 071 * Key is "line:column". Value is {@link DetailNode} tree. Map is stored in {@link ThreadLocal} 072 * to guarantee basic thread safety and avoid shared, mutable state when not necessary. 073 */ 074 private static final ThreadLocal<Map<String, ParseStatus>> TREE_CACHE = 075 ThreadLocal.withInitial(HashMap::new); 076 077 /** 078 * The file context. 079 * 080 * @noinspection ThreadLocalNotStaticFinal 081 */ 082 private final ThreadLocal<FileContext> context = ThreadLocal.withInitial(FileContext::new); 083 084 /** The javadoc tokens the check is interested in. */ 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 JavadocTokenTypes 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 JavadocTokenTypes 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 JavadocTokenTypes 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 */ 164 public final void setViolateExecutionOnNonTightHtml(boolean shouldReportViolation) { 165 violateExecutionOnNonTightHtml = shouldReportViolation; 166 } 167 168 /** 169 * Adds a set of tokens the check is interested in. 170 * 171 * @param strRep the string representation of the tokens interested in 172 */ 173 public final void setJavadocTokens(String... strRep) { 174 for (String str : strRep) { 175 javadocTokens.add(JavadocUtil.getTokenId(str)); 176 } 177 } 178 179 @Override 180 public void init() { 181 validateDefaultJavadocTokens(); 182 if (javadocTokens.isEmpty()) { 183 javadocTokens.addAll( 184 Arrays.stream(getDefaultJavadocTokens()).boxed().collect(Collectors.toList())); 185 } 186 else { 187 final int[] acceptableJavadocTokens = getAcceptableJavadocTokens(); 188 Arrays.sort(acceptableJavadocTokens); 189 for (Integer javadocTokenId : javadocTokens) { 190 if (Arrays.binarySearch(acceptableJavadocTokens, javadocTokenId) < 0) { 191 final String message = String.format(Locale.ROOT, "Javadoc Token \"%s\" was " 192 + "not found in Acceptable javadoc tokens list in check %s", 193 JavadocUtil.getTokenName(javadocTokenId), getClass().getName()); 194 throw new IllegalStateException(message); 195 } 196 } 197 } 198 } 199 200 /** 201 * Validates that check's required javadoc tokens are subset of default javadoc tokens. 202 * 203 * @throws IllegalStateException when validation of default javadoc tokens fails 204 */ 205 private void validateDefaultJavadocTokens() { 206 if (getRequiredJavadocTokens().length != 0) { 207 final int[] defaultJavadocTokens = getDefaultJavadocTokens(); 208 Arrays.sort(defaultJavadocTokens); 209 for (final int javadocToken : getRequiredJavadocTokens()) { 210 if (Arrays.binarySearch(defaultJavadocTokens, javadocToken) < 0) { 211 final String message = String.format(Locale.ROOT, 212 "Javadoc Token \"%s\" from required javadoc " 213 + "tokens was not found in default " 214 + "javadoc tokens list in check %s", 215 javadocToken, getClass().getName()); 216 throw new IllegalStateException(message); 217 } 218 } 219 } 220 } 221 222 /** 223 * Called before the starting to process a tree. 224 * 225 * @param rootAst 226 * the root of the tree 227 * @noinspection WeakerAccess 228 */ 229 public void beginJavadocTree(DetailNode rootAst) { 230 // No code by default, should be overridden only by demand at subclasses 231 } 232 233 /** 234 * Called after finished processing a tree. 235 * 236 * @param rootAst 237 * the root of the tree 238 * @noinspection WeakerAccess 239 */ 240 public void finishJavadocTree(DetailNode rootAst) { 241 // No code by default, should be overridden only by demand at subclasses 242 } 243 244 /** 245 * Called after all the child nodes have been process. 246 * 247 * @param ast 248 * the token leaving 249 */ 250 public void leaveJavadocToken(DetailNode ast) { 251 // No code by default, should be overridden only by demand at subclasses 252 } 253 254 /** 255 * Defined final to not allow JavadocChecks to change default tokens. 256 * 257 * @return default tokens 258 */ 259 @Override 260 public final int[] getDefaultTokens() { 261 return getRequiredTokens(); 262 } 263 264 @Override 265 public final int[] getAcceptableTokens() { 266 return getRequiredTokens(); 267 } 268 269 @Override 270 public final int[] getRequiredTokens() { 271 return new int[] {TokenTypes.BLOCK_COMMENT_BEGIN }; 272 } 273 274 /** 275 * Defined final because all JavadocChecks require comment nodes. 276 * 277 * @return true 278 */ 279 @Override 280 public final boolean isCommentNodesRequired() { 281 return true; 282 } 283 284 @Override 285 public final void beginTree(DetailAST rootAST) { 286 TREE_CACHE.get().clear(); 287 } 288 289 @Override 290 public final void finishTree(DetailAST rootAST) { 291 // No code, prevent override in subclasses 292 } 293 294 @Override 295 public final 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 String treeCacheKey = blockCommentNode.getLineNo() + ":" 301 + blockCommentNode.getColumnNo(); 302 303 final ParseStatus result; 304 305 if (TREE_CACHE.get().containsKey(treeCacheKey)) { 306 result = TREE_CACHE.get().get(treeCacheKey); 307 } 308 else { 309 result = context.get().parser 310 .parseJavadocAsDetailNode(blockCommentNode); 311 TREE_CACHE.get().put(treeCacheKey, result); 312 } 313 314 if (result.getParseErrorMessage() == null) { 315 if (acceptJavadocWithNonTightHtml() || !result.isNonTight()) { 316 processTree(result.getTree()); 317 } 318 319 if (violateExecutionOnNonTightHtml && result.isNonTight()) { 320 log(result.getFirstNonTightHtmlTag().getLine(), 321 JavadocDetailNodeParser.MSG_UNCLOSED_HTML_TAG, 322 result.getFirstNonTightHtmlTag().getText()); 323 } 324 } 325 else { 326 final ParseErrorMessage parseErrorMessage = result.getParseErrorMessage(); 327 log(parseErrorMessage.getLineNumber(), 328 parseErrorMessage.getMessageKey(), 329 parseErrorMessage.getMessageArguments()); 330 } 331 } 332 } 333 334 /** 335 * Getter for block comment in Java language syntax tree. 336 * 337 * @return A block comment in the syntax tree. 338 */ 339 protected DetailAST getBlockCommentAst() { 340 return context.get().blockCommentAst; 341 } 342 343 /** 344 * Processes JavadocAST tree notifying Check. 345 * 346 * @param root 347 * root of JavadocAST tree. 348 */ 349 private void processTree(DetailNode root) { 350 beginJavadocTree(root); 351 walk(root); 352 finishJavadocTree(root); 353 } 354 355 /** 356 * Processes a node calling Check at interested nodes. 357 * 358 * @param root 359 * the root of tree for process 360 */ 361 private void walk(DetailNode root) { 362 DetailNode curNode = root; 363 while (curNode != null) { 364 boolean waitsForProcessing = shouldBeProcessed(curNode); 365 366 if (waitsForProcessing) { 367 visitJavadocToken(curNode); 368 } 369 DetailNode toVisit = JavadocUtil.getFirstChild(curNode); 370 while (curNode != null && toVisit == null) { 371 if (waitsForProcessing) { 372 leaveJavadocToken(curNode); 373 } 374 375 toVisit = JavadocUtil.getNextSibling(curNode); 376 if (toVisit == null) { 377 curNode = curNode.getParent(); 378 if (curNode != null) { 379 waitsForProcessing = shouldBeProcessed(curNode); 380 } 381 } 382 } 383 curNode = toVisit; 384 } 385 } 386 387 /** 388 * Checks whether the current node should be processed by the check. 389 * 390 * @param curNode current node. 391 * @return true if the current node should be processed by the check. 392 */ 393 private boolean shouldBeProcessed(DetailNode curNode) { 394 return javadocTokens.contains(curNode.getType()); 395 } 396 397 @Override 398 public void destroy() { 399 super.destroy(); 400 context.remove(); 401 TREE_CACHE.remove(); 402 } 403 404 /** 405 * The file context holder. 406 */ 407 private static class FileContext { 408 409 /** 410 * Parses content of Javadoc comment as DetailNode tree. 411 */ 412 private final JavadocDetailNodeParser parser = new JavadocDetailNodeParser(); 413 414 /** 415 * DetailAST node of considered Javadoc comment that is just a block comment 416 * in Java language syntax tree. 417 */ 418 private DetailAST blockCommentAst; 419 420 } 421 422}