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}