001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2020 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.Collections; 024import java.util.HashSet; 025import java.util.Set; 026import java.util.regex.Pattern; 027 028import com.puppycrawl.tools.checkstyle.StatelessCheck; 029import com.puppycrawl.tools.checkstyle.api.DetailNode; 030import com.puppycrawl.tools.checkstyle.api.JavadocTokenTypes; 031import com.puppycrawl.tools.checkstyle.utils.CommonUtil; 032import com.puppycrawl.tools.checkstyle.utils.JavadocUtil; 033 034/** 035 * <p> 036 * Checks that 037 * <a href="https://www.oracle.com/technical-resources/articles/java/javadoc-tool.html#firstsentence"> 038 * Javadoc summary sentence</a> does not contain phrases that are not recommended to use. 039 * Summaries that contain only the {@code {@inheritDoc}} tag are skipped. 040 * Check also violate Javadoc that does not contain first sentence. 041 * </p> 042 * <ul> 043 * <li> 044 * Property {@code violateExecutionOnNonTightHtml} - Control when to print violations 045 * if the Javadoc being examined by this check violates the tight html rules defined at 046 * <a href="https://checkstyle.org/writingjavadocchecks.html#Tight-HTML_rules">Tight-HTML Rules</a>. 047 * Type is {@code boolean}. 048 * Default value is {@code false}. 049 * </li> 050 * <li> 051 * Property {@code forbiddenSummaryFragments} - Specify the regexp for forbidden summary fragments. 052 * Type is {@code java.util.regex.Pattern}. 053 * Default value is {@code "^$"}. 054 * </li> 055 * <li> 056 * Property {@code period} - Specify the period symbol at the end of first javadoc sentence. 057 * Type is {@code java.lang.String}. 058 * Default value is {@code "."}. 059 * </li> 060 * </ul> 061 * <p> 062 * To configure the default check to validate that first sentence is not empty and first 063 * sentence is not missing: 064 * </p> 065 * <pre> 066 * <module name="SummaryJavadocCheck"/> 067 * </pre> 068 * <p> 069 * Example of {@code {@inheritDoc}} without summary. 070 * </p> 071 * <pre> 072 * public class Test extends Exception { 073 * //Valid 074 * /** 075 * * {@inheritDoc} 076 * */ 077 * public String ValidFunction(){ 078 * return ""; 079 * } 080 * //Violation 081 * /** 082 * * 083 * */ 084 * public String InvalidFunction(){ 085 * return ""; 086 * } 087 * } 088 * </pre> 089 * <p> 090 * To ensure that summary do not contain phrase like "This method returns", 091 * use following config: 092 * </p> 093 * <pre> 094 * <module name="SummaryJavadocCheck"> 095 * <property name="forbiddenSummaryFragments" 096 * value="^This method returns.*"/> 097 * </module> 098 * </pre> 099 * <p> 100 * To specify period symbol at the end of first javadoc sentence: 101 * </p> 102 * <pre> 103 * <module name="SummaryJavadocCheck"> 104 * <property name="period" value="。"/> 105 * </module> 106 * </pre> 107 * <p> 108 * Example of period property. 109 * </p> 110 * <pre> 111 * public class TestClass { 112 * /** 113 * * This is invalid java doc. 114 * */ 115 * void invalidJavaDocMethod() { 116 * } 117 * /** 118 * * This is valid java doc。 119 * */ 120 * void validJavaDocMethod() { 121 * } 122 * } 123 * </pre> 124 * <p> 125 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker} 126 * </p> 127 * <p> 128 * Violation Message Keys: 129 * </p> 130 * <ul> 131 * <li> 132 * {@code javadoc.missed.html.close} 133 * </li> 134 * <li> 135 * {@code javadoc.parse.rule.error} 136 * </li> 137 * <li> 138 * {@code javadoc.wrong.singleton.html.tag} 139 * </li> 140 * <li> 141 * {@code summary.first.sentence} 142 * </li> 143 * <li> 144 * {@code summary.javaDoc} 145 * </li> 146 * <li> 147 * {@code summary.javaDoc.missing} 148 * </li> 149 * </ul> 150 * 151 * @since 6.0 152 */ 153@StatelessCheck 154public class SummaryJavadocCheck extends AbstractJavadocCheck { 155 156 /** 157 * A key is pointing to the warning message text in "messages.properties" 158 * file. 159 */ 160 public static final String MSG_SUMMARY_FIRST_SENTENCE = "summary.first.sentence"; 161 162 /** 163 * A key is pointing to the warning message text in "messages.properties" 164 * file. 165 */ 166 public static final String MSG_SUMMARY_JAVADOC = "summary.javaDoc"; 167 /** 168 * A key is pointing to the warning message text in "messages.properties" 169 * file. 170 */ 171 public static final String MSG_SUMMARY_JAVADOC_MISSING = "summary.javaDoc.missing"; 172 /** 173 * This regexp is used to convert multiline javadoc to single line without stars. 174 */ 175 private static final Pattern JAVADOC_MULTILINE_TO_SINGLELINE_PATTERN = 176 Pattern.compile("\n[ ]+(\\*)|^[ ]+(\\*)"); 177 178 /** Period literal. */ 179 private static final String PERIOD = "."; 180 181 /** Set of allowed Tokens tags in summary java doc. */ 182 private static final Set<Integer> ALLOWED_TYPES = Collections.unmodifiableSet( 183 new HashSet<>(Arrays.asList(JavadocTokenTypes.TEXT, 184 JavadocTokenTypes.WS)) 185 ); 186 187 /** Specify the regexp for forbidden summary fragments. */ 188 private Pattern forbiddenSummaryFragments = CommonUtil.createPattern("^$"); 189 190 /** Specify the period symbol at the end of first javadoc sentence. */ 191 private String period = PERIOD; 192 193 /** 194 * Setter to specify the regexp for forbidden summary fragments. 195 * 196 * @param pattern a pattern. 197 */ 198 public void setForbiddenSummaryFragments(Pattern pattern) { 199 forbiddenSummaryFragments = pattern; 200 } 201 202 /** 203 * Setter to specify the period symbol at the end of first javadoc sentence. 204 * 205 * @param period period's value. 206 */ 207 public void setPeriod(String period) { 208 this.period = period; 209 } 210 211 @Override 212 public int[] getDefaultJavadocTokens() { 213 return new int[] { 214 JavadocTokenTypes.JAVADOC, 215 }; 216 } 217 218 @Override 219 public int[] getRequiredJavadocTokens() { 220 return getAcceptableJavadocTokens(); 221 } 222 223 @Override 224 public void visitJavadocToken(DetailNode ast) { 225 if (!startsWithInheritDoc(ast)) { 226 final String summaryDoc = getSummarySentence(ast); 227 if (summaryDoc.isEmpty()) { 228 log(ast.getLineNumber(), MSG_SUMMARY_JAVADOC_MISSING); 229 } 230 else if (!period.isEmpty()) { 231 final String firstSentence = getFirstSentence(ast); 232 final int endOfSentence = firstSentence.lastIndexOf(period); 233 if (!summaryDoc.contains(period)) { 234 log(ast.getLineNumber(), MSG_SUMMARY_FIRST_SENTENCE); 235 } 236 if (endOfSentence != -1 237 && containsForbiddenFragment(firstSentence.substring(0, endOfSentence))) { 238 log(ast.getLineNumber(), MSG_SUMMARY_JAVADOC); 239 } 240 } 241 } 242 } 243 244 /** 245 * Checks if the node starts with an {@inheritDoc}. 246 * 247 * @param root The root node to examine. 248 * @return {@code true} if the javadoc starts with an {@inheritDoc}. 249 */ 250 private static boolean startsWithInheritDoc(DetailNode root) { 251 boolean found = false; 252 final DetailNode[] children = root.getChildren(); 253 254 for (int i = 0; !found; i++) { 255 final DetailNode child = children[i]; 256 if (child.getType() == JavadocTokenTypes.JAVADOC_INLINE_TAG 257 && child.getChildren()[1].getType() == JavadocTokenTypes.INHERIT_DOC_LITERAL) { 258 found = true; 259 } 260 else if (child.getType() != JavadocTokenTypes.LEADING_ASTERISK 261 && !CommonUtil.isBlank(child.getText())) { 262 break; 263 } 264 } 265 266 return found; 267 } 268 269 /** 270 * Checks if period is at the end of sentence. 271 * 272 * @param ast Javadoc root node. 273 * @return violation string 274 */ 275 private static String getSummarySentence(DetailNode ast) { 276 boolean flag = true; 277 final StringBuilder result = new StringBuilder(256); 278 for (DetailNode child : ast.getChildren()) { 279 if (ALLOWED_TYPES.contains(child.getType())) { 280 result.append(child.getText()); 281 } 282 else if (child.getType() == JavadocTokenTypes.HTML_ELEMENT 283 && CommonUtil.isBlank(result.toString().trim())) { 284 result.append(getStringInsideTag(result.toString(), 285 child.getChildren()[0].getChildren()[0])); 286 } 287 else if (child.getType() == JavadocTokenTypes.JAVADOC_TAG) { 288 flag = false; 289 } 290 if (!flag) { 291 break; 292 } 293 } 294 return result.toString().trim(); 295 } 296 297 /** 298 * Concatenates string within text of html tags. 299 * 300 * @param result javadoc string 301 * @param detailNode javadoc tag node 302 * @return java doc tag content appended in result 303 */ 304 private static String getStringInsideTag(String result, DetailNode detailNode) { 305 final StringBuilder contents = new StringBuilder(result); 306 DetailNode tempNode = detailNode; 307 while (tempNode != null) { 308 if (tempNode.getType() == JavadocTokenTypes.TEXT) { 309 contents.append(tempNode.getText()); 310 } 311 tempNode = JavadocUtil.getNextSibling(tempNode); 312 } 313 return contents.toString(); 314 } 315 316 /** 317 * Finds and returns first sentence. 318 * 319 * @param ast Javadoc root node. 320 * @return first sentence. 321 */ 322 private static String getFirstSentence(DetailNode ast) { 323 final StringBuilder result = new StringBuilder(256); 324 final String periodSuffix = PERIOD + ' '; 325 for (DetailNode child : ast.getChildren()) { 326 final String text; 327 if (child.getChildren().length == 0) { 328 text = child.getText(); 329 } 330 else { 331 text = getFirstSentence(child); 332 } 333 334 if (text.contains(periodSuffix)) { 335 result.append(text, 0, text.indexOf(periodSuffix) + 1); 336 break; 337 } 338 339 result.append(text); 340 } 341 return result.toString(); 342 } 343 344 /** 345 * Tests if first sentence contains forbidden summary fragment. 346 * 347 * @param firstSentence String with first sentence. 348 * @return true, if first sentence contains forbidden summary fragment. 349 */ 350 private boolean containsForbiddenFragment(String firstSentence) { 351 final String javadocText = JAVADOC_MULTILINE_TO_SINGLELINE_PATTERN 352 .matcher(firstSentence).replaceAll(" ").trim(); 353 return forbiddenSummaryFragments.matcher(trimExcessWhitespaces(javadocText)).find(); 354 } 355 356 /** 357 * Trims the given {@code text} of duplicate whitespaces. 358 * 359 * @param text The text to transform. 360 * @return The finalized form of the text. 361 */ 362 private static String trimExcessWhitespaces(String text) { 363 final StringBuilder result = new StringBuilder(100); 364 boolean previousWhitespace = true; 365 366 for (char letter : text.toCharArray()) { 367 final char print; 368 if (Character.isWhitespace(letter)) { 369 if (previousWhitespace) { 370 continue; 371 } 372 373 previousWhitespace = true; 374 print = ' '; 375 } 376 else { 377 previousWhitespace = false; 378 print = letter; 379 } 380 381 result.append(print); 382 } 383 384 return result.toString(); 385 } 386 387}