View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2024 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 ex) {
188                 throw new IllegalStateException("Unable to persist cache file.", ex);
189             }
190         }
191     }
192 
193     /**
194      * Removes a given listener.
195      *
196      * @param listener a listener to remove
197      */
198     public void removeListener(AuditListener listener) {
199         listeners.remove(listener);
200     }
201 
202     /**
203      * Sets base directory.
204      *
205      * @param basedir the base directory to strip off in file names
206      */
207     public void setBasedir(String basedir) {
208         this.basedir = basedir;
209     }
210 
211     @Override
212     public int process(List<File> files) throws CheckstyleException {
213         if (cacheFile != null) {
214             cacheFile.putExternalResources(getExternalResourceLocations());
215         }
216 
217         // Prepare to start
218         fireAuditStarted();
219         for (final FileSetCheck fsc : fileSetChecks) {
220             fsc.beginProcessing(charset);
221         }
222 
223         final List<File> targetFiles = files.stream()
224                 .filter(file -> CommonUtil.matchesFileExtension(file, fileExtensions))
225                 .collect(Collectors.toUnmodifiableList());
226         processFiles(targetFiles);
227 
228         // Finish up
229         // It may also log!!!
230         fileSetChecks.forEach(FileSetCheck::finishProcessing);
231 
232         // It may also log!!!
233         fileSetChecks.forEach(FileSetCheck::destroy);
234 
235         final int errorCount = counter.getCount();
236         fireAuditFinished();
237         return errorCount;
238     }
239 
240     /**
241      * Returns a set of external configuration resource locations which are used by all file set
242      * checks and filters.
243      *
244      * @return a set of external configuration resource locations which are used by all file set
245      *         checks and filters.
246      */
247     private Set<String> getExternalResourceLocations() {
248         return Stream.concat(fileSetChecks.stream(), filters.getFilters().stream())
249             .filter(ExternalResourceHolder.class::isInstance)
250             .flatMap(resource -> {
251                 return ((ExternalResourceHolder) resource)
252                         .getExternalResourceLocations().stream();
253             })
254             .collect(Collectors.toUnmodifiableSet());
255     }
256 
257     /** Notify all listeners about the audit start. */
258     private void fireAuditStarted() {
259         final AuditEvent event = new AuditEvent(this);
260         for (final AuditListener listener : listeners) {
261             listener.auditStarted(event);
262         }
263     }
264 
265     /** Notify all listeners about the audit end. */
266     private void fireAuditFinished() {
267         final AuditEvent event = new AuditEvent(this);
268         for (final AuditListener listener : listeners) {
269             listener.auditFinished(event);
270         }
271     }
272 
273     /**
274      * Processes a list of files with all FileSetChecks.
275      *
276      * @param files a list of files to process.
277      * @throws CheckstyleException if error condition within Checkstyle occurs.
278      * @throws Error wraps any java.lang.Error happened during execution
279      * @noinspection ProhibitedExceptionThrown
280      * @noinspectionreason ProhibitedExceptionThrown - There is no other way to
281      *      deliver filename that was under processing.
282      */
283     // -@cs[CyclomaticComplexity] no easy way to split this logic of processing the file
284     private void processFiles(List<File> files) throws CheckstyleException {
285         for (final File file : files) {
286             String fileName = null;
287             final String filePath = file.getPath();
288             try {
289                 fileName = file.getAbsolutePath();
290                 final long timestamp = file.lastModified();
291                 if (cacheFile != null && cacheFile.isInCache(fileName, timestamp)
292                         || !acceptFileStarted(fileName)) {
293                     continue;
294                 }
295                 if (cacheFile != null) {
296                     cacheFile.put(fileName, timestamp);
297                 }
298                 fireFileStarted(fileName);
299                 final SortedSet<Violation> fileMessages = processFile(file);
300                 fireErrors(fileName, fileMessages);
301                 fireFileFinished(fileName);
302             }
303             // -@cs[IllegalCatch] There is no other way to deliver filename that was under
304             // processing. See https://github.com/checkstyle/checkstyle/issues/2285
305             catch (Exception ex) {
306                 if (fileName != null && cacheFile != null) {
307                     cacheFile.remove(fileName);
308                 }
309 
310                 // We need to catch all exceptions to put a reason failure (file name) in exception
311                 throw new CheckstyleException(
312                         getLocalizedMessage("Checker.processFilesException", filePath), ex);
313             }
314             catch (Error error) {
315                 if (fileName != null && cacheFile != null) {
316                     cacheFile.remove(fileName);
317                 }
318 
319                 // We need to catch all errors to put a reason failure (file name) in error
320                 throw new Error("Error was thrown while processing " + filePath, error);
321             }
322         }
323     }
324 
325     /**
326      * Processes a file with all FileSetChecks.
327      *
328      * @param file a file to process.
329      * @return a sorted set of violations to be logged.
330      * @throws CheckstyleException if error condition within Checkstyle occurs.
331      * @noinspection ProhibitedExceptionThrown
332      * @noinspectionreason ProhibitedExceptionThrown - there is no other way to obey
333      *      haltOnException field
334      */
335     private SortedSet<Violation> processFile(File file) throws CheckstyleException {
336         final SortedSet<Violation> fileMessages = new TreeSet<>();
337         try {
338             final FileText theText = new FileText(file.getAbsoluteFile(), charset);
339             for (final FileSetCheck fsc : fileSetChecks) {
340                 fileMessages.addAll(fsc.process(file, theText));
341             }
342         }
343         catch (final IOException ioe) {
344             log.debug("IOException occurred.", ioe);
345             fileMessages.add(new Violation(1,
346                     Definitions.CHECKSTYLE_BUNDLE, EXCEPTION_MSG,
347                     new String[] {ioe.getMessage()}, null, getClass(), null));
348         }
349         // -@cs[IllegalCatch] There is no other way to obey haltOnException field
350         catch (Exception ex) {
351             if (haltOnException) {
352                 throw ex;
353             }
354 
355             log.debug("Exception occurred.", ex);
356 
357             final StringWriter sw = new StringWriter();
358             final PrintWriter pw = new PrintWriter(sw, true);
359 
360             ex.printStackTrace(pw);
361 
362             fileMessages.add(new Violation(1,
363                     Definitions.CHECKSTYLE_BUNDLE, EXCEPTION_MSG,
364                     new String[] {sw.getBuffer().toString()},
365                     null, getClass(), null));
366         }
367         return fileMessages;
368     }
369 
370     /**
371      * Check if all before execution file filters accept starting the file.
372      *
373      * @param fileName
374      *            the file to be audited
375      * @return {@code true} if the file is accepted.
376      */
377     private boolean acceptFileStarted(String fileName) {
378         final String stripped = CommonUtil.relativizePath(basedir, fileName);
379         return beforeExecutionFileFilters.accept(stripped);
380     }
381 
382     /**
383      * Notify all listeners about the beginning of a file audit.
384      *
385      * @param fileName
386      *            the file to be audited
387      */
388     @Override
389     public void fireFileStarted(String fileName) {
390         final String stripped = CommonUtil.relativizePath(basedir, fileName);
391         final AuditEvent event = new AuditEvent(this, stripped);
392         for (final AuditListener listener : listeners) {
393             listener.fileStarted(event);
394         }
395     }
396 
397     /**
398      * Notify all listeners about the errors in a file.
399      *
400      * @param fileName the audited file
401      * @param errors the audit errors from the file
402      */
403     @Override
404     public void fireErrors(String fileName, SortedSet<Violation> errors) {
405         final String stripped = CommonUtil.relativizePath(basedir, fileName);
406         boolean hasNonFilteredViolations = false;
407         for (final Violation element : errors) {
408             final AuditEvent event = new AuditEvent(this, stripped, element);
409             if (filters.accept(event)) {
410                 hasNonFilteredViolations = true;
411                 for (final AuditListener listener : listeners) {
412                     listener.addError(event);
413                 }
414             }
415         }
416         if (hasNonFilteredViolations && cacheFile != null) {
417             cacheFile.remove(fileName);
418         }
419     }
420 
421     /**
422      * Notify all listeners about the end of a file audit.
423      *
424      * @param fileName
425      *            the audited file
426      */
427     @Override
428     public void fireFileFinished(String fileName) {
429         final String stripped = CommonUtil.relativizePath(basedir, fileName);
430         final AuditEvent event = new AuditEvent(this, stripped);
431         for (final AuditListener listener : listeners) {
432             listener.fileFinished(event);
433         }
434     }
435 
436     @Override
437     protected void finishLocalSetup() throws CheckstyleException {
438         final Locale locale = new Locale(localeLanguage, localeCountry);
439         LocalizedMessage.setLocale(locale);
440 
441         if (moduleFactory == null) {
442             if (moduleClassLoader == null) {
443                 throw new CheckstyleException(getLocalizedMessage("Checker.finishLocalSetup"));
444             }
445 
446             final Set<String> packageNames = PackageNamesLoader
447                     .getPackageNames(moduleClassLoader);
448             moduleFactory = new PackageObjectFactory(packageNames,
449                     moduleClassLoader);
450         }
451 
452         final DefaultContext context = new DefaultContext();
453         context.add("charset", charset);
454         context.add("moduleFactory", moduleFactory);
455         context.add("severity", severity.getName());
456         context.add("basedir", basedir);
457         context.add("tabWidth", String.valueOf(tabWidth));
458         childContext = context;
459     }
460 
461     /**
462      * {@inheritDoc} Creates child module.
463      *
464      * @noinspection ChainOfInstanceofChecks
465      * @noinspectionreason ChainOfInstanceofChecks - we treat checks and filters differently
466      */
467     @Override
468     protected void setupChild(Configuration childConf)
469             throws CheckstyleException {
470         final String name = childConf.getName();
471         final Object child;
472 
473         try {
474             child = moduleFactory.createModule(name);
475 
476             if (child instanceof AbstractAutomaticBean) {
477                 final AbstractAutomaticBean bean = (AbstractAutomaticBean) child;
478                 bean.contextualize(childContext);
479                 bean.configure(childConf);
480             }
481         }
482         catch (final CheckstyleException ex) {
483             throw new CheckstyleException(
484                     getLocalizedMessage("Checker.setupChildModule", name, ex.getMessage()), ex);
485         }
486         if (child instanceof FileSetCheck) {
487             final FileSetCheck fsc = (FileSetCheck) child;
488             fsc.init();
489             addFileSetCheck(fsc);
490         }
491         else if (child instanceof BeforeExecutionFileFilter) {
492             final BeforeExecutionFileFilter filter = (BeforeExecutionFileFilter) child;
493             addBeforeExecutionFileFilter(filter);
494         }
495         else if (child instanceof Filter) {
496             final Filter filter = (Filter) child;
497             addFilter(filter);
498         }
499         else if (child instanceof AuditListener) {
500             final AuditListener listener = (AuditListener) child;
501             addListener(listener);
502         }
503         else {
504             throw new CheckstyleException(
505                     getLocalizedMessage("Checker.setupChildNotAllowed", name));
506         }
507     }
508 
509     /**
510      * Adds a FileSetCheck to the list of FileSetChecks
511      * that is executed in process().
512      *
513      * @param fileSetCheck the additional FileSetCheck
514      */
515     public void addFileSetCheck(FileSetCheck fileSetCheck) {
516         fileSetCheck.setMessageDispatcher(this);
517         fileSetChecks.add(fileSetCheck);
518     }
519 
520     /**
521      * Adds a before execution file filter to the end of the event chain.
522      *
523      * @param filter the additional filter
524      */
525     public void addBeforeExecutionFileFilter(BeforeExecutionFileFilter filter) {
526         beforeExecutionFileFilters.addBeforeExecutionFileFilter(filter);
527     }
528 
529     /**
530      * Adds a filter to the end of the audit event filter chain.
531      *
532      * @param filter the additional filter
533      */
534     public void addFilter(Filter filter) {
535         filters.addFilter(filter);
536     }
537 
538     @Override
539     public final void addListener(AuditListener listener) {
540         listeners.add(listener);
541     }
542 
543     /**
544      * Sets the file extensions that identify the files that pass the
545      * filter of this FileSetCheck.
546      *
547      * @param extensions the set of file extensions. A missing
548      *     initial '.' character of an extension is automatically added.
549      */
550     public final void setFileExtensions(String... extensions) {
551         if (extensions != null) {
552             fileExtensions = new String[extensions.length];
553             for (int i = 0; i < extensions.length; i++) {
554                 final String extension = extensions[i];
555                 if (extension.startsWith(EXTENSION_SEPARATOR)) {
556                     fileExtensions[i] = extension;
557                 }
558                 else {
559                     fileExtensions[i] = EXTENSION_SEPARATOR + extension;
560                 }
561             }
562         }
563     }
564 
565     /**
566      * Sets the factory for creating submodules.
567      *
568      * @param moduleFactory the factory for creating FileSetChecks
569      */
570     public void setModuleFactory(ModuleFactory moduleFactory) {
571         this.moduleFactory = moduleFactory;
572     }
573 
574     /**
575      * Sets locale country.
576      *
577      * @param localeCountry the country to report messages
578      */
579     public void setLocaleCountry(String localeCountry) {
580         this.localeCountry = localeCountry;
581     }
582 
583     /**
584      * Sets locale language.
585      *
586      * @param localeLanguage the language to report messages
587      */
588     public void setLocaleLanguage(String localeLanguage) {
589         this.localeLanguage = localeLanguage;
590     }
591 
592     /**
593      * Sets the severity level.  The string should be one of the names
594      * defined in the {@code SeverityLevel} class.
595      *
596      * @param severity  The new severity level
597      * @see SeverityLevel
598      */
599     public final void setSeverity(String severity) {
600         this.severity = SeverityLevel.getInstance(severity);
601     }
602 
603     @Override
604     public final void setModuleClassLoader(ClassLoader moduleClassLoader) {
605         this.moduleClassLoader = moduleClassLoader;
606     }
607 
608     /**
609      * Sets a named charset.
610      *
611      * @param charset the name of a charset
612      * @throws UnsupportedEncodingException if charset is unsupported.
613      */
614     public void setCharset(String charset)
615             throws UnsupportedEncodingException {
616         if (!Charset.isSupported(charset)) {
617             throw new UnsupportedEncodingException(
618                     getLocalizedMessage("Checker.setCharset", charset));
619         }
620         this.charset = charset;
621     }
622 
623     /**
624      * Sets the field haltOnException.
625      *
626      * @param haltOnException the new value.
627      */
628     public void setHaltOnException(boolean haltOnException) {
629         this.haltOnException = haltOnException;
630     }
631 
632     /**
633      * Set the tab width to report audit events with.
634      *
635      * @param tabWidth an {@code int} value
636      */
637     public final void setTabWidth(int tabWidth) {
638         this.tabWidth = tabWidth;
639     }
640 
641     /**
642      * Clears the cache.
643      */
644     public void clearCache() {
645         if (cacheFile != null) {
646             cacheFile.reset();
647         }
648     }
649 
650     /**
651      * Extracts localized messages from properties files.
652      *
653      * @param messageKey the key pointing to localized message in respective properties file.
654      * @param args the arguments of message in respective properties file.
655      * @return a string containing extracted localized message
656      */
657     private String getLocalizedMessage(String messageKey, Object... args) {
658         final LocalizedMessage localizedMessage = new LocalizedMessage(
659             Definitions.CHECKSTYLE_BUNDLE, getClass(),
660                     messageKey, args);
661 
662         return localizedMessage.getMessage();
663     }
664 
665 }