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.BeforeExecutionFileFilter;
044import com.puppycrawl.tools.checkstyle.api.BeforeExecutionFileFilterSet;
045import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
046import com.puppycrawl.tools.checkstyle.api.Configuration;
047import com.puppycrawl.tools.checkstyle.api.Context;
048import com.puppycrawl.tools.checkstyle.api.ExternalResourceHolder;
049import com.puppycrawl.tools.checkstyle.api.FileSetCheck;
050import com.puppycrawl.tools.checkstyle.api.FileText;
051import com.puppycrawl.tools.checkstyle.api.Filter;
052import com.puppycrawl.tools.checkstyle.api.FilterSet;
053import com.puppycrawl.tools.checkstyle.api.MessageDispatcher;
054import com.puppycrawl.tools.checkstyle.api.RootModule;
055import com.puppycrawl.tools.checkstyle.api.SeverityLevel;
056import com.puppycrawl.tools.checkstyle.api.SeverityLevelCounter;
057import com.puppycrawl.tools.checkstyle.api.Violation;
058import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
059
060/**
061 * This class provides the functionality to check a set of files.
062 */
063public class Checker extends AbstractAutomaticBean implements MessageDispatcher, RootModule {
064
065    /** Message to use when an exception occurs and should be printed as a violation. */
066    public static final String EXCEPTION_MSG = "general.exception";
067
068    /** The extension separator. */
069    private static final String EXTENSION_SEPARATOR = ".";
070
071    /** Logger for Checker. */
072    private final Log log;
073
074    /** Maintains error count. */
075    private final SeverityLevelCounter counter = new SeverityLevelCounter(
076            SeverityLevel.ERROR);
077
078    /** Vector of listeners. */
079    private final List<AuditListener> listeners = new ArrayList<>();
080
081    /** Vector of fileset checks. */
082    private final List<FileSetCheck> fileSetChecks = new ArrayList<>();
083
084    /** The audit event before execution file filters. */
085    private final BeforeExecutionFileFilterSet beforeExecutionFileFilters =
086            new BeforeExecutionFileFilterSet();
087
088    /** The audit event filters. */
089    private final FilterSet filters = new FilterSet();
090
091    /** The basedir to strip off in file names. */
092    private String basedir;
093
094    /** Locale country to report messages . **/
095    @XdocsPropertyType(PropertyType.LOCALE_COUNTRY)
096    private String localeCountry = Locale.getDefault().getCountry();
097    /** Locale language to report messages . **/
098    @XdocsPropertyType(PropertyType.LOCALE_LANGUAGE)
099    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 = CommonUtil.EMPTY_STRING_ARRAY;
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.toList());
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            .map(ExternalResourceHolder.class::cast)
251            .flatMap(resource -> resource.getExternalResourceLocations().stream())
252            .collect(Collectors.toSet());
253    }
254
255    /** Notify all listeners about the audit start. */
256    private void fireAuditStarted() {
257        final AuditEvent event = new AuditEvent(this);
258        for (final AuditListener listener : listeners) {
259            listener.auditStarted(event);
260        }
261    }
262
263    /** Notify all listeners about the audit end. */
264    private void fireAuditFinished() {
265        final AuditEvent event = new AuditEvent(this);
266        for (final AuditListener listener : listeners) {
267            listener.auditFinished(event);
268        }
269    }
270
271    /**
272     * Processes a list of files with all FileSetChecks.
273     *
274     * @param files a list of files to process.
275     * @throws CheckstyleException if error condition within Checkstyle occurs.
276     * @throws Error wraps any java.lang.Error happened during execution
277     * @noinspection ProhibitedExceptionThrown
278     * @noinspectionreason ProhibitedExceptionThrown - There is no other way to
279     *      deliver filename that was under processing.
280     */
281    // -@cs[CyclomaticComplexity] no easy way to split this logic of processing the file
282    private void processFiles(List<File> files) throws CheckstyleException {
283        for (final File file : files) {
284            String fileName = null;
285            try {
286                fileName = file.getAbsolutePath();
287                final long timestamp = file.lastModified();
288                if (cacheFile != null && cacheFile.isInCache(fileName, timestamp)
289                        || !acceptFileStarted(fileName)) {
290                    continue;
291                }
292                if (cacheFile != null) {
293                    cacheFile.put(fileName, timestamp);
294                }
295                fireFileStarted(fileName);
296                final SortedSet<Violation> fileMessages = processFile(file);
297                fireErrors(fileName, fileMessages);
298                fireFileFinished(fileName);
299            }
300            // -@cs[IllegalCatch] There is no other way to deliver filename that was under
301            // processing. See https://github.com/checkstyle/checkstyle/issues/2285
302            catch (Exception ex) {
303                if (fileName != null && cacheFile != null) {
304                    cacheFile.remove(fileName);
305                }
306
307                // We need to catch all exceptions to put a reason failure (file name) in exception
308                throw new CheckstyleException("Exception was thrown while processing "
309                        + file.getPath(), ex);
310            }
311            catch (Error error) {
312                if (fileName != null && cacheFile != null) {
313                    cacheFile.remove(fileName);
314                }
315
316                // We need to catch all errors to put a reason failure (file name) in error
317                throw new Error("Error was thrown while processing " + file.getPath(), error);
318            }
319        }
320    }
321
322    /**
323     * Processes a file with all FileSetChecks.
324     *
325     * @param file a file to process.
326     * @return a sorted set of violations to be logged.
327     * @throws CheckstyleException if error condition within Checkstyle occurs.
328     * @noinspection ProhibitedExceptionThrown
329     * @noinspectionreason ProhibitedExceptionThrown - there is no other way to obey
330     *      haltOnException field
331     */
332    private SortedSet<Violation> processFile(File file) throws CheckstyleException {
333        final SortedSet<Violation> fileMessages = new TreeSet<>();
334        try {
335            final FileText theText = new FileText(file.getAbsoluteFile(), charset);
336            for (final FileSetCheck fsc : fileSetChecks) {
337                fileMessages.addAll(fsc.process(file, theText));
338            }
339        }
340        catch (final IOException ioe) {
341            log.debug("IOException occurred.", ioe);
342            fileMessages.add(new Violation(1,
343                    Definitions.CHECKSTYLE_BUNDLE, EXCEPTION_MSG,
344                    new String[] {ioe.getMessage()}, null, getClass(), null));
345        }
346        // -@cs[IllegalCatch] There is no other way to obey haltOnException field
347        catch (Exception ex) {
348            if (haltOnException) {
349                throw ex;
350            }
351
352            log.debug("Exception occurred.", ex);
353
354            final StringWriter sw = new StringWriter();
355            final PrintWriter pw = new PrintWriter(sw, true);
356
357            ex.printStackTrace(pw);
358
359            fileMessages.add(new Violation(1,
360                    Definitions.CHECKSTYLE_BUNDLE, EXCEPTION_MSG,
361                    new String[] {sw.getBuffer().toString()},
362                    null, getClass(), null));
363        }
364        return fileMessages;
365    }
366
367    /**
368     * Check if all before execution file filters accept starting the file.
369     *
370     * @param fileName
371     *            the file to be audited
372     * @return {@code true} if the file is accepted.
373     */
374    private boolean acceptFileStarted(String fileName) {
375        final String stripped = CommonUtil.relativizeAndNormalizePath(basedir, fileName);
376        return beforeExecutionFileFilters.accept(stripped);
377    }
378
379    /**
380     * Notify all listeners about the beginning of a file audit.
381     *
382     * @param fileName
383     *            the file to be audited
384     */
385    @Override
386    public void fireFileStarted(String fileName) {
387        final String stripped = CommonUtil.relativizeAndNormalizePath(basedir, fileName);
388        final AuditEvent event = new AuditEvent(this, stripped);
389        for (final AuditListener listener : listeners) {
390            listener.fileStarted(event);
391        }
392    }
393
394    /**
395     * Notify all listeners about the errors in a file.
396     *
397     * @param fileName the audited file
398     * @param errors the audit errors from the file
399     */
400    @Override
401    public void fireErrors(String fileName, SortedSet<Violation> errors) {
402        final String stripped = CommonUtil.relativizeAndNormalizePath(basedir, fileName);
403        boolean hasNonFilteredViolations = false;
404        for (final Violation element : errors) {
405            final AuditEvent event = new AuditEvent(this, stripped, element);
406            if (filters.accept(event)) {
407                hasNonFilteredViolations = true;
408                for (final AuditListener listener : listeners) {
409                    listener.addError(event);
410                }
411            }
412        }
413        if (hasNonFilteredViolations && cacheFile != null) {
414            cacheFile.remove(fileName);
415        }
416    }
417
418    /**
419     * Notify all listeners about the end of a file audit.
420     *
421     * @param fileName
422     *            the audited file
423     */
424    @Override
425    public void fireFileFinished(String fileName) {
426        final String stripped = CommonUtil.relativizeAndNormalizePath(basedir, fileName);
427        final AuditEvent event = new AuditEvent(this, stripped);
428        for (final AuditListener listener : listeners) {
429            listener.fileFinished(event);
430        }
431    }
432
433    @Override
434    protected void finishLocalSetup() throws CheckstyleException {
435        final Locale locale = new Locale(localeLanguage, localeCountry);
436        LocalizedMessage.setLocale(locale);
437
438        if (moduleFactory == null) {
439            if (moduleClassLoader == null) {
440                throw new CheckstyleException(
441                        "if no custom moduleFactory is set, "
442                                + "moduleClassLoader must be specified");
443            }
444
445            final Set<String> packageNames = PackageNamesLoader
446                    .getPackageNames(moduleClassLoader);
447            moduleFactory = new PackageObjectFactory(packageNames,
448                    moduleClassLoader);
449        }
450
451        final DefaultContext context = new DefaultContext();
452        context.add("charset", charset);
453        context.add("moduleFactory", moduleFactory);
454        context.add("severity", severity.getName());
455        context.add("basedir", basedir);
456        context.add("tabWidth", String.valueOf(tabWidth));
457        childContext = context;
458    }
459
460    /**
461     * {@inheritDoc} Creates child module.
462     *
463     * @noinspection ChainOfInstanceofChecks
464     * @noinspectionreason ChainOfInstanceofChecks - we treat checks and filters differently
465     */
466    @Override
467    protected void setupChild(Configuration childConf)
468            throws CheckstyleException {
469        final String name = childConf.getName();
470        final Object child;
471
472        try {
473            child = moduleFactory.createModule(name);
474
475            if (child instanceof AbstractAutomaticBean) {
476                final AbstractAutomaticBean bean = (AbstractAutomaticBean) child;
477                bean.contextualize(childContext);
478                bean.configure(childConf);
479            }
480        }
481        catch (final CheckstyleException ex) {
482            throw new CheckstyleException("cannot initialize module " + name
483                    + " - " + ex.getMessage(), ex);
484        }
485        if (child instanceof FileSetCheck) {
486            final FileSetCheck fsc = (FileSetCheck) child;
487            fsc.init();
488            addFileSetCheck(fsc);
489        }
490        else if (child instanceof BeforeExecutionFileFilter) {
491            final BeforeExecutionFileFilter filter = (BeforeExecutionFileFilter) child;
492            addBeforeExecutionFileFilter(filter);
493        }
494        else if (child instanceof Filter) {
495            final Filter filter = (Filter) child;
496            addFilter(filter);
497        }
498        else if (child instanceof AuditListener) {
499            final AuditListener listener = (AuditListener) child;
500            addListener(listener);
501        }
502        else {
503            throw new CheckstyleException(name
504                    + " is not allowed as a child in Checker");
505        }
506    }
507
508    /**
509     * Adds a FileSetCheck to the list of FileSetChecks
510     * that is executed in process().
511     *
512     * @param fileSetCheck the additional FileSetCheck
513     */
514    public void addFileSetCheck(FileSetCheck fileSetCheck) {
515        fileSetCheck.setMessageDispatcher(this);
516        fileSetChecks.add(fileSetCheck);
517    }
518
519    /**
520     * Adds a before execution file filter to the end of the event chain.
521     *
522     * @param filter the additional filter
523     */
524    public void addBeforeExecutionFileFilter(BeforeExecutionFileFilter filter) {
525        beforeExecutionFileFilters.addBeforeExecutionFileFilter(filter);
526    }
527
528    /**
529     * Adds a filter to the end of the audit event filter chain.
530     *
531     * @param filter the additional filter
532     */
533    public void addFilter(Filter filter) {
534        filters.addFilter(filter);
535    }
536
537    @Override
538    public final void addListener(AuditListener listener) {
539        listeners.add(listener);
540    }
541
542    /**
543     * Sets the file extensions that identify the files that pass the
544     * filter of this FileSetCheck.
545     *
546     * @param extensions the set of file extensions. A missing
547     *     initial '.' character of an extension is automatically added.
548     */
549    public final void setFileExtensions(String... extensions) {
550        if (extensions == null) {
551            fileExtensions = null;
552        }
553        else {
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            final String message = "unsupported charset: '" + charset + "'";
620            throw new UnsupportedEncodingException(message);
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}