001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2017 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.util.Locale;
025import java.util.regex.Pattern;
026
027import antlr.RecognitionException;
028import antlr.TokenStreamException;
029import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
030import com.puppycrawl.tools.checkstyle.api.DetailAST;
031import com.puppycrawl.tools.checkstyle.api.DetailNode;
032import com.puppycrawl.tools.checkstyle.api.FileContents;
033import com.puppycrawl.tools.checkstyle.api.FileText;
034import com.puppycrawl.tools.checkstyle.api.TokenTypes;
035import com.puppycrawl.tools.checkstyle.utils.JavadocUtils;
036import com.puppycrawl.tools.checkstyle.utils.TokenUtils;
037
038/**
039 * Class for printing AST to String.
040 * @author Vladislav Lisetskii
041 */
042public final class AstTreeStringPrinter {
043
044    /** Newline pattern. */
045    private static final Pattern NEWLINE = Pattern.compile("\n");
046    /** Return pattern. */
047    private static final Pattern RETURN = Pattern.compile("\r");
048    /** Tab pattern. */
049    private static final Pattern TAB = Pattern.compile("\t");
050
051    /** OS specific line separator. */
052    private static final String LINE_SEPARATOR = System.getProperty("line.separator");
053
054    /** Prevent instances. */
055    private AstTreeStringPrinter() {
056        // no code
057    }
058
059    /**
060     * Parse a file and print the parse tree.
061     * @param file the file to print.
062     * @param withComments true to include comments to AST
063     * @return the AST of the file in String form.
064     * @throws IOException if the file could not be read.
065     * @throws CheckstyleException if the file is not a Java source.
066     */
067    public static String printFileAst(File file, boolean withComments)
068            throws IOException, CheckstyleException {
069        return printTree(parseFile(file, withComments));
070    }
071
072    /**
073     * Prints full AST (java + comments + javadoc) of the java file.
074     * @param file java file
075     * @return Full tree
076     * @throws IOException Failed to open a file
077     * @throws CheckstyleException error while parsing the file
078     */
079    public static String printJavaAndJavadocTree(File file)
080            throws IOException, CheckstyleException {
081        final DetailAST tree = parseFile(file, true);
082        return printJavaAndJavadocTree(tree);
083    }
084
085    /**
086     * Prints full tree (java + comments + javadoc) of the DetailAST.
087     * @param ast root DetailAST
088     * @return Full tree
089     */
090    private static String printJavaAndJavadocTree(DetailAST ast) {
091        final StringBuilder messageBuilder = new StringBuilder();
092        DetailAST node = ast;
093        while (node != null) {
094            messageBuilder.append(getIndentation(node))
095                .append(getNodeInfo(node))
096                .append(LINE_SEPARATOR);
097            if (node.getType() == TokenTypes.COMMENT_CONTENT
098                    && JavadocUtils.isJavadocComment(node.getParent())) {
099                final String javadocTree = parseAndPrintJavadocTree(node);
100                messageBuilder.append(javadocTree);
101            }
102            else {
103                messageBuilder.append(printJavaAndJavadocTree(node.getFirstChild()));
104            }
105            node = node.getNextSibling();
106        }
107        return messageBuilder.toString();
108    }
109
110    /**
111     * Parses block comment as javadoc and prints its tree.
112     * @param node block comment begin
113     * @return string javadoc tree
114     */
115    private static String parseAndPrintJavadocTree(DetailAST node) {
116        final DetailAST javadocBlock = node.getParent();
117        final DetailNode tree = DetailNodeTreeStringPrinter.parseJavadocAsDetailNode(javadocBlock);
118
119        String baseIndentation = getIndentation(node);
120        baseIndentation = baseIndentation.substring(0, baseIndentation.length() - 2);
121        final String rootPrefix = baseIndentation + "   `--";
122        final String prefix = baseIndentation + "       ";
123        return DetailNodeTreeStringPrinter.printTree(tree, rootPrefix, prefix);
124    }
125
126    /**
127     * Parse a file and print the parse tree.
128     * @param text the text to parse.
129     * @param withComments true to include comments to AST
130     * @return the AST of the file in String form.
131     * @throws CheckstyleException if the file is not a Java source.
132     */
133    public static String printAst(FileText text, boolean withComments) throws CheckstyleException {
134        return printTree(parseFileText(text, withComments));
135    }
136
137    /**
138     * Print AST.
139     * @param ast the root AST node.
140     * @return string AST.
141     */
142    private static String printTree(DetailAST ast) {
143        final StringBuilder messageBuilder = new StringBuilder();
144        DetailAST node = ast;
145        while (node != null) {
146            messageBuilder.append(getIndentation(node))
147                    .append(getNodeInfo(node))
148                    .append(LINE_SEPARATOR)
149                    .append(printTree(node.getFirstChild()));
150            node = node.getNextSibling();
151        }
152        return messageBuilder.toString();
153    }
154
155    /**
156     * Get string representation of the node as token name,
157     * node text, line number and column number.
158     * @param node DetailAST
159     * @return node info
160     */
161    private static String getNodeInfo(DetailAST node) {
162        return TokenUtils.getTokenName(node.getType())
163                + " -> " + escapeAllControlChars(node.getText())
164                + " [" + node.getLineNo() + ':' + node.getColumnNo() + ']';
165    }
166
167    /**
168     * Get indentation for an AST node.
169     * @param ast the AST to get the indentation for.
170     * @return the indentation in String format.
171     */
172    private static String getIndentation(DetailAST ast) {
173        final boolean isLastChild = ast.getNextSibling() == null;
174        DetailAST node = ast;
175        final StringBuilder indentation = new StringBuilder();
176        while (node.getParent() != null) {
177            node = node.getParent();
178            if (node.getParent() == null) {
179                if (isLastChild) {
180                    // only ASCII symbols must be used due to
181                    // problems with running tests on Windows
182                    indentation.append("`--");
183                }
184                else {
185                    indentation.append("|--");
186                }
187            }
188            else {
189                if (node.getNextSibling() == null) {
190                    indentation.insert(0, "    ");
191                }
192                else {
193                    indentation.insert(0, "|   ");
194                }
195            }
196        }
197        return indentation.toString();
198    }
199
200    /**
201     * Replace all control chars with escaped symbols.
202     * @param text the String to process.
203     * @return the processed String with all control chars escaped.
204     */
205    private static String escapeAllControlChars(String text) {
206        final String textWithoutNewlines = NEWLINE.matcher(text).replaceAll("\\\\n");
207        final String textWithoutReturns = RETURN.matcher(textWithoutNewlines).replaceAll("\\\\r");
208        return TAB.matcher(textWithoutReturns).replaceAll("\\\\t");
209    }
210
211    /**
212     * Parse a file and return the parse tree.
213     * @param file the file to parse.
214     * @param withComments true to include comment nodes to the tree
215     * @return the root node of the parse tree.
216     * @throws IOException if the file could not be read.
217     * @throws CheckstyleException if the file is not a Java source.
218     */
219    private static DetailAST parseFile(File file, boolean withComments)
220            throws IOException, CheckstyleException {
221        final FileText text = new FileText(file.getAbsoluteFile(),
222            System.getProperty("file.encoding", "UTF-8"));
223        return parseFileText(text, withComments);
224    }
225
226    /**
227     * Parse a text and return the parse tree.
228     * @param text the text to parse.
229     * @param withComments true to include comment nodes to the tree
230     * @return the root node of the parse tree.
231     * @throws CheckstyleException if the file is not a Java source.
232     */
233    private static DetailAST parseFileText(FileText text, boolean withComments)
234            throws CheckstyleException {
235        final FileContents contents = new FileContents(text);
236        final DetailAST result;
237        try {
238            if (withComments) {
239                result = TreeWalker.parseWithComments(contents);
240            }
241            else {
242                result = TreeWalker.parse(contents);
243            }
244        }
245        catch (RecognitionException | TokenStreamException ex) {
246            final String exceptionMsg = String.format(Locale.ROOT,
247                "%s occurred during the analysis of file %s.",
248                ex.getClass().getSimpleName(), text.getFile().getPath());
249            throw new CheckstyleException(exceptionMsg, ex);
250        }
251
252        return result;
253    }
254}