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 scope = Scope.PRIVATE;
072
073    /** the visibility scope where Javadoc comments shouldn't be checked **/
074    private Scope excludeScope;
075
076    /** Format for matching the end of a sentence. */
077    private String endOfSentenceFormat = "([.?!][ \t\n\r\f<])|([.?!]$)";
078
079    /** Regular expression for matching the end of a sentence. */
080    private Pattern endOfSentencePattern;
081
082    /**
083     * Indicates if the first sentence should be checked for proper end of
084     * sentence punctuation.
085     */
086    private boolean checkFirstSentence = true;
087
088    /**
089     * Indicates if the HTML within the comment should be checked.
090     */
091    private boolean checkHtml = true;
092
093    /**
094     * Indicates if empty javadoc statements should be checked.
095     */
096    private boolean checkEmptyJavadoc;
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 ast)
117    {
118        if (shouldCheck(ast)) {
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(ast.getFirstChild().getLineNo());
125
126            checkComment(ast, cmt);
127        }
128    }
129
130    /**
131     * Whether we should check this node.
132     * @param ast a given node.
133     * @return whether we should check a given node.
134     */
135    private boolean shouldCheck(final DetailAST ast)
136    {
137        if (ast.getType() == TokenTypes.PACKAGE_DEF) {
138            return getFileContents().inPackageInfo();
139        }
140
141        if (ScopeUtils.inCodeBlock(ast)) {
142            return false;
143        }
144
145        final Scope declaredScope;
146        if (ast.getType() == TokenTypes.ENUM_CONSTANT_DEF) {
147            declaredScope = Scope.PUBLIC;
148        }
149        else {
150            declaredScope = ScopeUtils.getScopeFromMods(
151                ast.findFirstToken(TokenTypes.MODIFIERS));
152        }
153
154        final Scope scope =
155            ScopeUtils.inInterfaceOrAnnotationBlock(ast)
156            ? Scope.PUBLIC : declaredScope;
157        final Scope surroundingScope = ScopeUtils.getSurroundingScope(ast);
158
159        return scope.isIn(this.scope)
160            && ((surroundingScope == null) || surroundingScope.isIn(this.scope))
161            && ((excludeScope == null)
162                || !scope.isIn(excludeScope)
163                || ((surroundingScope != null)
164                && !surroundingScope.isIn(excludeScope)));
165    }
166
167    /**
168     * Performs the various checks agains the Javadoc comment.
169     *
170     * @param ast the AST of the element being documented
171     * @param comment 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 ast, final TextBlock comment)
177    {
178        if (comment == 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(ast.getLineNo(), "javadoc.missing");
185            }
186            return;
187        }
188
189        if (checkFirstSentence) {
190            checkFirstSentence(ast, comment);
191        }
192
193        if (checkHtml) {
194            checkHtml(ast, comment);
195        }
196
197        if (checkEmptyJavadoc) {
198            checkEmptyJavadoc(comment);
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 ast the current node
210     * @param comment the source lines that make up the Javadoc comment.
211     */
212    private void checkFirstSentence(final DetailAST ast, TextBlock comment)
213    {
214        final String commentText = getCommentText(comment.getText());
215
216        if ((commentText.length() != 0)
217            && !getEndOfSentencePattern().matcher(commentText).find()
218            && !("{@inheritDoc}".equals(commentText)
219            && JavadocTagInfo.INHERIT_DOC.isValidOn(ast)))
220        {
221            log(comment.getStartLineNo(), "javadoc.noperiod");
222        }
223    }
224
225    /**
226     * Checks that the Javadoc is not empty.
227     *
228     * @param comment the source lines that make up the Javadoc comment.
229     */
230    private void checkEmptyJavadoc(TextBlock comment)
231    {
232        final String commentText = getCommentText(comment.getText());
233
234        if (commentText.length() == 0) {
235            log(comment.getStartLineNo(), "javadoc.empty");
236        }
237    }
238
239    /**
240     * Returns the comment text from the Javadoc.
241     * @param comments the lines of Javadoc.
242     * @return a comment text String.
243     */
244    private String getCommentText(String[] comments)
245    {
246        final StringBuffer buffer = new StringBuffer();
247        for (final String line : comments) {
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 line 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 line)
273    {
274        int textStart = -1;
275        for (int i = 0; i < line.length(); i++) {
276            if (!Character.isWhitespace(line.charAt(i))) {
277                if (line.regionMatches(i, "/**", 0, "/**".length())) {
278                    i += 2;
279                }
280                else if (line.regionMatches(i, "*/", 0, 2)) {
281                    i++;
282                }
283                else if (line.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 buffer the StringBuffer to trim.
295     */
296    private void trimTail(StringBuffer buffer)
297    {
298        for (int i = buffer.length() - 1; i >= 0; i--) {
299            if (Character.isWhitespace(buffer.charAt(i))) {
300                buffer.deleteCharAt(i);
301            }
302            else if ((i > 0)
303                     && (buffer.charAt(i - 1) == '*')
304                     && (buffer.charAt(i) == '/'))
305            {
306                buffer.deleteCharAt(i);
307                buffer.deleteCharAt(i - 1);
308                i--;
309                while (buffer.charAt(i - 1) == '*') {
310                    buffer.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 ast the node with the Javadoc
326     * @param comment the <code>TextBlock</code> which represents
327     *                 the Javadoc comment.
328     */
329    private void checkHtml(final DetailAST ast, final TextBlock comment)
330    {
331        final int lineno = comment.getStartLineNo();
332        final FastStack<HtmlTag> htmlStack = FastStack.newInstance();
333        final String[] text = comment.getText();
334        final List<String> typeParameters =
335            CheckUtils.getTypeParameterNames(ast);
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 htmlStack the stack of opened HTML tags.
395     * @param token the current HTML tag name that has been closed.
396     */
397    private void checkUnclosedTags(FastStack<HtmlTag> htmlStack, String token)
398    {
399        final FastStack<HtmlTag> unclosedTags = FastStack.newInstance();
400        HtmlTag lastOpenTag = htmlStack.pop();
401        while (!token.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 = htmlStack.pop();
406            }
407            else {
408                unclosedTags.push(lastOpenTag);
409                lastOpenTag = htmlStack.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 tag the HtmlTag to check.
432     * @return <code>true</code> if the HtmlTag is a single tag.
433     */
434    private boolean isSingleTag(HtmlTag tag)
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(tag.getId().toLowerCase());
441    }
442
443    /**
444     * Determines if the HtmlTag is one which is allowed in a javadoc.
445     *
446     * @param tag the HtmlTag to check.
447     * @return <code>true</code> if the HtmlTag is an allowed html tag.
448     */
449    private boolean isAllowedTag(HtmlTag tag)
450    {
451        return ALLOWED_TAGS.contains(tag.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 token an HTML tag id for which a close was found.
459     * @param htmlStack 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 token, FastStack<HtmlTag> htmlStack)
464    {
465        boolean isExtra = true;
466        for (final HtmlTag td : htmlStack) {
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 (token.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 from string to get the scope from
483     */
484    public void setScope(String from)
485    {
486        scope = Scope.getInstance(from);
487    }
488
489    /**
490     * Set the excludeScope.
491     * @param scope a <code>String</code> value
492     */
493    public void setExcludeScope(String scope)
494    {
495        excludeScope = Scope.getInstance(scope);
496    }
497
498    /**
499     * Set the format for matching the end of a sentence.
500     * @param format format for matching the end of a sentence.
501     */
502    public void setEndOfSentenceFormat(String format)
503    {
504        endOfSentenceFormat = format;
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 (endOfSentencePattern == null) {
515            endOfSentencePattern = Pattern.compile(endOfSentenceFormat);
516        }
517        return endOfSentencePattern;
518    }
519
520    /**
521     * Sets the flag that determines if the first sentence is checked for
522     * proper end of sentence punctuation.
523     * @param flag <code>true</code> if the first sentence is to be checked
524     */
525    public void setCheckFirstSentence(boolean flag)
526    {
527        checkFirstSentence = flag;
528    }
529
530    /**
531     * Sets the flag that determines if HTML checking is to be performed.
532     * @param flag <code>true</code> if HTML checking is to be performed.
533     */
534    public void setCheckHtml(boolean flag)
535    {
536        checkHtml = flag;
537    }
538
539    /**
540     * Sets the flag that determines if empty Javadoc checking should be done.
541     * @param flag <code>true</code> if empty Javadoc checking should be done.
542     */
543    public void setCheckEmptyJavadoc(boolean flag)
544    {
545        checkEmptyJavadoc = flag;
546    }
547}