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