blob: 9ecc9eff170026b18e7a1d8a96430519a1a45406 [file] [log] [blame]
////////////////////////////////////////////////////////////////////////////////
// checkstyle: Checks Java source code for adherence to a set of rules.
// Copyright (C) 2001-2017 the original author or authors.
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
////////////////////////////////////////////////////////////////////////////////
package com.puppycrawl.tools.checkstyle;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import com.puppycrawl.tools.checkstyle.api.AuditEvent;
import com.puppycrawl.tools.checkstyle.api.AuditListener;
import com.puppycrawl.tools.checkstyle.api.AutomaticBean;
import com.puppycrawl.tools.checkstyle.api.BeforeExecutionFileFilter;
import com.puppycrawl.tools.checkstyle.api.BeforeExecutionFileFilterSet;
import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
import com.puppycrawl.tools.checkstyle.api.Configuration;
import com.puppycrawl.tools.checkstyle.api.Context;
import com.puppycrawl.tools.checkstyle.api.ExternalResourceHolder;
import com.puppycrawl.tools.checkstyle.api.FileSetCheck;
import com.puppycrawl.tools.checkstyle.api.FileText;
import com.puppycrawl.tools.checkstyle.api.Filter;
import com.puppycrawl.tools.checkstyle.api.FilterSet;
import com.puppycrawl.tools.checkstyle.api.LocalizedMessage;
import com.puppycrawl.tools.checkstyle.api.MessageDispatcher;
import com.puppycrawl.tools.checkstyle.api.RootModule;
import com.puppycrawl.tools.checkstyle.api.SeverityLevel;
import com.puppycrawl.tools.checkstyle.api.SeverityLevelCounter;
import com.puppycrawl.tools.checkstyle.utils.CommonUtils;
/**
* This class provides the functionality to check a set of files.
* @author Oliver Burn
* @author <a href="mailto:stephane.bailliez@wanadoo.fr">Stephane Bailliez</a>
* @author lkuehne
* @author Andrei Selkin
*/
public class Checker extends AutomaticBean implements MessageDispatcher, RootModule {
/** Message to use when an exception occurs and should be printed as a violation. */
public static final String EXCEPTION_MSG = "general.exception";
/** Logger for Checker. */
private final Log log;
/** Maintains error count. */
private final SeverityLevelCounter counter = new SeverityLevelCounter(
SeverityLevel.ERROR);
/** Vector of listeners. */
private final List<AuditListener> listeners = new ArrayList<>();
/** Vector of fileset checks. */
private final List<FileSetCheck> fileSetChecks = new ArrayList<>();
/** The audit event before execution file filters. */
private final BeforeExecutionFileFilterSet beforeExecutionFileFilters =
new BeforeExecutionFileFilterSet();
/** The audit event filters. */
private final FilterSet filters = new FilterSet();
/** Class loader to resolve classes with. **/
private ClassLoader classLoader = Thread.currentThread()
.getContextClassLoader();
/** The basedir to strip off in file names. */
private String basedir;
/** Locale country to report messages . **/
private String localeCountry = Locale.getDefault().getCountry();
/** Locale language to report messages . **/
private String localeLanguage = Locale.getDefault().getLanguage();
/** The factory for instantiating submodules. */
private ModuleFactory moduleFactory;
/** The classloader used for loading Checkstyle module classes. */
private ClassLoader moduleClassLoader;
/** The context of all child components. */
private Context childContext;
/** The file extensions that are accepted. */
private String[] fileExtensions = CommonUtils.EMPTY_STRING_ARRAY;
/**
* The severity level of any violations found by submodules.
* The value of this property is passed to submodules via
* contextualize().
*
* <p>Note: Since the Checker is merely a container for modules
* it does not make sense to implement logging functionality
* here. Consequently Checker does not extend AbstractViolationReporter,
* leading to a bit of duplicated code for severity level setting.
*/
private SeverityLevel severityLevel = SeverityLevel.ERROR;
/** Name of a charset. */
private String charset = System.getProperty("file.encoding", StandardCharsets.UTF_8.name());
/** Cache file. **/
private PropertyCacheFile cache;
/** Controls whether exceptions should halt execution or not. */
private boolean haltOnException = true;
/**
* Creates a new {@code Checker} instance.
* The instance needs to be contextualized and configured.
*/
public Checker() {
addListener(counter);
log = LogFactory.getLog(Checker.class);
}
/**
* Sets cache file.
* @param fileName the cache file.
* @throws IOException if there are some problems with file loading.
*/
public void setCacheFile(String fileName) throws IOException {
final Configuration configuration = getConfiguration();
cache = new PropertyCacheFile(configuration, fileName);
cache.load();
}
/**
* Removes before execution file filter.
* @param filter before execution file filter to remove.
*/
public void removeBeforeExecutionFileFilter(BeforeExecutionFileFilter filter) {
beforeExecutionFileFilters.removeBeforeExecutionFileFilter(filter);
}
/**
* Removes filter.
* @param filter filter to remove.
*/
public void removeFilter(Filter filter) {
filters.removeFilter(filter);
}
@Override
public void destroy() {
listeners.clear();
fileSetChecks.clear();
beforeExecutionFileFilters.clear();
filters.clear();
if (cache != null) {
try {
cache.persist();
}
catch (IOException ex) {
throw new IllegalStateException("Unable to persist cache file.", ex);
}
}
}
/**
* Removes a given listener.
* @param listener a listener to remove
*/
public void removeListener(AuditListener listener) {
listeners.remove(listener);
}
/**
* Sets base directory.
* @param basedir the base directory to strip off in file names
*/
public void setBasedir(String basedir) {
this.basedir = basedir;
}
@Override
public int process(List<File> files) throws CheckstyleException {
if (cache != null) {
cache.putExternalResources(getExternalResourceLocations());
}
// Prepare to start
fireAuditStarted();
for (final FileSetCheck fsc : fileSetChecks) {
fsc.beginProcessing(charset);
}
processFiles(files);
// Finish up
// It may also log!!!
fileSetChecks.forEach(FileSetCheck::finishProcessing);
// It may also log!!!
fileSetChecks.forEach(FileSetCheck::destroy);
final int errorCount = counter.getCount();
fireAuditFinished();
return errorCount;
}
/**
* Returns a set of external configuration resource locations which are used by all file set
* checks and filters.
* @return a set of external configuration resource locations which are used by all file set
* checks and filters.
*/
private Set<String> getExternalResourceLocations() {
final Set<String> externalResources = new HashSet<>();
fileSetChecks.stream().filter(check -> check instanceof ExternalResourceHolder)
.forEach(check -> {
final Set<String> locations =
((ExternalResourceHolder) check).getExternalResourceLocations();
externalResources.addAll(locations);
});
filters.getFilters().stream().filter(filter -> filter instanceof ExternalResourceHolder)
.forEach(filter -> {
final Set<String> locations =
((ExternalResourceHolder) filter).getExternalResourceLocations();
externalResources.addAll(locations);
});
return externalResources;
}
/** Notify all listeners about the audit start. */
private void fireAuditStarted() {
final AuditEvent event = new AuditEvent(this);
for (final AuditListener listener : listeners) {
listener.auditStarted(event);
}
}
/** Notify all listeners about the audit end. */
private void fireAuditFinished() {
final AuditEvent event = new AuditEvent(this);
for (final AuditListener listener : listeners) {
listener.auditFinished(event);
}
}
/**
* Processes a list of files with all FileSetChecks.
* @param files a list of files to process.
* @throws CheckstyleException if error condition within Checkstyle occurs.
* @noinspection ProhibitedExceptionThrown
*/
private void processFiles(List<File> files) throws CheckstyleException {
for (final File file : files) {
try {
final String fileName = file.getAbsolutePath();
final long timestamp = file.lastModified();
if (cache != null && cache.isInCache(fileName, timestamp)
|| !CommonUtils.matchesFileExtension(file, fileExtensions)
|| !acceptFileStarted(fileName)) {
continue;
}
if (cache != null) {
cache.put(fileName, timestamp);
}
fireFileStarted(fileName);
final SortedSet<LocalizedMessage> fileMessages = processFile(file);
fireErrors(fileName, fileMessages);
fireFileFinished(fileName);
}
// -@cs[IllegalCatch] There is no other way to deliver filename that was under
// processing. See https://github.com/checkstyle/checkstyle/issues/2285
catch (Exception ex) {
// We need to catch all exceptions to put a reason failure (file name) in exception
throw new CheckstyleException("Exception was thrown while processing "
+ file.getPath(), ex);
}
catch (Error error) {
// We need to catch all errors to put a reason failure (file name) in error
throw new Error("Error was thrown while processing " + file.getPath(), error);
}
}
}
/**
* Processes a file with all FileSetChecks.
* @param file a file to process.
* @return a sorted set of messages to be logged.
* @throws CheckstyleException if error condition within Checkstyle occurs.
* @noinspection ProhibitedExceptionThrown
*/
private SortedSet<LocalizedMessage> processFile(File file) throws CheckstyleException {
final SortedSet<LocalizedMessage> fileMessages = new TreeSet<>();
try {
final FileText theText = new FileText(file.getAbsoluteFile(), charset);
for (final FileSetCheck fsc : fileSetChecks) {
fileMessages.addAll(fsc.process(file, theText));
}
}
catch (final IOException ioe) {
log.debug("IOException occurred.", ioe);
fileMessages.add(new LocalizedMessage(0,
Definitions.CHECKSTYLE_BUNDLE, EXCEPTION_MSG,
new String[] {ioe.getMessage()}, null, getClass(), null));
}
// -@cs[IllegalCatch] There is no other way to obey haltOnException field
catch (Exception ex) {
if (haltOnException) {
throw ex;
}
log.debug("Exception occurred.", ex);
final StringWriter sw = new StringWriter();
final PrintWriter pw = new PrintWriter(sw, true);
ex.printStackTrace(pw);
fileMessages.add(new LocalizedMessage(0,
Definitions.CHECKSTYLE_BUNDLE, EXCEPTION_MSG,
new String[] {sw.getBuffer().toString()},
null, getClass(), null));
}
return fileMessages;
}
/**
* Check if all before execution file filters accept starting the file.
*
* @param fileName
* the file to be audited
* @return {@code true} if the file is accepted.
*/
private boolean acceptFileStarted(String fileName) {
final String stripped = CommonUtils.relativizeAndNormalizePath(basedir, fileName);
return beforeExecutionFileFilters.accept(stripped);
}
/**
* Notify all listeners about the beginning of a file audit.
*
* @param fileName
* the file to be audited
*/
@Override
public void fireFileStarted(String fileName) {
final String stripped = CommonUtils.relativizeAndNormalizePath(basedir, fileName);
final AuditEvent event = new AuditEvent(this, stripped);
for (final AuditListener listener : listeners) {
listener.fileStarted(event);
}
}
/**
* Notify all listeners about the errors in a file.
*
* @param fileName the audited file
* @param errors the audit errors from the file
*/
@Override
public void fireErrors(String fileName, SortedSet<LocalizedMessage> errors) {
final String stripped = CommonUtils.relativizeAndNormalizePath(basedir, fileName);
boolean hasNonFilteredViolations = false;
for (final LocalizedMessage element : errors) {
final AuditEvent event = new AuditEvent(this, stripped, element);
if (filters.accept(event)) {
hasNonFilteredViolations = true;
for (final AuditListener listener : listeners) {
listener.addError(event);
}
}
}
if (hasNonFilteredViolations && cache != null) {
cache.remove(fileName);
}
}
/**
* Notify all listeners about the end of a file audit.
*
* @param fileName
* the audited file
*/
@Override
public void fireFileFinished(String fileName) {
final String stripped = CommonUtils.relativizeAndNormalizePath(basedir, fileName);
final AuditEvent event = new AuditEvent(this, stripped);
for (final AuditListener listener : listeners) {
listener.fileFinished(event);
}
}
@Override
protected void finishLocalSetup() throws CheckstyleException {
final Locale locale = new Locale(localeLanguage, localeCountry);
LocalizedMessage.setLocale(locale);
if (moduleFactory == null) {
if (moduleClassLoader == null) {
throw new CheckstyleException(
"if no custom moduleFactory is set, "
+ "moduleClassLoader must be specified");
}
final Set<String> packageNames = PackageNamesLoader
.getPackageNames(moduleClassLoader);
moduleFactory = new PackageObjectFactory(packageNames,
moduleClassLoader);
}
final DefaultContext context = new DefaultContext();
context.add("charset", charset);
context.add("classLoader", classLoader);
context.add("moduleFactory", moduleFactory);
context.add("severity", severityLevel.getName());
context.add("basedir", basedir);
childContext = context;
}
/**
* {@inheritDoc} Creates child module.
* @noinspection ChainOfInstanceofChecks
*/
@Override
protected void setupChild(Configuration childConf)
throws CheckstyleException {
final String name = childConf.getName();
final Object child;
try {
child = moduleFactory.createModule(name);
if (child instanceof AutomaticBean) {
final AutomaticBean bean = (AutomaticBean) child;
bean.contextualize(childContext);
bean.configure(childConf);
}
}
catch (final CheckstyleException ex) {
throw new CheckstyleException("cannot initialize module " + name
+ " - " + ex.getMessage(), ex);
}
if (child instanceof FileSetCheck) {
final FileSetCheck fsc = (FileSetCheck) child;
fsc.init();
addFileSetCheck(fsc);
}
else if (child instanceof BeforeExecutionFileFilter) {
final BeforeExecutionFileFilter filter = (BeforeExecutionFileFilter) child;
addBeforeExecutionFileFilter(filter);
}
else if (child instanceof Filter) {
final Filter filter = (Filter) child;
addFilter(filter);
}
else if (child instanceof AuditListener) {
final AuditListener listener = (AuditListener) child;
addListener(listener);
}
else {
throw new CheckstyleException(name
+ " is not allowed as a child in Checker");
}
}
/**
* Adds a FileSetCheck to the list of FileSetChecks
* that is executed in process().
* @param fileSetCheck the additional FileSetCheck
*/
public void addFileSetCheck(FileSetCheck fileSetCheck) {
fileSetCheck.setMessageDispatcher(this);
fileSetChecks.add(fileSetCheck);
}
/**
* Adds a before execution file filter to the end of the event chain.
* @param filter the additional filter
*/
public void addBeforeExecutionFileFilter(BeforeExecutionFileFilter filter) {
beforeExecutionFileFilters.addBeforeExecutionFileFilter(filter);
}
/**
* Adds a filter to the end of the audit event filter chain.
* @param filter the additional filter
*/
public void addFilter(Filter filter) {
filters.addFilter(filter);
}
@Override
public final void addListener(AuditListener listener) {
listeners.add(listener);
}
/**
* Sets the file extensions that identify the files that pass the
* filter of this FileSetCheck.
* @param extensions the set of file extensions. A missing
* initial '.' character of an extension is automatically added.
*/
public final void setFileExtensions(String... extensions) {
if (extensions == null) {
fileExtensions = null;
}
else {
fileExtensions = new String[extensions.length];
for (int i = 0; i < extensions.length; i++) {
final String extension = extensions[i];
if (CommonUtils.startsWithChar(extension, '.')) {
fileExtensions[i] = extension;
}
else {
fileExtensions[i] = "." + extension;
}
}
}
}
/**
* Sets the factory for creating submodules.
*
* @param moduleFactory the factory for creating FileSetChecks
*/
public void setModuleFactory(ModuleFactory moduleFactory) {
this.moduleFactory = moduleFactory;
}
/**
* Sets locale country.
* @param localeCountry the country to report messages
*/
public void setLocaleCountry(String localeCountry) {
this.localeCountry = localeCountry;
}
/**
* Sets locale language.
* @param localeLanguage the language to report messages
*/
public void setLocaleLanguage(String localeLanguage) {
this.localeLanguage = localeLanguage;
}
/**
* Sets the severity level. The string should be one of the names
* defined in the {@code SeverityLevel} class.
*
* @param severity The new severity level
* @see SeverityLevel
*/
public final void setSeverity(String severity) {
severityLevel = SeverityLevel.getInstance(severity);
}
/**
* Sets the classloader that is used to contextualize fileset checks.
* Some Check implementations will use that classloader to improve the
* quality of their reports, e.g. to load a class and then analyze it via
* reflection.
* @param classLoader the new classloader
*/
public final void setClassLoader(ClassLoader classLoader) {
this.classLoader = classLoader;
}
@Override
public final void setModuleClassLoader(ClassLoader moduleClassLoader) {
this.moduleClassLoader = moduleClassLoader;
}
/**
* Sets a named charset.
* @param charset the name of a charset
* @throws UnsupportedEncodingException if charset is unsupported.
*/
public void setCharset(String charset)
throws UnsupportedEncodingException {
if (!Charset.isSupported(charset)) {
final String message = "unsupported charset: '" + charset + "'";
throw new UnsupportedEncodingException(message);
}
this.charset = charset;
}
/**
* Sets the field haltOnException.
* @param haltOnException the new value.
*/
public void setHaltOnException(boolean haltOnException) {
this.haltOnException = haltOnException;
}
/**
* Clears the cache.
*/
public void clearCache() {
if (cache != null) {
cache.reset();
}
}
}