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.HashSet;
024import java.util.Set;
025import java.util.regex.Pattern;
026
027import com.google.common.base.CharMatcher;
028import com.puppycrawl.tools.checkstyle.api.DetailNode;
029import com.puppycrawl.tools.checkstyle.api.JavadocTokenTypes;
030import com.puppycrawl.tools.checkstyle.api.TokenTypes;
031import com.puppycrawl.tools.checkstyle.utils.CommonUtils;
032
033/**
034 * <p>
035 * Checks that <a href=
036 * "http://www.oracle.com/technetwork/java/javase/documentation/index-137868.html#firstsentence">
037 * Javadoc summary sentence</a> does not contain phrases that are not recommended to use.
038 * By default Check validate that first sentence is not empty:</p><br>
039 * <pre>
040 * &lt;module name=&quot;SummaryJavadocCheck&quot;/&gt;
041 * </pre>
042 *
043 * <p>To ensure that summary do not contain phrase like "This method returns",
044 *  use following config:
045 *
046 * <pre>
047 * &lt;module name=&quot;SummaryJavadocCheck&quot;&gt;
048 *     &lt;property name=&quot;forbiddenSummaryFragments&quot;
049 *     value=&quot;^This method returns.*&quot;/&gt;
050 * &lt;/module&gt;
051 * </pre>
052 * <p>
053 * To specify period symbol at the end of first javadoc sentence - use following config:
054 * </p>
055 * <pre>
056 * &lt;module name=&quot;SummaryJavadocCheck&quot;&gt;
057 *     &lt;property name=&quot;period&quot;
058 *     value=&quot;period&quot;/&gt;
059 * &lt;/module&gt;
060 * </pre>
061 *
062 *
063 * @author max
064 * @author <a href="mailto:nesterenko-aleksey@list.ru">Aleksey Nesterenko</a>
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     * This regexp is used to convert multiline javadoc to single line without stars.
081     */
082    private static final Pattern JAVADOC_MULTILINE_TO_SINGLELINE_PATTERN =
083            Pattern.compile("\n[ ]+(\\*)|^[ ]+(\\*)");
084
085    /** Period literal. */
086    private static final String PERIOD = ".";
087
088    /**
089     * Stores allowed values in document for inherit doc literal.
090     */
091    private static final Set<Integer> SKIP_TOKENS = new HashSet<>(
092        Arrays.asList(JavadocTokenTypes.NEWLINE,
093                       JavadocTokenTypes.LEADING_ASTERISK,
094                       JavadocTokenTypes.EOF)
095    );
096    /**
097     * Regular expression for forbidden summary fragments.
098     */
099    private Pattern forbiddenSummaryFragments = CommonUtils.createPattern("^$");
100
101    /**
102     * Period symbol at the end of first javadoc sentence.
103     */
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 int[] getAcceptableTokens() {
136        return new int[] {TokenTypes.BLOCK_COMMENT_BEGIN };
137    }
138
139    @Override
140    public int[] getRequiredTokens() {
141        return getAcceptableTokens();
142    }
143
144    @Override
145    public void visitJavadocToken(DetailNode ast) {
146        String firstSentence = getFirstSentence(ast);
147        final int endOfSentence = firstSentence.lastIndexOf(period);
148        if (endOfSentence == -1) {
149            if (!isOnlyInheritDoc(ast)) {
150                log(ast.getLineNumber(), MSG_SUMMARY_FIRST_SENTENCE);
151            }
152        }
153        else {
154            firstSentence = firstSentence.substring(0, endOfSentence);
155            if (containsForbiddenFragment(firstSentence)) {
156                log(ast.getLineNumber(), MSG_SUMMARY_JAVADOC);
157            }
158        }
159    }
160
161    /**
162     * Finds if inheritDoc is placed properly in java doc.
163     * @param ast Javadoc root node.
164     * @return true if inheritDoc is valid or false.
165     */
166    private static boolean isOnlyInheritDoc(DetailNode ast) {
167        boolean extraTextFound = false;
168        boolean containsInheritDoc = false;
169        for (DetailNode child : ast.getChildren()) {
170            if (child.getType() == JavadocTokenTypes.TEXT) {
171                if (!child.getText().trim().isEmpty()) {
172                    extraTextFound = true;
173                }
174            }
175            else if (child.getType() == JavadocTokenTypes.JAVADOC_INLINE_TAG) {
176                if (child.getChildren()[1].getType() == JavadocTokenTypes.INHERIT_DOC_LITERAL) {
177                    containsInheritDoc = true;
178                }
179                else {
180                    extraTextFound = true;
181                }
182            }
183            else if (!SKIP_TOKENS.contains(child.getType())) {
184                extraTextFound = true;
185            }
186            if (extraTextFound) {
187                break;
188            }
189        }
190        return containsInheritDoc && !extraTextFound;
191    }
192
193    /**
194     * Finds and returns first sentence.
195     * @param ast Javadoc root node.
196     * @return first sentence.
197     */
198    private static String getFirstSentence(DetailNode ast) {
199        final StringBuilder result = new StringBuilder();
200        final String periodSuffix = PERIOD + ' ';
201        for (DetailNode child : ast.getChildren()) {
202            final String text = child.getText();
203
204            if (child.getType() != JavadocTokenTypes.JAVADOC_INLINE_TAG
205                && text.contains(periodSuffix)) {
206                result.append(text.substring(0, text.indexOf(periodSuffix) + 1));
207                break;
208            }
209            else {
210                result.append(text);
211            }
212        }
213        return result.toString();
214    }
215
216    /**
217     * Tests if first sentence contains forbidden summary fragment.
218     * @param firstSentence String with first sentence.
219     * @return true, if first sentence contains forbidden summary fragment.
220     */
221    private boolean containsForbiddenFragment(String firstSentence) {
222        String javadocText = JAVADOC_MULTILINE_TO_SINGLELINE_PATTERN
223                .matcher(firstSentence).replaceAll(" ");
224        javadocText = CharMatcher.WHITESPACE.trimAndCollapseFrom(javadocText, ' ');
225        return forbiddenSummaryFragments.matcher(javadocText).find();
226    }
227}