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.xpath; 021 022import java.util.ArrayList; 023import java.util.List; 024import java.util.stream.Collectors; 025 026import com.puppycrawl.tools.checkstyle.api.DetailAST; 027import com.puppycrawl.tools.checkstyle.api.FileText; 028import com.puppycrawl.tools.checkstyle.api.TokenTypes; 029import com.puppycrawl.tools.checkstyle.utils.CommonUtils; 030import com.puppycrawl.tools.checkstyle.utils.TokenUtils; 031 032/** 033 * Generates xpath queries. Xpath queries are generated based on received 034 * {@code DetailAst} element, line number and column number. 035 * 036 * <p> 037 * Example class 038 * </p> 039 * <pre> 040 * public class Main { 041 * 042 * public String sayHello(String name) { 043 * return "Hello, " + name; 044 * } 045 * } 046 * </pre> 047 * 048 * <p> 049 * Following expression returns list of queries. Each query is the string representing full 050 * path to the node inside Xpath tree, whose line number is 3 and column number is 4. 051 * </p> 052 * <pre> 053 * new XpathQueryGenerator(rootAst, 3, 4).generate(); 054 * </pre> 055 * 056 * <p> 057 * Result list 058 * </p> 059 * <ul> 060 * <li> 061 * /CLASS_DEF[@text='Main']/OBJBLOCK/METHOD_DEF[@text='sayHello'] 062 * </li> 063 * <li> 064 * /CLASS_DEF[@text='Main']/OBJBLOCK/METHOD_DEF[@text='sayHello']/MODIFIERS 065 * </li> 066 * <li> 067 * /CLASS_DEF[@text='Main']/OBJBLOCK/METHOD_DEF[@text='sayHello']/MODIFIERS/LITERAL_PUBLIC 068 * </li> 069 * </ul> 070 * 071 * @author Timur Tibeyev. 072 */ 073public class XpathQueryGenerator { 074 /** The root ast. */ 075 private final DetailAST rootAst; 076 /** The line number of the element for which the query should be generated. */ 077 private final int lineNumber; 078 /** The column number of the element for which the query should be generated. */ 079 private final int columnNumber; 080 /** The {@code FileText} object, representing content of the file. */ 081 private final FileText fileText; 082 /** The distance between tab stop position. */ 083 private final int tabWidth; 084 085 /** 086 * Creates a new {@code XpathQueryGenerator} instance. 087 * 088 * @param rootAst root ast 089 * @param lineNumber line number of the element for which the query should be generated 090 * @param columnNumber column number of the element for which the query should be generated 091 * @param fileText the {@code FileText} object 092 * @param tabWidth distance between tab stop position 093 */ 094 public XpathQueryGenerator(DetailAST rootAst, int lineNumber, int columnNumber, 095 FileText fileText, int tabWidth) { 096 this.rootAst = rootAst; 097 this.lineNumber = lineNumber; 098 this.columnNumber = columnNumber; 099 this.fileText = fileText; 100 this.tabWidth = tabWidth; 101 } 102 103 /** 104 * Returns list of xpath queries of nodes, matching line and column number. 105 * This approach uses DetailAST traversal. DetailAST means detail abstract syntax tree. 106 * @return list of xpath queries of nodes, matching line and column number 107 */ 108 public List<String> generate() { 109 return getMatchingAstElements() 110 .stream() 111 .map(XpathQueryGenerator::generateXpathQuery) 112 .collect(Collectors.toList()); 113 } 114 115 /** 116 * Returns child {@code DetailAst} element of the given root, 117 * which has child element with token type equals to {@link TokenTypes#IDENT}. 118 * @param root {@code DetailAST} root ast 119 * @return child {@code DetailAst} element of the given root 120 */ 121 private static DetailAST findChildWithIdent(DetailAST root) { 122 return TokenUtils.findFirstTokenByPredicate(root, 123 cur -> { 124 return cur.findFirstToken(TokenTypes.IDENT) != null; 125 }).orElse(null); 126 } 127 128 /** 129 * Returns full xpath query for given ast element. 130 * @param ast {@code DetailAST} ast element 131 * @return full xpath query for given ast element 132 */ 133 private static String generateXpathQuery(DetailAST ast) { 134 String xpathQuery = getXpathQuery(null, ast); 135 if (!isUniqueAst(ast)) { 136 final DetailAST child = findChildWithIdent(ast); 137 if (child != null) { 138 xpathQuery += "[." + getXpathQuery(ast, child) + ']'; 139 } 140 } 141 return xpathQuery; 142 } 143 144 /** 145 * Returns list of nodes matching defined line and column number. 146 * @return list of nodes matching defined line and column number 147 */ 148 private List<DetailAST> getMatchingAstElements() { 149 final List<DetailAST> result = new ArrayList<>(); 150 DetailAST curNode = rootAst; 151 while (curNode != null && curNode.getLineNo() <= lineNumber) { 152 if (isMatchingByLineAndColumnAndNotIdent(curNode)) { 153 result.add(curNode); 154 } 155 DetailAST toVisit = curNode.getFirstChild(); 156 while (curNode != null && toVisit == null) { 157 toVisit = curNode.getNextSibling(); 158 if (toVisit == null) { 159 curNode = curNode.getParent(); 160 } 161 } 162 163 curNode = toVisit; 164 } 165 return result; 166 } 167 168 /** 169 * Returns relative xpath query for given ast element from root. 170 * @param root {@code DetailAST} root element 171 * @param ast {@code DetailAST} ast element 172 * @return relative xpath query for given ast element from root 173 */ 174 private static String getXpathQuery(DetailAST root, DetailAST ast) { 175 final StringBuilder resultBuilder = new StringBuilder(1024); 176 DetailAST cur = ast; 177 while (cur != root) { 178 final StringBuilder curNodeQueryBuilder = new StringBuilder(256); 179 curNodeQueryBuilder.append('/') 180 .append(TokenUtils.getTokenName(cur.getType())); 181 final DetailAST identAst = cur.findFirstToken(TokenTypes.IDENT); 182 if (identAst != null) { 183 curNodeQueryBuilder.append("[@text='") 184 .append(identAst.getText()) 185 .append("']"); 186 } 187 resultBuilder.insert(0, curNodeQueryBuilder); 188 cur = cur.getParent(); 189 } 190 return resultBuilder.toString(); 191 } 192 193 /** 194 * Checks if the given ast element has unique {@code TokenTypes} among siblings. 195 * @param ast {@code DetailAST} ast element 196 * @return if the given ast element has unique {@code TokenTypes} among siblings 197 */ 198 private static boolean hasAtLeastOneSiblingWithSameTokenType(DetailAST ast) { 199 boolean result = false; 200 if (ast.getParent() == null) { 201 DetailAST prev = ast.getPreviousSibling(); 202 while (prev != null) { 203 if (prev.getType() == ast.getType()) { 204 result = true; 205 break; 206 } 207 prev = prev.getPreviousSibling(); 208 } 209 if (!result) { 210 DetailAST next = ast.getNextSibling(); 211 while (next != null) { 212 if (next.getType() == ast.getType()) { 213 result = true; 214 break; 215 } 216 next = next.getNextSibling(); 217 } 218 } 219 } 220 else { 221 result = ast.getParent().getChildCount(ast.getType()) > 1; 222 } 223 return result; 224 } 225 226 /** 227 * Returns the column number with tabs expanded. 228 * @param ast {@code DetailAST} root ast 229 * @return the column number with tabs expanded 230 */ 231 private int expandedTabColumn(DetailAST ast) { 232 return 1 + CommonUtils.lengthExpandedTabs(fileText.get(lineNumber - 1), 233 ast.getColumnNo(), tabWidth); 234 } 235 236 /** 237 * Checks if the given {@code DetailAST} node is matching line and column number and 238 * it is not {@link TokenTypes#IDENT}. 239 * @param ast {@code DetailAST} ast element 240 * @return true if the given {@code DetailAST} node is matching 241 */ 242 private boolean isMatchingByLineAndColumnAndNotIdent(DetailAST ast) { 243 return ast.getType() != TokenTypes.IDENT 244 && ast.getLineNo() == lineNumber 245 && expandedTabColumn(ast) == columnNumber; 246 } 247 248 /** 249 * To be sure that generated xpath query will return exactly required ast element, the element 250 * should be checked for uniqueness. If ast element has {@link TokenTypes#IDENT} as the child 251 * or there is no sibling with the same {@code TokenTypes} then element is supposed to be 252 * unique. This method finds if {@code DetailAst} element is unique. 253 * @param ast {@code DetailAST} ast element 254 * @return if {@code DetailAst} element is unique 255 */ 256 private static boolean isUniqueAst(DetailAST ast) { 257 return ast.findFirstToken(TokenTypes.IDENT) != null 258 || !hasAtLeastOneSiblingWithSameTokenType(ast); 259 } 260}