1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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.TreeWalkerAuditEvent;
27 import com.puppycrawl.tools.checkstyle.api.DetailAST;
28 import com.puppycrawl.tools.checkstyle.api.FileText;
29 import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
30 import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
31 import com.puppycrawl.tools.checkstyle.utils.XpathUtil;
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76 public class XpathQueryGenerator {
77
78
79 private final DetailAST rootAst;
80
81 private final int lineNumber;
82
83 private final int columnNumber;
84
85 private final int tokenType;
86
87 private final FileText fileText;
88
89 private final int tabWidth;
90
91
92
93
94
95
96
97 public XpathQueryGenerator(TreeWalkerAuditEvent event, int tabWidth) {
98 this(event.getRootAst(), event.getLine(), event.getColumn(), event.getTokenType(),
99 event.getFileContents().getText(), tabWidth);
100 }
101
102
103
104
105
106
107
108
109
110
111 public XpathQueryGenerator(DetailAST rootAst, int lineNumber, int columnNumber,
112 FileText fileText, int tabWidth) {
113 this(rootAst, lineNumber, columnNumber, 0, fileText, tabWidth);
114 }
115
116
117
118
119
120
121
122
123
124
125
126 public XpathQueryGenerator(DetailAST rootAst, int lineNumber, int columnNumber, int tokenType,
127 FileText fileText, int tabWidth) {
128 this.rootAst = rootAst;
129 this.lineNumber = lineNumber;
130 this.columnNumber = columnNumber;
131 this.tokenType = tokenType;
132 this.fileText = fileText;
133 this.tabWidth = tabWidth;
134 }
135
136
137
138
139
140
141 public List<String> generate() {
142 return getMatchingAstElements()
143 .stream()
144 .map(XpathQueryGenerator::generateXpathQuery)
145 .collect(Collectors.toList());
146 }
147
148
149
150
151
152
153 private static DetailAST findChildWithTextAttribute(DetailAST root) {
154 return TokenUtil.findFirstTokenByPredicate(root,
155 XpathUtil::supportsTextAttribute).orElse(null);
156 }
157
158
159
160
161
162
163
164 private static DetailAST findChildWithTextAttributeRecursively(DetailAST root) {
165 DetailAST res = findChildWithTextAttribute(root);
166 for (DetailAST ast = root.getFirstChild(); ast != null && res == null;
167 ast = ast.getNextSibling()) {
168 res = findChildWithTextAttributeRecursively(ast);
169 }
170 return res;
171 }
172
173
174
175
176
177
178 private static String generateXpathQuery(DetailAST ast) {
179 final StringBuilder xpathQueryBuilder = new StringBuilder(getXpathQuery(null, ast));
180 if (!isXpathQueryForNodeIsAccurateEnough(ast)) {
181 xpathQueryBuilder.append('[');
182 final DetailAST child = findChildWithTextAttributeRecursively(ast);
183 if (child == null) {
184 xpathQueryBuilder.append(findPositionAmongSiblings(ast));
185 }
186 else {
187 xpathQueryBuilder.append('.').append(getXpathQuery(ast, child));
188 }
189 xpathQueryBuilder.append(']');
190 }
191 return xpathQueryBuilder.toString();
192 }
193
194
195
196
197
198
199 private static int findPositionAmongSiblings(DetailAST ast) {
200 DetailAST cur = ast;
201 int pos = 0;
202 while (cur != null) {
203 if (cur.getType() == ast.getType()) {
204 pos++;
205 }
206 cur = cur.getPreviousSibling();
207 }
208 return pos;
209 }
210
211
212
213
214
215
216 private static boolean isXpathQueryForNodeIsAccurateEnough(DetailAST ast) {
217 return !hasAtLeastOneSiblingWithSameTokenType(ast)
218 || XpathUtil.supportsTextAttribute(ast)
219 || findChildWithTextAttribute(ast) != null;
220 }
221
222
223
224
225
226 private List<DetailAST> getMatchingAstElements() {
227 final List<DetailAST> result = new ArrayList<>();
228 DetailAST curNode = rootAst;
229 while (curNode != null) {
230 if (isMatchingByLineAndColumnAndTokenType(curNode)) {
231 result.add(curNode);
232 }
233 DetailAST toVisit = curNode.getFirstChild();
234 while (curNode != null && toVisit == null) {
235 toVisit = curNode.getNextSibling();
236 curNode = curNode.getParent();
237 }
238
239 curNode = toVisit;
240 }
241 return result;
242 }
243
244
245
246
247
248
249
250 private static String getXpathQuery(DetailAST root, DetailAST ast) {
251 final StringBuilder resultBuilder = new StringBuilder(1024);
252 DetailAST cur = ast;
253 while (cur != root) {
254 final StringBuilder curNodeQueryBuilder = new StringBuilder(256);
255 curNodeQueryBuilder.append('/')
256 .append(TokenUtil.getTokenName(cur.getType()));
257 if (XpathUtil.supportsTextAttribute(cur)) {
258 curNodeQueryBuilder.append("[@text='")
259 .append(XpathUtil.getTextAttributeValue(cur))
260 .append("']");
261 }
262 else {
263 final DetailAST child = findChildWithTextAttribute(cur);
264 if (child != null && child != ast) {
265 curNodeQueryBuilder.append("[.")
266 .append(getXpathQuery(cur, child))
267 .append(']');
268 }
269 }
270
271 resultBuilder.insert(0, curNodeQueryBuilder);
272 cur = cur.getParent();
273 }
274 return resultBuilder.toString();
275 }
276
277
278
279
280
281
282 private static boolean hasAtLeastOneSiblingWithSameTokenType(DetailAST ast) {
283 boolean result = false;
284 DetailAST prev = ast.getPreviousSibling();
285 while (prev != null) {
286 if (prev.getType() == ast.getType()) {
287 result = true;
288 break;
289 }
290 prev = prev.getPreviousSibling();
291 }
292 DetailAST next = ast.getNextSibling();
293 while (next != null) {
294 if (next.getType() == ast.getType()) {
295 result = true;
296 break;
297 }
298 next = next.getNextSibling();
299 }
300 return result;
301 }
302
303
304
305
306
307
308 private int expandedTabColumn(DetailAST ast) {
309 return 1 + CommonUtil.lengthExpandedTabs(fileText.get(lineNumber - 1),
310 ast.getColumnNo(), tabWidth);
311 }
312
313
314
315
316
317
318
319 private boolean isMatchingByLineAndColumnAndTokenType(DetailAST ast) {
320 return ast.getLineNo() == lineNumber
321 && expandedTabColumn(ast) == columnNumber
322 && (tokenType == 0 || tokenType == ast.getType());
323 }
324 }