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