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