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