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