001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2018 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 java.io.File;
023import java.io.IOException;
024import java.io.Reader;
025import java.io.StringReader;
026import java.nio.charset.StandardCharsets;
027import java.util.Locale;
028
029import antlr.CommonHiddenStreamToken;
030import antlr.RecognitionException;
031import antlr.Token;
032import antlr.TokenStreamException;
033import antlr.TokenStreamHiddenTokenFilter;
034import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
035import com.puppycrawl.tools.checkstyle.api.DetailAST;
036import com.puppycrawl.tools.checkstyle.api.FileContents;
037import com.puppycrawl.tools.checkstyle.api.FileText;
038import com.puppycrawl.tools.checkstyle.api.TokenTypes;
039import com.puppycrawl.tools.checkstyle.grammars.GeneratedJavaLexer;
040import com.puppycrawl.tools.checkstyle.grammars.GeneratedJavaRecognizer;
041import com.puppycrawl.tools.checkstyle.utils.CommonUtils;
042
043/**
044 * Helper methods to parse java source files.
045 *
046 */
047public final class JavaParser {
048
049    /**
050     * Enum to be used for test if comments should be used.
051     */
052    public enum Options {
053
054        /**
055         * Comments nodes should be processed.
056         */
057        WITH_COMMENTS,
058
059        /**
060         * Comments nodes should be ignored.
061         */
062        WITHOUT_COMMENTS
063
064    }
065
066    /** Stop instances being created. **/
067    private JavaParser() {
068    }
069
070    /**
071     * Static helper method to parses a Java source file.
072     * @param contents contains the contents of the file
073     * @return the root of the AST
074     * @throws CheckstyleException if the contents is not a valid Java source
075     */
076    public static DetailAST parse(FileContents contents)
077            throws CheckstyleException {
078        final String fullText = contents.getText().getFullText().toString();
079        final Reader reader = new StringReader(fullText);
080        final GeneratedJavaLexer lexer = new GeneratedJavaLexer(reader);
081        lexer.setCommentListener(contents);
082        lexer.setTokenObjectClass("antlr.CommonHiddenStreamToken");
083
084        final TokenStreamHiddenTokenFilter filter = new TokenStreamHiddenTokenFilter(lexer);
085        filter.hide(TokenTypes.SINGLE_LINE_COMMENT);
086        filter.hide(TokenTypes.BLOCK_COMMENT_BEGIN);
087
088        final GeneratedJavaRecognizer parser = new GeneratedJavaRecognizer(filter);
089        parser.setFilename(contents.getFileName());
090        parser.setASTNodeClass(DetailAST.class.getName());
091        try {
092            parser.compilationUnit();
093        }
094        catch (RecognitionException | TokenStreamException ex) {
095            final String exceptionMsg = String.format(Locale.ROOT,
096                "%s occurred while parsing file %s.",
097                ex.getClass().getSimpleName(), contents.getFileName());
098            throw new CheckstyleException(exceptionMsg, ex);
099        }
100
101        return (DetailAST) parser.getAST();
102    }
103
104    /**
105     * Parse a text and return the parse tree.
106     * @param text the text to parse
107     * @param options {@link Options} to control inclusion of comment nodes
108     * @return the root node of the parse tree
109     * @throws CheckstyleException if the text is not a valid Java source
110     */
111    public static DetailAST parseFileText(FileText text, Options options)
112            throws CheckstyleException {
113        final FileContents contents = new FileContents(text);
114        DetailAST ast = parse(contents);
115        if (options == Options.WITH_COMMENTS) {
116            ast = appendHiddenCommentNodes(ast);
117        }
118        return ast;
119    }
120
121    /**
122     * Parses Java source file.
123     * @param file the file to parse
124     * @param options {@link Options} to control inclusion of comment nodes
125     * @return DetailAST tree
126     * @throws IOException if the file could not be read
127     * @throws CheckstyleException if the file is not a valid Java source file
128     */
129    public static DetailAST parseFile(File file, Options options)
130            throws IOException, CheckstyleException {
131        final FileText text = new FileText(file.getAbsoluteFile(),
132            System.getProperty("file.encoding", StandardCharsets.UTF_8.name()));
133        return parseFileText(text, options);
134    }
135
136    /**
137     * Appends comment nodes to existing AST.
138     * It traverses each node in AST, looks for hidden comment tokens
139     * and appends found comment tokens as nodes in AST.
140     * @param root of AST
141     * @return root of AST with comment nodes
142     */
143    public static DetailAST appendHiddenCommentNodes(DetailAST root) {
144        DetailAST result = root;
145        DetailAST curNode = root;
146        DetailAST lastNode = root;
147
148        while (curNode != null) {
149            if (isPositionGreater(curNode, lastNode)) {
150                lastNode = curNode;
151            }
152
153            CommonHiddenStreamToken tokenBefore = curNode.getHiddenBefore();
154            DetailAST currentSibling = curNode;
155            while (tokenBefore != null) {
156                final DetailAST newCommentNode =
157                         createCommentAstFromToken(tokenBefore);
158
159                currentSibling.addPreviousSibling(newCommentNode);
160
161                if (currentSibling == result) {
162                    result = newCommentNode;
163                }
164
165                currentSibling = newCommentNode;
166                tokenBefore = tokenBefore.getHiddenBefore();
167            }
168
169            DetailAST toVisit = curNode.getFirstChild();
170            while (curNode != null && toVisit == null) {
171                toVisit = curNode.getNextSibling();
172                if (toVisit == null) {
173                    curNode = curNode.getParent();
174                }
175            }
176            curNode = toVisit;
177        }
178        if (lastNode != null) {
179            CommonHiddenStreamToken tokenAfter = lastNode.getHiddenAfter();
180            DetailAST currentSibling = lastNode;
181            while (tokenAfter != null) {
182                final DetailAST newCommentNode =
183                        createCommentAstFromToken(tokenAfter);
184
185                currentSibling.addNextSibling(newCommentNode);
186
187                currentSibling = newCommentNode;
188                tokenAfter = tokenAfter.getHiddenAfter();
189            }
190        }
191        return result;
192    }
193
194    /**
195     * Checks if position of first DetailAST is greater than position of
196     * second DetailAST. Position is line number and column number in source file.
197     * @param ast1 first DetailAST node
198     * @param ast2 second DetailAST node
199     * @return true if position of ast1 is greater than position of ast2
200     */
201    private static boolean isPositionGreater(DetailAST ast1, DetailAST ast2) {
202        boolean isGreater = ast1.getLineNo() > ast2.getLineNo();
203        if (!isGreater && ast1.getLineNo() == ast2.getLineNo()) {
204            isGreater = ast1.getColumnNo() > ast2.getColumnNo();
205        }
206        return isGreater;
207    }
208
209    /**
210     * Create comment AST from token. Depending on token type
211     * SINGLE_LINE_COMMENT or BLOCK_COMMENT_BEGIN is created.
212     * @param token to create the AST
213     * @return DetailAST of comment node
214     */
215    private static DetailAST createCommentAstFromToken(Token token) {
216        final DetailAST commentAst;
217        if (token.getType() == TokenTypes.SINGLE_LINE_COMMENT) {
218            commentAst = createSlCommentNode(token);
219        }
220        else {
221            commentAst = CommonUtils.createBlockCommentNode(token);
222        }
223        return commentAst;
224    }
225
226    /**
227     * Create single-line comment from token.
228     * @param token to create the AST
229     * @return DetailAST with SINGLE_LINE_COMMENT type
230     */
231    private static DetailAST createSlCommentNode(Token token) {
232        final DetailAST slComment = new DetailAST();
233        slComment.setType(TokenTypes.SINGLE_LINE_COMMENT);
234        slComment.setText("//");
235
236        // column counting begins from 0
237        slComment.setColumnNo(token.getColumn() - 1);
238        slComment.setLineNo(token.getLine());
239
240        final DetailAST slCommentContent = new DetailAST();
241        slCommentContent.setType(TokenTypes.COMMENT_CONTENT);
242
243        // column counting begins from 0
244        // plus length of '//'
245        slCommentContent.setColumnNo(token.getColumn() - 1 + 2);
246        slCommentContent.setLineNo(token.getLine());
247        slCommentContent.setText(token.getText());
248
249        slComment.addChild(slCommentContent);
250        return slComment;
251    }
252
253}