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