001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2018 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.google.common.base.CharMatcher; 029import com.puppycrawl.tools.checkstyle.api.DetailNode; 030import com.puppycrawl.tools.checkstyle.api.JavadocTokenTypes; 031import com.puppycrawl.tools.checkstyle.utils.CommonUtils; 032import com.puppycrawl.tools.checkstyle.utils.JavadocUtils; 033 034/** 035 * <p> 036 * Checks that <a href= 037 * "http://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 * @author max 066 * @author <a href="mailto:nesterenko-aleksey@list.ru">Aleksey Nesterenko</a> 067 */ 068public class SummaryJavadocCheck extends AbstractJavadocCheck { 069 070 /** 071 * A key is pointing to the warning message text in "messages.properties" 072 * file. 073 */ 074 public static final String MSG_SUMMARY_FIRST_SENTENCE = "summary.first.sentence"; 075 076 /** 077 * A key is pointing to the warning message text in "messages.properties" 078 * file. 079 */ 080 public static final String MSG_SUMMARY_JAVADOC = "summary.javaDoc"; 081 /** 082 * A key is pointing to the warning message text in "messages.properties" 083 * file. 084 */ 085 public static final String MSG_SUMMARY_JAVADOC_MISSING = "summary.javaDoc.missing"; 086 /** 087 * This regexp is used to convert multiline javadoc to single line without stars. 088 */ 089 private static final Pattern JAVADOC_MULTILINE_TO_SINGLELINE_PATTERN = 090 Pattern.compile("\n[ ]+(\\*)|^[ ]+(\\*)"); 091 092 /** Period literal. */ 093 private static final String PERIOD = "."; 094 095 /** Set of allowed Tokens tags in summary java doc. */ 096 private static final Set<Integer> ALLOWED_TYPES = Collections.unmodifiableSet( 097 new HashSet<>(Arrays.asList(JavadocTokenTypes.TEXT, 098 JavadocTokenTypes.WS)) 099 ); 100 101 /** Regular expression for forbidden summary fragments. */ 102 private Pattern forbiddenSummaryFragments = CommonUtils.createPattern("^$"); 103 104 /** Period symbol at the end of first javadoc sentence. */ 105 private String period = PERIOD; 106 107 /** 108 * Sets custom value of regular expression for forbidden summary fragments. 109 * @param pattern a pattern. 110 */ 111 public void setForbiddenSummaryFragments(Pattern pattern) { 112 forbiddenSummaryFragments = pattern; 113 } 114 115 /** 116 * Sets value of period symbol at the end of first javadoc sentence. 117 * @param period period's value. 118 */ 119 public void setPeriod(String period) { 120 this.period = period; 121 } 122 123 @Override 124 public int[] getDefaultJavadocTokens() { 125 return new int[] { 126 JavadocTokenTypes.JAVADOC, 127 }; 128 } 129 130 @Override 131 public int[] getRequiredJavadocTokens() { 132 return getAcceptableJavadocTokens(); 133 } 134 135 @Override 136 public void visitJavadocToken(DetailNode ast) { 137 if (!startsWithInheritDoc(ast)) { 138 final String summaryDoc = getSummarySentence(ast); 139 if (summaryDoc.isEmpty()) { 140 log(ast.getLineNumber(), MSG_SUMMARY_JAVADOC_MISSING); 141 } 142 else if (!period.isEmpty()) { 143 final String firstSentence = getFirstSentence(ast); 144 final int endOfSentence = firstSentence.lastIndexOf(period); 145 if (!summaryDoc.contains(period)) { 146 log(ast.getLineNumber(), MSG_SUMMARY_FIRST_SENTENCE); 147 } 148 if (endOfSentence != -1 149 && containsForbiddenFragment(firstSentence.substring(0, endOfSentence))) { 150 log(ast.getLineNumber(), MSG_SUMMARY_JAVADOC); 151 } 152 } 153 } 154 } 155 156 /** 157 * Checks if the node starts with an {@inheritDoc}. 158 * @param root The root node to examine. 159 * @return {@code true} if the javadoc starts with an {@inheritDoc}. 160 */ 161 private static boolean startsWithInheritDoc(DetailNode root) { 162 boolean found = false; 163 final DetailNode[] children = root.getChildren(); 164 165 for (int i = 0; !found && i < children.length - 1; i++) { 166 final DetailNode child = children[i]; 167 if (child.getType() == JavadocTokenTypes.JAVADOC_INLINE_TAG 168 && child.getChildren()[1].getType() == JavadocTokenTypes.INHERIT_DOC_LITERAL) { 169 found = true; 170 } 171 else if (child.getType() != JavadocTokenTypes.LEADING_ASTERISK 172 && !CommonUtils.isBlank(child.getText())) { 173 break; 174 } 175 } 176 177 return found; 178 } 179 180 /** 181 * Checks if period is at the end of sentence. 182 * @param ast Javadoc root node. 183 * @return error string 184 */ 185 private static String getSummarySentence(DetailNode ast) { 186 boolean flag = true; 187 final StringBuilder result = new StringBuilder(256); 188 for (DetailNode child : ast.getChildren()) { 189 if (ALLOWED_TYPES.contains(child.getType())) { 190 result.append(child.getText()); 191 } 192 else if (child.getType() == JavadocTokenTypes.HTML_ELEMENT 193 && CommonUtils.isBlank(result.toString().trim())) { 194 result.append(getStringInsideTag(result.toString(), 195 child.getChildren()[0].getChildren()[0])); 196 } 197 else if (child.getType() == JavadocTokenTypes.JAVADOC_TAG) { 198 flag = false; 199 } 200 if (!flag) { 201 break; 202 } 203 } 204 return result.toString().trim(); 205 } 206 207 /** 208 * Concatenates string within text of html tags. 209 * @param result javadoc string 210 * @param detailNode javadoc tag node 211 * @return java doc tag content appended in result 212 */ 213 private static String getStringInsideTag(String result, DetailNode detailNode) { 214 final StringBuilder contents = new StringBuilder(result); 215 DetailNode tempNode = detailNode; 216 while (tempNode != null) { 217 if (tempNode.getType() == JavadocTokenTypes.TEXT) { 218 contents.append(tempNode.getText()); 219 } 220 tempNode = JavadocUtils.getNextSibling(tempNode); 221 } 222 return contents.toString(); 223 } 224 225 /** 226 * Finds and returns first sentence. 227 * @param ast Javadoc root node. 228 * @return first sentence. 229 */ 230 private static String getFirstSentence(DetailNode ast) { 231 final StringBuilder result = new StringBuilder(256); 232 final String periodSuffix = PERIOD + ' '; 233 for (DetailNode child : ast.getChildren()) { 234 final String text; 235 if (child.getChildren().length == 0) { 236 text = child.getText(); 237 } 238 else { 239 text = getFirstSentence(child); 240 } 241 242 if (child.getType() != JavadocTokenTypes.JAVADOC_INLINE_TAG 243 && text.contains(periodSuffix)) { 244 result.append(text.substring(0, text.indexOf(periodSuffix) + 1)); 245 break; 246 } 247 else { 248 result.append(text); 249 } 250 } 251 return result.toString(); 252 } 253 254 /** 255 * Tests if first sentence contains forbidden summary fragment. 256 * @param firstSentence String with first sentence. 257 * @return true, if first sentence contains forbidden summary fragment. 258 */ 259 private boolean containsForbiddenFragment(String firstSentence) { 260 String javadocText = JAVADOC_MULTILINE_TO_SINGLELINE_PATTERN 261 .matcher(firstSentence).replaceAll(" "); 262 javadocText = CharMatcher.whitespace().trimAndCollapseFrom(javadocText, ' '); 263 return forbiddenSummaryFragments.matcher(javadocText).find(); 264 } 265 266}