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 @Override
142 public void setupChild(Configuration childConf)
143 throws CheckstyleException {
144 final String name = childConf.getName();
145 final Object module;
146
147 try {
148 module = moduleFactory.createModule(name);
149 if (module instanceof AbstractAutomaticBean bean) {
150 bean.contextualize(childContext);
151 bean.configure(childConf);
152 }
153 }
154 catch (final CheckstyleException exc) {
155 throw new CheckstyleException("cannot initialize module " + name, exc);
156 }
157 switch (module) {
158 case AbstractCheck check -> {
159 check.init();
160 registerCheck(check);
161 }
162 case TreeWalkerFilter filter -> filters.add(filter);
163 case null, default -> throw new CheckstyleException(
164 "TreeWalker is not allowed as a parent of " + name
165 + " Please review 'Parent Module' section for this Check in web"
166 + " documentation if Check is standard.");
167 }
168 }
169
170
171
172
173
174
175
176
177 @Override
178 protected void processFiltered(File file, FileText fileText) throws CheckstyleException {
179
180 if (!ordinaryChecks.isEmpty() || !commentChecks.isEmpty()) {
181 final FileContents contents = getFileContents();
182 DetailAST rootAST = null;
183
184 boolean skip = false;
185 try {
186 rootAST = JavaParser.parse(contents);
187 }
188
189 catch (Exception exc) {
190 if (!skipFileOnJavaParseException) {
191 throw exc;
192 }
193 skip = true;
194 violations.add(new Violation(1, Definitions.CHECKSTYLE_BUNDLE, PARSE_EXCEPTION_MSG,
195 new Object[] {exc.getMessage()}, javaParseExceptionSeverity, null,
196 getClass(), null));
197 addViolations(violations);
198 }
199
200 if (!skip) {
201 if (!ordinaryChecks.isEmpty()) {
202 walk(rootAST, contents, AstState.ORDINARY);
203 }
204 if (!commentChecks.isEmpty()) {
205 final DetailAST astWithComments = JavaParser.appendHiddenCommentNodes(rootAST);
206 walk(astWithComments, contents, AstState.WITH_COMMENTS);
207 }
208 if (filters.isEmpty()) {
209 addViolations(violations);
210 }
211 else {
212 final SortedSet<Violation> filteredViolations =
213 getFilteredViolations(file.getAbsolutePath(), contents, rootAST);
214 addViolations(filteredViolations);
215 }
216 }
217 violations.clear();
218 }
219 }
220
221
222
223
224
225
226
227
228
229 private SortedSet<Violation> getFilteredViolations(
230 String fileName, FileContents fileContents, DetailAST rootAST) {
231 final SortedSet<Violation> result = new TreeSet<>(violations);
232 for (Violation element : violations) {
233 final TreeWalkerAuditEvent event =
234 new TreeWalkerAuditEvent(fileContents, fileName, element, rootAST);
235 for (TreeWalkerFilter filter : filters) {
236 if (!filter.accept(event)) {
237 result.remove(element);
238 break;
239 }
240 }
241 }
242 return result;
243 }
244
245
246
247
248
249
250
251 private void registerCheck(AbstractCheck check) throws CheckstyleException {
252 final int[] tokens;
253 final Set<String> checkTokens = check.getTokenNames();
254 if (checkTokens.isEmpty()) {
255 tokens = check.getDefaultTokens();
256 }
257 else {
258 tokens = check.getRequiredTokens();
259
260
261 final int[] acceptableTokens = check.getAcceptableTokens();
262 Arrays.sort(acceptableTokens);
263 for (String token : checkTokens) {
264 final int tokenId = TokenUtil.getTokenId(token);
265 if (Arrays.binarySearch(acceptableTokens, tokenId) >= 0) {
266 registerCheck(tokenId, check);
267 }
268 else {
269 final String message = String.format(Locale.ROOT, "Token \"%s\" was "
270 + "not found in Acceptable tokens list in check %s",
271 token, check.getClass().getName());
272 throw new CheckstyleException(message);
273 }
274 }
275 }
276 for (int element : tokens) {
277 registerCheck(element, check);
278 }
279 if (check.isCommentNodesRequired()) {
280 commentChecks.add(check);
281 }
282 else {
283 ordinaryChecks.add(check);
284 }
285 }
286
287
288
289
290
291
292
293
294 private void registerCheck(int tokenId, AbstractCheck check) throws CheckstyleException {
295 if (check.isCommentNodesRequired()) {
296 tokenToCommentChecks.computeIfAbsent(tokenId, empty -> createNewCheckSortedSet())
297 .add(check);
298 }
299 else if (TokenUtil.isCommentType(tokenId)) {
300 final String message = String.format(Locale.ROOT, "Check '%s' waits for comment type "
301 + "token ('%s') and should override 'isCommentNodesRequired()' "
302 + "method to return 'true'", check.getClass().getName(),
303 TokenUtil.getTokenName(tokenId));
304 throw new CheckstyleException(message);
305 }
306 else {
307 tokenToOrdinaryChecks.computeIfAbsent(tokenId, empty -> createNewCheckSortedSet())
308 .add(check);
309 }
310 }
311
312
313
314
315
316
317
318
319 private void walk(DetailAST ast, FileContents contents,
320 AstState astState) {
321 notifyBegin(ast, contents, astState);
322 processIter(ast, astState);
323 notifyEnd(ast, astState);
324 }
325
326
327
328
329
330
331
332
333 private void notifyBegin(DetailAST rootAST, FileContents contents,
334 AstState astState) {
335 final Set<AbstractCheck> checks;
336
337 if (astState == AstState.WITH_COMMENTS) {
338 checks = commentChecks;
339 }
340 else {
341 checks = ordinaryChecks;
342 }
343
344 for (AbstractCheck check : checks) {
345 check.setFileContents(contents);
346 check.clearViolations();
347 check.beginTree(rootAST);
348 }
349 }
350
351
352
353
354
355
356
357 private void notifyEnd(DetailAST rootAST, AstState astState) {
358 final Set<AbstractCheck> checks;
359
360 if (astState == AstState.WITH_COMMENTS) {
361 checks = commentChecks;
362 }
363 else {
364 checks = ordinaryChecks;
365 }
366
367 for (AbstractCheck check : checks) {
368 check.finishTree(rootAST);
369 violations.addAll(check.getViolations());
370 }
371 }
372
373
374
375
376
377
378
379 private void notifyVisit(DetailAST ast, AstState astState) {
380 final Collection<AbstractCheck> visitors = getListOfChecks(ast, astState);
381
382 if (visitors != null) {
383 for (AbstractCheck check : visitors) {
384 check.visitToken(ast);
385 }
386 }
387 }
388
389
390
391
392
393
394
395
396 private void notifyLeave(DetailAST ast, AstState astState) {
397 final Collection<AbstractCheck> visitors = getListOfChecks(ast, astState);
398
399 if (visitors != null) {
400 for (AbstractCheck check : visitors) {
401 check.leaveToken(ast);
402 }
403 }
404 }
405
406
407
408
409
410
411
412
413
414
415 private Collection<AbstractCheck> getListOfChecks(DetailAST ast, AstState astState) {
416 final Collection<AbstractCheck> visitors;
417 final int tokenId = ast.getType();
418
419 if (astState == AstState.WITH_COMMENTS) {
420 visitors = tokenToCommentChecks.get(tokenId);
421 }
422 else {
423 visitors = tokenToOrdinaryChecks.get(tokenId);
424 }
425 return visitors;
426 }
427
428 @Override
429 public void destroy() {
430 ordinaryChecks.forEach(AbstractCheck::destroy);
431 commentChecks.forEach(AbstractCheck::destroy);
432 super.destroy();
433 }
434
435 @Override
436 public Set<String> getExternalResourceLocations() {
437 return Stream.concat(filters.stream(),
438 Stream.concat(ordinaryChecks.stream(), commentChecks.stream()))
439 .filter(ExternalResourceHolder.class::isInstance)
440 .flatMap(resource -> {
441 return ((ExternalResourceHolder) resource)
442 .getExternalResourceLocations().stream();
443 })
444 .collect(Collectors.toUnmodifiableSet());
445 }
446
447
448
449
450
451
452
453
454 private void processIter(DetailAST root, AstState astState) {
455 DetailAST curNode = root;
456 while (curNode != null) {
457 notifyVisit(curNode, astState);
458 DetailAST toVisit = curNode.getFirstChild();
459 while (curNode != null && toVisit == null) {
460 notifyLeave(curNode, astState);
461 toVisit = curNode.getNextSibling();
462 curNode = curNode.getParent();
463 }
464 curNode = toVisit;
465 }
466 }
467
468
469
470
471
472
473
474 private static SortedSet<AbstractCheck> createNewCheckSortedSet() {
475 return new TreeSet<>(
476 Comparator.<AbstractCheck, String>comparing(check -> check.getClass().getName())
477 .thenComparing(AbstractCheck::getId,
478 Comparator.nullsLast(Comparator.naturalOrder()))
479 .thenComparingInt(AbstractCheck::hashCode));
480 }
481
482
483
484
485
486 private enum AstState {
487
488
489
490
491 ORDINARY,
492
493
494
495
496 WITH_COMMENTS,
497
498 }
499
500 }