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.io.IOException;
24 import java.io.PrintWriter;
25 import java.io.StringWriter;
26 import java.io.UnsupportedEncodingException;
27 import java.nio.charset.Charset;
28 import java.nio.charset.StandardCharsets;
29 import java.util.ArrayList;
30 import java.util.List;
31 import java.util.Locale;
32 import java.util.Set;
33 import java.util.SortedSet;
34 import java.util.TreeSet;
35 import java.util.stream.Collectors;
36 import java.util.stream.Stream;
37
38 import org.apache.commons.logging.Log;
39 import org.apache.commons.logging.LogFactory;
40
41 import com.puppycrawl.tools.checkstyle.api.AuditEvent;
42 import com.puppycrawl.tools.checkstyle.api.AuditListener;
43 import com.puppycrawl.tools.checkstyle.api.BeforeExecutionFileFilter;
44 import com.puppycrawl.tools.checkstyle.api.BeforeExecutionFileFilterSet;
45 import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
46 import com.puppycrawl.tools.checkstyle.api.Configuration;
47 import com.puppycrawl.tools.checkstyle.api.Context;
48 import com.puppycrawl.tools.checkstyle.api.ExternalResourceHolder;
49 import com.puppycrawl.tools.checkstyle.api.FileSetCheck;
50 import com.puppycrawl.tools.checkstyle.api.FileText;
51 import com.puppycrawl.tools.checkstyle.api.Filter;
52 import com.puppycrawl.tools.checkstyle.api.FilterSet;
53 import com.puppycrawl.tools.checkstyle.api.MessageDispatcher;
54 import com.puppycrawl.tools.checkstyle.api.RootModule;
55 import com.puppycrawl.tools.checkstyle.api.SeverityLevel;
56 import com.puppycrawl.tools.checkstyle.api.SeverityLevelCounter;
57 import com.puppycrawl.tools.checkstyle.api.Violation;
58 import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
59
60
61
62
63 public class Checker extends AbstractAutomaticBean implements MessageDispatcher, RootModule {
64
65
66 public static final String EXCEPTION_MSG = "general.exception";
67
68
69 private static final String EXTENSION_SEPARATOR = ".";
70
71
72 private final Log log;
73
74
75 private final SeverityLevelCounter counter = new SeverityLevelCounter(
76 SeverityLevel.ERROR);
77
78
79 private final List<AuditListener> listeners = new ArrayList<>();
80
81
82 private final List<FileSetCheck> fileSetChecks = new ArrayList<>();
83
84
85 private final BeforeExecutionFileFilterSet beforeExecutionFileFilters =
86 new BeforeExecutionFileFilterSet();
87
88
89 private final FilterSet filters = new FilterSet();
90
91
92 private String basedir;
93
94
95 @XdocsPropertyType(PropertyType.LOCALE_COUNTRY)
96 private String localeCountry = Locale.getDefault().getCountry();
97
98 @XdocsPropertyType(PropertyType.LOCALE_LANGUAGE)
99 private String localeLanguage = Locale.getDefault().getLanguage();
100
101
102 private ModuleFactory moduleFactory;
103
104
105 private ClassLoader moduleClassLoader;
106
107
108 private Context childContext;
109
110
111 private String[] fileExtensions;
112
113
114
115
116
117
118
119
120
121
122
123 private SeverityLevel severity = SeverityLevel.ERROR;
124
125
126 private String charset = StandardCharsets.UTF_8.name();
127
128
129 @XdocsPropertyType(PropertyType.FILE)
130 private PropertyCacheFile cacheFile;
131
132
133 private boolean haltOnException = true;
134
135
136 private int tabWidth = CommonUtil.DEFAULT_TAB_WIDTH;
137
138
139
140
141
142 public Checker() {
143 addListener(counter);
144 log = LogFactory.getLog(Checker.class);
145 }
146
147
148
149
150
151
152
153 public void setCacheFile(String fileName) throws IOException {
154 final Configuration configuration = getConfiguration();
155 cacheFile = new PropertyCacheFile(configuration, fileName);
156 cacheFile.load();
157 }
158
159
160
161
162
163
164 public void removeBeforeExecutionFileFilter(BeforeExecutionFileFilter filter) {
165 beforeExecutionFileFilters.removeBeforeExecutionFileFilter(filter);
166 }
167
168
169
170
171
172
173 public void removeFilter(Filter filter) {
174 filters.removeFilter(filter);
175 }
176
177 @Override
178 public void destroy() {
179 listeners.clear();
180 fileSetChecks.clear();
181 beforeExecutionFileFilters.clear();
182 filters.clear();
183 if (cacheFile != null) {
184 try {
185 cacheFile.persist();
186 }
187 catch (IOException exc) {
188 throw new IllegalStateException(
189 getLocalizedMessage("Checker.cacheFilesException"), exc);
190 }
191 }
192 }
193
194
195
196
197
198
199 public void removeListener(AuditListener listener) {
200 listeners.remove(listener);
201 }
202
203
204
205
206
207
208 public void setBasedir(String basedir) {
209 this.basedir = basedir;
210 }
211
212 @Override
213 public int process(List<File> files) throws CheckstyleException {
214 if (cacheFile != null) {
215 cacheFile.putExternalResources(getExternalResourceLocations());
216 }
217
218
219 final int errorCount;
220 try (AuditCompletion auditCompletion = new AuditCompletion()) {
221 for (final FileSetCheck fsc : fileSetChecks) {
222 fsc.beginProcessing(charset);
223 }
224
225 final List<File> targetFiles = files.stream()
226 .filter(file -> CommonUtil.matchesFileExtension(file, fileExtensions))
227 .toList();
228 processFiles(targetFiles);
229
230
231
232 fileSetChecks.forEach(FileSetCheck::finishProcessing);
233
234
235 fileSetChecks.forEach(FileSetCheck::destroy);
236
237 errorCount = counter.getCount();
238 }
239 return errorCount;
240 }
241
242
243
244
245
246
247
248
249 private Set<String> getExternalResourceLocations() {
250 return Stream.concat(fileSetChecks.stream(), filters.getFilters().stream())
251 .filter(ExternalResourceHolder.class::isInstance)
252 .flatMap(resource -> {
253 return ((ExternalResourceHolder) resource)
254 .getExternalResourceLocations().stream();
255 })
256 .collect(Collectors.toUnmodifiableSet());
257 }
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275 private void processFiles(List<File> files) throws CheckstyleException {
276 for (final File file : files) {
277 String fileName = null;
278 final String filePath = file.getPath();
279 try {
280 fileName = file.getAbsolutePath();
281 final long timestamp = file.lastModified();
282 if (cacheFile != null && cacheFile.isInCache(fileName, timestamp)
283 || !acceptFileStarted(fileName)) {
284 continue;
285 }
286 if (cacheFile != null) {
287 cacheFile.put(fileName, timestamp);
288 }
289 try (FileAuditCompletion completion = new FileAuditCompletion(fileName)) {
290 final SortedSet<Violation> fileMessages = processFile(file);
291 fireErrors(fileName, fileMessages);
292 }
293 }
294
295
296 catch (Exception exc) {
297 if (fileName != null && cacheFile != null) {
298 cacheFile.remove(fileName);
299 }
300
301
302 throw new CheckstyleException(
303 getLocalizedMessage("Checker.processFilesException", filePath), exc);
304 }
305 catch (Error error) {
306 if (fileName != null && cacheFile != null) {
307 cacheFile.remove(fileName);
308 }
309
310
311 throw new Error(getLocalizedMessage("Checker.error", filePath), error);
312 }
313 }
314 }
315
316
317
318
319
320
321
322
323
324
325
326 private SortedSet<Violation> processFile(File file) throws CheckstyleException {
327 final SortedSet<Violation> fileMessages = new TreeSet<>();
328 try {
329 final FileText theText = new FileText(file.getAbsoluteFile(), charset);
330 for (final FileSetCheck fsc : fileSetChecks) {
331 fileMessages.addAll(fsc.process(file, theText));
332 }
333 }
334 catch (final IOException ioe) {
335 log.debug("IOException occurred.", ioe);
336 fileMessages.add(new Violation(1,
337 Definitions.CHECKSTYLE_BUNDLE, EXCEPTION_MSG,
338 new String[] {ioe.getMessage()}, null, getClass(), null));
339 }
340
341 catch (Exception exc) {
342 if (haltOnException) {
343 throw exc;
344 }
345
346 log.debug("Exception occurred.", exc);
347
348 final StringWriter sw = new StringWriter();
349 final PrintWriter pw = new PrintWriter(sw, true);
350
351 exc.printStackTrace(pw);
352
353 fileMessages.add(new Violation(1,
354 Definitions.CHECKSTYLE_BUNDLE, EXCEPTION_MSG,
355 new String[] {sw.getBuffer().toString()},
356 null, getClass(), null));
357 }
358 return fileMessages;
359 }
360
361
362
363
364
365
366
367
368 private boolean acceptFileStarted(String fileName) {
369 final String stripped = relativizePathWithCatch(fileName);
370 return beforeExecutionFileFilters.accept(stripped);
371 }
372
373
374
375
376
377
378
379 @Override
380 public void fireFileStarted(String fileName) {
381 final String stripped = relativizePathWithCatch(fileName);
382 final AuditEvent event = new AuditEvent(this, stripped);
383 for (final AuditListener listener : listeners) {
384 listener.fileStarted(event);
385 }
386 }
387
388
389
390
391
392
393
394 @Override
395 public void fireErrors(String fileName, SortedSet<Violation> errors) {
396 final String stripped = relativizePathWithCatch(fileName);
397 boolean hasNonFilteredViolations = false;
398 for (final Violation element : errors) {
399 final AuditEvent event = new AuditEvent(this, stripped, element);
400 if (filters.accept(event)) {
401 hasNonFilteredViolations = true;
402 for (final AuditListener listener : listeners) {
403 listener.addError(event);
404 }
405 }
406 }
407 if (hasNonFilteredViolations && cacheFile != null) {
408 cacheFile.remove(fileName);
409 }
410 }
411
412
413
414
415
416
417
418 @Override
419 public void fireFileFinished(String fileName) {
420 final String stripped = relativizePathWithCatch(fileName);
421 final AuditEvent event = new AuditEvent(this, stripped);
422 for (final AuditListener listener : listeners) {
423 listener.fileFinished(event);
424 }
425 }
426
427 @Override
428 protected void finishLocalSetup() throws CheckstyleException {
429 final Locale locale = Locale.of(localeLanguage, localeCountry);
430 LocalizedMessage.setLocale(locale);
431
432 if (moduleFactory == null) {
433 if (moduleClassLoader == null) {
434 throw new CheckstyleException(getLocalizedMessage("Checker.finishLocalSetup"));
435 }
436
437 final Set<String> packageNames = PackageNamesLoader
438 .getPackageNames(moduleClassLoader);
439 moduleFactory = new PackageObjectFactory(packageNames,
440 moduleClassLoader);
441 }
442
443 final DefaultContext context = new DefaultContext();
444 context.add("charset", charset);
445 context.add("moduleFactory", moduleFactory);
446 context.add("severity", severity.getName());
447 context.add("basedir", basedir);
448 context.add("tabWidth", String.valueOf(tabWidth));
449 childContext = context;
450 }
451
452
453
454
455 @Override
456 protected void setupChild(Configuration childConf)
457 throws CheckstyleException {
458 final String name = childConf.getName();
459 final Object child;
460
461 try {
462 child = moduleFactory.createModule(name);
463
464 if (child instanceof AbstractAutomaticBean bean) {
465 bean.contextualize(childContext);
466 bean.configure(childConf);
467 }
468 }
469 catch (final CheckstyleException exc) {
470 throw new CheckstyleException(
471 getLocalizedMessage("Checker.setupChildModule", name, exc.getMessage()), exc);
472 }
473 switch (child) {
474 case FileSetCheck fsc -> {
475 fsc.init();
476 addFileSetCheck(fsc);
477 }
478 case BeforeExecutionFileFilter filter -> addBeforeExecutionFileFilter(filter);
479 case Filter filter -> addFilter(filter);
480 case AuditListener listener -> addListener(listener);
481 case null, default -> throw new CheckstyleException(
482 getLocalizedMessage("Checker.setupChildNotAllowed", name));
483 }
484 }
485
486
487
488
489
490
491
492 public void addFileSetCheck(FileSetCheck fileSetCheck) {
493 fileSetCheck.setMessageDispatcher(this);
494 fileSetChecks.add(fileSetCheck);
495 }
496
497
498
499
500
501
502 public void addBeforeExecutionFileFilter(BeforeExecutionFileFilter filter) {
503 beforeExecutionFileFilters.addBeforeExecutionFileFilter(filter);
504 }
505
506
507
508
509
510
511 public void addFilter(Filter filter) {
512 filters.addFilter(filter);
513 }
514
515 @Override
516 public final void addListener(AuditListener listener) {
517 listeners.add(listener);
518 }
519
520
521
522
523
524
525
526
527 public final void setFileExtensions(String... extensions) {
528 if (extensions != null) {
529 fileExtensions = new String[extensions.length];
530 for (int index = 0; index < extensions.length; index++) {
531 final String extension = extensions[index];
532 if (extension.startsWith(EXTENSION_SEPARATOR)) {
533 fileExtensions[index] = extension;
534 }
535 else {
536 fileExtensions[index] = EXTENSION_SEPARATOR + extension;
537 }
538 }
539 }
540 }
541
542
543
544
545
546
547 public void setModuleFactory(ModuleFactory moduleFactory) {
548 this.moduleFactory = moduleFactory;
549 }
550
551
552
553
554
555
556 public void setLocaleCountry(String localeCountry) {
557 this.localeCountry = localeCountry;
558 }
559
560
561
562
563
564
565 public void setLocaleLanguage(String localeLanguage) {
566 this.localeLanguage = localeLanguage;
567 }
568
569
570
571
572
573
574
575
576 public final void setSeverity(String severity) {
577 this.severity = SeverityLevel.getInstance(severity);
578 }
579
580 @Override
581 public final void setModuleClassLoader(ClassLoader moduleClassLoader) {
582 this.moduleClassLoader = moduleClassLoader;
583 }
584
585
586
587
588
589
590
591 public void setCharset(String charset)
592 throws UnsupportedEncodingException {
593 if (!Charset.isSupported(charset)) {
594 throw new UnsupportedEncodingException(
595 getLocalizedMessage("Checker.setCharset", charset));
596 }
597 this.charset = charset;
598 }
599
600
601
602
603
604
605 public void setHaltOnException(boolean haltOnException) {
606 this.haltOnException = haltOnException;
607 }
608
609
610
611
612
613
614 public final void setTabWidth(int tabWidth) {
615 this.tabWidth = tabWidth;
616 }
617
618
619
620
621 public void clearCache() {
622 if (cacheFile != null) {
623 cacheFile.reset();
624 }
625 }
626
627
628
629
630
631
632
633
634 private String getLocalizedMessage(String messageKey, Object... args) {
635 final LocalizedMessage localizedMessage = new LocalizedMessage(
636 Definitions.CHECKSTYLE_BUNDLE, getClass(),
637 messageKey, args);
638
639 return localizedMessage.getMessage();
640 }
641
642
643
644
645
646
647
648
649 private String relativizePathWithCatch(String fileName) {
650 try {
651 return CommonUtil.relativizePath(basedir, fileName);
652 }
653
654 catch (Exception exception) {
655 throw new IllegalStateException(
656 getLocalizedMessage("general.relativizePath",
657 fileName, basedir), exception);
658 }
659 }
660
661
662 private final class AuditCompletion implements AutoCloseable {
663
664
665 private AuditCompletion() {
666 final AuditEvent event = new AuditEvent(Checker.this);
667 for (final AuditListener listener : listeners) {
668 listener.auditStarted(event);
669 }
670 }
671
672
673 @Override
674 public void close() {
675 final AuditEvent event = new AuditEvent(Checker.this);
676 for (final AuditListener listener : listeners) {
677 listener.auditFinished(event);
678 }
679 }
680 }
681
682
683 private final class FileAuditCompletion implements AutoCloseable {
684
685
686 private final String fileName;
687
688
689
690
691
692
693 private FileAuditCompletion(String fileName) {
694 this.fileName = fileName;
695 fireFileStarted(fileName);
696 }
697
698
699 @Override
700 public void close() {
701 fireFileFinished(fileName);
702 }
703 }
704
705 }