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