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         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             // Finish up
231             // It may also log!!!
232             fileSetChecks.forEach(FileSetCheck::finishProcessing);
233 
234             // It may also log!!!
235             fileSetChecks.forEach(FileSetCheck::destroy);
236 
237             errorCount = counter.getCount();
238         }
239         return errorCount;
240     }
241 
242     /**
243      * Returns a set of external configuration resource locations which are used by all file set
244      * checks and filters.
245      *
246      * @return a set of external configuration resource locations which are used by all file set
247      *         checks and filters.
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      * Processes a list of files with all FileSetChecks.
261      *
262      * @param files a list of files to process.
263      * @throws CheckstyleException if error condition within Checkstyle occurs.
264      * @throws Error wraps any java.lang.Error happened during execution
265      * @noinspection NestedTryStatement, ProhibitedExceptionThrown
266      * @noinspectionreason NestedTryStatement - false positive: the inner try-with-resources
267      *      has no catch or finally to merge. Remove this suppression when the inspection is fixed.
268      *      See <a href="https://youtrack.jetbrains.com/issue/IDEA-393895">IDEA-393895</a>.
269      *      It completes file notifications before the outer handlers remove failed files from
270      *      the cache and wrap failures.
271      * @noinspectionreason ProhibitedExceptionThrown - preserve Error propagation while adding
272      *      the name of the file that was being processed.
273      */
274     // -@cs[CyclomaticComplexity] no easy way to split this logic of processing the file
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             // -@cs[IllegalCatch] There is no other way to deliver filename that was under
295             // processing. See https://github.com/checkstyle/checkstyle/issues/2285
296             catch (Exception exc) {
297                 if (fileName != null && cacheFile != null) {
298                     cacheFile.remove(fileName);
299                 }
300 
301                 // We need to catch all exceptions to put a reason failure (file name) in exception
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                 // We need to catch all errors to put a reason failure (file name) in error
311                 throw new Error(getLocalizedMessage("Checker.error", filePath), error);
312             }
313         }
314     }
315 
316     /**
317      * Processes a file with all FileSetChecks.
318      *
319      * @param file a file to process.
320      * @return a sorted set of violations to be logged.
321      * @throws CheckstyleException if error condition within Checkstyle occurs.
322      * @noinspection ProhibitedExceptionThrown
323      * @noinspectionreason ProhibitedExceptionThrown - there is no other way to obey
324      *      haltOnException field
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         // -@cs[IllegalCatch] There is no other way to obey haltOnException field
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      * Check if all before execution file filters accept starting the file.
363      *
364      * @param fileName
365      *            the file to be audited
366      * @return {@code true} if the file is accepted.
367      */
368     private boolean acceptFileStarted(String fileName) {
369         final String stripped = relativizePathWithCatch(fileName);
370         return beforeExecutionFileFilters.accept(stripped);
371     }
372 
373     /**
374      * Notify all listeners about the beginning of a file audit.
375      *
376      * @param fileName
377      *            the file to be audited
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      * Notify all listeners about the errors in a file.
390      *
391      * @param fileName the audited file
392      * @param errors the audit errors from the file
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      * Notify all listeners about the end of a file audit.
414      *
415      * @param fileName
416      *            the audited file
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      * {@inheritDoc} Creates child module.
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      * Adds a FileSetCheck to the list of FileSetChecks
488      * that is executed in process().
489      *
490      * @param fileSetCheck the additional FileSetCheck
491      */
492     public void addFileSetCheck(FileSetCheck fileSetCheck) {
493         fileSetCheck.setMessageDispatcher(this);
494         fileSetChecks.add(fileSetCheck);
495     }
496 
497     /**
498      * Adds a before execution file filter to the end of the event chain.
499      *
500      * @param filter the additional filter
501      */
502     public void addBeforeExecutionFileFilter(BeforeExecutionFileFilter filter) {
503         beforeExecutionFileFilters.addBeforeExecutionFileFilter(filter);
504     }
505 
506     /**
507      * Adds a filter to the end of the audit event filter chain.
508      *
509      * @param filter the additional filter
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      * Sets the file extensions that identify the files that pass the
522      * filter of this FileSetCheck.
523      *
524      * @param extensions the set of file extensions. A missing
525      *     initial '.' character of an extension is automatically added.
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      * Sets the factory for creating submodules.
544      *
545      * @param moduleFactory the factory for creating FileSetChecks
546      */
547     public void setModuleFactory(ModuleFactory moduleFactory) {
548         this.moduleFactory = moduleFactory;
549     }
550 
551     /**
552      * Sets locale country.
553      *
554      * @param localeCountry the country to report messages
555      */
556     public void setLocaleCountry(String localeCountry) {
557         this.localeCountry = localeCountry;
558     }
559 
560     /**
561      * Sets locale language.
562      *
563      * @param localeLanguage the language to report messages
564      */
565     public void setLocaleLanguage(String localeLanguage) {
566         this.localeLanguage = localeLanguage;
567     }
568 
569     /**
570      * Sets the severity level.  The string should be one of the names
571      * defined in the {@code SeverityLevel} class.
572      *
573      * @param severity  The new severity level
574      * @see SeverityLevel
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      * Sets a named charset.
587      *
588      * @param charset the name of a charset
589      * @throws UnsupportedEncodingException if charset is unsupported.
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      * Sets the field haltOnException.
602      *
603      * @param haltOnException the new value.
604      */
605     public void setHaltOnException(boolean haltOnException) {
606         this.haltOnException = haltOnException;
607     }
608 
609     /**
610      * Set the tab width to report audit events with.
611      *
612      * @param tabWidth an {@code int} value
613      */
614     public final void setTabWidth(int tabWidth) {
615         this.tabWidth = tabWidth;
616     }
617 
618     /**
619      * Clears the cache.
620      */
621     public void clearCache() {
622         if (cacheFile != null) {
623             cacheFile.reset();
624         }
625     }
626 
627     /**
628      * Extracts localized messages from properties files.
629      *
630      * @param messageKey the key pointing to localized message in respective properties file.
631      * @param args the arguments of message in respective properties file.
632      * @return a string containing extracted localized message
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      * Relativizes a path and wraps any exception with a user-friendly localized message.
644      *
645      * @param fileName the file path to relativize
646      * @return the relativized path
647      * @throws IllegalStateException if any exception occurs during relativization
648      */
649     private String relativizePathWithCatch(String fileName) {
650         try {
651             return CommonUtil.relativizePath(basedir, fileName);
652         }
653         // -@cs[IllegalCatch] Catching generic Exception to include fileName and basedir context
654         catch (Exception exception) {
655             throw new IllegalStateException(
656                 getLocalizedMessage("general.relativizePath",
657                         fileName, basedir), exception);
658         }
659     }
660 
661     /** Starts the audit and completes it when processing ends. */
662     private final class AuditCompletion implements AutoCloseable {
663 
664         /** Starts the audit. */
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         /** Completes the audit. */
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     /** Starts the file audit and completes it when processing ends. */
683     private final class FileAuditCompletion implements AutoCloseable {
684 
685         /** Name of the file being audited. */
686         private final String fileName;
687 
688         /**
689          * Starts auditing the file.
690          *
691          * @param fileName the name of the file to audit
692          */
693         private FileAuditCompletion(String fileName) {
694             this.fileName = fileName;
695             fireFileStarted(fileName);
696         }
697 
698         /** Completes the file audit. */
699         @Override
700         public void close() {
701             fireFileFinished(fileName);
702         }
703     }
704 
705 }