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