View Javadoc
1   ////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code for adherence to a set of rules.
3   // Copyright (C) 2001-2018 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  
75      /** The root ast. */
76      private final DetailAST rootAst;
77      /** The line number of the element for which the query should be generated. */
78      private final int lineNumber;
79      /** The column number of the element for which the query should be generated. */
80      private final int columnNumber;
81      /** The {@code FileText} object, representing content of the file. */
82      private final FileText fileText;
83      /** The distance between tab stop position. */
84      private final int tabWidth;
85  
86      /**
87       * Creates a new {@code XpathQueryGenerator} instance.
88       *
89       * @param rootAst root ast
90       * @param lineNumber line number of the element for which the query should be generated
91       * @param columnNumber column number of the element for which the query should be generated
92       * @param fileText the {@code FileText} object
93       * @param tabWidth distance between tab stop position
94       */
95      public XpathQueryGenerator(DetailAST rootAst, int lineNumber, int columnNumber,
96                                 FileText fileText, int tabWidth) {
97          this.rootAst = rootAst;
98          this.lineNumber = lineNumber;
99          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 }