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