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