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