001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2014  Oliver Burn
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////////////////////////////////////////////////////////////////////////////////
019package com.puppycrawl.tools.checkstyle.checks.javadoc;
020
021import com.google.common.collect.ImmutableSortedSet;
022import com.puppycrawl.tools.checkstyle.api.Check;
023import com.puppycrawl.tools.checkstyle.api.DetailAST;
024import com.puppycrawl.tools.checkstyle.api.FastStack;
025import com.puppycrawl.tools.checkstyle.api.FileContents;
026import com.puppycrawl.tools.checkstyle.api.JavadocTagInfo;
027import com.puppycrawl.tools.checkstyle.api.Scope;
028import com.puppycrawl.tools.checkstyle.api.ScopeUtils;
029import com.puppycrawl.tools.checkstyle.api.TextBlock;
030import com.puppycrawl.tools.checkstyle.api.TokenTypes;
031import com.puppycrawl.tools.checkstyle.checks.CheckUtils;
032import java.util.List;
033import java.util.Set;
034import java.util.regex.Pattern;
035
036/**
037 * Custom Checkstyle Check to validate Javadoc.
038 *
039 * @author Chris Stillwell
040 * @author Daniel Grenner
041 * @author Travis Schneeberger
042 * @version 1.2
043 */
044public class JavadocStyleCheck
045    extends Check
046{
047    /** Message property key for the Unclosed HTML message. */
048    private static final String UNCLOSED_HTML = "javadoc.unclosedhtml";
049
050    /** Message property key for the Extra HTML message. */
051    private static final String EXTRA_HTML = "javadoc.extrahtml";
052
053    /** HTML tags that do not require a close tag. */
054    private static final Set<String> SINGLE_TAGS = ImmutableSortedSet.of(
055            "br", "li", "dt", "dd", "hr", "img", "p", "td", "tr", "th");
056
057    /** HTML tags that are allowed in java docs.
058     * From http://www.w3schools.com/tags/default.asp
059     * The froms and structure tags are not allowed
060     */
061    private static final Set<String> ALLOWED_TAGS = ImmutableSortedSet.of(
062            "a", "abbr", "acronym", "address", "area", "b", "bdo", "big",
063            "blockquote", "br", "caption", "cite", "code", "colgroup", "dd",
064            "del", "div", "dfn", "dl", "dt", "em", "fieldset", "font", "h1",
065            "h2", "h3", "h4", "h5", "h6", "hr", "i", "img", "ins", "kbd",
066            "li", "ol", "p", "pre", "q", "samp", "small", "span", "strong",
067            "style", "sub", "sup", "table", "tbody", "td", "tfoot", "th",
068            "thead", "tr", "tt", "u", "ul");
069
070    /** The scope to check. */
071    private Scope mScope = Scope.PRIVATE;
072
073    /** the visibility scope where Javadoc comments shouldn't be checked **/
074    private Scope mExcludeScope;
075
076    /** Format for matching the end of a sentence. */
077    private String mEndOfSentenceFormat = "([.?!][ \t\n\r\f<])|([.?!]$)";
078
079    /** Regular expression for matching the end of a sentence. */
080    private Pattern mEndOfSentencePattern;
081
082    /**
083     * Indicates if the first sentence should be checked for proper end of
084     * sentence punctuation.
085     */
086    private boolean mCheckFirstSentence = true;
087
088    /**
089     * Indicates if the HTML within the comment should be checked.
090     */
091    private boolean mCheckHtml = true;
092
093    /**
094     * Indicates if empty javadoc statements should be checked.
095     */
096    private boolean mCheckEmptyJavadoc;
097
098    @Override
099    public int[] getDefaultTokens()
100    {
101        return new int[] {
102            TokenTypes.INTERFACE_DEF,
103            TokenTypes.CLASS_DEF,
104            TokenTypes.ANNOTATION_DEF,
105            TokenTypes.ENUM_DEF,
106            TokenTypes.METHOD_DEF,
107            TokenTypes.CTOR_DEF,
108            TokenTypes.VARIABLE_DEF,
109            TokenTypes.ENUM_CONSTANT_DEF,
110            TokenTypes.ANNOTATION_FIELD_DEF,
111            TokenTypes.PACKAGE_DEF,
112        };
113    }
114
115    @Override
116    public void visitToken(DetailAST aAST)
117    {
118        if (shouldCheck(aAST)) {
119            final FileContents contents = getFileContents();
120            // Need to start searching for the comment before the annotations
121            // that may exist. Even if annotations are not defined on the
122            // package, the ANNOTATIONS AST is defined.
123            final TextBlock cmt =
124                contents.getJavadocBefore(aAST.getFirstChild().getLineNo());
125
126            checkComment(aAST, cmt);
127        }
128    }
129
130    /**
131     * Whether we should check this node.
132     * @param aAST a given node.
133     * @return whether we should check a given node.
134     */
135    private boolean shouldCheck(final DetailAST aAST)
136    {
137        if (aAST.getType() == TokenTypes.PACKAGE_DEF) {
138            return getFileContents().inPackageInfo();
139        }
140
141        if (ScopeUtils.inCodeBlock(aAST)) {
142            return false;
143        }
144
145        final Scope declaredScope;
146        if (aAST.getType() == TokenTypes.ENUM_CONSTANT_DEF) {
147            declaredScope = Scope.PUBLIC;
148        }
149        else {
150            declaredScope = ScopeUtils.getScopeFromMods(
151                aAST.findFirstToken(TokenTypes.MODIFIERS));
152        }
153
154        final Scope scope =
155            ScopeUtils.inInterfaceOrAnnotationBlock(aAST)
156            ? Scope.PUBLIC : declaredScope;
157        final Scope surroundingScope = ScopeUtils.getSurroundingScope(aAST);
158
159        return scope.isIn(mScope)
160            && ((surroundingScope == null) || surroundingScope.isIn(mScope))
161            && ((mExcludeScope == null)
162                || !scope.isIn(mExcludeScope)
163                || ((surroundingScope != null)
164                && !surroundingScope.isIn(mExcludeScope)));
165    }
166
167    /**
168     * Performs the various checks agains the Javadoc comment.
169     *
170     * @param aAST the AST of the element being documented
171     * @param aComment the source lines that make up the Javadoc comment.
172     *
173     * @see #checkFirstSentence(DetailAST, TextBlock)
174     * @see #checkHtml(DetailAST, TextBlock)
175     */
176    private void checkComment(final DetailAST aAST, final TextBlock aComment)
177    {
178        if (aComment == null) {
179            /*checking for missing docs in JavadocStyleCheck is not consistent
180            with the rest of CheckStyle...  Even though, I didn't think it
181            made sense to make another csheck just to ensure that the
182            package-info.java file actually contains package Javadocs.*/
183            if (getFileContents().inPackageInfo()) {
184                log(aAST.getLineNo(), "javadoc.missing");
185            }
186            return;
187        }
188
189        if (mCheckFirstSentence) {
190            checkFirstSentence(aAST, aComment);
191        }
192
193        if (mCheckHtml) {
194            checkHtml(aAST, aComment);
195        }
196
197        if (mCheckEmptyJavadoc) {
198            checkEmptyJavadoc(aComment);
199        }
200    }
201
202    /**
203     * Checks that the first sentence ends with proper punctuation.  This method
204     * uses a regular expression that checks for the presence of a period,
205     * question mark, or exclamation mark followed either by whitespace, an
206     * HTML element, or the end of string. This method ignores {_AT_inheritDoc}
207     * comments for TokenTypes that are valid for {_AT_inheritDoc}.
208     *
209     * @param aAST the current node
210     * @param aComment the source lines that make up the Javadoc comment.
211     */
212    private void checkFirstSentence(final DetailAST aAST, TextBlock aComment)
213    {
214        final String commentText = getCommentText(aComment.getText());
215
216        if ((commentText.length() != 0)
217            && !getEndOfSentencePattern().matcher(commentText).find()
218            && !("{@inheritDoc}".equals(commentText)
219            && JavadocTagInfo.INHERIT_DOC.isValidOn(aAST)))
220        {
221            log(aComment.getStartLineNo(), "javadoc.noperiod");
222        }
223    }
224
225    /**
226     * Checks that the Javadoc is not empty.
227     *
228     * @param aComment the source lines that make up the Javadoc comment.
229     */
230    private void checkEmptyJavadoc(TextBlock aComment)
231    {
232        final String commentText = getCommentText(aComment.getText());
233
234        if (commentText.length() == 0) {
235            log(aComment.getStartLineNo(), "javadoc.empty");
236        }
237    }
238
239    /**
240     * Returns the comment text from the Javadoc.
241     * @param aComments the lines of Javadoc.
242     * @return a comment text String.
243     */
244    private String getCommentText(String[] aComments)
245    {
246        final StringBuffer buffer = new StringBuffer();
247        for (final String line : aComments) {
248            final int textStart = findTextStart(line);
249
250            if (textStart != -1) {
251                if (line.charAt(textStart) == '@') {
252                    //we have found the tag section
253                    break;
254                }
255                buffer.append(line.substring(textStart));
256                trimTail(buffer);
257                buffer.append('\n');
258            }
259        }
260
261        return buffer.toString().trim();
262    }
263
264    /**
265     * Finds the index of the first non-whitespace character ignoring the
266     * Javadoc comment start and end strings (&#47** and *&#47) as well as any
267     * leading asterisk.
268     * @param aLine the Javadoc comment line of text to scan.
269     * @return the int index relative to 0 for the start of text
270     *         or -1 if not found.
271     */
272    private int findTextStart(String aLine)
273    {
274        int textStart = -1;
275        for (int i = 0; i < aLine.length(); i++) {
276            if (!Character.isWhitespace(aLine.charAt(i))) {
277                if (aLine.regionMatches(i, "/**", 0, "/**".length())) {
278                    i += 2;
279                }
280                else if (aLine.regionMatches(i, "*/", 0, 2)) {
281                    i++;
282                }
283                else if (aLine.charAt(i) != '*') {
284                    textStart = i;
285                    break;
286                }
287            }
288        }
289        return textStart;
290    }
291
292    /**
293     * Trims any trailing whitespace or the end of Javadoc comment string.
294     * @param aBuffer the StringBuffer to trim.
295     */
296    private void trimTail(StringBuffer aBuffer)
297    {
298        for (int i = aBuffer.length() - 1; i >= 0; i--) {
299            if (Character.isWhitespace(aBuffer.charAt(i))) {
300                aBuffer.deleteCharAt(i);
301            }
302            else if ((i > 0)
303                     && (aBuffer.charAt(i - 1) == '*')
304                     && (aBuffer.charAt(i) == '/'))
305            {
306                aBuffer.deleteCharAt(i);
307                aBuffer.deleteCharAt(i - 1);
308                i--;
309                while (aBuffer.charAt(i - 1) == '*') {
310                    aBuffer.deleteCharAt(i - 1);
311                    i--;
312                }
313            }
314            else {
315                break;
316            }
317        }
318    }
319
320    /**
321     * Checks the comment for HTML tags that do not have a corresponding close
322     * tag or a close tag that has no previous open tag.  This code was
323     * primarily copied from the DocCheck checkHtml method.
324     *
325     * @param aAST the node with the Javadoc
326     * @param aComment the <code>TextBlock</code> which represents
327     *                 the Javadoc comment.
328     */
329    private void checkHtml(final DetailAST aAST, final TextBlock aComment)
330    {
331        final int lineno = aComment.getStartLineNo();
332        final FastStack<HtmlTag> htmlStack = FastStack.newInstance();
333        final String[] text = aComment.getText();
334        final List<String> typeParameters =
335            CheckUtils.getTypeParameterNames(aAST);
336
337        TagParser parser = null;
338        parser = new TagParser(text, lineno);
339
340        while (parser.hasNextTag()) {
341            final HtmlTag tag = parser.nextTag();
342
343            if (tag.isIncompleteTag()) {
344                log(tag.getLineno(), "javadoc.incompleteTag",
345                    text[tag.getLineno() - lineno]);
346                return;
347            }
348            if (tag.isClosedTag()) {
349                //do nothing
350                continue;
351            }
352            if (!tag.isCloseTag()) {
353                //We only push html tags that are allowed
354                if (isAllowedTag(tag)) {
355                    htmlStack.push(tag);
356                }
357            }
358            else {
359                // We have found a close tag.
360                if (isExtraHtml(tag.getId(), htmlStack)) {
361                    // No corresponding open tag was found on the stack.
362                    log(tag.getLineno(),
363                        tag.getPosition(),
364                        EXTRA_HTML,
365                        tag);
366                }
367                else {
368                    // See if there are any unclosed tags that were opened
369                    // after this one.
370                    checkUnclosedTags(htmlStack, tag.getId());
371                }
372            }
373        }
374
375        // Identify any tags left on the stack.
376        String lastFound = ""; // Skip multiples, like <b>...<b>
377        for (final HtmlTag htag : htmlStack) {
378            if (!isSingleTag(htag)
379                && !htag.getId().equals(lastFound)
380                && !typeParameters.contains(htag.getId()))
381            {
382                log(htag.getLineno(), htag.getPosition(), UNCLOSED_HTML, htag);
383                lastFound = htag.getId();
384            }
385        }
386    }
387
388    /**
389     * Checks to see if there are any unclosed tags on the stack.  The token
390     * represents a html tag that has been closed and has a corresponding open
391     * tag on the stack.  Any tags, except single tags, that were opened
392     * (pushed on the stack) after the token are missing a close.
393     *
394     * @param aHtmlStack the stack of opened HTML tags.
395     * @param aToken the current HTML tag name that has been closed.
396     */
397    private void checkUnclosedTags(FastStack<HtmlTag> aHtmlStack, String aToken)
398    {
399        final FastStack<HtmlTag> unclosedTags = FastStack.newInstance();
400        HtmlTag lastOpenTag = aHtmlStack.pop();
401        while (!aToken.equalsIgnoreCase(lastOpenTag.getId())) {
402            // Find unclosed elements. Put them on a stack so the
403            // output order won't be back-to-front.
404            if (isSingleTag(lastOpenTag)) {
405                lastOpenTag = aHtmlStack.pop();
406            }
407            else {
408                unclosedTags.push(lastOpenTag);
409                lastOpenTag = aHtmlStack.pop();
410            }
411        }
412
413        // Output the unterminated tags, if any
414        String lastFound = ""; // Skip multiples, like <b>..<b>
415        for (final HtmlTag htag : unclosedTags) {
416            lastOpenTag = htag;
417            if (lastOpenTag.getId().equals(lastFound)) {
418                continue;
419            }
420            lastFound = lastOpenTag.getId();
421            log(lastOpenTag.getLineno(),
422                lastOpenTag.getPosition(),
423                UNCLOSED_HTML,
424                lastOpenTag);
425        }
426    }
427
428    /**
429     * Determines if the HtmlTag is one which does not require a close tag.
430     *
431     * @param aTag the HtmlTag to check.
432     * @return <code>true</code> if the HtmlTag is a single tag.
433     */
434    private boolean isSingleTag(HtmlTag aTag)
435    {
436        // If its a singleton tag (<p>, <br>, etc.), ignore it
437        // Can't simply not put them on the stack, since singletons
438        // like <dt> and <dd> (unhappily) may either be terminated
439        // or not terminated. Both options are legal.
440        return SINGLE_TAGS.contains(aTag.getId().toLowerCase());
441    }
442
443    /**
444     * Determines if the HtmlTag is one which is allowed in a javadoc.
445     *
446     * @param aTag the HtmlTag to check.
447     * @return <code>true</code> if the HtmlTag is an allowed html tag.
448     */
449    private boolean isAllowedTag(HtmlTag aTag)
450    {
451        return ALLOWED_TAGS.contains(aTag.getId().toLowerCase());
452    }
453
454    /**
455     * Determines if the given token is an extra HTML tag. This indicates that
456     * a close tag was found that does not have a corresponding open tag.
457     *
458     * @param aToken an HTML tag id for which a close was found.
459     * @param aHtmlStack a Stack of previous open HTML tags.
460     * @return <code>false</code> if a previous open tag was found
461     *         for the token.
462     */
463    private boolean isExtraHtml(String aToken, FastStack<HtmlTag> aHtmlStack)
464    {
465        boolean isExtra = true;
466        for (final HtmlTag td : aHtmlStack) {
467            // Loop, looking for tags that are closed.
468            // The loop is needed in case there are unclosed
469            // tags on the stack. In that case, the stack would
470            // not be empty, but this tag would still be extra.
471            if (aToken.equalsIgnoreCase(td.getId())) {
472                isExtra = false;
473                break;
474            }
475        }
476
477        return isExtra;
478    }
479
480    /**
481     * Sets the scope to check.
482     * @param aFrom string to get the scope from
483     */
484    public void setScope(String aFrom)
485    {
486        mScope = Scope.getInstance(aFrom);
487    }
488
489    /**
490     * Set the excludeScope.
491     * @param aScope a <code>String</code> value
492     */
493    public void setExcludeScope(String aScope)
494    {
495        mExcludeScope = Scope.getInstance(aScope);
496    }
497
498    /**
499     * Set the format for matching the end of a sentence.
500     * @param aFormat format for matching the end of a sentence.
501     */
502    public void setEndOfSentenceFormat(String aFormat)
503    {
504        mEndOfSentenceFormat = aFormat;
505    }
506
507    /**
508     * Returns a regular expression for matching the end of a sentence.
509     *
510     * @return a regular expression for matching the end of a sentence.
511     */
512    private Pattern getEndOfSentencePattern()
513    {
514        if (mEndOfSentencePattern == null) {
515            mEndOfSentencePattern = Pattern.compile(mEndOfSentenceFormat);
516        }
517        return mEndOfSentencePattern;
518    }
519
520    /**
521     * Sets the flag that determines if the first sentence is checked for
522     * proper end of sentence punctuation.
523     * @param aFlag <code>true</code> if the first sentence is to be checked
524     */
525    public void setCheckFirstSentence(boolean aFlag)
526    {
527        mCheckFirstSentence = aFlag;
528    }
529
530    /**
531     * Sets the flag that determines if HTML checking is to be performed.
532     * @param aFlag <code>true</code> if HTML checking is to be performed.
533     */
534    public void setCheckHtml(boolean aFlag)
535    {
536        mCheckHtml = aFlag;
537    }
538
539    /**
540     * Sets the flag that determines if empty JavaDoc checking should be done.
541     * @param aFlag <code>true</code> if empty JavaDoc checking should be done.
542     */
543    public void setCheckEmptyJavadoc(boolean aFlag)
544    {
545        mCheckEmptyJavadoc = aFlag;
546    }
547}