View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2025 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 = 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 }