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