View Javadoc
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                                 FileText fileText, int tabWidth) {
96          this.rootAst = rootAst;
97          this.lineNumber = lineNumber;
98          this.columnNumber = columnNumber;
99          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 }