001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2020 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.HashSet;
031import java.util.List;
032import java.util.Locale;
033import java.util.Set;
034import java.util.SortedSet;
035import java.util.TreeSet;
036import java.util.stream.Collectors;
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.LocalizedMessage;
055import com.puppycrawl.tools.checkstyle.api.MessageDispatcher;
056import com.puppycrawl.tools.checkstyle.api.RootModule;
057import com.puppycrawl.tools.checkstyle.api.SeverityLevel;
058import com.puppycrawl.tools.checkstyle.api.SeverityLevelCounter;
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    private String localeCountry = Locale.getDefault().getCountry();
094    /** Locale language to report messages . **/
095    private String localeLanguage = Locale.getDefault().getLanguage();
096
097    /** The factory for instantiating submodules. */
098    private ModuleFactory moduleFactory;
099
100    /** The classloader used for loading Checkstyle module classes. */
101    private ClassLoader moduleClassLoader;
102
103    /** The context of all child components. */
104    private Context childContext;
105
106    /** The file extensions that are accepted. */
107    private String[] fileExtensions = CommonUtil.EMPTY_STRING_ARRAY;
108
109    /**
110     * The severity level of any violations found by submodules.
111     * The value of this property is passed to submodules via
112     * contextualize().
113     *
114     * <p>Note: Since the Checker is merely a container for modules
115     * it does not make sense to implement logging functionality
116     * here. Consequently Checker does not extend AbstractViolationReporter,
117     * leading to a bit of duplicated code for severity level setting.
118     */
119    private SeverityLevel severity = SeverityLevel.ERROR;
120
121    /** Name of a charset. */
122    private String charset = System.getProperty("file.encoding", StandardCharsets.UTF_8.name());
123
124    /** Cache file. **/
125    private PropertyCacheFile cacheFile;
126
127    /** Controls whether exceptions should halt execution or not. */
128    private boolean haltOnException = true;
129
130    /** The tab width for column reporting. */
131    private int tabWidth = CommonUtil.DEFAULT_TAB_WIDTH;
132
133    /**
134     * Creates a new {@code Checker} instance.
135     * The instance needs to be contextualized and configured.
136     */
137    public Checker() {
138        addListener(counter);
139        log = LogFactory.getLog(Checker.class);
140    }
141
142    /**
143     * Sets cache file.
144     * @param fileName the cache file.
145     * @throws IOException if there are some problems with file loading.
146     */
147    public void setCacheFile(String fileName) throws IOException {
148        final Configuration configuration = getConfiguration();
149        cacheFile = new PropertyCacheFile(configuration, fileName);
150        cacheFile.load();
151    }
152
153    /**
154     * Removes before execution file filter.
155     * @param filter before execution file filter to remove.
156     */
157    public void removeBeforeExecutionFileFilter(BeforeExecutionFileFilter filter) {
158        beforeExecutionFileFilters.removeBeforeExecutionFileFilter(filter);
159    }
160
161    /**
162     * Removes filter.
163     * @param filter filter to remove.
164     */
165    public void removeFilter(Filter filter) {
166        filters.removeFilter(filter);
167    }
168
169    @Override
170    public void destroy() {
171        listeners.clear();
172        fileSetChecks.clear();
173        beforeExecutionFileFilters.clear();
174        filters.clear();
175        if (cacheFile != null) {
176            try {
177                cacheFile.persist();
178            }
179            catch (IOException ex) {
180                throw new IllegalStateException("Unable to persist cache file.", ex);
181            }
182        }
183    }
184
185    /**
186     * Removes a given listener.
187     * @param listener a listener to remove
188     */
189    public void removeListener(AuditListener listener) {
190        listeners.remove(listener);
191    }
192
193    /**
194     * Sets base directory.
195     * @param basedir the base directory to strip off in file names
196     */
197    public void setBasedir(String basedir) {
198        this.basedir = basedir;
199    }
200
201    @Override
202    public int process(List<File> files) throws CheckstyleException {
203        if (cacheFile != null) {
204            cacheFile.putExternalResources(getExternalResourceLocations());
205        }
206
207        // Prepare to start
208        fireAuditStarted();
209        for (final FileSetCheck fsc : fileSetChecks) {
210            fsc.beginProcessing(charset);
211        }
212
213        final List<File> targetFiles = files.stream()
214                .filter(file -> CommonUtil.matchesFileExtension(file, fileExtensions))
215                .collect(Collectors.toList());
216        processFiles(targetFiles);
217
218        // Finish up
219        // It may also log!!!
220        fileSetChecks.forEach(FileSetCheck::finishProcessing);
221
222        // It may also log!!!
223        fileSetChecks.forEach(FileSetCheck::destroy);
224
225        final int errorCount = counter.getCount();
226        fireAuditFinished();
227        return errorCount;
228    }
229
230    /**
231     * Returns a set of external configuration resource locations which are used by all file set
232     * checks and filters.
233     * @return a set of external configuration resource locations which are used by all file set
234     *         checks and filters.
235     */
236    private Set<String> getExternalResourceLocations() {
237        final Set<String> externalResources = new HashSet<>();
238        fileSetChecks.stream().filter(check -> check instanceof ExternalResourceHolder)
239            .forEach(check -> {
240                final Set<String> locations =
241                    ((ExternalResourceHolder) check).getExternalResourceLocations();
242                externalResources.addAll(locations);
243            });
244        filters.getFilters().stream().filter(filter -> filter instanceof ExternalResourceHolder)
245            .forEach(filter -> {
246                final Set<String> locations =
247                    ((ExternalResourceHolder) filter).getExternalResourceLocations();
248                externalResources.addAll(locations);
249            });
250        return externalResources;
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     * @param files a list of files to process.
272     * @throws CheckstyleException if error condition within Checkstyle occurs.
273     * @throws Error wraps any java.lang.Error happened during execution
274     * @noinspection ProhibitedExceptionThrown
275     */
276    //-@cs[CyclomaticComplexity] no easy way to split this logic of processing the file
277    private void processFiles(List<File> files) throws CheckstyleException {
278        for (final File file : files) {
279            String fileName = null;
280            try {
281                fileName = file.getAbsolutePath();
282                final long timestamp = file.lastModified();
283                if (cacheFile != null && cacheFile.isInCache(fileName, timestamp)
284                        || !acceptFileStarted(fileName)) {
285                    continue;
286                }
287                if (cacheFile != null) {
288                    cacheFile.put(fileName, timestamp);
289                }
290                fireFileStarted(fileName);
291                final SortedSet<LocalizedMessage> fileMessages = processFile(file);
292                fireErrors(fileName, fileMessages);
293                fireFileFinished(fileName);
294            }
295            // -@cs[IllegalCatch] There is no other way to deliver filename that was under
296            // processing. See https://github.com/checkstyle/checkstyle/issues/2285
297            catch (Exception ex) {
298                if (fileName != null && cacheFile != null) {
299                    cacheFile.remove(fileName);
300                }
301
302                // We need to catch all exceptions to put a reason failure (file name) in exception
303                throw new CheckstyleException("Exception was thrown while processing "
304                        + file.getPath(), ex);
305            }
306            catch (Error error) {
307                if (fileName != null && cacheFile != null) {
308                    cacheFile.remove(fileName);
309                }
310
311                // We need to catch all errors to put a reason failure (file name) in error
312                throw new Error("Error was thrown while processing " + file.getPath(), error);
313            }
314        }
315    }
316
317    /**
318     * Processes a file with all FileSetChecks.
319     * @param file a file to process.
320     * @return a sorted set of messages to be logged.
321     * @throws CheckstyleException if error condition within Checkstyle occurs.
322     * @noinspection ProhibitedExceptionThrown
323     */
324    private SortedSet<LocalizedMessage> processFile(File file) throws CheckstyleException {
325        final SortedSet<LocalizedMessage> fileMessages = new TreeSet<>();
326        try {
327            final FileText theText = new FileText(file.getAbsoluteFile(), charset);
328            for (final FileSetCheck fsc : fileSetChecks) {
329                fileMessages.addAll(fsc.process(file, theText));
330            }
331        }
332        catch (final IOException ioe) {
333            log.debug("IOException occurred.", ioe);
334            fileMessages.add(new LocalizedMessage(1,
335                    Definitions.CHECKSTYLE_BUNDLE, EXCEPTION_MSG,
336                    new String[] {ioe.getMessage()}, null, getClass(), null));
337        }
338        // -@cs[IllegalCatch] There is no other way to obey haltOnException field
339        catch (Exception ex) {
340            if (haltOnException) {
341                throw ex;
342            }
343
344            log.debug("Exception occurred.", ex);
345
346            final StringWriter sw = new StringWriter();
347            final PrintWriter pw = new PrintWriter(sw, true);
348
349            ex.printStackTrace(pw);
350
351            fileMessages.add(new LocalizedMessage(1,
352                    Definitions.CHECKSTYLE_BUNDLE, EXCEPTION_MSG,
353                    new String[] {sw.getBuffer().toString()},
354                    null, getClass(), null));
355        }
356        return fileMessages;
357    }
358
359    /**
360     * Check if all before execution file filters accept starting the file.
361     *
362     * @param fileName
363     *            the file to be audited
364     * @return {@code true} if the file is accepted.
365     */
366    private boolean acceptFileStarted(String fileName) {
367        final String stripped = CommonUtil.relativizeAndNormalizePath(basedir, fileName);
368        return beforeExecutionFileFilters.accept(stripped);
369    }
370
371    /**
372     * Notify all listeners about the beginning of a file audit.
373     *
374     * @param fileName
375     *            the file to be audited
376     */
377    @Override
378    public void fireFileStarted(String fileName) {
379        final String stripped = CommonUtil.relativizeAndNormalizePath(basedir, fileName);
380        final AuditEvent event = new AuditEvent(this, stripped);
381        for (final AuditListener listener : listeners) {
382            listener.fileStarted(event);
383        }
384    }
385
386    /**
387     * Notify all listeners about the errors in a file.
388     *
389     * @param fileName the audited file
390     * @param errors the audit errors from the file
391     */
392    @Override
393    public void fireErrors(String fileName, SortedSet<LocalizedMessage> errors) {
394        final String stripped = CommonUtil.relativizeAndNormalizePath(basedir, fileName);
395        boolean hasNonFilteredViolations = false;
396        for (final LocalizedMessage element : errors) {
397            final AuditEvent event = new AuditEvent(this, stripped, element);
398            if (filters.accept(event)) {
399                hasNonFilteredViolations = true;
400                for (final AuditListener listener : listeners) {
401                    listener.addError(event);
402                }
403            }
404        }
405        if (hasNonFilteredViolations && cacheFile != null) {
406            cacheFile.remove(fileName);
407        }
408    }
409
410    /**
411     * Notify all listeners about the end of a file audit.
412     *
413     * @param fileName
414     *            the audited file
415     */
416    @Override
417    public void fireFileFinished(String fileName) {
418        final String stripped = CommonUtil.relativizeAndNormalizePath(basedir, fileName);
419        final AuditEvent event = new AuditEvent(this, stripped);
420        for (final AuditListener listener : listeners) {
421            listener.fileFinished(event);
422        }
423    }
424
425    @Override
426    protected void finishLocalSetup() throws CheckstyleException {
427        final Locale locale = new Locale(localeLanguage, localeCountry);
428        LocalizedMessage.setLocale(locale);
429
430        if (moduleFactory == null) {
431            if (moduleClassLoader == null) {
432                throw new CheckstyleException(
433                        "if no custom moduleFactory is set, "
434                                + "moduleClassLoader must be specified");
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     * @noinspection ChainOfInstanceofChecks
455     */
456    @Override
457    protected void setupChild(Configuration childConf)
458            throws CheckstyleException {
459        final String name = childConf.getName();
460        final Object child;
461
462        try {
463            child = moduleFactory.createModule(name);
464
465            if (child instanceof AutomaticBean) {
466                final AutomaticBean bean = (AutomaticBean) child;
467                bean.contextualize(childContext);
468                bean.configure(childConf);
469            }
470        }
471        catch (final CheckstyleException ex) {
472            throw new CheckstyleException("cannot initialize module " + name
473                    + " - " + ex.getMessage(), ex);
474        }
475        if (child instanceof FileSetCheck) {
476            final FileSetCheck fsc = (FileSetCheck) child;
477            fsc.init();
478            addFileSetCheck(fsc);
479        }
480        else if (child instanceof BeforeExecutionFileFilter) {
481            final BeforeExecutionFileFilter filter = (BeforeExecutionFileFilter) child;
482            addBeforeExecutionFileFilter(filter);
483        }
484        else if (child instanceof Filter) {
485            final Filter filter = (Filter) child;
486            addFilter(filter);
487        }
488        else if (child instanceof AuditListener) {
489            final AuditListener listener = (AuditListener) child;
490            addListener(listener);
491        }
492        else {
493            throw new CheckstyleException(name
494                    + " is not allowed as a child in Checker");
495        }
496    }
497
498    /**
499     * Adds a FileSetCheck to the list of FileSetChecks
500     * that is executed in process().
501     * @param fileSetCheck the additional FileSetCheck
502     */
503    public void addFileSetCheck(FileSetCheck fileSetCheck) {
504        fileSetCheck.setMessageDispatcher(this);
505        fileSetChecks.add(fileSetCheck);
506    }
507
508    /**
509     * Adds a before execution file filter to the end of the event chain.
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     * @param filter the additional filter
519     */
520    public void addFilter(Filter filter) {
521        filters.addFilter(filter);
522    }
523
524    @Override
525    public final void addListener(AuditListener listener) {
526        listeners.add(listener);
527    }
528
529    /**
530     * Sets the file extensions that identify the files that pass the
531     * filter of this FileSetCheck.
532     * @param extensions the set of file extensions. A missing
533     *     initial '.' character of an extension is automatically added.
534     */
535    public final void setFileExtensions(String... extensions) {
536        if (extensions == null) {
537            fileExtensions = null;
538        }
539        else {
540            fileExtensions = new String[extensions.length];
541            for (int i = 0; i < extensions.length; i++) {
542                final String extension = extensions[i];
543                if (CommonUtil.startsWithChar(extension, '.')) {
544                    fileExtensions[i] = extension;
545                }
546                else {
547                    fileExtensions[i] = "." + extension;
548                }
549            }
550        }
551    }
552
553    /**
554     * Sets the factory for creating submodules.
555     *
556     * @param moduleFactory the factory for creating FileSetChecks
557     */
558    public void setModuleFactory(ModuleFactory moduleFactory) {
559        this.moduleFactory = moduleFactory;
560    }
561
562    /**
563     * Sets locale country.
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     * @param localeLanguage the language to report messages
573     */
574    public void setLocaleLanguage(String localeLanguage) {
575        this.localeLanguage = localeLanguage;
576    }
577
578    /**
579     * Sets the severity level.  The string should be one of the names
580     * defined in the {@code SeverityLevel} class.
581     *
582     * @param severity  The new severity level
583     * @see SeverityLevel
584     */
585    public final void setSeverity(String severity) {
586        this.severity = SeverityLevel.getInstance(severity);
587    }
588
589    /**
590     * Sets the classloader that is used to contextualize fileset checks.
591     * Some Check implementations will use that classloader to improve the
592     * quality of their reports, e.g. to load a class and then analyze it via
593     * reflection.
594     * @param classLoader the new classloader
595     * @deprecated Checkstyle is not type aware tool and all class loading is potentially
596     *     unstable.
597     */
598    @Deprecated
599    public final void setClassLoader(ClassLoader classLoader) {
600        // no code
601    }
602
603    @Override
604    public final void setModuleClassLoader(ClassLoader moduleClassLoader) {
605        this.moduleClassLoader = moduleClassLoader;
606    }
607
608    /**
609     * Sets a named charset.
610     * @param charset the name of a charset
611     * @throws UnsupportedEncodingException if charset is unsupported.
612     */
613    public void setCharset(String charset)
614            throws UnsupportedEncodingException {
615        if (!Charset.isSupported(charset)) {
616            final String message = "unsupported charset: '" + charset + "'";
617            throw new UnsupportedEncodingException(message);
618        }
619        this.charset = charset;
620    }
621
622    /**
623     * Sets the field haltOnException.
624     * @param haltOnException the new value.
625     */
626    public void setHaltOnException(boolean haltOnException) {
627        this.haltOnException = haltOnException;
628    }
629
630    /**
631     * Set the tab width to report audit events with.
632     * @param tabWidth an {@code int} value
633     */
634    public final void setTabWidth(int tabWidth) {
635        this.tabWidth = tabWidth;
636    }
637
638    /**
639     * Clears the cache.
640     */
641    public void clearCache() {
642        if (cacheFile != null) {
643            cacheFile.reset();
644        }
645    }
646
647}