001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2024 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.nio.charset.StandardCharsets;
025import java.util.List;
026import java.util.ListIterator;
027import java.util.Locale;
028
029import org.antlr.v4.runtime.BaseErrorListener;
030import org.antlr.v4.runtime.CharStream;
031import org.antlr.v4.runtime.CharStreams;
032import org.antlr.v4.runtime.CommonToken;
033import org.antlr.v4.runtime.CommonTokenStream;
034import org.antlr.v4.runtime.RecognitionException;
035import org.antlr.v4.runtime.Recognizer;
036import org.antlr.v4.runtime.Token;
037
038import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
039import com.puppycrawl.tools.checkstyle.api.DetailAST;
040import com.puppycrawl.tools.checkstyle.api.FileContents;
041import com.puppycrawl.tools.checkstyle.api.FileText;
042import com.puppycrawl.tools.checkstyle.api.TokenTypes;
043import com.puppycrawl.tools.checkstyle.grammar.CompositeLexerContextCache;
044import com.puppycrawl.tools.checkstyle.grammar.java.JavaLanguageLexer;
045import com.puppycrawl.tools.checkstyle.grammar.java.JavaLanguageParser;
046import com.puppycrawl.tools.checkstyle.utils.ParserUtil;
047
048/**
049 * Helper methods to parse java source files.
050 *
051 */
052// -@cs[ClassDataAbstractionCoupling] No way to split up class usage.
053public final class JavaParser {
054
055    /**
056     * Enum to be used for test if comments should be used.
057     */
058    public enum Options {
059
060        /**
061         * Comments nodes should be processed.
062         */
063        WITH_COMMENTS,
064
065        /**
066         * Comments nodes should be ignored.
067         */
068        WITHOUT_COMMENTS,
069
070    }
071
072    /** Stop instances being created. **/
073    private JavaParser() {
074    }
075
076    /**
077     * Static helper method to parses a Java source file.
078     *
079     * @param contents contains the contents of the file
080     * @return the root of the AST
081     * @throws CheckstyleException if the contents is not a valid Java source
082     */
083    public static DetailAST parse(FileContents contents)
084            throws CheckstyleException {
085        final String fullText = contents.getText().getFullText().toString();
086        final CharStream codePointCharStream = CharStreams.fromString(fullText);
087        final JavaLanguageLexer lexer = new JavaLanguageLexer(codePointCharStream, true);
088        final CompositeLexerContextCache contextCache = new CompositeLexerContextCache(lexer);
089        lexer.setCommentListener(contents);
090        lexer.setContextCache(contextCache);
091
092        final CommonTokenStream tokenStream = new CommonTokenStream(lexer);
093        final JavaLanguageParser parser =
094                new JavaLanguageParser(tokenStream, JavaLanguageParser.CLEAR_DFA_LIMIT);
095        parser.setErrorHandler(new CheckstyleParserErrorStrategy());
096        parser.removeErrorListeners();
097        parser.addErrorListener(new CheckstyleErrorListener());
098
099        final JavaLanguageParser.CompilationUnitContext compilationUnit;
100        try {
101            compilationUnit = parser.compilationUnit();
102        }
103        catch (IllegalStateException ex) {
104            final String exceptionMsg = String.format(Locale.ROOT,
105                "%s occurred while parsing file %s.",
106                ex.getClass().getSimpleName(), contents.getFileName());
107            throw new CheckstyleException(exceptionMsg, ex);
108        }
109
110        return new JavaAstVisitor(tokenStream).visit(compilationUnit);
111    }
112
113    /**
114     * Parse a text and return the parse tree.
115     *
116     * @param text the text to parse
117     * @param options {@link Options} to control inclusion of comment nodes
118     * @return the root node of the parse tree
119     * @throws CheckstyleException if the text is not a valid Java source
120     */
121    public static DetailAST parseFileText(FileText text, Options options)
122            throws CheckstyleException {
123        final FileContents contents = new FileContents(text);
124        final DetailAST ast = parse(contents);
125        if (options == Options.WITH_COMMENTS) {
126            appendHiddenCommentNodes(ast);
127        }
128        return ast;
129    }
130
131    /**
132     * Parses Java source file.
133     *
134     * @param file the file to parse
135     * @param options {@link Options} to control inclusion of comment nodes
136     * @return DetailAST tree
137     * @throws IOException if the file could not be read
138     * @throws CheckstyleException if the file is not a valid Java source file
139     */
140    public static DetailAST parseFile(File file, Options options)
141            throws IOException, CheckstyleException {
142        final FileText text = new FileText(file,
143            StandardCharsets.UTF_8.name());
144        return parseFileText(text, options);
145    }
146
147    /**
148     * Appends comment nodes to existing AST.
149     * It traverses each node in AST, looks for hidden comment tokens
150     * and appends found comment tokens as nodes in AST.
151     *
152     * @param root of AST
153     * @return root of AST with comment nodes
154     */
155    public static DetailAST appendHiddenCommentNodes(DetailAST root) {
156        DetailAST curNode = root;
157        DetailAST lastNode = root;
158
159        while (curNode != null) {
160            lastNode = curNode;
161
162            final List<Token> hiddenBefore = ((DetailAstImpl) curNode).getHiddenBefore();
163            if (hiddenBefore != null) {
164                DetailAST currentSibling = curNode;
165
166                final ListIterator<Token> reverseCommentsIterator =
167                        hiddenBefore.listIterator(hiddenBefore.size());
168
169                while (reverseCommentsIterator.hasPrevious()) {
170                    final DetailAST newCommentNode =
171                            createCommentAstFromToken((CommonToken)
172                                    reverseCommentsIterator.previous());
173                    ((DetailAstImpl) currentSibling).addPreviousSibling(newCommentNode);
174
175                    currentSibling = newCommentNode;
176                }
177            }
178
179            DetailAST toVisit = curNode.getFirstChild();
180            while (curNode != null && toVisit == null) {
181                toVisit = curNode.getNextSibling();
182                curNode = curNode.getParent();
183            }
184            curNode = toVisit;
185        }
186        if (lastNode != null) {
187            final List<Token> hiddenAfter = ((DetailAstImpl) lastNode).getHiddenAfter();
188            if (hiddenAfter != null) {
189                DetailAST currentSibling = lastNode;
190                for (Token token : hiddenAfter) {
191                    final DetailAST newCommentNode =
192                            createCommentAstFromToken((CommonToken) token);
193
194                    ((DetailAstImpl) currentSibling).addNextSibling(newCommentNode);
195
196                    currentSibling = newCommentNode;
197                }
198            }
199        }
200        return root;
201    }
202
203    /**
204     * Create comment AST from token. Depending on token type
205     * SINGLE_LINE_COMMENT or BLOCK_COMMENT_BEGIN is created.
206     *
207     * @param token to create the AST
208     * @return DetailAST of comment node
209     */
210    private static DetailAST createCommentAstFromToken(CommonToken token) {
211        final DetailAST commentAst;
212        if (token.getType() == TokenTypes.SINGLE_LINE_COMMENT) {
213            commentAst = createSlCommentNode(token);
214        }
215        else {
216            commentAst = ParserUtil.createBlockCommentNode(token);
217        }
218        return commentAst;
219    }
220
221    /**
222     * Create single-line comment from token.
223     *
224     * @param token to create the AST
225     * @return DetailAST with SINGLE_LINE_COMMENT type
226     */
227    private static DetailAST createSlCommentNode(Token token) {
228        final DetailAstImpl slComment = new DetailAstImpl();
229        slComment.setType(TokenTypes.SINGLE_LINE_COMMENT);
230        slComment.setText("//");
231
232        slComment.setColumnNo(token.getCharPositionInLine());
233        slComment.setLineNo(token.getLine());
234
235        final DetailAstImpl slCommentContent = new DetailAstImpl();
236        slCommentContent.setType(TokenTypes.COMMENT_CONTENT);
237
238        // plus length of '//'
239        slCommentContent.setColumnNo(token.getCharPositionInLine() + 2);
240        slCommentContent.setLineNo(token.getLine());
241        slCommentContent.setText(token.getText());
242
243        slComment.addChild(slCommentContent);
244        return slComment;
245    }
246
247    /**
248     * Custom error listener to provide detailed exception message.
249     */
250    private static final class CheckstyleErrorListener extends BaseErrorListener {
251
252        @Override
253        public void syntaxError(Recognizer<?, ?> recognizer, Object offendingSymbol,
254                                int line, int charPositionInLine,
255                                String msg, RecognitionException ex) {
256            final String message = line + ":" + charPositionInLine + ": " + msg;
257            throw new IllegalStateException(message, ex);
258        }
259    }
260}