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;
21
22 import java.io.File;
23 import java.util.Arrays;
24 import java.util.Collection;
25 import java.util.Comparator;
26 import java.util.HashMap;
27 import java.util.HashSet;
28 import java.util.Locale;
29 import java.util.Map;
30 import java.util.Set;
31 import java.util.SortedSet;
32 import java.util.TreeSet;
33 import java.util.stream.Collectors;
34 import java.util.stream.Stream;
35
36 import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
37 import com.puppycrawl.tools.checkstyle.api.AbstractFileSetCheck;
38 import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
39 import com.puppycrawl.tools.checkstyle.api.Configuration;
40 import com.puppycrawl.tools.checkstyle.api.Context;
41 import com.puppycrawl.tools.checkstyle.api.DetailAST;
42 import com.puppycrawl.tools.checkstyle.api.ExternalResourceHolder;
43 import com.puppycrawl.tools.checkstyle.api.FileContents;
44 import com.puppycrawl.tools.checkstyle.api.FileText;
45 import com.puppycrawl.tools.checkstyle.api.SeverityLevel;
46 import com.puppycrawl.tools.checkstyle.api.Violation;
47 import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
48
49
50
51
52
53
54 @FileStatefulCheck
55 public final class TreeWalker extends AbstractFileSetCheck implements ExternalResourceHolder {
56
57
58 public static final String PARSE_EXCEPTION_MSG = "parse.exception";
59
60
61 private final Map<Integer, Set<AbstractCheck>> tokenToOrdinaryChecks =
62 new HashMap<>();
63
64
65 private final Map<Integer, Set<AbstractCheck>> tokenToCommentChecks =
66 new HashMap<>();
67
68
69 private final Set<AbstractCheck> ordinaryChecks = createNewCheckSortedSet();
70
71
72 private final Set<AbstractCheck> commentChecks = createNewCheckSortedSet();
73
74
75 private final Set<TreeWalkerFilter> filters = new HashSet<>();
76
77
78 private final SortedSet<Violation> violations = new TreeSet<>();
79
80
81 private Context childContext;
82
83
84 private ModuleFactory moduleFactory;
85
86
87 private boolean skipFileOnJavaParseException;
88
89
90 private SeverityLevel javaParseExceptionSeverity = SeverityLevel.ERROR;
91
92
93
94
95 public TreeWalker() {
96 setFileExtensions("java");
97 }
98
99
100
101
102
103
104 public void setModuleFactory(ModuleFactory moduleFactory) {
105 this.moduleFactory = moduleFactory;
106 }
107
108
109
110
111
112
113
114 public void setSkipFileOnJavaParseException(boolean skipFileOnJavaParseException) {
115 this.skipFileOnJavaParseException = skipFileOnJavaParseException;
116 }
117
118
119
120
121
122
123
124
125
126 public void setJavaParseExceptionSeverity(SeverityLevel javaParseExceptionSeverity) {
127 this.javaParseExceptionSeverity = javaParseExceptionSeverity;
128 }
129
130 @Override
131 public void finishLocalSetup() {
132 final DefaultContext checkContext = new DefaultContext();
133 checkContext.add("severity", getSeverity());
134 checkContext.add("tabWidth", String.valueOf(getTabWidth()));
135 childContext = checkContext;
136 }
137
138
139
140
141
142
143
144 @Override
145 public void setupChild(Configuration childConf)
146 throws CheckstyleException {
147 final String name = childConf.getName();
148 final Object module;
149
150 try {
151 module = moduleFactory.createModule(name);
152 if (module instanceof AbstractAutomaticBean bean) {
153 bean.contextualize(childContext);
154 bean.configure(childConf);
155 }
156 }
157 catch (final CheckstyleException exc) {
158 throw new CheckstyleException("cannot initialize module " + name
159 + " - " + exc.getMessage(), exc);
160 }
161 if (module instanceof AbstractCheck check) {
162 check.init();
163 registerCheck(check);
164 }
165 else if (module instanceof TreeWalkerFilter filter) {
166 filters.add(filter);
167 }
168 else {
169 throw new CheckstyleException(
170 "TreeWalker is not allowed as a parent of " + name
171 + " Please review 'Parent Module' section for this Check in web"
172 + " documentation if Check is standard.");
173 }
174 }
175
176
177
178
179
180
181
182
183 @Override
184 protected void processFiltered(File file, FileText fileText) throws CheckstyleException {
185
186 if (!ordinaryChecks.isEmpty() || !commentChecks.isEmpty()) {
187 final FileContents contents = getFileContents();
188 DetailAST rootAST = null;
189
190 boolean skip = false;
191 try {
192 rootAST = JavaParser.parse(contents);
193 }
194
195 catch (Exception exc) {
196 if (!skipFileOnJavaParseException) {
197 throw exc;
198 }
199 skip = true;
200 violations.add(new Violation(1, Definitions.CHECKSTYLE_BUNDLE, PARSE_EXCEPTION_MSG,
201 new Object[] {exc.getMessage()}, javaParseExceptionSeverity, null,
202 getClass(), null));
203 addViolations(violations);
204 }
205
206 if (!skip) {
207 if (!ordinaryChecks.isEmpty()) {
208 walk(rootAST, contents, AstState.ORDINARY);
209 }
210 if (!commentChecks.isEmpty()) {
211 final DetailAST astWithComments = JavaParser.appendHiddenCommentNodes(rootAST);
212 walk(astWithComments, contents, AstState.WITH_COMMENTS);
213 }
214 if (filters.isEmpty()) {
215 addViolations(violations);
216 }
217 else {
218 final SortedSet<Violation> filteredViolations =
219 getFilteredViolations(file.getAbsolutePath(), contents, rootAST);
220 addViolations(filteredViolations);
221 }
222 }
223 violations.clear();
224 }
225 }
226
227
228
229
230
231
232
233
234
235 private SortedSet<Violation> getFilteredViolations(
236 String fileName, FileContents fileContents, DetailAST rootAST) {
237 final SortedSet<Violation> result = new TreeSet<>(violations);
238 for (Violation element : violations) {
239 final TreeWalkerAuditEvent event =
240 new TreeWalkerAuditEvent(fileContents, fileName, element, rootAST);
241 for (TreeWalkerFilter filter : filters) {
242 if (!filter.accept(event)) {
243 result.remove(element);
244 break;
245 }
246 }
247 }
248 return result;
249 }
250
251
252
253
254
255
256
257 private void registerCheck(AbstractCheck check) throws CheckstyleException {
258 final int[] tokens;
259 final Set<String> checkTokens = check.getTokenNames();
260 if (checkTokens.isEmpty()) {
261 tokens = check.getDefaultTokens();
262 }
263 else {
264 tokens = check.getRequiredTokens();
265
266
267 final int[] acceptableTokens = check.getAcceptableTokens();
268 Arrays.sort(acceptableTokens);
269 for (String token : checkTokens) {
270 final int tokenId = TokenUtil.getTokenId(token);
271 if (Arrays.binarySearch(acceptableTokens, tokenId) >= 0) {
272 registerCheck(tokenId, check);
273 }
274 else {
275 final String message = String.format(Locale.ROOT, "Token \"%s\" was "
276 + "not found in Acceptable tokens list in check %s",
277 token, check.getClass().getName());
278 throw new CheckstyleException(message);
279 }
280 }
281 }
282 for (int element : tokens) {
283 registerCheck(element, check);
284 }
285 if (check.isCommentNodesRequired()) {
286 commentChecks.add(check);
287 }
288 else {
289 ordinaryChecks.add(check);
290 }
291 }
292
293
294
295
296
297
298
299
300 private void registerCheck(int tokenId, AbstractCheck check) throws CheckstyleException {
301 if (check.isCommentNodesRequired()) {
302 tokenToCommentChecks.computeIfAbsent(tokenId, empty -> createNewCheckSortedSet())
303 .add(check);
304 }
305 else if (TokenUtil.isCommentType(tokenId)) {
306 final String message = String.format(Locale.ROOT, "Check '%s' waits for comment type "
307 + "token ('%s') and should override 'isCommentNodesRequired()' "
308 + "method to return 'true'", check.getClass().getName(),
309 TokenUtil.getTokenName(tokenId));
310 throw new CheckstyleException(message);
311 }
312 else {
313 tokenToOrdinaryChecks.computeIfAbsent(tokenId, empty -> createNewCheckSortedSet())
314 .add(check);
315 }
316 }
317
318
319
320
321
322
323
324
325 private void walk(DetailAST ast, FileContents contents,
326 AstState astState) {
327 notifyBegin(ast, contents, astState);
328 processIter(ast, astState);
329 notifyEnd(ast, astState);
330 }
331
332
333
334
335
336
337
338
339 private void notifyBegin(DetailAST rootAST, FileContents contents,
340 AstState astState) {
341 final Set<AbstractCheck> checks;
342
343 if (astState == AstState.WITH_COMMENTS) {
344 checks = commentChecks;
345 }
346 else {
347 checks = ordinaryChecks;
348 }
349
350 for (AbstractCheck check : checks) {
351 check.setFileContents(contents);
352 check.clearViolations();
353 check.beginTree(rootAST);
354 }
355 }
356
357
358
359
360
361
362
363 private void notifyEnd(DetailAST rootAST, AstState astState) {
364 final Set<AbstractCheck> checks;
365
366 if (astState == AstState.WITH_COMMENTS) {
367 checks = commentChecks;
368 }
369 else {
370 checks = ordinaryChecks;
371 }
372
373 for (AbstractCheck check : checks) {
374 check.finishTree(rootAST);
375 violations.addAll(check.getViolations());
376 }
377 }
378
379
380
381
382
383
384
385 private void notifyVisit(DetailAST ast, AstState astState) {
386 final Collection<AbstractCheck> visitors = getListOfChecks(ast, astState);
387
388 if (visitors != null) {
389 for (AbstractCheck check : visitors) {
390 check.visitToken(ast);
391 }
392 }
393 }
394
395
396
397
398
399
400
401
402 private void notifyLeave(DetailAST ast, AstState astState) {
403 final Collection<AbstractCheck> visitors = getListOfChecks(ast, astState);
404
405 if (visitors != null) {
406 for (AbstractCheck check : visitors) {
407 check.leaveToken(ast);
408 }
409 }
410 }
411
412
413
414
415
416
417
418
419
420
421 private Collection<AbstractCheck> getListOfChecks(DetailAST ast, AstState astState) {
422 final Collection<AbstractCheck> visitors;
423 final int tokenId = ast.getType();
424
425 if (astState == AstState.WITH_COMMENTS) {
426 visitors = tokenToCommentChecks.get(tokenId);
427 }
428 else {
429 visitors = tokenToOrdinaryChecks.get(tokenId);
430 }
431 return visitors;
432 }
433
434 @Override
435 public void destroy() {
436 ordinaryChecks.forEach(AbstractCheck::destroy);
437 commentChecks.forEach(AbstractCheck::destroy);
438 super.destroy();
439 }
440
441 @Override
442 public Set<String> getExternalResourceLocations() {
443 return Stream.concat(filters.stream(),
444 Stream.concat(ordinaryChecks.stream(), commentChecks.stream()))
445 .filter(ExternalResourceHolder.class::isInstance)
446 .flatMap(resource -> {
447 return ((ExternalResourceHolder) resource)
448 .getExternalResourceLocations().stream();
449 })
450 .collect(Collectors.toUnmodifiableSet());
451 }
452
453
454
455
456
457
458
459
460 private void processIter(DetailAST root, AstState astState) {
461 DetailAST curNode = root;
462 while (curNode != null) {
463 notifyVisit(curNode, astState);
464 DetailAST toVisit = curNode.getFirstChild();
465 while (curNode != null && toVisit == null) {
466 notifyLeave(curNode, astState);
467 toVisit = curNode.getNextSibling();
468 curNode = curNode.getParent();
469 }
470 curNode = toVisit;
471 }
472 }
473
474
475
476
477
478
479
480 private static SortedSet<AbstractCheck> createNewCheckSortedSet() {
481 return new TreeSet<>(
482 Comparator.<AbstractCheck, String>comparing(check -> check.getClass().getName())
483 .thenComparing(AbstractCheck::getId,
484 Comparator.nullsLast(Comparator.naturalOrder()))
485 .thenComparingInt(AbstractCheck::hashCode));
486 }
487
488
489
490
491
492 private enum AstState {
493
494
495
496
497 ORDINARY,
498
499
500
501
502 WITH_COMMENTS,
503
504 }
505
506 }