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