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