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