Coverage Report - com.puppycrawl.tools.checkstyle.xpath.XpathQueryGenerator
 
Classes in this File Line Coverage Branch Coverage Complexity
XpathQueryGenerator
100%
73/73
100%
46/46
0
 
 1  
 ////////////////////////////////////////////////////////////////////////////////
 2  
 // checkstyle: Checks Java source code for adherence to a set of rules.
 3  
 // Copyright (C) 2001-2017 the original author or authors.
 4  
 //
 5  
 // This library is free software; you can redistribute it and/or
 6  
 // modify it under the terms of the GNU Lesser General Public
 7  
 // License as published by the Free Software Foundation; either
 8  
 // version 2.1 of the License, or (at your option) any later version.
 9  
 //
 10  
 // This library is distributed in the hope that it will be useful,
 11  
 // but WITHOUT ANY WARRANTY; without even the implied warranty of
 12  
 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 13  
 // Lesser General Public License for more details.
 14  
 //
 15  
 // You should have received a copy of the GNU Lesser General Public
 16  
 // License along with this library; if not, write to the Free Software
 17  
 // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 18  
 ////////////////////////////////////////////////////////////////////////////////
 19  
 
 20  
 package com.puppycrawl.tools.checkstyle.xpath;
 21  
 
 22  
 import java.util.ArrayList;
 23  
 import java.util.List;
 24  
 import java.util.stream.Collectors;
 25  
 
 26  
 import com.puppycrawl.tools.checkstyle.api.DetailAST;
 27  
 import com.puppycrawl.tools.checkstyle.api.FileText;
 28  
 import com.puppycrawl.tools.checkstyle.api.TokenTypes;
 29  
 import com.puppycrawl.tools.checkstyle.utils.CommonUtils;
 30  
 import com.puppycrawl.tools.checkstyle.utils.TokenUtils;
 31  
 
 32  
 /**
 33  
  * Generates xpath queries. Xpath queries are generated based on received
 34  
  * {@code DetailAst} element, line number and column number.
 35  
  *
 36  
  * <p>
 37  
  *     Example class
 38  
  * </p>
 39  
  * <pre>
 40  
  * public class Main {
 41  
  *
 42  
  *     public String sayHello(String name) {
 43  
  *         return "Hello, " + name;
 44  
  *     }
 45  
  * }
 46  
  * </pre>
 47  
  *
 48  
  * <p>
 49  
  *     Following expression returns list of queries. Each query is the string representing full
 50  
  *     path to the node inside Xpath tree, whose line number is 3 and column number is 4.
 51  
  * </p>
 52  
  * <pre>
 53  
  *     new XpathQueryGenerator(rootAst, 3, 4).generate();
 54  
  * </pre>
 55  
  *
 56  
  * <p>
 57  
  *     Result list
 58  
  * </p>
 59  
  * <ul>
 60  
  *     <li>
 61  
  *         /CLASS_DEF[@text='Main']/OBJBLOCK/METHOD_DEF[@text='sayHello']
 62  
  *     </li>
 63  
  *     <li>
 64  
  *         /CLASS_DEF[@text='Main']/OBJBLOCK/METHOD_DEF[@text='sayHello']/MODIFIERS
 65  
  *     </li>
 66  
  *     <li>
 67  
  *         /CLASS_DEF[@text='Main']/OBJBLOCK/METHOD_DEF[@text='sayHello']/MODIFIERS/LITERAL_PUBLIC
 68  
  *     </li>
 69  
  * </ul>
 70  
  *
 71  
  * @author Timur Tibeyev.
 72  
  */
 73  
 public class XpathQueryGenerator {
 74  
     /** The root ast. */
 75  
     private final DetailAST rootAst;
 76  
     /** The line number of the element for which the query should be generated. */
 77  
     private final int lineNumber;
 78  
     /** The column number of the element for which the query should be generated. */
 79  
     private final int columnNumber;
 80  
     /** The {@code FileText} object, representing content of the file. */
 81  
     private final FileText fileText;
 82  
     /** The distance between tab stop position. */
 83  
     private final int tabWidth;
 84  
 
 85  
     /**
 86  
      * Creates a new {@code XpathQueryGenerator} instance.
 87  
      *
 88  
      * @param rootAst root ast
 89  
      * @param lineNumber line number of the element for which the query should be generated
 90  
      * @param columnNumber column number of the element for which the query should be generated
 91  
      * @param fileText the {@code FileText} object
 92  
      * @param tabWidth distance between tab stop position
 93  
      */
 94  
     public XpathQueryGenerator(DetailAST rootAst, int lineNumber, int columnNumber,
 95  22
                                FileText fileText, int tabWidth) {
 96  22
         this.rootAst = rootAst;
 97  22
         this.lineNumber = lineNumber;
 98  22
         this.columnNumber = columnNumber;
 99  22
         this.fileText = fileText;
 100  22
         this.tabWidth = tabWidth;
 101  22
     }
 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  44
         return getMatchingAstElements()
 110  22
             .stream()
 111  22
             .map(XpathQueryGenerator::generateXpathQuery)
 112  22
             .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  8
         return TokenUtils.findFirstTokenByPredicate(root,
 123  
             cur -> {
 124  5
                 return cur.findFirstToken(TokenTypes.IDENT) != null;
 125  4
             }).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  38
         String xpathQuery = getXpathQuery(null, ast);
 135  38
         if (!isUniqueAst(ast)) {
 136  4
             final DetailAST child = findChildWithIdent(ast);
 137  4
             if (child != null) {
 138  3
                 xpathQuery += "[." + getXpathQuery(ast, child) + ']';
 139  
             }
 140  
         }
 141  38
         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  22
         final List<DetailAST> result = new ArrayList<>();
 150  22
         DetailAST curNode = rootAst;
 151  4652
         while (curNode != null && curNode.getLineNo() <= lineNumber) {
 152  4630
             if (isMatchingByLineAndColumnAndNotIdent(curNode)) {
 153  38
                 result.add(curNode);
 154  
             }
 155  4630
             DetailAST toVisit = curNode.getFirstChild();
 156  9192
             while (curNode != null && toVisit == null) {
 157  4562
                 toVisit = curNode.getNextSibling();
 158  4562
                 if (toVisit == null) {
 159  2034
                     curNode = curNode.getParent();
 160  
                 }
 161  
             }
 162  
 
 163  4630
             curNode = toVisit;
 164  4630
         }
 165  22
         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  41
         final StringBuilder resultBuilder = new StringBuilder(1024);
 176  41
         DetailAST cur = ast;
 177  224
         while (cur != root) {
 178  183
             final StringBuilder curNodeQueryBuilder = new StringBuilder(256);
 179  183
             curNodeQueryBuilder.append('/')
 180  183
                     .append(TokenUtils.getTokenName(cur.getType()));
 181  183
             final DetailAST identAst = cur.findFirstToken(TokenTypes.IDENT);
 182  183
             if (identAst != null) {
 183  77
                 curNodeQueryBuilder.append("[@text='")
 184  77
                         .append(identAst.getText())
 185  77
                         .append("']");
 186  
             }
 187  183
             resultBuilder.insert(0, curNodeQueryBuilder);
 188  183
             cur = cur.getParent();
 189  183
         }
 190  41
         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  32
         boolean result = false;
 200  32
         if (ast.getParent() == null) {
 201  4
             DetailAST prev = ast.getPreviousSibling();
 202  7
             while (prev != null) {
 203  5
                 if (prev.getType() == ast.getType()) {
 204  2
                     result = true;
 205  2
                     break;
 206  
                 }
 207  3
                 prev = prev.getPreviousSibling();
 208  
             }
 209  4
             if (!result) {
 210  2
                 DetailAST next = ast.getNextSibling();
 211  9
                 while (next != null) {
 212  8
                     if (next.getType() == ast.getType()) {
 213  1
                         result = true;
 214  1
                         break;
 215  
                     }
 216  7
                     next = next.getNextSibling();
 217  
                 }
 218  
             }
 219  4
         }
 220  
         else {
 221  28
             result = ast.getParent().getChildCount(ast.getType()) > 1;
 222  
         }
 223  32
         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  352
         return 1 + CommonUtils.lengthExpandedTabs(fileText.get(lineNumber - 1),
 233  176
                 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  9260
         return ast.getType() != TokenTypes.IDENT
 244  3494
                 && ast.getLineNo() == lineNumber
 245  176
                 && 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  76
         return ast.findFirstToken(TokenTypes.IDENT) != null
 258  32
             || !hasAtLeastOneSiblingWithSameTokenType(ast);
 259  
     }
 260  
 }