View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2026 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
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   * This class provides the functionality to check a set of files.
62   */
63  public class Checker extends AbstractAutomaticBean implements MessageDispatcher, RootModule {
64  
65      /** Message to use when an exception occurs and should be printed as a violation. */
66      public static final String EXCEPTION_MSG = "general.exception";
67  
68      /** The extension separator. */
69      private static final String EXTENSION_SEPARATOR = ".";
70  
71      /** Logger for Checker. */
72      private final Log log;
73  
74      /** Maintains error count. */
75      private final SeverityLevelCounter counter = new SeverityLevelCounter(
76              SeverityLevel.ERROR);
77  
78      /** Vector of listeners. */
79      private final List<AuditListener> listeners = new ArrayList<>();
80  
81      /** Vector of fileset checks. */
82      private final List<FileSetCheck> fileSetChecks = new ArrayList<>();
83  
84      /** The audit event before execution file filters. */
85      private final BeforeExecutionFileFilterSet beforeExecutionFileFilters =
86              new BeforeExecutionFileFilterSet();
87  
88      /** The audit event filters. */
89      private final FilterSet filters = new FilterSet();
90  
91      /** The basedir to strip off in file names. */
92      private String basedir;
93  
94      /** Locale country to report messages . **/
95      @XdocsPropertyType(PropertyType.LOCALE_COUNTRY)
96      private String localeCountry = Locale.getDefault().getCountry();
97      /** Locale language to report messages . **/
98      @XdocsPropertyType(PropertyType.LOCALE_LANGUAGE)
99      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 = relativizePathWithCatch(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 = relativizePathWithCatch(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 = relativizePathWithCatch(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 = relativizePathWithCatch(fileName);
431         final AuditEvent event = new AuditEvent(this, stripped);
432         for (final AuditListener listener : listeners) {
433             listener.fileFinished(event);
434         }
435     }
436 
437     @Override
438     protected void finishLocalSetup() throws CheckstyleException {
439         final Locale locale = Locale.of(localeLanguage, localeCountry);
440         LocalizedMessage.setLocale(locale);
441 
442         if (moduleFactory == null) {
443             if (moduleClassLoader == null) {
444                 throw new CheckstyleException(getLocalizedMessage("Checker.finishLocalSetup"));
445             }
446 
447             final Set<String> packageNames = PackageNamesLoader
448                     .getPackageNames(moduleClassLoader);
449             moduleFactory = new PackageObjectFactory(packageNames,
450                     moduleClassLoader);
451         }
452 
453         final DefaultContext context = new DefaultContext();
454         context.add("charset", charset);
455         context.add("moduleFactory", moduleFactory);
456         context.add("severity", severity.getName());
457         context.add("basedir", basedir);
458         context.add("tabWidth", String.valueOf(tabWidth));
459         childContext = context;
460     }
461 
462     /**
463      * {@inheritDoc} Creates child module.
464      *
465      * @noinspection ChainOfInstanceofChecks
466      * @noinspectionreason ChainOfInstanceofChecks - we treat checks and filters differently
467      */
468     @Override
469     protected void setupChild(Configuration childConf)
470             throws CheckstyleException {
471         final String name = childConf.getName();
472         final Object child;
473 
474         try {
475             child = moduleFactory.createModule(name);
476 
477             if (child instanceof AbstractAutomaticBean bean) {
478                 bean.contextualize(childContext);
479                 bean.configure(childConf);
480             }
481         }
482         catch (final CheckstyleException exc) {
483             throw new CheckstyleException(
484                     getLocalizedMessage("Checker.setupChildModule", name, exc.getMessage()), exc);
485         }
486         if (child instanceof FileSetCheck fsc) {
487             fsc.init();
488             addFileSetCheck(fsc);
489         }
490         else if (child instanceof BeforeExecutionFileFilter filter) {
491             addBeforeExecutionFileFilter(filter);
492         }
493         else if (child instanceof Filter filter) {
494             addFilter(filter);
495         }
496         else if (child instanceof AuditListener listener) {
497             addListener(listener);
498         }
499         else {
500             throw new CheckstyleException(
501                     getLocalizedMessage("Checker.setupChildNotAllowed", name));
502         }
503     }
504 
505     /**
506      * Adds a FileSetCheck to the list of FileSetChecks
507      * that is executed in process().
508      *
509      * @param fileSetCheck the additional FileSetCheck
510      */
511     public void addFileSetCheck(FileSetCheck fileSetCheck) {
512         fileSetCheck.setMessageDispatcher(this);
513         fileSetChecks.add(fileSetCheck);
514     }
515 
516     /**
517      * Adds a before execution file filter to the end of the event chain.
518      *
519      * @param filter the additional filter
520      */
521     public void addBeforeExecutionFileFilter(BeforeExecutionFileFilter filter) {
522         beforeExecutionFileFilters.addBeforeExecutionFileFilter(filter);
523     }
524 
525     /**
526      * Adds a filter to the end of the audit event filter chain.
527      *
528      * @param filter the additional filter
529      */
530     public void addFilter(Filter filter) {
531         filters.addFilter(filter);
532     }
533 
534     @Override
535     public final void addListener(AuditListener listener) {
536         listeners.add(listener);
537     }
538 
539     /**
540      * Sets the file extensions that identify the files that pass the
541      * filter of this FileSetCheck.
542      *
543      * @param extensions the set of file extensions. A missing
544      *     initial '.' character of an extension is automatically added.
545      */
546     public final void setFileExtensions(String... extensions) {
547         if (extensions != null) {
548             fileExtensions = new String[extensions.length];
549             for (int i = 0; i < extensions.length; i++) {
550                 final String extension = extensions[i];
551                 if (extension.startsWith(EXTENSION_SEPARATOR)) {
552                     fileExtensions[i] = extension;
553                 }
554                 else {
555                     fileExtensions[i] = EXTENSION_SEPARATOR + extension;
556                 }
557             }
558         }
559     }
560 
561     /**
562      * Sets the factory for creating submodules.
563      *
564      * @param moduleFactory the factory for creating FileSetChecks
565      */
566     public void setModuleFactory(ModuleFactory moduleFactory) {
567         this.moduleFactory = moduleFactory;
568     }
569 
570     /**
571      * Sets locale country.
572      *
573      * @param localeCountry the country to report messages
574      */
575     public void setLocaleCountry(String localeCountry) {
576         this.localeCountry = localeCountry;
577     }
578 
579     /**
580      * Sets locale language.
581      *
582      * @param localeLanguage the language to report messages
583      */
584     public void setLocaleLanguage(String localeLanguage) {
585         this.localeLanguage = localeLanguage;
586     }
587 
588     /**
589      * Sets the severity level.  The string should be one of the names
590      * defined in the {@code SeverityLevel} class.
591      *
592      * @param severity  The new severity level
593      * @see SeverityLevel
594      */
595     public final void setSeverity(String severity) {
596         this.severity = SeverityLevel.getInstance(severity);
597     }
598 
599     @Override
600     public final void setModuleClassLoader(ClassLoader moduleClassLoader) {
601         this.moduleClassLoader = moduleClassLoader;
602     }
603 
604     /**
605      * Sets a named charset.
606      *
607      * @param charset the name of a charset
608      * @throws UnsupportedEncodingException if charset is unsupported.
609      */
610     public void setCharset(String charset)
611             throws UnsupportedEncodingException {
612         if (!Charset.isSupported(charset)) {
613             throw new UnsupportedEncodingException(
614                     getLocalizedMessage("Checker.setCharset", charset));
615         }
616         this.charset = charset;
617     }
618 
619     /**
620      * Sets the field haltOnException.
621      *
622      * @param haltOnException the new value.
623      */
624     public void setHaltOnException(boolean haltOnException) {
625         this.haltOnException = haltOnException;
626     }
627 
628     /**
629      * Set the tab width to report audit events with.
630      *
631      * @param tabWidth an {@code int} value
632      */
633     public final void setTabWidth(int tabWidth) {
634         this.tabWidth = tabWidth;
635     }
636 
637     /**
638      * Clears the cache.
639      */
640     public void clearCache() {
641         if (cacheFile != null) {
642             cacheFile.reset();
643         }
644     }
645 
646     /**
647      * Extracts localized messages from properties files.
648      *
649      * @param messageKey the key pointing to localized message in respective properties file.
650      * @param args the arguments of message in respective properties file.
651      * @return a string containing extracted localized message
652      */
653     private String getLocalizedMessage(String messageKey, Object... args) {
654         final LocalizedMessage localizedMessage = new LocalizedMessage(
655             Definitions.CHECKSTYLE_BUNDLE, getClass(),
656                     messageKey, args);
657 
658         return localizedMessage.getMessage();
659     }
660 
661     /**
662      * Relativizes a path and wraps any exception with a user-friendly localized message.
663      *
664      * @param fileName the file path to relativize
665      * @return the relativized path
666      * @throws IllegalStateException if any exception occurs during relativization
667      */
668     private String relativizePathWithCatch(String fileName) {
669         try {
670             return CommonUtil.relativizePath(basedir, fileName);
671         }
672         // -@cs[IllegalCatch] Catching generic Exception to include fileName and basedir context
673         catch (Exception exception) {
674             throw new IllegalStateException(
675                 getLocalizedMessage("general.relativizePath",
676                         fileName, basedir), exception);
677         }
678     }
679 
680 }