001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2020 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.utils;
021
022import java.io.File;
023import java.io.IOException;
024import java.util.ArrayList;
025import java.util.List;
026import java.util.Locale;
027import java.util.Set;
028import java.util.stream.Collectors;
029import java.util.stream.Stream;
030
031import com.puppycrawl.tools.checkstyle.AstTreeStringPrinter;
032import com.puppycrawl.tools.checkstyle.JavaParser;
033import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
034import com.puppycrawl.tools.checkstyle.api.DetailAST;
035import com.puppycrawl.tools.checkstyle.api.TokenTypes;
036import com.puppycrawl.tools.checkstyle.xpath.AbstractNode;
037import com.puppycrawl.tools.checkstyle.xpath.ElementNode;
038import com.puppycrawl.tools.checkstyle.xpath.RootNode;
039import net.sf.saxon.Configuration;
040import net.sf.saxon.om.Item;
041import net.sf.saxon.sxpath.XPathDynamicContext;
042import net.sf.saxon.sxpath.XPathEvaluator;
043import net.sf.saxon.sxpath.XPathExpression;
044import net.sf.saxon.trans.XPathException;
045
046/**
047 * Contains utility methods for xpath.
048 *
049 */
050public final class XpathUtil {
051
052    /**
053     * List of token types which support text attribute.
054     * These token types were selected based on analysis that all others do not match required
055     * criteria - text attribute of the token must be useful and help to retrieve more precise
056     * results.
057     * There are three types of AST tokens:
058     * 1. Tokens for which the texts are equal to the name of the token. Or in other words,
059     * nodes for which the following expression is always true:
060     * <pre>
061     *     detailAst.getText().equals(TokenUtil.getTokenName(detailAst.getType()))
062     * </pre>
063     * For example:
064     * <pre>
065     *     //MODIFIERS[@text='MODIFIERS']
066     *     //OBJBLOCK[@text='OBJBLOCK']
067     * </pre>
068     * These tokens do not match required criteria because their texts do not carry any additional
069     * information, they do not affect the xpath requests and do not help to get more accurate
070     * results. The texts of these nodes are useless. No matter what code you analyze, these
071     * texts are always the same.
072     * In addition, they make xpath queries more complex, less readable and verbose.
073     * 2. Tokens for which the texts differ from token names, but texts are always constant.
074     * For example:
075     * <pre>
076     *     //LITERAL_VOID[@text='void']
077     *     //RCURLY[@text='}']
078     * </pre>
079     * These tokens are not used for the same reasons as were described in the previous part.
080     * 3. Tokens for which texts are not constant. The texts of these nodes are closely related
081     * to a concrete class, method, variable and so on.
082     * For example:
083     * <pre>
084     *     String greeting = "HelloWorld";
085     *     //STRING_LITERAL[@text='HelloWorld']
086     * </pre>
087     * <pre>
088     *     int year = 2017;
089     *     //NUM_INT[@text=2017]
090     * </pre>
091     * <pre>
092     *     int age = 23;
093     *     //NUM_INT[@text=23]
094     * </pre>
095     * As you can see same {@code NUM_INT} token type can have different texts, depending on
096     * context.
097     * <pre>
098     *     public class MyClass {}
099     *     //IDENT[@text='MyClass']
100     * </pre>
101     * Only these tokens support text attribute because they make our xpath queries more accurate.
102     * These token types are listed below.
103     * */
104    private static final Set<Integer> TOKEN_TYPES_WITH_TEXT_ATTRIBUTE =
105        Stream.of(
106            TokenTypes.IDENT, TokenTypes.STRING_LITERAL, TokenTypes.CHAR_LITERAL,
107            TokenTypes.NUM_LONG, TokenTypes.NUM_INT, TokenTypes.NUM_DOUBLE, TokenTypes.NUM_FLOAT)
108        .collect(Collectors.toSet());
109
110    /** Delimiter to separate xpath results. */
111    private static final String DELIMITER = "---------" + System.lineSeparator();
112
113    /** Stop instances being created. **/
114    private XpathUtil() {
115    }
116
117    /**
118     * Iterates siblings of the given node and creates new Xpath-nodes.
119     *
120     * @param root the root node
121     * @param parent the parent node
122     * @param firstChild the first DetailAST
123     * @return children list
124     */
125    public static List<AbstractNode> createChildren(AbstractNode root, AbstractNode parent,
126                                                    DetailAST firstChild) {
127        DetailAST currentChild = firstChild;
128        final int depth = parent.getDepth() + 1;
129        final List<AbstractNode> result = new ArrayList<>();
130        while (currentChild != null) {
131            final int index = result.size();
132            final ElementNode child = new ElementNode(root, parent, currentChild, depth, index);
133            result.add(child);
134            currentChild = currentChild.getNextSibling();
135        }
136        return result;
137    }
138
139    /**
140     * Checks, if specified node can have {@code @text} attribute.
141     *
142     * @param ast {@code DetailAst} element
143     * @return true if element supports {@code @text} attribute, false otherwise
144     */
145    public static boolean supportsTextAttribute(DetailAST ast) {
146        return TOKEN_TYPES_WITH_TEXT_ATTRIBUTE.contains(ast.getType());
147    }
148
149    /**
150     * Returns content of the text attribute of the ast element.
151     *
152     * @param ast {@code DetailAst} element
153     * @return text attribute of the ast element
154     */
155    public static String getTextAttributeValue(DetailAST ast) {
156        String text = ast.getText();
157        if (ast.getType() == TokenTypes.STRING_LITERAL) {
158            text = text.substring(1, text.length() - 1);
159        }
160        return text;
161    }
162
163    /**
164     * Returns xpath query results on file as string.
165     *
166     * @param xpath query to evaluate
167     * @param file file to run on
168     * @return all results as string separated by delimiter
169     * @throws CheckstyleException if some parsing error happens
170     * @throws IOException if an error occurs
171     */
172    public static String printXpathBranch(String xpath, File file) throws CheckstyleException,
173            IOException {
174        final XPathEvaluator xpathEvaluator = new XPathEvaluator(Configuration.newConfiguration());
175        try {
176            final RootNode rootNode = new RootNode(JavaParser.parseFile(file,
177                JavaParser.Options.WITH_COMMENTS));
178            final XPathExpression xpathExpression = xpathEvaluator.createExpression(xpath);
179            final XPathDynamicContext xpathDynamicContext =
180                xpathExpression.createDynamicContext(rootNode);
181            final List<Item> matchingItems = xpathExpression.evaluate(xpathDynamicContext);
182            return matchingItems.stream()
183                .map(item -> ((AbstractNode) item).getUnderlyingNode())
184                .map(AstTreeStringPrinter::printBranch)
185                .collect(Collectors.joining(DELIMITER));
186        }
187        catch (XPathException ex) {
188            final String errMsg = String.format(Locale.ROOT,
189                "Error during evaluation for xpath: %s, file: %s", xpath, file.getCanonicalPath());
190            throw new CheckstyleException(errMsg, ex);
191        }
192    }
193
194}