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 java.io.ByteArrayInputStream;
022import java.io.IOException;
023import java.io.InputStream;
024import java.nio.charset.Charset;
025import java.util.HashMap;
026import java.util.Map;
027
028import org.antlr.v4.runtime.ANTLRInputStream;
029import org.antlr.v4.runtime.BailErrorStrategy;
030import org.antlr.v4.runtime.BaseErrorListener;
031import org.antlr.v4.runtime.CommonTokenStream;
032import org.antlr.v4.runtime.ParserRuleContext;
033import org.antlr.v4.runtime.RecognitionException;
034import org.antlr.v4.runtime.Recognizer;
035import org.antlr.v4.runtime.RuleContext;
036import org.antlr.v4.runtime.Token;
037import org.antlr.v4.runtime.misc.ParseCancellationException;
038import org.antlr.v4.runtime.tree.ParseTree;
039import org.antlr.v4.runtime.tree.TerminalNode;
040
041import com.google.common.base.CaseFormat;
042import com.google.common.primitives.Ints;
043import com.puppycrawl.tools.checkstyle.api.Check;
044import com.puppycrawl.tools.checkstyle.api.DetailAST;
045import com.puppycrawl.tools.checkstyle.api.DetailNode;
046import com.puppycrawl.tools.checkstyle.api.JavadocTokenTypes;
047import com.puppycrawl.tools.checkstyle.api.TokenTypes;
048import com.puppycrawl.tools.checkstyle.grammars.javadoc.JavadocLexer;
049import com.puppycrawl.tools.checkstyle.grammars.javadoc.JavadocParser;
050
051/**
052 * Base class for Checks that process Javadoc comments.
053 * @author Baratali Izmailov
054 */
055public abstract class AbstractJavadocCheck extends Check
056{
057    /**
058     * Error message key for common javadoc errors.
059     */
060    private static final String PARSE_ERROR_MESSAGE_KEY = "javadoc.parse.error";
061
062    /**
063     * Unrecognized error from antlr parser
064     */
065    private static final String UNRECOGNIZED_ANTLR_ERROR_MESSAGE_KEY =
066            "javadoc.unrecognized.antlr.error";
067
068    /**
069     * key is "line:column"
070     * value is DetailNode tree
071     */
072    private static final Map<String, ParseStatus> TREE_CACHE = new HashMap<String, ParseStatus>();
073
074    /**
075     * Custom error listener.
076     */
077    private final DescriptiveErrorListener errorListener =
078            new DescriptiveErrorListener();
079
080    /**
081     * DetailAST node of considered Javadoc comment that is just a block comment
082     * in Java language syntax tree.
083     */
084    private DetailAST blockCommentAst;
085
086    /**
087     * Returns the default token types a check is interested in.
088     * @return the default token types
089     * @see JavadocTokenTypes
090     */
091    public abstract int[] getDefaultJavadocTokens();
092
093    /**
094     * Called before the starting to process a tree.
095     * @param rootAst
096     *        the root of the tree
097     */
098    public void beginJavadocTree(DetailNode rootAst)
099    {
100    }
101
102    /**
103     * Called after finished processing a tree.
104     * @param rootAst
105     *        the root of the tree
106     */
107    public void finishJavadocTree(DetailNode rootAst)
108    {
109    }
110
111    /**
112     * Called to process a Javadoc token.
113     * @param ast
114     *        the token to process
115     */
116    public void visitJavadocToken(DetailNode ast)
117    {
118    }
119
120    /**
121     * Called after all the child nodes have been process.
122     * @param ast
123     *        the token leaving
124     */
125    public void leaveJavadocToken(DetailNode ast)
126    {
127    }
128
129    /**
130     * Defined final to not allow JavadocChecks to change default tokens.
131     * @return default tokens
132     */
133    @Override
134    public final int[] getDefaultTokens()
135    {
136        return new int[] {TokenTypes.BLOCK_COMMENT_BEGIN };
137    }
138
139    /**
140     * Defined final to not allow JavadocChecks to change acceptable tokens.
141     * @return acceptable tokens
142     */
143    @Override
144    public final int[] getAcceptableTokens()
145    {
146        return super.getAcceptableTokens();
147    }
148
149    /**
150     * Defined final to not allow JavadocChecks to change required tokens.
151     * @return required tokens
152     */
153    @Override
154    public final int[] getRequiredTokens()
155    {
156        return super.getRequiredTokens();
157    }
158
159    /**
160     * Defined final because all JavadocChecks require comment nodes.
161     * @return true
162     */
163    @Override
164    public final boolean isCommentNodesRequired()
165    {
166        return true;
167    }
168
169    @Override
170    public final void beginTree(DetailAST rootAST)
171    {
172        TREE_CACHE.clear();
173    }
174
175    @Override
176    public final void finishTree(DetailAST rootAST)
177    {
178        TREE_CACHE.clear();
179    }
180
181    @Override
182    public final void leaveToken(DetailAST ast)
183    {
184    }
185
186    @Override
187    public final void visitToken(DetailAST blockCommentAst)
188    {
189        if (JavadocUtils.isJavadocComment(blockCommentAst)) {
190            this.blockCommentAst = blockCommentAst;
191
192            final String treeCacheKey = blockCommentAst.getLineNo() + ":"
193                    + blockCommentAst.getColumnNo();
194
195            ParseStatus ps;
196
197            if (TREE_CACHE.containsKey(treeCacheKey)) {
198                ps = TREE_CACHE.get(treeCacheKey);
199            }
200            else {
201                ps = parseJavadocAsDetailNode(blockCommentAst);
202                TREE_CACHE.put(treeCacheKey, ps);
203            }
204
205            if (ps.getParseErrorMessage() == null) {
206                processTree(ps.getTree());
207            }
208            else {
209                final ParseErrorMessage parseErrorMessage = ps.getParseErrorMessage();
210                log(parseErrorMessage.getLineNumber(),
211                        parseErrorMessage.getMessageKey(),
212                        parseErrorMessage.getMessageArguments());
213            }
214        }
215
216    }
217
218    protected DetailAST getBlockCommentAst()
219    {
220        return blockCommentAst;
221    }
222
223    /**
224     * Parses Javadoc comment as DetailNode tree.
225     * @param javadocCommentAst
226     *        DetailAST of Javadoc comment
227     * @return DetailNode tree of Javadoc comment
228     */
229    private ParseStatus parseJavadocAsDetailNode(DetailAST javadocCommentAst)
230    {
231        final String javadocComment = JavadocUtils.getJavadocCommentContent(javadocCommentAst);
232
233        // Log messages should have line number in scope of file,
234        // not in scope of Javadoc comment.
235        // Offset is line number of beginning of Javadoc comment.
236        errorListener.setOffset(javadocCommentAst.getLineNo() - 1);
237
238        final ParseStatus result = new ParseStatus();
239        ParseTree parseTree = null;
240        ParseErrorMessage parseErrorMessage = null;
241
242        try {
243            parseTree = parseJavadocAsParseTree(javadocComment);
244        }
245        catch (IOException e) {
246            // Antlr can not initiate its ANTLRInputStream
247            parseErrorMessage = new ParseErrorMessage(javadocCommentAst.getLineNo(),
248                    PARSE_ERROR_MESSAGE_KEY,
249                    javadocCommentAst.getColumnNo(), e.getMessage());
250        }
251        catch (ParseCancellationException e) {
252            // If syntax error occurs then message is printed by error listener
253            // and parser throws this runtime exception to stop parsing.
254            // Just stop processing current Javadoc comment.
255            parseErrorMessage = errorListener.getErrorMessage();
256
257            // There are cases when antlr error listener does not handle syntax error
258            if (parseErrorMessage == null) {
259                parseErrorMessage = new ParseErrorMessage(javadocCommentAst.getLineNo(),
260                        UNRECOGNIZED_ANTLR_ERROR_MESSAGE_KEY,
261                        javadocCommentAst.getColumnNo(), e.getMessage());
262            }
263        }
264
265        if (parseErrorMessage == null) {
266            final DetailNode tree = convertParseTree2DetailNode(parseTree);
267            result.setTree(tree);
268        }
269        else {
270            result.setParseErrorMessage(parseErrorMessage);
271        }
272
273        return result;
274    }
275
276    /**
277     * Converts ParseTree (that is generated by ANTLRv4) to DetailNode tree.
278     *
279     * @param rootParseTree root node of ParseTree
280     * @return root of DetailNode tree
281     */
282    private DetailNode convertParseTree2DetailNode(ParseTree rootParseTree)
283    {
284        final ParseTree currentParseTreeNode = rootParseTree;
285        final JavadocNodeImpl rootJavadocNode = createJavadocNode(currentParseTreeNode, null, -1);
286
287        int childCount = currentParseTreeNode.getChildCount();
288        JavadocNodeImpl[] children = (JavadocNodeImpl[]) rootJavadocNode.getChildren();
289
290        for (int i = 0; i < childCount; i++) {
291            final JavadocNodeImpl child = createJavadocNode(currentParseTreeNode.getChild(i)
292                    , rootJavadocNode, i);
293            children[i] = child;
294        }
295
296        JavadocNodeImpl currentJavadocParent = rootJavadocNode;
297        ParseTree currentParseTreeParent = currentParseTreeNode;
298
299        while (currentJavadocParent != null) {
300            children = (JavadocNodeImpl[]) currentJavadocParent.getChildren();
301            childCount = children.length;
302
303            for (int i = 0; i < childCount; i++) {
304                final JavadocNodeImpl currentJavadocNode = children[i];
305                final ParseTree currentParseTreeNodeChild = currentParseTreeParent.getChild(i);
306
307                final JavadocNodeImpl[] subChildren = (JavadocNodeImpl[]) currentJavadocNode
308                        .getChildren();
309
310                for (int j = 0; j < subChildren.length; j++) {
311                    final JavadocNodeImpl child =
312                            createJavadocNode(currentParseTreeNodeChild.getChild(j)
313                                    , currentJavadocNode, j);
314
315                    subChildren[j] = child;
316                }
317            }
318
319            if (childCount > 0) {
320                currentJavadocParent = children[0];
321                currentParseTreeParent = currentParseTreeParent.getChild(0);
322            }
323            else {
324                JavadocNodeImpl nextJavadocSibling = (JavadocNodeImpl) JavadocUtils
325                        .getNextSibling(currentJavadocParent);
326
327                ParseTree nextParseTreeSibling = getNextSibling(currentParseTreeParent);
328
329                if (nextJavadocSibling == null) {
330                    JavadocNodeImpl tempJavadocParent =
331                            (JavadocNodeImpl) currentJavadocParent.getParent();
332
333                    ParseTree tempParseTreeParent = currentParseTreeParent.getParent();
334
335                    while (nextJavadocSibling == null && tempJavadocParent != null) {
336
337                        nextJavadocSibling = (JavadocNodeImpl) JavadocUtils
338                                .getNextSibling(tempJavadocParent);
339
340                        nextParseTreeSibling = getNextSibling(tempParseTreeParent);
341
342                        tempJavadocParent = (JavadocNodeImpl) tempJavadocParent.getParent();
343                        tempParseTreeParent = tempParseTreeParent.getParent();
344                    }
345                }
346                currentJavadocParent = nextJavadocSibling;
347                currentParseTreeParent = nextParseTreeSibling;
348            }
349        }
350
351        return rootJavadocNode;
352    }
353
354    /**
355     * Creates JavadocNodeImpl node on base of ParseTree node.
356     *
357     * @param parseTree ParseTree node
358     * @param parent DetailNode that will be parent of new node
359     * @param index child index that has new node
360     * @return JavadocNodeImpl node on base of ParseTree node.
361     */
362    private JavadocNodeImpl createJavadocNode(ParseTree parseTree, DetailNode parent, int index)
363    {
364        final JavadocNodeImpl node = new JavadocNodeImpl();
365        node.setText(parseTree.getText());
366        node.setColumnNumber(getColumn(parseTree));
367        node.setLineNumber(getLine(parseTree) + blockCommentAst.getLineNo());
368        node.setIndex(index);
369        node.setType(getTokenType(parseTree));
370        node.setParent(parent);
371        node.setChildren(new JavadocNodeImpl[parseTree.getChildCount()]);
372        return node;
373    }
374
375    /**
376     * Gets next sibling of ParseTree node.
377     * @param node ParseTree node
378     * @return next sibling of ParseTree node.
379     */
380    private static ParseTree getNextSibling(ParseTree node)
381    {
382        if (node.getParent() == null) {
383            return null;
384        }
385
386        final ParseTree parent = node.getParent();
387        final int childCount = parent.getChildCount();
388
389        for (int i = 0; i < childCount; i++) {
390            final ParseTree currentNode = parent.getChild(i);
391            if (currentNode.equals(node)) {
392                if (i == childCount - 1) {
393                    return null;
394                }
395                return parent.getChild(i + 1);
396            }
397        }
398        return null;
399    }
400
401    /**
402     * Gets token type of ParseTree node from JavadocTokenTypes class.
403     * @param node ParseTree node.
404     * @return token type from JavadocTokenTypes
405     */
406    private static int getTokenType(ParseTree node)
407    {
408        int tokenType = Integer.MIN_VALUE;
409
410        if (node.getChildCount() == 0) {
411            tokenType = ((TerminalNode) node).getSymbol().getType();
412        }
413        else {
414            final String className = getNodeClassNameWithoutContext(node);
415            final String typeName =
416                    CaseFormat.UPPER_CAMEL.to(CaseFormat.UPPER_UNDERSCORE, className);
417            tokenType = JavadocUtils.getTokenId(typeName);
418        }
419
420        return tokenType;
421    }
422
423    /**
424     * Gets class name of ParseTree node and removes 'Context' postfix at the
425     * end.
426     * @param node
427     *        ParseTree node.
428     * @return class name without 'Context'
429     */
430    private static String getNodeClassNameWithoutContext(ParseTree node)
431    {
432        final String className = node.getClass().getSimpleName();
433        // remove 'Context' at the end
434        final int contextLength = 7;
435        return className.substring(0, className.length() - contextLength);
436    }
437
438    /**
439     * Gets line number from ParseTree node.
440     * @param tree
441     *        ParseTree node
442     * @return line number
443     */
444    private static int getLine(ParseTree tree)
445    {
446        if (tree instanceof TerminalNode) {
447            return ((TerminalNode) tree).getSymbol().getLine() - 1;
448        }
449        else {
450            final ParserRuleContext rule = (ParserRuleContext) tree;
451            return rule.start.getLine() - 1;
452        }
453    }
454
455    /**
456     * Gets column number from ParseTree node.
457     * @param tree
458     *        ParseTree node
459     * @return column number
460     */
461    private static int getColumn(ParseTree tree)
462    {
463        if (tree instanceof TerminalNode) {
464            return ((TerminalNode) tree).getSymbol().getCharPositionInLine();
465        }
466        else {
467            final ParserRuleContext rule = (ParserRuleContext) tree;
468            return rule.start.getCharPositionInLine();
469        }
470    }
471
472    /**
473     * Parses block comment content as javadoc comment.
474     * @param blockComment
475     *        block comment content.
476     * @return parse tree
477     * @throws IOException
478     *         errors in ANTLRInputStream
479     */
480    private ParseTree parseJavadocAsParseTree(String blockComment)
481        throws IOException
482    {
483        final Charset utf8Charset = Charset.forName("UTF-8");
484        final InputStream in = new ByteArrayInputStream(blockComment.getBytes(utf8Charset));
485
486        final ANTLRInputStream input = new ANTLRInputStream(in);
487
488        final JavadocLexer lexer = new JavadocLexer(input);
489
490        // remove default error listeners
491        lexer.removeErrorListeners();
492
493        // add custom error listener that logs parsing errors
494        lexer.addErrorListener(errorListener);
495
496        final CommonTokenStream tokens = new CommonTokenStream(lexer);
497
498        final JavadocParser parser = new JavadocParser(tokens);
499
500        // remove default error listeners
501        parser.removeErrorListeners();
502
503        // add custom error listener that logs syntax errors
504        parser.addErrorListener(errorListener);
505
506        // This strategy stops parsing when parser error occurs.
507        // By default it uses Error Recover Strategy which is slow and useless.
508        parser.setErrorHandler(new BailErrorStrategy());
509
510        return parser.javadoc();
511    }
512
513    /**
514     * Processes JavadocAST tree notifying Check.
515     * @param root
516     *        root of JavadocAST tree.
517     */
518    private void processTree(DetailNode root)
519    {
520        beginJavadocTree(root);
521        walk(root);
522        finishJavadocTree(root);
523    }
524
525    /**
526     * Processes a node calling Check at interested nodes.
527     * @param root
528     *        the root of tree for process
529     */
530    private void walk(DetailNode root)
531    {
532        final int[] defaultTokenTypes = getDefaultJavadocTokens();
533
534        if (defaultTokenTypes == null) {
535            return;
536        }
537
538        DetailNode curNode = root;
539        while (curNode != null) {
540            final boolean waitsFor = Ints.contains(defaultTokenTypes, curNode.getType());
541
542            if (waitsFor) {
543                visitJavadocToken(curNode);
544            }
545            DetailNode toVisit = JavadocUtils.getFirstChild(curNode);
546            while ((curNode != null) && (toVisit == null)) {
547
548                if (waitsFor) {
549                    leaveJavadocToken(curNode);
550                }
551
552                toVisit = JavadocUtils.getNextSibling(curNode);
553                if (toVisit == null) {
554                    curNode = curNode.getParent();
555                }
556            }
557            curNode = toVisit;
558        }
559    }
560
561    /**
562     * Custom error listener for JavadocParser that prints user readable errors.
563     */
564    class DescriptiveErrorListener extends BaseErrorListener
565    {
566        /**
567         * Parse error while token recognition.
568         */
569        private static final String JAVADOC_PARSE_TOKEN_ERROR = "javadoc.parse.token.error";
570
571        /**
572         * Parse error while rule recognition.
573         */
574        private static final String JAVADOC_PARSE_RULE_ERROR = "javadoc.parse.rule.error";
575
576        /**
577         * Message key of error message. Missed close HTML tag breaks structure
578         * of parse tree, so parser stops parsing and generates such error
579         * message. This case is special because parser prints error like
580         * {@code "no viable alternative at input 'b \n *\n'"} and it is not
581         * clear that error is about missed close HTML tag.
582         */
583        private static final String JAVADOC_MISSED_HTML_CLOSE = "javadoc.missed.html.close";
584
585        /**
586         * Message key of error message.
587         */
588        private static final String JAVADOC_WRONG_SINGLETON_TAG =
589                "javadoc.wrong.singleton.html.tag";
590
591        /**
592         * Offset is line number of beginning of the Javadoc comment. Log
593         * messages should have line number in scope of file, not in scope of
594         * Javadoc comment.
595         */
596        private int offset;
597
598        /**
599         * Error message that appeared while parsing.
600         */
601        private ParseErrorMessage errorMessage;
602
603        public ParseErrorMessage getErrorMessage()
604        {
605            return errorMessage;
606        }
607
608        /**
609         * Sets offset. Offset is line number of beginning of the Javadoc
610         * comment. Log messages should have line number in scope of file, not
611         * in scope of Javadoc comment.
612         * @param offset
613         *        offset line number
614         */
615        public void setOffset(int offset)
616        {
617            this.offset = offset;
618        }
619
620        /**
621         * Logs parser errors in Checkstyle manner. Parser can generate error
622         * messages. There is special error that parser can generate. It is
623         * missed close HTML tag. This case is special because parser prints
624         * error like {@code "no viable alternative at input 'b \n *\n'"} and it
625         * is not clear that error is about missed close HTML tag. Other error
626         * messages are not special and logged simply as "Parse Error...".
627         * <p>
628         * {@inheritDoc}
629         */
630        @Override
631        public void syntaxError(
632                Recognizer<?, ?> recognizer, Object offendingSymbol,
633                int line, int charPositionInLine,
634                String msg, RecognitionException ex)
635        {
636            final int lineNumber = offset + line;
637            final Token token = (Token) offendingSymbol;
638
639            if (JAVADOC_MISSED_HTML_CLOSE.equals(msg)) {
640                errorMessage = new ParseErrorMessage(lineNumber,
641                        JAVADOC_MISSED_HTML_CLOSE, charPositionInLine, token.getText());
642
643                throw new ParseCancellationException();
644            }
645            else if (JAVADOC_WRONG_SINGLETON_TAG.equals(msg)) {
646                errorMessage = new ParseErrorMessage(lineNumber,
647                        JAVADOC_WRONG_SINGLETON_TAG, charPositionInLine, token.getText());
648
649                throw new ParseCancellationException();
650            }
651            else {
652                final RuleContext ruleContext = ex.getCtx();
653                if (ruleContext != null) {
654                    final int ruleIndex = ex.getCtx().getRuleIndex();
655                    final String ruleName = recognizer.getRuleNames()[ruleIndex];
656                    final String upperCaseRuleName = CaseFormat.UPPER_CAMEL.to(
657                            CaseFormat.UPPER_UNDERSCORE, ruleName);
658
659                    errorMessage = new ParseErrorMessage(lineNumber,
660                            JAVADOC_PARSE_RULE_ERROR, charPositionInLine, msg, upperCaseRuleName);
661                }
662                else {
663                    errorMessage = new ParseErrorMessage(lineNumber, JAVADOC_PARSE_TOKEN_ERROR,
664                            charPositionInLine, msg, charPositionInLine);
665                }
666            }
667        }
668    }
669
670    /**
671     * Contains result of parsing javadoc comment: DetailNode tree and parse
672     * error message.
673     */
674    private static class ParseStatus
675    {
676        /**
677         * DetailNode tree (is null if parsing fails)
678         */
679        private DetailNode tree;
680
681        /**
682         * Parse error message (is null if parsing is successful)
683         */
684        private ParseErrorMessage parseErrorMessage;
685
686        public DetailNode getTree()
687        {
688            return tree;
689        }
690
691        public void setTree(DetailNode tree)
692        {
693            this.tree = tree;
694        }
695
696        public ParseErrorMessage getParseErrorMessage()
697        {
698            return parseErrorMessage;
699        }
700
701        public void setParseErrorMessage(ParseErrorMessage parseErrorMessage)
702        {
703            this.parseErrorMessage = parseErrorMessage;
704        }
705
706    }
707
708    /**
709     * Contains information about parse error message.
710     */
711    private static class ParseErrorMessage
712    {
713        /**
714         * Line number where parse error occurred.
715         */
716        private int lineNumber;
717
718        /**
719         * Key for error message.
720         */
721        private String messageKey;
722
723        /**
724         * Error message arguments.
725         */
726        private Object[] messageArguments;
727
728        /**
729         * Initializes parse error message.
730         *
731         * @param lineNumber line number
732         * @param messageKey message key
733         * @param messageArguments message arguments
734         */
735        public ParseErrorMessage(int lineNumber, String messageKey, Object ... messageArguments)
736        {
737            this.lineNumber = lineNumber;
738            this.messageKey = messageKey;
739            this.messageArguments = messageArguments;
740        }
741
742        public int getLineNumber()
743        {
744            return lineNumber;
745        }
746
747        public String getMessageKey()
748        {
749            return messageKey;
750        }
751
752        public Object[] getMessageArguments()
753        {
754            return messageArguments;
755        }
756
757    }
758
759}