1   ////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code for adherence to a set of rules.
3   // Copyright (C) 2001-2019 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.puppycrawl.tools.checkstyle.StatelessCheck;
29  import com.puppycrawl.tools.checkstyle.api.DetailNode;
30  import com.puppycrawl.tools.checkstyle.api.JavadocTokenTypes;
31  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
32  import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
33  
34  /**
35   * <p>
36   * Checks that <a href=
37   * "https://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   */
66  @StatelessCheck
67  public class SummaryJavadocCheck extends AbstractJavadocCheck {
68  
69      /**
70       * A key is pointing to the warning message text in "messages.properties"
71       * file.
72       */
73      public static final String MSG_SUMMARY_FIRST_SENTENCE = "summary.first.sentence";
74  
75      /**
76       * A key is pointing to the warning message text in "messages.properties"
77       * file.
78       */
79      public static final String MSG_SUMMARY_JAVADOC = "summary.javaDoc";
80      /**
81       * A key is pointing to the warning message text in "messages.properties"
82       * file.
83       */
84      public static final String MSG_SUMMARY_JAVADOC_MISSING = "summary.javaDoc.missing";
85      /**
86       * This regexp is used to convert multiline javadoc to single line without stars.
87       */
88      private static final Pattern JAVADOC_MULTILINE_TO_SINGLELINE_PATTERN =
89              Pattern.compile("\n[ ]+(\\*)|^[ ]+(\\*)");
90  
91      /** Period literal. */
92      private static final String PERIOD = ".";
93  
94      /** Set of allowed Tokens tags in summary java doc. */
95      private static final Set<Integer> ALLOWED_TYPES = Collections.unmodifiableSet(
96              new HashSet<>(Arrays.asList(JavadocTokenTypes.TEXT,
97                      JavadocTokenTypes.WS))
98      );
99  
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 {&#64;inheritDoc}.
157      * @param root The root node to examine.
158      * @return {@code true} if the javadoc starts with an {&#64;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 error 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             else {
246                 result.append(text);
247             }
248         }
249         return result.toString();
250     }
251 
252     /**
253      * Tests if first sentence contains forbidden summary fragment.
254      * @param firstSentence String with first sentence.
255      * @return true, if first sentence contains forbidden summary fragment.
256      */
257     private boolean containsForbiddenFragment(String firstSentence) {
258         final String javadocText = JAVADOC_MULTILINE_TO_SINGLELINE_PATTERN
259                 .matcher(firstSentence).replaceAll(" ").trim();
260         return forbiddenSummaryFragments.matcher(trimExcessWhitespaces(javadocText)).find();
261     }
262 
263     /**
264      * Trims the given {@code text} of duplicate whitespaces.
265      * @param text The text to transform.
266      * @return The finalized form of the text.
267      */
268     private static String trimExcessWhitespaces(String text) {
269         final StringBuilder result = new StringBuilder(100);
270         boolean previousWhitespace = true;
271 
272         for (char letter : text.toCharArray()) {
273             final char print;
274             if (Character.isWhitespace(letter)) {
275                 if (previousWhitespace) {
276                     continue;
277                 }
278 
279                 previousWhitespace = true;
280                 print = ' ';
281             }
282             else {
283                 previousWhitespace = false;
284                 print = letter;
285             }
286 
287             result.append(print);
288         }
289 
290         return result.toString();
291     }
292 
293 }