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 javax.annotation.Nullable;
27
28 import com.puppycrawl.tools.checkstyle.TreeWalkerAuditEvent;
29 import com.puppycrawl.tools.checkstyle.api.DetailAST;
30 import com.puppycrawl.tools.checkstyle.api.FileText;
31 import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
32 import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
33 import com.puppycrawl.tools.checkstyle.utils.XpathUtil;
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
77
78 public class XpathQueryGenerator {
79
80
81 private final DetailAST rootAst;
82
83 private final int lineNumber;
84
85 private final int columnNumber;
86
87 private final int tokenType;
88
89 private final FileText fileText;
90
91 private final int tabWidth;
92
93
94
95
96
97
98
99 public XpathQueryGenerator(TreeWalkerAuditEvent event, int tabWidth) {
100 this(event.getRootAst(), event.getLine(), event.getColumn(), event.getTokenType(),
101 event.getFileContents().getText(), tabWidth);
102 }
103
104
105
106
107
108
109
110
111
112
113 public XpathQueryGenerator(DetailAST rootAst, int lineNumber, int columnNumber,
114 FileText fileText, int tabWidth) {
115 this(rootAst, lineNumber, columnNumber, 0, fileText, tabWidth);
116 }
117
118
119
120
121
122
123
124
125
126
127
128 public XpathQueryGenerator(DetailAST rootAst, int lineNumber, int columnNumber, int tokenType,
129 FileText fileText, int tabWidth) {
130 this.rootAst = rootAst;
131 this.lineNumber = lineNumber;
132 this.columnNumber = columnNumber;
133 this.tokenType = tokenType;
134 this.fileText = fileText;
135 this.tabWidth = tabWidth;
136 }
137
138
139
140
141
142
143
144 public List<String> generate() {
145 return getMatchingAstElements()
146 .stream()
147 .map(XpathQueryGenerator::generateXpathQuery)
148 .collect(Collectors.toUnmodifiableList());
149 }
150
151
152
153
154
155
156
157 @Nullable
158 private static DetailAST findChildWithTextAttribute(DetailAST root) {
159 return TokenUtil.findFirstTokenByPredicate(root,
160 XpathUtil::supportsTextAttribute).orElse(null);
161 }
162
163
164
165
166
167
168
169
170 @Nullable
171 private static DetailAST findChildWithTextAttributeRecursively(DetailAST root) {
172 DetailAST res = findChildWithTextAttribute(root);
173 for (DetailAST ast = root.getFirstChild(); ast != null && res == null;
174 ast = ast.getNextSibling()) {
175 res = findChildWithTextAttributeRecursively(ast);
176 }
177 return res;
178 }
179
180
181
182
183
184
185
186 public static String generateXpathQuery(DetailAST ast) {
187 final StringBuilder xpathQueryBuilder = new StringBuilder(getXpathQuery(null, ast));
188 if (!isXpathQueryForNodeIsAccurateEnough(ast)) {
189 xpathQueryBuilder.append('[');
190 final DetailAST child = findChildWithTextAttributeRecursively(ast);
191 if (child == null) {
192 xpathQueryBuilder.append(findPositionAmongSiblings(ast));
193 }
194 else {
195 xpathQueryBuilder.append('.').append(getXpathQuery(ast, child));
196 }
197 xpathQueryBuilder.append(']');
198 }
199 return xpathQueryBuilder.toString();
200 }
201
202
203
204
205
206
207
208 private static int findPositionAmongSiblings(DetailAST ast) {
209 DetailAST cur = ast;
210 int pos = 0;
211 while (cur != null) {
212 if (cur.getType() == ast.getType()) {
213 pos++;
214 }
215 cur = cur.getPreviousSibling();
216 }
217 return pos;
218 }
219
220
221
222
223
224
225
226 private static boolean isXpathQueryForNodeIsAccurateEnough(DetailAST ast) {
227 return !hasAtLeastOneSiblingWithSameTokenType(ast)
228 || XpathUtil.supportsTextAttribute(ast)
229 || findChildWithTextAttribute(ast) != null;
230 }
231
232
233
234
235
236
237 private List<DetailAST> getMatchingAstElements() {
238 final List<DetailAST> result = new ArrayList<>();
239 DetailAST curNode = rootAst;
240 while (curNode != null) {
241 if (isMatchingByLineAndColumnAndTokenType(curNode)) {
242 result.add(curNode);
243 }
244 DetailAST toVisit = curNode.getFirstChild();
245 while (curNode != null && toVisit == null) {
246 toVisit = curNode.getNextSibling();
247 curNode = curNode.getParent();
248 }
249
250 curNode = toVisit;
251 }
252 return result;
253 }
254
255
256
257
258
259
260
261
262 private static String getXpathQuery(DetailAST root, DetailAST ast) {
263 final StringBuilder resultBuilder = new StringBuilder(1024);
264 DetailAST cur = ast;
265 while (cur != root) {
266 final StringBuilder curNodeQueryBuilder = new StringBuilder(256);
267 curNodeQueryBuilder.append('/')
268 .append(TokenUtil.getTokenName(cur.getType()));
269 if (XpathUtil.supportsTextAttribute(cur)) {
270 curNodeQueryBuilder.append("[@text='")
271 .append(encode(XpathUtil.getTextAttributeValue(cur)))
272 .append("']");
273 }
274 else {
275 final DetailAST child = findChildWithTextAttribute(cur);
276 if (child != null && child != ast) {
277 curNodeQueryBuilder.append("[.")
278 .append(getXpathQuery(cur, child))
279 .append(']');
280 }
281 }
282
283 resultBuilder.insert(0, curNodeQueryBuilder);
284 cur = cur.getParent();
285 }
286 return resultBuilder.toString();
287 }
288
289
290
291
292
293
294
295 private static boolean hasAtLeastOneSiblingWithSameTokenType(DetailAST ast) {
296 boolean result = false;
297 DetailAST prev = ast.getPreviousSibling();
298 while (prev != null) {
299 if (prev.getType() == ast.getType()) {
300 result = true;
301 break;
302 }
303 prev = prev.getPreviousSibling();
304 }
305 DetailAST next = ast.getNextSibling();
306 while (next != null) {
307 if (next.getType() == ast.getType()) {
308 result = true;
309 break;
310 }
311 next = next.getNextSibling();
312 }
313 return result;
314 }
315
316
317
318
319
320
321
322 private int expandedTabColumn(DetailAST ast) {
323 return 1 + CommonUtil.lengthExpandedTabs(fileText.get(lineNumber - 1),
324 ast.getColumnNo(), tabWidth);
325 }
326
327
328
329
330
331
332
333
334 private boolean isMatchingByLineAndColumnAndTokenType(DetailAST ast) {
335 return ast.getLineNo() == lineNumber
336 && expandedTabColumn(ast) == columnNumber
337 && (tokenType == 0 || tokenType == ast.getType());
338 }
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358 private static String encode(String value) {
359 final StringBuilder sb = new StringBuilder(256);
360 value.codePoints().forEach(
361 chr -> {
362 sb.append(encodeCharacter(Character.toChars(chr)[0]));
363 }
364 );
365 return sb.toString();
366 }
367
368
369
370
371
372
373
374
375
376 private static String encodeCharacter(char chr) {
377 final String encode;
378 switch (chr) {
379 case '<':
380 encode = "<";
381 break;
382 case '>':
383 encode = ">";
384 break;
385 case '\'':
386 encode = "''";
387 break;
388 case '\"':
389 encode = """;
390 break;
391 case '&':
392 encode = "&";
393 break;
394 default:
395 encode = String.valueOf(chr);
396 break;
397 }
398 return encode;
399 }
400 }