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        if (parseTree.getChildCount() == 0
305                || "Text".equals(getNodeClassNameWithoutContext(parseTree))) {
306            node.setText(parseTree.getText());
307        }
308        else {
309            node.setText(getFormattedNodeClassNameWithoutContext(parseTree));
310        }
311        node.setColumnNumber(getColumn(parseTree));
312        node.setLineNumber(getLine(parseTree) + blockCommentLineNumber);
313        node.setIndex(index);
314        node.setType(getTokenType(parseTree));
315        node.setParent(parent);
316        node.setChildren((DetailNode[]) new JavadocNodeImpl[parseTree.getChildCount()]);
317        return node;
318    }
319
320    /**
321     * Adjust first line nodes to javadoc indent.
322     * @param tree DetailNode tree root
323     * @param javadocColumnNumber javadoc indent
324     */
325    private void adjustFirstLineToJavadocIndent(DetailNode tree, int javadocColumnNumber) {
326        if (tree.getLineNumber() == blockCommentLineNumber) {
327            ((JavadocNodeImpl) tree).setColumnNumber(tree.getColumnNumber() + javadocColumnNumber);
328            final DetailNode[] children = tree.getChildren();
329            for (DetailNode child : children) {
330                adjustFirstLineToJavadocIndent(child, javadocColumnNumber);
331            }
332        }
333    }
334
335    /**
336     * Gets line number from ParseTree node.
337     * @param tree
338     *        ParseTree node
339     * @return line number
340     */
341    private static int getLine(ParseTree tree) {
342        final int line;
343        if (tree instanceof TerminalNode) {
344            line = ((TerminalNode) tree).getSymbol().getLine() - 1;
345        }
346        else {
347            final ParserRuleContext rule = (ParserRuleContext) tree;
348            line = rule.start.getLine() - 1;
349        }
350        return line;
351    }
352
353    /**
354     * Gets column number from ParseTree node.
355     * @param tree
356     *        ParseTree node
357     * @return column number
358     */
359    private static int getColumn(ParseTree tree) {
360        final int column;
361        if (tree instanceof TerminalNode) {
362            column = ((TerminalNode) tree).getSymbol().getCharPositionInLine();
363        }
364        else {
365            final ParserRuleContext rule = (ParserRuleContext) tree;
366            column = rule.start.getCharPositionInLine();
367        }
368        return column;
369    }
370
371    /**
372     * Gets next sibling of ParseTree node.
373     * @param node ParseTree node
374     * @return next sibling of ParseTree node.
375     */
376    private static ParseTree getNextSibling(ParseTree node) {
377        ParseTree nextSibling = null;
378
379        if (node.getParent() != null) {
380            final ParseTree parent = node.getParent();
381            final int childCount = parent.getChildCount();
382
383            int index = 0;
384            while (true) {
385                final ParseTree currentNode = parent.getChild(index);
386                if (currentNode.equals(node)) {
387                    if (index != childCount - 1) {
388                        nextSibling = parent.getChild(index + 1);
389                    }
390                    break;
391                }
392                index++;
393            }
394        }
395        return nextSibling;
396    }
397
398    /**
399     * Gets token type of ParseTree node from JavadocTokenTypes class.
400     * @param node ParseTree node.
401     * @return token type from JavadocTokenTypes
402     */
403    private static int getTokenType(ParseTree node) {
404        final int tokenType;
405
406        if (node.getChildCount() == 0) {
407            tokenType = ((TerminalNode) node).getSymbol().getType();
408        }
409        else {
410            final String className = getNodeClassNameWithoutContext(node);
411            final String typeName =
412                    CaseFormat.UPPER_CAMEL.to(CaseFormat.UPPER_UNDERSCORE, className);
413            tokenType = JavadocUtils.getTokenId(typeName);
414        }
415
416        return tokenType;
417    }
418
419    /**
420     * Gets class name of ParseTree node and removes 'Context' postfix at the
421     * end and formats it.
422     * @param node {@code ParseTree} node whose class name is to be formatted and returned
423     * @return uppercased class name without the word 'Context' and with appropriately
424     *     inserted underscores
425     */
426    private static String getFormattedNodeClassNameWithoutContext(ParseTree node) {
427        final String classNameWithoutContext = getNodeClassNameWithoutContext(node);
428        return CaseFormat.UPPER_CAMEL.to(CaseFormat.UPPER_UNDERSCORE, classNameWithoutContext);
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     * Custom error listener for JavadocParser that prints user readable errors.
447     */
448    private static class DescriptiveErrorListener extends BaseErrorListener {
449
450        /**
451         * Offset is line number of beginning of the Javadoc comment. Log
452         * messages should have line number in scope of file, not in scope of
453         * Javadoc comment.
454         */
455        private int offset;
456
457        /**
458         * Error message that appeared while parsing.
459         */
460        private ParseErrorMessage errorMessage;
461
462        /**
463         * Getter for error message during parsing.
464         * @return Error message during parsing.
465         */
466        private ParseErrorMessage getErrorMessage() {
467            return errorMessage;
468        }
469
470        /**
471         * Sets offset. Offset is line number of beginning of the Javadoc
472         * comment. Log messages should have line number in scope of file, not
473         * in scope of Javadoc comment.
474         * @param offset
475         *        offset line number
476         */
477        public void setOffset(int offset) {
478            this.offset = offset;
479        }
480
481        /**
482         * Logs parser errors in Checkstyle manner. Parser can generate error
483         * messages. There is special error that parser can generate. It is
484         * missed close HTML tag. This case is special because parser prints
485         * error like {@code "no viable alternative at input 'b \n *\n'"} and it
486         * is not clear that error is about missed close HTML tag. Other error
487         * messages are not special and logged simply as "Parse Error...".
488         *
489         * <p>{@inheritDoc}
490         */
491        @Override
492        public void syntaxError(
493                Recognizer<?, ?> recognizer, Object offendingSymbol,
494                int line, int charPositionInLine,
495                String msg, RecognitionException ex) {
496            final int lineNumber = offset + line;
497            final Token token = (Token) offendingSymbol;
498
499            if (MSG_JAVADOC_MISSED_HTML_CLOSE.equals(msg)) {
500                errorMessage = new ParseErrorMessage(lineNumber,
501                        MSG_JAVADOC_MISSED_HTML_CLOSE, charPositionInLine, token.getText());
502
503                throw new IllegalArgumentException(msg);
504            }
505            else if (MSG_JAVADOC_WRONG_SINGLETON_TAG.equals(msg)) {
506                errorMessage = new ParseErrorMessage(lineNumber,
507                        MSG_JAVADOC_WRONG_SINGLETON_TAG, charPositionInLine, token.getText());
508
509                throw new IllegalArgumentException(msg);
510            }
511            else {
512                final int ruleIndex = ex.getCtx().getRuleIndex();
513                final String ruleName = recognizer.getRuleNames()[ruleIndex];
514                final String upperCaseRuleName = CaseFormat.UPPER_CAMEL.to(
515                        CaseFormat.UPPER_UNDERSCORE, ruleName);
516
517                errorMessage = new ParseErrorMessage(lineNumber,
518                        MSG_JAVADOC_PARSE_RULE_ERROR, charPositionInLine, msg, upperCaseRuleName);
519            }
520        }
521    }
522
523    /**
524     * Contains result of parsing javadoc comment: DetailNode tree and parse
525     * error message.
526     */
527    public static class ParseStatus {
528        /**
529         * DetailNode tree (is null if parsing fails).
530         */
531        private DetailNode tree;
532
533        /**
534         * Parse error message (is null if parsing is successful).
535         */
536        private ParseErrorMessage parseErrorMessage;
537
538        /**
539         * Getter for DetailNode tree.
540         * @return DetailNode tree if parsing was successful, null otherwise.
541         */
542        public DetailNode getTree() {
543            return tree;
544        }
545
546        /**
547         * Sets DetailNode tree.
548         * @param tree DetailNode tree.
549         */
550        public void setTree(DetailNode tree) {
551            this.tree = tree;
552        }
553
554        /**
555         * Getter for error message during parsing.
556         * @return Error message if parsing was unsuccessful, null otherwise.
557         */
558        public ParseErrorMessage getParseErrorMessage() {
559            return parseErrorMessage;
560        }
561
562        /**
563         * Sets parse error message.
564         * @param parseErrorMessage Parse error message.
565         */
566        public void setParseErrorMessage(ParseErrorMessage parseErrorMessage) {
567            this.parseErrorMessage = parseErrorMessage;
568        }
569
570    }
571
572    /**
573     * Contains information about parse error message.
574     */
575    public static class ParseErrorMessage {
576        /**
577         * Line number where parse error occurred.
578         */
579        private final int lineNumber;
580
581        /**
582         * Key for error message.
583         */
584        private final String messageKey;
585
586        /**
587         * Error message arguments.
588         */
589        private final Object[] messageArguments;
590
591        /**
592         * Initializes parse error message.
593         *
594         * @param lineNumber line number
595         * @param messageKey message key
596         * @param messageArguments message arguments
597         */
598        ParseErrorMessage(int lineNumber, String messageKey, Object... messageArguments) {
599            this.lineNumber = lineNumber;
600            this.messageKey = messageKey;
601            this.messageArguments = messageArguments.clone();
602        }
603
604        /**
605         * Getter for line number where parse error occurred.
606         * @return Line number where parse error occurred.
607         */
608        public int getLineNumber() {
609            return lineNumber;
610        }
611
612        /**
613         * Getter for key for error message.
614         * @return Key for error message.
615         */
616        public String getMessageKey() {
617            return messageKey;
618        }
619
620        /**
621         * Getter for error message arguments.
622         * @return Array of error message arguments.
623         */
624        public Object[] getMessageArguments() {
625            return messageArguments.clone();
626        }
627    }
628
629}