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}