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