001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2017 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.CommonUtils;
032import com.puppycrawl.tools.checkstyle.utils.JavadocUtils;
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 * @author max
066 * @author <a href="mailto:nesterenko-aleksey@list.ru">Aleksey Nesterenko</a>
067 */
068public class SummaryJavadocCheck extends AbstractJavadocCheck {
069
070    /**
071     * A key is pointing to the warning message text in "messages.properties"
072     * file.
073     */
074    public static final String MSG_SUMMARY_FIRST_SENTENCE = "summary.first.sentence";
075
076    /**
077     * A key is pointing to the warning message text in "messages.properties"
078     * file.
079     */
080    public static final String MSG_SUMMARY_JAVADOC = "summary.javaDoc";
081    /**
082     * A key is pointing to the warning message text in "messages.properties"
083     * file.
084     */
085    public static final String MSG_SUMMARY_JAVADOC_MISSING = "summary.javaDoc.missing";
086    /**
087     * This regexp is used to convert multiline javadoc to single line without stars.
088     */
089    private static final Pattern JAVADOC_MULTILINE_TO_SINGLELINE_PATTERN =
090            Pattern.compile("\n[ ]+(\\*)|^[ ]+(\\*)");
091
092    /** Period literal. */
093    private static final String PERIOD = ".";
094
095    /** Set of allowed Tokens tags in summary java doc. */
096    private static final Set<Integer> ALLOWED_TYPES = Collections.unmodifiableSet(
097            new HashSet<>(Arrays.asList(JavadocTokenTypes.TEXT,
098                    JavadocTokenTypes.WS))
099    );
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}