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