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.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 075 /** The root ast. */ 076 private final DetailAST rootAst; 077 /** The line number of the element for which the query should be generated. */ 078 private final int lineNumber; 079 /** The column number of the element for which the query should be generated. */ 080 private final int columnNumber; 081 /** The {@code FileText} object, representing content of the file. */ 082 private final FileText fileText; 083 /** The distance between tab stop position. */ 084 private final int tabWidth; 085 086 /** 087 * Creates a new {@code XpathQueryGenerator} instance. 088 * 089 * @param rootAst root ast 090 * @param lineNumber line number of the element for which the query should be generated 091 * @param columnNumber column number of the element for which the query should be generated 092 * @param fileText the {@code FileText} object 093 * @param tabWidth distance between tab stop position 094 */ 095 public XpathQueryGenerator(DetailAST rootAst, int lineNumber, int columnNumber, 096 FileText fileText, int tabWidth) { 097 this.rootAst = rootAst; 098 this.lineNumber = lineNumber; 099 this.columnNumber = columnNumber; 100 this.fileText = fileText; 101 this.tabWidth = tabWidth; 102 } 103 104 /** 105 * Returns list of xpath queries of nodes, matching line and column number. 106 * This approach uses DetailAST traversal. DetailAST means detail abstract syntax tree. 107 * @return list of xpath queries of nodes, matching line and column number 108 */ 109 public List<String> generate() { 110 return getMatchingAstElements() 111 .stream() 112 .map(XpathQueryGenerator::generateXpathQuery) 113 .collect(Collectors.toList()); 114 } 115 116 /** 117 * Returns child {@code DetailAst} element of the given root, 118 * which has child element with token type equals to {@link TokenTypes#IDENT}. 119 * @param root {@code DetailAST} root ast 120 * @return child {@code DetailAst} element of the given root 121 */ 122 private static DetailAST findChildWithIdent(DetailAST root) { 123 return TokenUtils.findFirstTokenByPredicate(root, 124 cur -> { 125 return cur.findFirstToken(TokenTypes.IDENT) != null; 126 }).orElse(null); 127 } 128 129 /** 130 * Returns full xpath query for given ast element. 131 * @param ast {@code DetailAST} ast element 132 * @return full xpath query for given ast element 133 */ 134 private static String generateXpathQuery(DetailAST ast) { 135 String xpathQuery = getXpathQuery(null, ast); 136 if (!isUniqueAst(ast)) { 137 final DetailAST child = findChildWithIdent(ast); 138 if (child != null) { 139 xpathQuery += "[." + getXpathQuery(ast, child) + ']'; 140 } 141 } 142 return xpathQuery; 143 } 144 145 /** 146 * Returns list of nodes matching defined line and column number. 147 * @return list of nodes matching defined line and column number 148 */ 149 private List<DetailAST> getMatchingAstElements() { 150 final List<DetailAST> result = new ArrayList<>(); 151 DetailAST curNode = rootAst; 152 while (curNode != null && curNode.getLineNo() <= lineNumber) { 153 if (isMatchingByLineAndColumnAndNotIdent(curNode)) { 154 result.add(curNode); 155 } 156 DetailAST toVisit = curNode.getFirstChild(); 157 while (curNode != null && toVisit == null) { 158 toVisit = curNode.getNextSibling(); 159 if (toVisit == null) { 160 curNode = curNode.getParent(); 161 } 162 } 163 164 curNode = toVisit; 165 } 166 return result; 167 } 168 169 /** 170 * Returns relative xpath query for given ast element from root. 171 * @param root {@code DetailAST} root element 172 * @param ast {@code DetailAST} ast element 173 * @return relative xpath query for given ast element from root 174 */ 175 private static String getXpathQuery(DetailAST root, DetailAST ast) { 176 final StringBuilder resultBuilder = new StringBuilder(1024); 177 DetailAST cur = ast; 178 while (cur != root) { 179 final StringBuilder curNodeQueryBuilder = new StringBuilder(256); 180 curNodeQueryBuilder.append('/') 181 .append(TokenUtils.getTokenName(cur.getType())); 182 final DetailAST identAst = cur.findFirstToken(TokenTypes.IDENT); 183 if (identAst != null) { 184 curNodeQueryBuilder.append("[@text='") 185 .append(identAst.getText()) 186 .append("']"); 187 } 188 resultBuilder.insert(0, curNodeQueryBuilder); 189 cur = cur.getParent(); 190 } 191 return resultBuilder.toString(); 192 } 193 194 /** 195 * Checks if the given ast element has unique {@code TokenTypes} among siblings. 196 * @param ast {@code DetailAST} ast element 197 * @return if the given ast element has unique {@code TokenTypes} among siblings 198 */ 199 private static boolean hasAtLeastOneSiblingWithSameTokenType(DetailAST ast) { 200 boolean result = false; 201 if (ast.getParent() == null) { 202 DetailAST prev = ast.getPreviousSibling(); 203 while (prev != null) { 204 if (prev.getType() == ast.getType()) { 205 result = true; 206 break; 207 } 208 prev = prev.getPreviousSibling(); 209 } 210 if (!result) { 211 DetailAST next = ast.getNextSibling(); 212 while (next != null) { 213 if (next.getType() == ast.getType()) { 214 result = true; 215 break; 216 } 217 next = next.getNextSibling(); 218 } 219 } 220 } 221 else { 222 result = ast.getParent().getChildCount(ast.getType()) > 1; 223 } 224 return result; 225 } 226 227 /** 228 * Returns the column number with tabs expanded. 229 * @param ast {@code DetailAST} root ast 230 * @return the column number with tabs expanded 231 */ 232 private int expandedTabColumn(DetailAST ast) { 233 return 1 + CommonUtils.lengthExpandedTabs(fileText.get(lineNumber - 1), 234 ast.getColumnNo(), tabWidth); 235 } 236 237 /** 238 * Checks if the given {@code DetailAST} node is matching line and column number and 239 * it is not {@link TokenTypes#IDENT}. 240 * @param ast {@code DetailAST} ast element 241 * @return true if the given {@code DetailAST} node is matching 242 */ 243 private boolean isMatchingByLineAndColumnAndNotIdent(DetailAST ast) { 244 return ast.getType() != TokenTypes.IDENT 245 && ast.getLineNo() == lineNumber 246 && expandedTabColumn(ast) == columnNumber; 247 } 248 249 /** 250 * To be sure that generated xpath query will return exactly required ast element, the element 251 * should be checked for uniqueness. If ast element has {@link TokenTypes#IDENT} as the child 252 * or there is no sibling with the same {@code TokenTypes} then element is supposed to be 253 * unique. This method finds if {@code DetailAst} element is unique. 254 * @param ast {@code DetailAST} ast element 255 * @return if {@code DetailAst} element is unique 256 */ 257 private static boolean isUniqueAst(DetailAST ast) { 258 return ast.findFirstToken(TokenTypes.IDENT) != null 259 || !hasAtLeastOneSiblingWithSameTokenType(ast); 260 } 261 262}