blob: 35b8e9de6ce10b3d9cad7b7d2ea7e15082293b9e [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.ant;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.ResourceBundle;
import java.util.stream.Collectors;
import org.apache.tools.ant.AntClassLoader;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.DirectoryScanner;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.taskdefs.LogOutputStream;
import org.apache.tools.ant.types.EnumeratedAttribute;
import org.apache.tools.ant.types.FileSet;
import org.apache.tools.ant.types.Path;
import org.apache.tools.ant.types.Reference;
import com.google.common.io.Closeables;
import com.puppycrawl.tools.checkstyle.Checker;
import com.puppycrawl.tools.checkstyle.ConfigurationLoader;
import com.puppycrawl.tools.checkstyle.DefaultLogger;
import com.puppycrawl.tools.checkstyle.ModuleFactory;
import com.puppycrawl.tools.checkstyle.PackageObjectFactory;
import com.puppycrawl.tools.checkstyle.PropertiesExpander;
import com.puppycrawl.tools.checkstyle.ThreadModeSettings;
import com.puppycrawl.tools.checkstyle.XMLLogger;
import com.puppycrawl.tools.checkstyle.api.AuditListener;
import com.puppycrawl.tools.checkstyle.api.AutomaticBean;
import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
import com.puppycrawl.tools.checkstyle.api.Configuration;
import com.puppycrawl.tools.checkstyle.api.RootModule;
import com.puppycrawl.tools.checkstyle.api.SeverityLevel;
import com.puppycrawl.tools.checkstyle.api.SeverityLevelCounter;
/**
* An implementation of a ANT task for calling checkstyle. See the documentation
* of the task for usage.
* @author Oliver Burn
* @noinspection ClassLoaderInstantiation
*/
public class CheckstyleAntTask extends Task {
/** Poor man's enum for an xml formatter. */
private static final String E_XML = "xml";
/** Poor man's enum for an plain formatter. */
private static final String E_PLAIN = "plain";
/** Suffix for time string. */
private static final String TIME_SUFFIX = " ms.";
/** Contains the paths to process. */
private final List<Path> paths = new ArrayList<>();
/** Contains the filesets to process. */
private final List<FileSet> fileSets = new ArrayList<>();
/** Contains the formatters to log to. */
private final List<Formatter> formatters = new ArrayList<>();
/** Contains the Properties to override. */
private final List<Property> overrideProps = new ArrayList<>();
/** Class path to locate class files. */
private Path classpath;
/** Name of file to check. */
private String fileName;
/** Config file containing configuration. */
private String config;
/** Whether to fail build on violations. */
private boolean failOnViolation = true;
/** Property to set on violations. */
private String failureProperty;
/** The name of the properties file. */
private File properties;
/** The maximum number of errors that are tolerated. */
private int maxErrors;
/** The maximum number of warnings that are tolerated. */
private int maxWarnings = Integer.MAX_VALUE;
/**
* Whether to execute ignored modules - some modules may log above
* their severity depending on their configuration (e.g. WriteTag) so
* need to be included
*/
private boolean executeIgnoredModules;
////////////////////////////////////////////////////////////////////////////
// Setters for ANT specific attributes
////////////////////////////////////////////////////////////////////////////
/**
* Tells this task to write failure message to the named property when there
* is a violation.
* @param propertyName the name of the property to set
* in the event of an failure.
*/
public void setFailureProperty(String propertyName) {
failureProperty = propertyName;
}
/**
* Sets flag - whether to fail if a violation is found.
* @param fail whether to fail if a violation is found
*/
public void setFailOnViolation(boolean fail) {
failOnViolation = fail;
}
/**
* Sets the maximum number of errors allowed. Default is 0.
* @param maxErrors the maximum number of errors allowed.
*/
public void setMaxErrors(int maxErrors) {
this.maxErrors = maxErrors;
}
/**
* Sets the maximum number of warnings allowed. Default is
* {@link Integer#MAX_VALUE}.
* @param maxWarnings the maximum number of warnings allowed.
*/
public void setMaxWarnings(int maxWarnings) {
this.maxWarnings = maxWarnings;
}
/**
* Adds a path.
* @param path the path to add.
*/
public void addPath(Path path) {
paths.add(path);
}
/**
* Adds set of files (nested fileset attribute).
* @param fileSet the file set to add
*/
public void addFileset(FileSet fileSet) {
fileSets.add(fileSet);
}
/**
* Add a formatter.
* @param formatter the formatter to add for logging.
*/
public void addFormatter(Formatter formatter) {
formatters.add(formatter);
}
/**
* Add an override property.
* @param property the property to add
*/
public void addProperty(Property property) {
overrideProps.add(property);
}
/**
* Set the class path.
* @param classpath the path to locate classes
*/
public void setClasspath(Path classpath) {
if (this.classpath == null) {
this.classpath = classpath;
}
else {
this.classpath.append(classpath);
}
}
/**
* Set the class path from a reference defined elsewhere.
* @param classpathRef the reference to an instance defining the classpath
*/
public void setClasspathRef(Reference classpathRef) {
createClasspath().setRefid(classpathRef);
}
/**
* Creates classpath.
* @return a created path for locating classes
*/
public Path createClasspath() {
if (classpath == null) {
classpath = new Path(getProject());
}
return classpath.createPath();
}
/**
* Sets file to be checked.
* @param file the file to be checked
*/
public void setFile(File file) {
fileName = file.getAbsolutePath();
}
/**
* Sets configuration file.
* @param configuration the configuration file, URL, or resource to use
*/
public void setConfig(String configuration) {
if (config != null) {
throw new BuildException("Attribute 'config' has already been set");
}
config = configuration;
}
/**
* Sets flag - whether to execute ignored modules.
* @param omit whether to execute ignored modules
*/
public void setExecuteIgnoredModules(boolean omit) {
executeIgnoredModules = omit;
}
////////////////////////////////////////////////////////////////////////////
// Setters for Root Module's configuration attributes
////////////////////////////////////////////////////////////////////////////
/**
* Sets a properties file for use instead
* of individually setting them.
* @param props the properties File to use
*/
public void setProperties(File props) {
properties = props;
}
////////////////////////////////////////////////////////////////////////////
// The doers
////////////////////////////////////////////////////////////////////////////
@Override
public void execute() {
final long startTime = System.currentTimeMillis();
try {
// output version info in debug mode
final ResourceBundle compilationProperties = ResourceBundle
.getBundle("checkstylecompilation", Locale.ROOT);
final String version = compilationProperties
.getString("checkstyle.compile.version");
final String compileTimestamp = compilationProperties
.getString("checkstyle.compile.timestamp");
log("checkstyle version " + version, Project.MSG_VERBOSE);
log("compiled on " + compileTimestamp, Project.MSG_VERBOSE);
// Check for no arguments
if (fileName == null
&& fileSets.isEmpty()
&& paths.isEmpty()) {
throw new BuildException(
"Must specify at least one of 'file' or nested 'fileset' or 'path'.",
getLocation());
}
if (config == null) {
throw new BuildException("Must specify 'config'.", getLocation());
}
realExecute(version);
}
finally {
final long endTime = System.currentTimeMillis();
log("Total execution took " + (endTime - startTime) + TIME_SUFFIX,
Project.MSG_VERBOSE);
}
}
/**
* Helper implementation to perform execution.
* @param checkstyleVersion Checkstyle compile version.
*/
private void realExecute(String checkstyleVersion) {
// Create the root module
RootModule rootModule = null;
try {
rootModule = createRootModule();
// setup the listeners
final AuditListener[] listeners = getListeners();
for (AuditListener element : listeners) {
rootModule.addListener(element);
}
final SeverityLevelCounter warningCounter =
new SeverityLevelCounter(SeverityLevel.WARNING);
rootModule.addListener(warningCounter);
processFiles(rootModule, warningCounter, checkstyleVersion);
}
finally {
destroyRootModule(rootModule);
}
}
/**
* Destroy root module. This method exists only due to bug in cobertura library
* https://github.com/cobertura/cobertura/issues/170
* @param rootModule Root module that was used to process files
*/
private static void destroyRootModule(RootModule rootModule) {
if (rootModule != null) {
rootModule.destroy();
}
}
/**
* Scans and processes files by means given root module.
* @param rootModule Root module to process files
* @param warningCounter Root Module's counter of warnings
* @param checkstyleVersion Checkstyle compile version
*/
private void processFiles(RootModule rootModule, final SeverityLevelCounter warningCounter,
final String checkstyleVersion) {
final long startTime = System.currentTimeMillis();
final List<File> files = getFilesToCheck();
final long endTime = System.currentTimeMillis();
log("To locate the files took " + (endTime - startTime) + TIME_SUFFIX,
Project.MSG_VERBOSE);
log("Running Checkstyle " + checkstyleVersion + " on " + files.size()
+ " files", Project.MSG_INFO);
log("Using configuration " + config, Project.MSG_VERBOSE);
final int numErrs;
try {
final long processingStartTime = System.currentTimeMillis();
numErrs = rootModule.process(files);
final long processingEndTime = System.currentTimeMillis();
log("To process the files took " + (processingEndTime - processingStartTime)
+ TIME_SUFFIX, Project.MSG_VERBOSE);
}
catch (CheckstyleException ex) {
throw new BuildException("Unable to process files: " + files, ex);
}
final int numWarnings = warningCounter.getCount();
final boolean okStatus = numErrs <= maxErrors && numWarnings <= maxWarnings;
// Handle the return status
if (!okStatus) {
final String failureMsg =
"Got " + numErrs + " errors and " + numWarnings
+ " warnings.";
if (failureProperty != null) {
getProject().setProperty(failureProperty, failureMsg);
}
if (failOnViolation) {
throw new BuildException(failureMsg, getLocation());
}
}
}
/**
* Creates new instance of the root module.
* @return new instance of the root module
*/
private RootModule createRootModule() {
final RootModule rootModule;
try {
final Properties props = createOverridingProperties();
final ThreadModeSettings threadModeSettings =
ThreadModeSettings.SINGLE_THREAD_MODE_INSTANCE;
final ConfigurationLoader.IgnoredModulesOptions ignoredModulesOptions;
if (executeIgnoredModules) {
ignoredModulesOptions = ConfigurationLoader.IgnoredModulesOptions.EXECUTE;
}
else {
ignoredModulesOptions = ConfigurationLoader.IgnoredModulesOptions.OMIT;
}
final Configuration configuration = ConfigurationLoader.loadConfiguration(config,
new PropertiesExpander(props), ignoredModulesOptions, threadModeSettings);
final ClassLoader moduleClassLoader =
Checker.class.getClassLoader();
final ModuleFactory factory = new PackageObjectFactory(
Checker.class.getPackage().getName() + ".", moduleClassLoader);
rootModule = (RootModule) factory.createModule(configuration.getName());
rootModule.setModuleClassLoader(moduleClassLoader);
if (rootModule instanceof Checker) {
final ClassLoader loader = new AntClassLoader(getProject(),
classpath);
((Checker) rootModule).setClassLoader(loader);
}
rootModule.configure(configuration);
}
catch (final CheckstyleException ex) {
throw new BuildException(String.format(Locale.ROOT, "Unable to create Root Module: "
+ "config {%s}, classpath {%s}.", config, classpath), ex);
}
return rootModule;
}
/**
* Create the Properties object based on the arguments specified
* to the ANT task.
* @return the properties for property expansion expansion
* @throws BuildException if an error occurs
*/
private Properties createOverridingProperties() {
final Properties returnValue = new Properties();
// Load the properties file if specified
if (properties != null) {
FileInputStream inStream = null;
try {
inStream = new FileInputStream(properties);
returnValue.load(inStream);
}
catch (final IOException ex) {
throw new BuildException("Error loading Properties file '"
+ properties + "'", ex, getLocation());
}
finally {
Closeables.closeQuietly(inStream);
}
}
// override with Ant properties like ${basedir}
final Map<String, Object> antProps = getProject().getProperties();
for (Map.Entry<String, Object> entry : antProps.entrySet()) {
final String value = String.valueOf(entry.getValue());
returnValue.setProperty(entry.getKey(), value);
}
// override with properties specified in subelements
for (Property p : overrideProps) {
returnValue.setProperty(p.getKey(), p.getValue());
}
return returnValue;
}
/**
* Return the list of listeners set in this task.
* @return the list of listeners.
*/
private AuditListener[] getListeners() {
final int formatterCount = Math.max(1, formatters.size());
final AuditListener[] listeners = new AuditListener[formatterCount];
// formatters
try {
if (formatters.isEmpty()) {
final OutputStream debug = new LogOutputStream(this, Project.MSG_DEBUG);
final OutputStream err = new LogOutputStream(this, Project.MSG_ERR);
listeners[0] = new DefaultLogger(debug, AutomaticBean.OutputStreamOptions.CLOSE,
err, AutomaticBean.OutputStreamOptions.CLOSE);
}
else {
for (int i = 0; i < formatterCount; i++) {
final Formatter formatter = formatters.get(i);
listeners[i] = formatter.createListener(this);
}
}
}
catch (IOException ex) {
throw new BuildException(String.format(Locale.ROOT, "Unable to create listeners: "
+ "formatters {%s}.", formatters), ex);
}
return listeners;
}
/**
* Returns the list of files (full path name) to process.
* @return the list of files included via the fileName, filesets and paths.
*/
private List<File> getFilesToCheck() {
final List<File> allFiles = new ArrayList<>();
if (fileName != null) {
// oops we've got an additional one to process, don't
// forget it. No sweat, it's fully resolved via the setter.
log("Adding standalone file for audit", Project.MSG_VERBOSE);
allFiles.add(new File(fileName));
}
final List<File> filesFromFileSets = scanFileSets();
allFiles.addAll(filesFromFileSets);
final List<File> filesFromPaths = scanPaths();
allFiles.addAll(filesFromPaths);
return allFiles;
}
/**
* Retrieves all files from the defined paths.
* @return a list of files defined via paths.
*/
private List<File> scanPaths() {
final List<File> allFiles = new ArrayList<>();
for (int i = 0; i < paths.size(); i++) {
final Path currentPath = paths.get(i);
final List<File> pathFiles = scanPath(currentPath, i + 1);
allFiles.addAll(pathFiles);
}
return allFiles;
}
/**
* Scans the given path and retrieves all files for the given path.
*
* @param path A path to scan.
* @param pathIndex The index of the given path. Used in log messages only.
* @return A list of files, extracted from the given path.
*/
private List<File> scanPath(Path path, int pathIndex) {
final String[] resources = path.list();
log(pathIndex + ") Scanning path " + path, Project.MSG_VERBOSE);
final List<File> allFiles = new ArrayList<>();
int concreteFilesCount = 0;
for (String resource : resources) {
final File file = new File(resource);
if (file.isFile()) {
concreteFilesCount++;
allFiles.add(file);
}
else {
final DirectoryScanner scanner = new DirectoryScanner();
scanner.setBasedir(file);
scanner.scan();
final List<File> scannedFiles = retrieveAllScannedFiles(scanner, pathIndex);
allFiles.addAll(scannedFiles);
}
}
if (concreteFilesCount > 0) {
log(String.format(Locale.ROOT, "%d) Adding %d files from path %s",
pathIndex, concreteFilesCount, path), Project.MSG_VERBOSE);
}
return allFiles;
}
/**
* Returns the list of files (full path name) to process.
* @return the list of files included via the filesets.
*/
protected List<File> scanFileSets() {
final List<File> allFiles = new ArrayList<>();
for (int i = 0; i < fileSets.size(); i++) {
final FileSet fileSet = fileSets.get(i);
final DirectoryScanner scanner = fileSet.getDirectoryScanner(getProject());
final List<File> scannedFiles = retrieveAllScannedFiles(scanner, i);
allFiles.addAll(scannedFiles);
}
return allFiles;
}
/**
* Retrieves all matched files from the given scanner.
*
* @param scanner A directory scanner. Note, that {@link DirectoryScanner#scan()}
* must be called before calling this method.
* @param logIndex A log entry index. Used only for log messages.
* @return A list of files, retrieved from the given scanner.
*/
private List<File> retrieveAllScannedFiles(DirectoryScanner scanner, int logIndex) {
final String[] fileNames = scanner.getIncludedFiles();
log(String.format(Locale.ROOT, "%d) Adding %d files from directory %s",
logIndex, fileNames.length, scanner.getBasedir()), Project.MSG_VERBOSE);
return Arrays.stream(fileNames)
.map(name -> scanner.getBasedir() + File.separator + name)
.map(File::new)
.collect(Collectors.toList());
}
/**
* Poor mans enumeration for the formatter types.
* @author Oliver Burn
*/
public static class FormatterType extends EnumeratedAttribute {
/** My possible values. */
private static final String[] VALUES = {E_XML, E_PLAIN};
@Override
public String[] getValues() {
return VALUES.clone();
}
}
/**
* Details about a formatter to be used.
* @author Oliver Burn
*/
public static class Formatter {
/** The formatter type. */
private FormatterType type;
/** The file to output to. */
private File toFile;
/** Whether or not the write to the named file. */
private boolean useFile = true;
/**
* Set the type of the formatter.
* @param type the type
*/
public void setType(FormatterType type) {
this.type = type;
}
/**
* Set the file to output to.
* @param destination destination the file to output to
*/
public void setTofile(File destination) {
toFile = destination;
}
/**
* Sets whether or not we write to a file if it is provided.
* @param use whether not not to use provided file.
*/
public void setUseFile(boolean use) {
useFile = use;
}
/**
* Creates a listener for the formatter.
* @param task the task running
* @return a listener
* @throws IOException if an error occurs
*/
public AuditListener createListener(Task task) throws IOException {
final AuditListener listener;
if (type != null
&& E_XML.equals(type.getValue())) {
listener = createXmlLogger(task);
}
else {
listener = createDefaultLogger(task);
}
return listener;
}
/**
* Creates default logger.
* @param task the task to possibly log to
* @return a DefaultLogger instance
* @throws IOException if an error occurs
*/
private AuditListener createDefaultLogger(Task task)
throws IOException {
final AuditListener defaultLogger;
if (toFile == null || !useFile) {
defaultLogger = new DefaultLogger(
new LogOutputStream(task, Project.MSG_DEBUG),
AutomaticBean.OutputStreamOptions.CLOSE,
new LogOutputStream(task, Project.MSG_ERR),
AutomaticBean.OutputStreamOptions.CLOSE
);
}
else {
final FileOutputStream infoStream = new FileOutputStream(toFile);
defaultLogger =
new DefaultLogger(infoStream, AutomaticBean.OutputStreamOptions.CLOSE,
infoStream, AutomaticBean.OutputStreamOptions.NONE);
}
return defaultLogger;
}
/**
* Creates XML logger.
* @param task the task to possibly log to
* @return an XMLLogger instance
* @throws IOException if an error occurs
*/
private AuditListener createXmlLogger(Task task) throws IOException {
final AuditListener xmlLogger;
if (toFile == null || !useFile) {
xmlLogger = new XMLLogger(new LogOutputStream(task, Project.MSG_INFO),
AutomaticBean.OutputStreamOptions.CLOSE);
}
else {
xmlLogger = new XMLLogger(new FileOutputStream(toFile),
AutomaticBean.OutputStreamOptions.CLOSE);
}
return xmlLogger;
}
}
/**
* Represents a property that consists of a key and value.
*/
public static class Property {
/** The property key. */
private String key;
/** The property value. */
private String value;
/**
* Gets key.
* @return the property key
*/
public String getKey() {
return key;
}
/**
* Sets key.
* @param key sets the property key
*/
public void setKey(String key) {
this.key = key;
}
/**
* Gets value.
* @return the property value
*/
public String getValue() {
return value;
}
/**
* Sets value.
* @param value set the property value
*/
public void setValue(String value) {
this.value = value;
}
/**
* Sets the property value from a File.
* @param file set the property value from a File
*/
public void setFile(File file) {
value = file.getAbsolutePath();
}
}
/** Represents a custom listener. */
public static class Listener {
/** Class name of the listener class. */
private String className;
/**
* Gets class name.
* @return the class name
*/
public String getClassname() {
return className;
}
/**
* Sets class name.
* @param name set the class name
*/
public void setClassname(String name) {
className = name;
}
}
}