View Javadoc
1   ////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code for adherence to a set of rules.
3   // Copyright (C) 2001-2017 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ////////////////////////////////////////////////////////////////////////////////
19  
20  package com.puppycrawl.tools.checkstyle.checks.javadoc;
21  
22  import java.util.Arrays;
23  import java.util.Collections;
24  import java.util.HashSet;
25  import java.util.Set;
26  import java.util.regex.Pattern;
27  
28  import com.google.common.base.CharMatcher;
29  import com.puppycrawl.tools.checkstyle.api.DetailNode;
30  import com.puppycrawl.tools.checkstyle.api.JavadocTokenTypes;
31  import com.puppycrawl.tools.checkstyle.utils.CommonUtils;
32  import com.puppycrawl.tools.checkstyle.utils.JavadocUtils;
33  
34  /**
35   * <p>
36   * Checks that <a href=
37   * "http://www.oracle.com/technetwork/java/javase/documentation/index-137868.html#firstsentence">
38   * Javadoc summary sentence</a> does not contain phrases that are not recommended to use.
39   * Check also violate javadoc that does not contain first sentence.
40   * By default Check validate that first sentence is not empty:</p><br>
41   * <pre>
42   * &lt;module name=&quot;SummaryJavadocCheck&quot;/&gt;
43   * </pre>
44   *
45   * <p>To ensure that summary do not contain phrase like "This method returns",
46   *  use following config:
47   *
48   * <pre>
49   * &lt;module name=&quot;SummaryJavadocCheck&quot;&gt;
50   *     &lt;property name=&quot;forbiddenSummaryFragments&quot;
51   *     value=&quot;^This method returns.*&quot;/&gt;
52   * &lt;/module&gt;
53   * </pre>
54   * <p>
55   * To specify period symbol at the end of first javadoc sentence - use following config:
56   * </p>
57   * <pre>
58   * &lt;module name=&quot;SummaryJavadocCheck&quot;&gt;
59   *     &lt;property name=&quot;period&quot;
60   *     value=&quot;period&quot;/&gt;
61   * &lt;/module&gt;
62   * </pre>
63   *
64   *
65   * @author max
66   * @author <a href="mailto:nesterenko-aleksey@list.ru">Aleksey Nesterenko</a>
67   */
68  public class SummaryJavadocCheck extends AbstractJavadocCheck {
69  
70      /**
71       * A key is pointing to the warning message text in "messages.properties"
72       * file.
73       */
74      public static final String MSG_SUMMARY_FIRST_SENTENCE = "summary.first.sentence";
75  
76      /**
77       * A key is pointing to the warning message text in "messages.properties"
78       * file.
79       */
80      public static final String MSG_SUMMARY_JAVADOC = "summary.javaDoc";
81      /**
82       * A key is pointing to the warning message text in "messages.properties"
83       * file.
84       */
85      public static final String MSG_SUMMARY_JAVADOC_MISSING = "summary.javaDoc.missing";
86      /**
87       * This regexp is used to convert multiline javadoc to single line without stars.
88       */
89      private static final Pattern JAVADOC_MULTILINE_TO_SINGLELINE_PATTERN =
90              Pattern.compile("\n[ ]+(\\*)|^[ ]+(\\*)");
91  
92      /** Period literal. */
93      private static final String PERIOD = ".";
94  
95      /** Set of allowed Tokens tags in summary java doc. */
96      private static final Set<Integer> ALLOWED_TYPES = Collections.unmodifiableSet(
97              new HashSet<>(Arrays.asList(JavadocTokenTypes.TEXT,
98                      JavadocTokenTypes.WS))
99      );
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 {&#64;inheritDoc}.
158      * @param root The root node to examine.
159      * @return {@code true} if the javadoc starts with an {&#64;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 }