001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2025 the original author or authors.
004//
005// This library is free software; you can redistribute it and/or
006// modify it under the terms of the GNU Lesser General Public
007// License as published by the Free Software Foundation; either
008// version 2.1 of the License, or (at your option) any later version.
009//
010// This library is distributed in the hope that it will be useful,
011// but WITHOUT ANY WARRANTY; without even the implied warranty of
012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
013// Lesser General Public License for more details.
014//
015// You should have received a copy of the GNU Lesser General Public
016// License along with this library; if not, write to the Free Software
017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
018///////////////////////////////////////////////////////////////////////////////////////////////
019
020package com.puppycrawl.tools.checkstyle;
021
022import java.io.File;
023import java.io.IOException;
024import java.io.PrintWriter;
025import java.io.StringWriter;
026import java.io.UnsupportedEncodingException;
027import java.nio.charset.Charset;
028import java.nio.charset.StandardCharsets;
029import java.util.ArrayList;
030import java.util.List;
031import java.util.Locale;
032import java.util.Set;
033import java.util.SortedSet;
034import java.util.TreeSet;
035import java.util.stream.Collectors;
036import java.util.stream.Stream;
037
038import org.apache.commons.logging.Log;
039import org.apache.commons.logging.LogFactory;
040
041import com.puppycrawl.tools.checkstyle.api.AuditEvent;
042import com.puppycrawl.tools.checkstyle.api.AuditListener;
043import com.puppycrawl.tools.checkstyle.api.BeforeExecutionFileFilter;
044import com.puppycrawl.tools.checkstyle.api.BeforeExecutionFileFilterSet;
045import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
046import com.puppycrawl.tools.checkstyle.api.Configuration;
047import com.puppycrawl.tools.checkstyle.api.Context;
048import com.puppycrawl.tools.checkstyle.api.ExternalResourceHolder;
049import com.puppycrawl.tools.checkstyle.api.FileSetCheck;
050import com.puppycrawl.tools.checkstyle.api.FileText;
051import com.puppycrawl.tools.checkstyle.api.Filter;
052import com.puppycrawl.tools.checkstyle.api.FilterSet;
053import com.puppycrawl.tools.checkstyle.api.MessageDispatcher;
054import com.puppycrawl.tools.checkstyle.api.RootModule;
055import com.puppycrawl.tools.checkstyle.api.SeverityLevel;
056import com.puppycrawl.tools.checkstyle.api.SeverityLevelCounter;
057import com.puppycrawl.tools.checkstyle.api.Violation;
058import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
059
060/**
061 * This class provides the functionality to check a set of files.
062 */
063public class Checker extends AbstractAutomaticBean implements MessageDispatcher, RootModule {
064
065 /** Message to use when an exception occurs and should be printed as a violation. */
066 public static final String EXCEPTION_MSG = "general.exception";
067
068 /** The extension separator. */
069 private static final String EXTENSION_SEPARATOR = ".";
070
071 /** Logger for Checker. */
072 private final Log log;
073
074 /** Maintains error count. */
075 private final SeverityLevelCounter counter = new SeverityLevelCounter(
076 SeverityLevel.ERROR);
077
078 /** Vector of listeners. */
079 private final List<AuditListener> listeners = new ArrayList<>();
080
081 /** Vector of fileset checks. */
082 private final List<FileSetCheck> fileSetChecks = new ArrayList<>();
083
084 /** The audit event before execution file filters. */
085 private final BeforeExecutionFileFilterSet beforeExecutionFileFilters =
086 new BeforeExecutionFileFilterSet();
087
088 /** The audit event filters. */
089 private final FilterSet filters = new FilterSet();
090
091 /** The basedir to strip off in file names. */
092 private String basedir;
093
094 /** Locale country to report messages . **/
095 @XdocsPropertyType(PropertyType.LOCALE_COUNTRY)
096 private String localeCountry = Locale.getDefault().getCountry();
097 /** Locale language to report messages . **/
098 @XdocsPropertyType(PropertyType.LOCALE_LANGUAGE)
099 private String localeLanguage = Locale.getDefault().getLanguage();
100
101 /** The factory for instantiating submodules. */
102 private ModuleFactory moduleFactory;
103
104 /** The classloader used for loading Checkstyle module classes. */
105 private ClassLoader moduleClassLoader;
106
107 /** The context of all child components. */
108 private Context childContext;
109
110 /** The file extensions that are accepted. */
111 private String[] fileExtensions;
112
113 /**
114 * The severity level of any violations found by submodules.
115 * The value of this property is passed to submodules via
116 * contextualize().
117 *
118 * <p>Note: Since the Checker is merely a container for modules
119 * it does not make sense to implement logging functionality
120 * here. Consequently, Checker does not extend AbstractViolationReporter,
121 * leading to a bit of duplicated code for severity level setting.
122 */
123 private SeverityLevel severity = SeverityLevel.ERROR;
124
125 /** Name of a charset. */
126 private String charset = StandardCharsets.UTF_8.name();
127
128 /** Cache file. **/
129 @XdocsPropertyType(PropertyType.FILE)
130 private PropertyCacheFile cacheFile;
131
132 /** Controls whether exceptions should halt execution or not. */
133 private boolean haltOnException = true;
134
135 /** The tab width for column reporting. */
136 private int tabWidth = CommonUtil.DEFAULT_TAB_WIDTH;
137
138 /**
139 * Creates a new {@code Checker} instance.
140 * The instance needs to be contextualized and configured.
141 */
142 public Checker() {
143 addListener(counter);
144 log = LogFactory.getLog(Checker.class);
145 }
146
147 /**
148 * Sets cache file.
149 *
150 * @param fileName the cache file.
151 * @throws IOException if there are some problems with file loading.
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 * Removes before execution file filter.
161 *
162 * @param filter before execution file filter to remove.
163 */
164 public void removeBeforeExecutionFileFilter(BeforeExecutionFileFilter filter) {
165 beforeExecutionFileFilters.removeBeforeExecutionFileFilter(filter);
166 }
167
168 /**
169 * Removes filter.
170 *
171 * @param filter filter to remove.
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 * Removes a given listener.
196 *
197 * @param listener a listener to remove
198 */
199 public void removeListener(AuditListener listener) {
200 listeners.remove(listener);
201 }
202
203 /**
204 * Sets base directory.
205 *
206 * @param basedir the base directory to strip off in file names
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 // Prepare to start
219 fireAuditStarted();
220 for (final FileSetCheck fsc : fileSetChecks) {
221 fsc.beginProcessing(charset);
222 }
223
224 final List<File> targetFiles = files.stream()
225 .filter(file -> CommonUtil.matchesFileExtension(file, fileExtensions))
226 .toList();
227 processFiles(targetFiles);
228
229 // Finish up
230 // It may also log!!!
231 fileSetChecks.forEach(FileSetCheck::finishProcessing);
232
233 // It may also log!!!
234 fileSetChecks.forEach(FileSetCheck::destroy);
235
236 final int errorCount = counter.getCount();
237 fireAuditFinished();
238 return errorCount;
239 }
240
241 /**
242 * Returns a set of external configuration resource locations which are used by all file set
243 * checks and filters.
244 *
245 * @return a set of external configuration resource locations which are used by all file set
246 * checks and filters.
247 */
248 private Set<String> getExternalResourceLocations() {
249 return Stream.concat(fileSetChecks.stream(), filters.getFilters().stream())
250 .filter(ExternalResourceHolder.class::isInstance)
251 .flatMap(resource -> {
252 return ((ExternalResourceHolder) resource)
253 .getExternalResourceLocations().stream();
254 })
255 .collect(Collectors.toUnmodifiableSet());
256 }
257
258 /** Notify all listeners about the audit start. */
259 private void fireAuditStarted() {
260 final AuditEvent event = new AuditEvent(this);
261 for (final AuditListener listener : listeners) {
262 listener.auditStarted(event);
263 }
264 }
265
266 /** Notify all listeners about the audit end. */
267 private void fireAuditFinished() {
268 final AuditEvent event = new AuditEvent(this);
269 for (final AuditListener listener : listeners) {
270 listener.auditFinished(event);
271 }
272 }
273
274 /**
275 * Processes a list of files with all FileSetChecks.
276 *
277 * @param files a list of files to process.
278 * @throws CheckstyleException if error condition within Checkstyle occurs.
279 * @throws Error wraps any java.lang.Error happened during execution
280 * @noinspection ProhibitedExceptionThrown
281 * @noinspectionreason ProhibitedExceptionThrown - There is no other way to
282 * deliver filename that was under processing.
283 */
284 // -@cs[CyclomaticComplexity] no easy way to split this logic of processing the file
285 private void processFiles(List<File> files) throws CheckstyleException {
286 for (final File file : files) {
287 String fileName = null;
288 final String filePath = file.getPath();
289 try {
290 fileName = file.getAbsolutePath();
291 final long timestamp = file.lastModified();
292 if (cacheFile != null && cacheFile.isInCache(fileName, timestamp)
293 || !acceptFileStarted(fileName)) {
294 continue;
295 }
296 if (cacheFile != null) {
297 cacheFile.put(fileName, timestamp);
298 }
299 fireFileStarted(fileName);
300 final SortedSet<Violation> fileMessages = processFile(file);
301 fireErrors(fileName, fileMessages);
302 fireFileFinished(fileName);
303 }
304 // -@cs[IllegalCatch] There is no other way to deliver filename that was under
305 // processing. See https://github.com/checkstyle/checkstyle/issues/2285
306 catch (Exception exc) {
307 if (fileName != null && cacheFile != null) {
308 cacheFile.remove(fileName);
309 }
310
311 // We need to catch all exceptions to put a reason failure (file name) in exception
312 throw new CheckstyleException(
313 getLocalizedMessage("Checker.processFilesException", filePath), exc);
314 }
315 catch (Error error) {
316 if (fileName != null && cacheFile != null) {
317 cacheFile.remove(fileName);
318 }
319
320 // We need to catch all errors to put a reason failure (file name) in error
321 throw new Error(getLocalizedMessage("Checker.error", filePath), error);
322 }
323 }
324 }
325
326 /**
327 * Processes a file with all FileSetChecks.
328 *
329 * @param file a file to process.
330 * @return a sorted set of violations to be logged.
331 * @throws CheckstyleException if error condition within Checkstyle occurs.
332 * @noinspection ProhibitedExceptionThrown
333 * @noinspectionreason ProhibitedExceptionThrown - there is no other way to obey
334 * haltOnException field
335 */
336 private SortedSet<Violation> processFile(File file) throws CheckstyleException {
337 final SortedSet<Violation> fileMessages = new TreeSet<>();
338 try {
339 final FileText theText = new FileText(file.getAbsoluteFile(), charset);
340 for (final FileSetCheck fsc : fileSetChecks) {
341 fileMessages.addAll(fsc.process(file, theText));
342 }
343 }
344 catch (final IOException ioe) {
345 log.debug("IOException occurred.", ioe);
346 fileMessages.add(new Violation(1,
347 Definitions.CHECKSTYLE_BUNDLE, EXCEPTION_MSG,
348 new String[] {ioe.getMessage()}, null, getClass(), null));
349 }
350 // -@cs[IllegalCatch] There is no other way to obey haltOnException field
351 catch (Exception exc) {
352 if (haltOnException) {
353 throw exc;
354 }
355
356 log.debug("Exception occurred.", exc);
357
358 final StringWriter sw = new StringWriter();
359 final PrintWriter pw = new PrintWriter(sw, true);
360
361 exc.printStackTrace(pw);
362
363 fileMessages.add(new Violation(1,
364 Definitions.CHECKSTYLE_BUNDLE, EXCEPTION_MSG,
365 new String[] {sw.getBuffer().toString()},
366 null, getClass(), null));
367 }
368 return fileMessages;
369 }
370
371 /**
372 * Check if all before execution file filters accept starting the file.
373 *
374 * @param fileName
375 * the file to be audited
376 * @return {@code true} if the file is accepted.
377 */
378 private boolean acceptFileStarted(String fileName) {
379 final String stripped = CommonUtil.relativizePath(basedir, fileName);
380 return beforeExecutionFileFilters.accept(stripped);
381 }
382
383 /**
384 * Notify all listeners about the beginning of a file audit.
385 *
386 * @param fileName
387 * the file to be audited
388 */
389 @Override
390 public void fireFileStarted(String fileName) {
391 final String stripped = CommonUtil.relativizePath(basedir, fileName);
392 final AuditEvent event = new AuditEvent(this, stripped);
393 for (final AuditListener listener : listeners) {
394 listener.fileStarted(event);
395 }
396 }
397
398 /**
399 * Notify all listeners about the errors in a file.
400 *
401 * @param fileName the audited file
402 * @param errors the audit errors from the file
403 */
404 @Override
405 public void fireErrors(String fileName, SortedSet<Violation> errors) {
406 final String stripped = CommonUtil.relativizePath(basedir, fileName);
407 boolean hasNonFilteredViolations = false;
408 for (final Violation element : errors) {
409 final AuditEvent event = new AuditEvent(this, stripped, element);
410 if (filters.accept(event)) {
411 hasNonFilteredViolations = true;
412 for (final AuditListener listener : listeners) {
413 listener.addError(event);
414 }
415 }
416 }
417 if (hasNonFilteredViolations && cacheFile != null) {
418 cacheFile.remove(fileName);
419 }
420 }
421
422 /**
423 * Notify all listeners about the end of a file audit.
424 *
425 * @param fileName
426 * the audited file
427 */
428 @Override
429 public void fireFileFinished(String fileName) {
430 final String stripped = CommonUtil.relativizePath(basedir, fileName);
431 final AuditEvent event = new AuditEvent(this, stripped);
432 for (final AuditListener listener : listeners) {
433 listener.fileFinished(event);
434 }
435 }
436
437 /**
438 * Performs final setup of the Checker after configuration is complete.
439 *
440 * @noinspection deprecation
441 * @noinspectionreason Disabled until #17646
442 */
443 @Override
444 protected void finishLocalSetup() throws CheckstyleException {
445 final Locale locale = new Locale(localeLanguage, localeCountry);
446 LocalizedMessage.setLocale(locale);
447
448 if (moduleFactory == null) {
449 if (moduleClassLoader == null) {
450 throw new CheckstyleException(getLocalizedMessage("Checker.finishLocalSetup"));
451 }
452
453 final Set<String> packageNames = PackageNamesLoader
454 .getPackageNames(moduleClassLoader);
455 moduleFactory = new PackageObjectFactory(packageNames,
456 moduleClassLoader);
457 }
458
459 final DefaultContext context = new DefaultContext();
460 context.add("charset", charset);
461 context.add("moduleFactory", moduleFactory);
462 context.add("severity", severity.getName());
463 context.add("basedir", basedir);
464 context.add("tabWidth", String.valueOf(tabWidth));
465 childContext = context;
466 }
467
468 /**
469 * {@inheritDoc} Creates child module.
470 *
471 * @noinspection ChainOfInstanceofChecks
472 * @noinspectionreason ChainOfInstanceofChecks - we treat checks and filters differently
473 */
474 @Override
475 protected void setupChild(Configuration childConf)
476 throws CheckstyleException {
477 final String name = childConf.getName();
478 final Object child;
479
480 try {
481 child = moduleFactory.createModule(name);
482
483 if (child instanceof AbstractAutomaticBean bean) {
484 bean.contextualize(childContext);
485 bean.configure(childConf);
486 }
487 }
488 catch (final CheckstyleException exc) {
489 throw new CheckstyleException(
490 getLocalizedMessage("Checker.setupChildModule", name, exc.getMessage()), exc);
491 }
492 if (child instanceof FileSetCheck fsc) {
493 fsc.init();
494 addFileSetCheck(fsc);
495 }
496 else if (child instanceof BeforeExecutionFileFilter filter) {
497 addBeforeExecutionFileFilter(filter);
498 }
499 else if (child instanceof Filter filter) {
500 addFilter(filter);
501 }
502 else if (child instanceof AuditListener listener) {
503 addListener(listener);
504 }
505 else {
506 throw new CheckstyleException(
507 getLocalizedMessage("Checker.setupChildNotAllowed", name));
508 }
509 }
510
511 /**
512 * Adds a FileSetCheck to the list of FileSetChecks
513 * that is executed in process().
514 *
515 * @param fileSetCheck the additional FileSetCheck
516 */
517 public void addFileSetCheck(FileSetCheck fileSetCheck) {
518 fileSetCheck.setMessageDispatcher(this);
519 fileSetChecks.add(fileSetCheck);
520 }
521
522 /**
523 * Adds a before execution file filter to the end of the event chain.
524 *
525 * @param filter the additional filter
526 */
527 public void addBeforeExecutionFileFilter(BeforeExecutionFileFilter filter) {
528 beforeExecutionFileFilters.addBeforeExecutionFileFilter(filter);
529 }
530
531 /**
532 * Adds a filter to the end of the audit event filter chain.
533 *
534 * @param filter the additional filter
535 */
536 public void addFilter(Filter filter) {
537 filters.addFilter(filter);
538 }
539
540 @Override
541 public final void addListener(AuditListener listener) {
542 listeners.add(listener);
543 }
544
545 /**
546 * Sets the file extensions that identify the files that pass the
547 * filter of this FileSetCheck.
548 *
549 * @param extensions the set of file extensions. A missing
550 * initial '.' character of an extension is automatically added.
551 */
552 public final void setFileExtensions(String... extensions) {
553 if (extensions != null) {
554 fileExtensions = new String[extensions.length];
555 for (int i = 0; i < extensions.length; i++) {
556 final String extension = extensions[i];
557 if (extension.startsWith(EXTENSION_SEPARATOR)) {
558 fileExtensions[i] = extension;
559 }
560 else {
561 fileExtensions[i] = EXTENSION_SEPARATOR + extension;
562 }
563 }
564 }
565 }
566
567 /**
568 * Sets the factory for creating submodules.
569 *
570 * @param moduleFactory the factory for creating FileSetChecks
571 */
572 public void setModuleFactory(ModuleFactory moduleFactory) {
573 this.moduleFactory = moduleFactory;
574 }
575
576 /**
577 * Sets locale country.
578 *
579 * @param localeCountry the country to report messages
580 */
581 public void setLocaleCountry(String localeCountry) {
582 this.localeCountry = localeCountry;
583 }
584
585 /**
586 * Sets locale language.
587 *
588 * @param localeLanguage the language to report messages
589 */
590 public void setLocaleLanguage(String localeLanguage) {
591 this.localeLanguage = localeLanguage;
592 }
593
594 /**
595 * Sets the severity level. The string should be one of the names
596 * defined in the {@code SeverityLevel} class.
597 *
598 * @param severity The new severity level
599 * @see SeverityLevel
600 */
601 public final void setSeverity(String severity) {
602 this.severity = SeverityLevel.getInstance(severity);
603 }
604
605 @Override
606 public final void setModuleClassLoader(ClassLoader moduleClassLoader) {
607 this.moduleClassLoader = moduleClassLoader;
608 }
609
610 /**
611 * Sets a named charset.
612 *
613 * @param charset the name of a charset
614 * @throws UnsupportedEncodingException if charset is unsupported.
615 */
616 public void setCharset(String charset)
617 throws UnsupportedEncodingException {
618 if (!Charset.isSupported(charset)) {
619 throw new UnsupportedEncodingException(
620 getLocalizedMessage("Checker.setCharset", charset));
621 }
622 this.charset = charset;
623 }
624
625 /**
626 * Sets the field haltOnException.
627 *
628 * @param haltOnException the new value.
629 */
630 public void setHaltOnException(boolean haltOnException) {
631 this.haltOnException = haltOnException;
632 }
633
634 /**
635 * Set the tab width to report audit events with.
636 *
637 * @param tabWidth an {@code int} value
638 */
639 public final void setTabWidth(int tabWidth) {
640 this.tabWidth = tabWidth;
641 }
642
643 /**
644 * Clears the cache.
645 */
646 public void clearCache() {
647 if (cacheFile != null) {
648 cacheFile.reset();
649 }
650 }
651
652 /**
653 * Extracts localized messages from properties files.
654 *
655 * @param messageKey the key pointing to localized message in respective properties file.
656 * @param args the arguments of message in respective properties file.
657 * @return a string containing extracted localized message
658 */
659 private String getLocalizedMessage(String messageKey, Object... args) {
660 final LocalizedMessage localizedMessage = new LocalizedMessage(
661 Definitions.CHECKSTYLE_BUNDLE, getClass(),
662 messageKey, args);
663
664 return localizedMessage.getMessage();
665 }
666
667}