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