001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2017 the original author or authors.
004//
005// This library is free software; you can redistribute it and/or
006// modify it under the terms of the GNU Lesser General Public
007// License as published by the Free Software Foundation; either
008// version 2.1 of the License, or (at your option) any later version.
009//
010// This library is distributed in the hope that it will be useful,
011// but WITHOUT ANY WARRANTY; without even the implied warranty of
012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013// Lesser General Public License for more details.
014//
015// You should have received a copy of the GNU Lesser General Public
016// License along with this library; if not, write to the Free Software
017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
018////////////////////////////////////////////////////////////////////////////////
019
020package com.puppycrawl.tools.checkstyle;
021
022import org.antlr.v4.runtime.ANTLRInputStream;
023import org.antlr.v4.runtime.BailErrorStrategy;
024import org.antlr.v4.runtime.BaseErrorListener;
025import org.antlr.v4.runtime.CommonTokenStream;
026import org.antlr.v4.runtime.ParserRuleContext;
027import org.antlr.v4.runtime.RecognitionException;
028import org.antlr.v4.runtime.Recognizer;
029import org.antlr.v4.runtime.Token;
030import org.antlr.v4.runtime.misc.ParseCancellationException;
031import org.antlr.v4.runtime.tree.ParseTree;
032import org.antlr.v4.runtime.tree.TerminalNode;
033
034import com.google.common.base.CaseFormat;
035import com.puppycrawl.tools.checkstyle.api.DetailAST;
036import com.puppycrawl.tools.checkstyle.api.DetailNode;
037import com.puppycrawl.tools.checkstyle.api.JavadocTokenTypes;
038import com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocNodeImpl;
039import com.puppycrawl.tools.checkstyle.grammars.javadoc.JavadocLexer;
040import com.puppycrawl.tools.checkstyle.grammars.javadoc.JavadocParser;
041import com.puppycrawl.tools.checkstyle.utils.JavadocUtils;
042
043/**
044 * Used for parsing Javadoc comment as DetailNode tree.
045 * @author bizmailov
046 *
047 */
048public class JavadocDetailNodeParser {
049
050    /**
051     * Message key of error message. Missed close HTML tag breaks structure
052     * of parse tree, so parser stops parsing and generates such error
053     * message. This case is special because parser prints error like
054     * {@code "no viable alternative at input 'b \n *\n'"} and it is not
055     * clear that error is about missed close HTML tag.
056     */
057    public static final String MSG_JAVADOC_MISSED_HTML_CLOSE = "javadoc.missed.html.close";
058
059    /**
060     * Message key of error message.
061     */
062    public static final String MSG_JAVADOC_WRONG_SINGLETON_TAG =
063        "javadoc.wrong.singleton.html.tag";
064
065    /**
066     * Parse error while rule recognition.
067     */
068    public static final String MSG_JAVADOC_PARSE_RULE_ERROR = "javadoc.parse.rule.error";
069
070    /**
071     * Error message key for common javadoc errors.
072     */
073    public static final String MSG_KEY_PARSE_ERROR = "javadoc.parse.error";
074
075    /**
076     * Unrecognized error from antlr parser.
077     */
078    public static final String MSG_KEY_UNRECOGNIZED_ANTLR_ERROR =
079            "javadoc.unrecognized.antlr.error";
080
081    /** Symbols with which javadoc starts. */
082    private static final String JAVADOC_START = "/**";
083
084    /**
085     * Line number of the Block comment AST that is being parsed.
086     */
087    private int blockCommentLineNumber;
088
089    /**
090     * Custom error listener.
091     */
092    private DescriptiveErrorListener errorListener;
093
094    /**
095     * Parses Javadoc comment as DetailNode tree.
096     * @param javadocCommentAst
097     *        DetailAST of Javadoc comment
098     * @return DetailNode tree of Javadoc comment
099     */
100    public ParseStatus parseJavadocAsDetailNode(DetailAST javadocCommentAst) {
101        blockCommentLineNumber = javadocCommentAst.getLineNo();
102
103        final String javadocComment = JavadocUtils.getJavadocCommentContent(javadocCommentAst);
104
105        // Use a new error listener each time to be able to use
106        // one check instance for multiple files to be checked
107        // without getting side effects.
108        errorListener = new DescriptiveErrorListener();
109
110        // Log messages should have line number in scope of file,
111        // not in scope of Javadoc comment.
112        // Offset is line number of beginning of Javadoc comment.
113        errorListener.setOffset(javadocCommentAst.getLineNo() - 1);
114
115        final ParseStatus result = new ParseStatus();
116
117        try {
118            final ParseTree parseTree = parseJavadocAsParseTree(javadocComment);
119
120            final DetailNode tree = convertParseTreeToDetailNode(parseTree);
121            // adjust first line to indent of /**
122            adjustFirstLineToJavadocIndent(tree,
123                        javadocCommentAst.getColumnNo()
124                                + JAVADOC_START.length());
125            result.setTree(tree);
126        }
127        catch (ParseCancellationException | IllegalArgumentException ex) {
128            // If syntax error occurs then message is printed by error listener
129            // and parser throws this runtime exception to stop parsing.
130            // Just stop processing current Javadoc comment.
131            ParseErrorMessage parseErrorMessage = errorListener.getErrorMessage();
132
133            // There are cases when antlr error listener does not handle syntax error
134            if (parseErrorMessage == null) {
135                parseErrorMessage = new ParseErrorMessage(javadocCommentAst.getLineNo(),
136                        MSG_KEY_UNRECOGNIZED_ANTLR_ERROR,
137                        javadocCommentAst.getColumnNo(), ex.getMessage());
138            }
139
140            result.setParseErrorMessage(parseErrorMessage);
141        }
142
143        return result;
144    }
145
146    /**
147     * Parses block comment content as javadoc comment.
148     * @param blockComment
149     *        block comment content.
150     * @return parse tree
151     */
152    private ParseTree parseJavadocAsParseTree(String blockComment) {
153        final ANTLRInputStream input = new ANTLRInputStream(blockComment);
154
155        final JavadocLexer lexer = new JavadocLexer(input);
156
157        // remove default error listeners
158        lexer.removeErrorListeners();
159
160        // add custom error listener that logs parsing errors
161        lexer.addErrorListener(errorListener);
162
163        final CommonTokenStream tokens = new CommonTokenStream(lexer);
164
165        final JavadocParser parser = new JavadocParser(tokens);
166
167        // remove default error listeners
168        parser.removeErrorListeners();
169
170        // add custom error listener that logs syntax errors
171        parser.addErrorListener(errorListener);
172
173        // This strategy stops parsing when parser error occurs.
174        // By default it uses Error Recover Strategy which is slow and useless.
175        parser.setErrorHandler(new BailErrorStrategy());
176
177        return parser.javadoc();
178    }
179
180    /**
181     * Converts ParseTree (that is generated by ANTLRv4) to DetailNode tree.
182     *
183     * @param parseTreeNode root node of ParseTree
184     * @return root of DetailNode tree
185     */
186    private DetailNode convertParseTreeToDetailNode(ParseTree parseTreeNode) {
187        final JavadocNodeImpl rootJavadocNode = createRootJavadocNode(parseTreeNode);
188
189        JavadocNodeImpl currentJavadocParent = rootJavadocNode;
190        ParseTree parseTreeParent = parseTreeNode;
191
192        while (currentJavadocParent != null) {
193            // remove unnecessary children tokens
194            if (currentJavadocParent.getType() == JavadocTokenTypes.TEXT) {
195                currentJavadocParent
196                        .setChildren((DetailNode[]) JavadocNodeImpl.EMPTY_DETAIL_NODE_ARRAY);
197            }
198
199            final JavadocNodeImpl[] children =
200                    (JavadocNodeImpl[]) currentJavadocParent.getChildren();
201
202            insertChildrenNodes(children, parseTreeParent);
203
204            if (children.length > 0) {
205                currentJavadocParent = children[0];
206                parseTreeParent = parseTreeParent.getChild(0);
207            }
208            else {
209                JavadocNodeImpl nextJavadocSibling = (JavadocNodeImpl) JavadocUtils
210                        .getNextSibling(currentJavadocParent);
211
212                ParseTree nextParseTreeSibling = getNextSibling(parseTreeParent);
213
214                if (nextJavadocSibling == null) {
215                    JavadocNodeImpl tempJavadocParent =
216                            (JavadocNodeImpl) currentJavadocParent.getParent();
217
218                    ParseTree tempParseTreeParent = parseTreeParent.getParent();
219
220                    while (nextJavadocSibling == null && tempJavadocParent != null) {
221
222                        nextJavadocSibling = (JavadocNodeImpl) JavadocUtils
223                                .getNextSibling(tempJavadocParent);
224
225                        nextParseTreeSibling = getNextSibling(tempParseTreeParent);
226
227                        tempJavadocParent = (JavadocNodeImpl) tempJavadocParent.getParent();
228                        tempParseTreeParent = tempParseTreeParent.getParent();
229                    }
230                }
231                currentJavadocParent = nextJavadocSibling;
232                parseTreeParent = nextParseTreeSibling;
233            }
234        }
235
236        return rootJavadocNode;
237    }
238
239    /**
240     * Creates child nodes for each node from 'nodes' array.
241     * @param parseTreeParent original ParseTree parent node
242     * @param nodes array of JavadocNodeImpl nodes
243     */
244    private void insertChildrenNodes(final JavadocNodeImpl[] nodes, ParseTree parseTreeParent) {
245        for (int i = 0; i < nodes.length; i++) {
246            final JavadocNodeImpl currentJavadocNode = nodes[i];
247            final ParseTree currentParseTreeNodeChild = parseTreeParent.getChild(i);
248            final JavadocNodeImpl[] subChildren =
249                    createChildrenNodes(currentJavadocNode, currentParseTreeNodeChild);
250            currentJavadocNode.setChildren((DetailNode[]) subChildren);
251        }
252    }
253
254    /**
255     * Creates children Javadoc nodes base on ParseTree node's children.
256     * @param parentJavadocNode node that will be parent for created children
257     * @param parseTreeNode original ParseTree node
258     * @return array of Javadoc nodes
259     */
260    private JavadocNodeImpl[]
261            createChildrenNodes(JavadocNodeImpl parentJavadocNode, ParseTree parseTreeNode) {
262        final JavadocNodeImpl[] children =
263                new JavadocNodeImpl[parseTreeNode.getChildCount()];
264
265        for (int j = 0; j < children.length; j++) {
266            final JavadocNodeImpl child =
267                    createJavadocNode(parseTreeNode.getChild(j), parentJavadocNode, j);
268
269            children[j] = child;
270        }
271        return children;
272    }
273
274    /**
275     * Creates root JavadocNodeImpl node base on ParseTree root node.
276     * @param parseTreeNode ParseTree root node
277     * @return root Javadoc node
278     */
279    private JavadocNodeImpl createRootJavadocNode(ParseTree parseTreeNode) {
280        final JavadocNodeImpl rootJavadocNode = createJavadocNode(parseTreeNode, null, -1);
281
282        final int childCount = parseTreeNode.getChildCount();
283        final JavadocNodeImpl[] children = new JavadocNodeImpl[childCount];
284
285        for (int i = 0; i < childCount; i++) {
286            final JavadocNodeImpl child = createJavadocNode(parseTreeNode.getChild(i),
287                    rootJavadocNode, i);
288            children[i] = child;
289        }
290        rootJavadocNode.setChildren((DetailNode[]) children);
291        return rootJavadocNode;
292    }
293
294    /**
295     * Creates JavadocNodeImpl node on base of ParseTree node.
296     *
297     * @param parseTree ParseTree node
298     * @param parent DetailNode that will be parent of new node
299     * @param index child index that has new node
300     * @return JavadocNodeImpl node on base of ParseTree node.
301     */
302    private JavadocNodeImpl createJavadocNode(ParseTree parseTree, DetailNode parent, int index) {
303        final JavadocNodeImpl node = new JavadocNodeImpl();
304        node.setText(parseTree.getText());
305        node.setColumnNumber(getColumn(parseTree));
306        node.setLineNumber(getLine(parseTree) + blockCommentLineNumber);
307        node.setIndex(index);
308        node.setType(getTokenType(parseTree));
309        node.setParent(parent);
310        node.setChildren((DetailNode[]) new JavadocNodeImpl[parseTree.getChildCount()]);
311        return node;
312    }
313
314    /**
315     * Adjust first line nodes to javadoc indent.
316     * @param tree DetailNode tree root
317     * @param javadocColumnNumber javadoc indent
318     */
319    private void adjustFirstLineToJavadocIndent(DetailNode tree, int javadocColumnNumber) {
320        if (tree.getLineNumber() == blockCommentLineNumber) {
321            ((JavadocNodeImpl) tree).setColumnNumber(tree.getColumnNumber() + javadocColumnNumber);
322            final DetailNode[] children = tree.getChildren();
323            for (DetailNode child : children) {
324                adjustFirstLineToJavadocIndent(child, javadocColumnNumber);
325            }
326        }
327    }
328
329    /**
330     * Gets line number from ParseTree node.
331     * @param tree
332     *        ParseTree node
333     * @return line number
334     */
335    private static int getLine(ParseTree tree) {
336        final int line;
337        if (tree instanceof TerminalNode) {
338            line = ((TerminalNode) tree).getSymbol().getLine() - 1;
339        }
340        else {
341            final ParserRuleContext rule = (ParserRuleContext) tree;
342            line = rule.start.getLine() - 1;
343        }
344        return line;
345    }
346
347    /**
348     * Gets column number from ParseTree node.
349     * @param tree
350     *        ParseTree node
351     * @return column number
352     */
353    private static int getColumn(ParseTree tree) {
354        final int column;
355        if (tree instanceof TerminalNode) {
356            column = ((TerminalNode) tree).getSymbol().getCharPositionInLine();
357        }
358        else {
359            final ParserRuleContext rule = (ParserRuleContext) tree;
360            column = rule.start.getCharPositionInLine();
361        }
362        return column;
363    }
364
365    /**
366     * Gets next sibling of ParseTree node.
367     * @param node ParseTree node
368     * @return next sibling of ParseTree node.
369     */
370    private static ParseTree getNextSibling(ParseTree node) {
371        ParseTree nextSibling = null;
372
373        if (node.getParent() != null) {
374            final ParseTree parent = node.getParent();
375            final int childCount = parent.getChildCount();
376
377            int index = 0;
378            while (true) {
379                final ParseTree currentNode = parent.getChild(index);
380                if (currentNode.equals(node)) {
381                    if (index != childCount - 1) {
382                        nextSibling = parent.getChild(index + 1);
383                    }
384                    break;
385                }
386                index++;
387            }
388        }
389        return nextSibling;
390    }
391
392    /**
393     * Gets token type of ParseTree node from JavadocTokenTypes class.
394     * @param node ParseTree node.
395     * @return token type from JavadocTokenTypes
396     */
397    private static int getTokenType(ParseTree node) {
398        final int tokenType;
399
400        if (node.getChildCount() == 0) {
401            tokenType = ((TerminalNode) node).getSymbol().getType();
402        }
403        else {
404            final String className = getNodeClassNameWithoutContext(node);
405            final String typeName =
406                    CaseFormat.UPPER_CAMEL.to(CaseFormat.UPPER_UNDERSCORE, className);
407            tokenType = JavadocUtils.getTokenId(typeName);
408        }
409
410        return tokenType;
411    }
412
413    /**
414     * Gets class name of ParseTree node and removes 'Context' postfix at the
415     * end.
416     * @param node
417     *        ParseTree node.
418     * @return class name without 'Context'
419     */
420    private static String getNodeClassNameWithoutContext(ParseTree node) {
421        final String className = node.getClass().getSimpleName();
422        // remove 'Context' at the end
423        final int contextLength = 7;
424        return className.substring(0, className.length() - contextLength);
425    }
426
427    /**
428     * Custom error listener for JavadocParser that prints user readable errors.
429     */
430    private static class DescriptiveErrorListener extends BaseErrorListener {
431
432        /**
433         * Offset is line number of beginning of the Javadoc comment. Log
434         * messages should have line number in scope of file, not in scope of
435         * Javadoc comment.
436         */
437        private int offset;
438
439        /**
440         * Error message that appeared while parsing.
441         */
442        private ParseErrorMessage errorMessage;
443
444        /**
445         * Getter for error message during parsing.
446         * @return Error message during parsing.
447         */
448        private ParseErrorMessage getErrorMessage() {
449            return errorMessage;
450        }
451
452        /**
453         * Sets offset. Offset is line number of beginning of the Javadoc
454         * comment. Log messages should have line number in scope of file, not
455         * in scope of Javadoc comment.
456         * @param offset
457         *        offset line number
458         */
459        public void setOffset(int offset) {
460            this.offset = offset;
461        }
462
463        /**
464         * Logs parser errors in Checkstyle manner. Parser can generate error
465         * messages. There is special error that parser can generate. It is
466         * missed close HTML tag. This case is special because parser prints
467         * error like {@code "no viable alternative at input 'b \n *\n'"} and it
468         * is not clear that error is about missed close HTML tag. Other error
469         * messages are not special and logged simply as "Parse Error...".
470         *
471         * <p>{@inheritDoc}
472         */
473        @Override
474        public void syntaxError(
475                Recognizer<?, ?> recognizer, Object offendingSymbol,
476                int line, int charPositionInLine,
477                String msg, RecognitionException ex) {
478            final int lineNumber = offset + line;
479            final Token token = (Token) offendingSymbol;
480
481            if (MSG_JAVADOC_MISSED_HTML_CLOSE.equals(msg)) {
482                errorMessage = new ParseErrorMessage(lineNumber,
483                        MSG_JAVADOC_MISSED_HTML_CLOSE, charPositionInLine, token.getText());
484
485                throw new IllegalArgumentException(msg);
486            }
487            else if (MSG_JAVADOC_WRONG_SINGLETON_TAG.equals(msg)) {
488                errorMessage = new ParseErrorMessage(lineNumber,
489                        MSG_JAVADOC_WRONG_SINGLETON_TAG, charPositionInLine, token.getText());
490
491                throw new IllegalArgumentException(msg);
492            }
493            else {
494                final int ruleIndex = ex.getCtx().getRuleIndex();
495                final String ruleName = recognizer.getRuleNames()[ruleIndex];
496                final String upperCaseRuleName = CaseFormat.UPPER_CAMEL.to(
497                        CaseFormat.UPPER_UNDERSCORE, ruleName);
498
499                errorMessage = new ParseErrorMessage(lineNumber,
500                        MSG_JAVADOC_PARSE_RULE_ERROR, charPositionInLine, msg, upperCaseRuleName);
501            }
502        }
503    }
504
505    /**
506     * Contains result of parsing javadoc comment: DetailNode tree and parse
507     * error message.
508     */
509    public static class ParseStatus {
510        /**
511         * DetailNode tree (is null if parsing fails).
512         */
513        private DetailNode tree;
514
515        /**
516         * Parse error message (is null if parsing is successful).
517         */
518        private ParseErrorMessage parseErrorMessage;
519
520        /**
521         * Getter for DetailNode tree.
522         * @return DetailNode tree if parsing was successful, null otherwise.
523         */
524        public DetailNode getTree() {
525            return tree;
526        }
527
528        /**
529         * Sets DetailNode tree.
530         * @param tree DetailNode tree.
531         */
532        public void setTree(DetailNode tree) {
533            this.tree = tree;
534        }
535
536        /**
537         * Getter for error message during parsing.
538         * @return Error message if parsing was unsuccessful, null otherwise.
539         */
540        public ParseErrorMessage getParseErrorMessage() {
541            return parseErrorMessage;
542        }
543
544        /**
545         * Sets parse error message.
546         * @param parseErrorMessage Parse error message.
547         */
548        public void setParseErrorMessage(ParseErrorMessage parseErrorMessage) {
549            this.parseErrorMessage = parseErrorMessage;
550        }
551
552    }
553
554    /**
555     * Contains information about parse error message.
556     */
557    public static class ParseErrorMessage {
558        /**
559         * Line number where parse error occurred.
560         */
561        private final int lineNumber;
562
563        /**
564         * Key for error message.
565         */
566        private final String messageKey;
567
568        /**
569         * Error message arguments.
570         */
571        private final Object[] messageArguments;
572
573        /**
574         * Initializes parse error message.
575         *
576         * @param lineNumber line number
577         * @param messageKey message key
578         * @param messageArguments message arguments
579         */
580        ParseErrorMessage(int lineNumber, String messageKey, Object... messageArguments) {
581            this.lineNumber = lineNumber;
582            this.messageKey = messageKey;
583            this.messageArguments = messageArguments.clone();
584        }
585
586        /**
587         * Getter for line number where parse error occurred.
588         * @return Line number where parse error occurred.
589         */
590        public int getLineNumber() {
591            return lineNumber;
592        }
593
594        /**
595         * Getter for key for error message.
596         * @return Key for error message.
597         */
598        public String getMessageKey() {
599            return messageKey;
600        }
601
602        /**
603         * Getter for error message arguments.
604         * @return Array of error message arguments.
605         */
606        public Object[] getMessageArguments() {
607            return messageArguments.clone();
608        }
609    }
610
611}