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