//////////////////////////////////////////////////////////////////////////////// // 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.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import java.util.Properties; import java.util.logging.ConsoleHandler; import java.util.logging.Filter; import java.util.logging.Level; import java.util.logging.LogRecord; import java.util.logging.Logger; import java.util.regex.Pattern; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.CommandLineParser; import org.apache.commons.cli.DefaultParser; import org.apache.commons.cli.HelpFormatter; import org.apache.commons.cli.Options; import org.apache.commons.cli.ParseException; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import com.google.common.io.Closeables; import com.puppycrawl.tools.checkstyle.api.AuditListener; 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.utils.CommonUtils; /** * Wrapper command line program for the Checker. * @author the original author or authors. * **/ public final class Main { /** Logger for Main. */ private static final Log LOG = LogFactory.getLog(Main.class); /** Width of CLI help option. */ private static final int HELP_WIDTH = 100; /** Exit code returned when execution finishes with {@link CheckstyleException}. */ private static final int EXIT_WITH_CHECKSTYLE_EXCEPTION_CODE = -2; /** Name for the option 'v'. */ private static final String OPTION_V_NAME = "v"; /** Name for the option 'c'. */ private static final String OPTION_C_NAME = "c"; /** Name for the option 'f'. */ private static final String OPTION_F_NAME = "f"; /** Name for the option 'p'. */ private static final String OPTION_P_NAME = "p"; /** Name for the option 'o'. */ private static final String OPTION_O_NAME = "o"; /** Name for the option 't'. */ private static final String OPTION_T_NAME = "t"; /** Name for the option '--tree'. */ private static final String OPTION_TREE_NAME = "tree"; /** Name for the option '-T'. */ private static final String OPTION_CAPITAL_T_NAME = "T"; /** Name for the option '--treeWithComments'. */ private static final String OPTION_TREE_COMMENT_NAME = "treeWithComments"; /** Name for the option '-j'. */ private static final String OPTION_J_NAME = "j"; /** Name for the option '--javadocTree'. */ private static final String OPTION_JAVADOC_TREE_NAME = "javadocTree"; /** Name for the option '-J'. */ private static final String OPTION_CAPITAL_J_NAME = "J"; /** Name for the option '--treeWithJavadoc'. */ private static final String OPTION_TREE_JAVADOC_NAME = "treeWithJavadoc"; /** Name for the option '-d'. */ private static final String OPTION_D_NAME = "d"; /** Name for the option '--debug'. */ private static final String OPTION_DEBUG_NAME = "debug"; /** Name for the option 'e'. */ private static final String OPTION_E_NAME = "e"; /** Name for the option '--exclude'. */ private static final String OPTION_EXCLUDE_NAME = "exclude"; /** Name for the option 'x'. */ private static final String OPTION_X_NAME = "x"; /** Name for the option '--exclude-regexp'. */ private static final String OPTION_EXCLUDE_REGEXP_NAME = "exclude-regexp"; /** Name for 'xml' format. */ private static final String XML_FORMAT_NAME = "xml"; /** Name for 'plain' format. */ private static final String PLAIN_FORMAT_NAME = "plain"; /** Don't create instance of this class, use {@link #main(String[])} method instead. */ private Main() { } /** * Loops over the files specified checking them for errors. The exit code * is the number of errors found in all the files. * @param args the command line arguments. * @throws IOException if there is a problem with files access * @noinspection CallToPrintStackTrace **/ public static void main(String... args) throws IOException { int errorCounter = 0; boolean cliViolations = false; // provide proper exit code based on results. final int exitWithCliViolation = -1; int exitStatus = 0; try { //parse CLI arguments final CommandLine commandLine = parseCli(args); // show version and exit if it is requested if (commandLine.hasOption(OPTION_V_NAME)) { System.out.println("Checkstyle version: " + Main.class.getPackage().getImplementationVersion()); exitStatus = 0; } else { final List<File> filesToProcess = getFilesToProcess(getExclusions(commandLine), commandLine.getArgs()); // return error if something is wrong in arguments final List<String> messages = validateCli(commandLine, filesToProcess); cliViolations = !messages.isEmpty(); if (cliViolations) { exitStatus = exitWithCliViolation; errorCounter = 1; messages.forEach(System.out::println); } else { errorCounter = runCli(commandLine, filesToProcess); exitStatus = errorCounter; } } } catch (ParseException pex) { // something wrong with arguments - print error and manual cliViolations = true; exitStatus = exitWithCliViolation; errorCounter = 1; System.out.println(pex.getMessage()); printUsage(); } catch (CheckstyleException ex) { exitStatus = EXIT_WITH_CHECKSTYLE_EXCEPTION_CODE; errorCounter = 1; ex.printStackTrace(); } finally { // return exit code base on validation of Checker if (errorCounter != 0 && !cliViolations) { System.out.println(String.format("Checkstyle ends with %d errors.", errorCounter)); } if (exitStatus != 0) { System.exit(exitStatus); } } } /** * Parses and executes Checkstyle based on passed arguments. * @param args * command line parameters * @return parsed information about passed parameters * @throws ParseException * when passed arguments are not valid */ private static CommandLine parseCli(String... args) throws ParseException { // parse the parameters final CommandLineParser clp = new DefaultParser(); // always returns not null value return clp.parse(buildOptions(), args); } /** * Gets the list of exclusions provided through the command line argument. * @param commandLine command line object * @return List of exclusion patterns. */ private static List<Pattern> getExclusions(CommandLine commandLine) { final List<Pattern> result = new ArrayList<>(); if (commandLine.hasOption(OPTION_E_NAME)) { for (String value : commandLine.getOptionValues(OPTION_E_NAME)) { result.add(Pattern.compile("^" + Pattern.quote(new File(value).getAbsolutePath()) + "$")); } } if (commandLine.hasOption(OPTION_X_NAME)) { for (String value : commandLine.getOptionValues(OPTION_X_NAME)) { result.add(Pattern.compile(value)); } } return result; } /** * Do validation of Command line options. * @param cmdLine command line object * @param filesToProcess List of files to process found from the command line. * @return list of violations */ // -@cs[CyclomaticComplexity] Breaking apart will damage encapsulation private static List<String> validateCli(CommandLine cmdLine, List<File> filesToProcess) { final List<String> result = new ArrayList<>(); if (filesToProcess.isEmpty()) { result.add("Files to process must be specified, found 0."); } // ensure there is no conflicting options else if (cmdLine.hasOption(OPTION_T_NAME) || cmdLine.hasOption(OPTION_CAPITAL_T_NAME) || cmdLine.hasOption(OPTION_J_NAME) || cmdLine.hasOption(OPTION_CAPITAL_J_NAME)) { if (cmdLine.hasOption(OPTION_C_NAME) || cmdLine.hasOption(OPTION_P_NAME) || cmdLine.hasOption(OPTION_F_NAME) || cmdLine.hasOption(OPTION_O_NAME)) { result.add("Option '-t' cannot be used with other options."); } else if (filesToProcess.size() > 1) { result.add("Printing AST is allowed for only one file."); } } // ensure a configuration file is specified else if (cmdLine.hasOption(OPTION_C_NAME)) { final String configLocation = cmdLine.getOptionValue(OPTION_C_NAME); try { // test location only CommonUtils.getUriByFilename(configLocation); } catch (CheckstyleException ignored) { result.add(String.format("Could not find config XML file '%s'.", configLocation)); } // validate optional parameters if (cmdLine.hasOption(OPTION_F_NAME)) { final String format = cmdLine.getOptionValue(OPTION_F_NAME); if (!PLAIN_FORMAT_NAME.equals(format) && !XML_FORMAT_NAME.equals(format)) { result.add(String.format("Invalid output format." + " Found '%s' but expected '%s' or '%s'.", format, PLAIN_FORMAT_NAME, XML_FORMAT_NAME)); } } if (cmdLine.hasOption(OPTION_P_NAME)) { final String propertiesLocation = cmdLine.getOptionValue(OPTION_P_NAME); final File file = new File(propertiesLocation); if (!file.exists()) { result.add(String.format("Could not find file '%s'.", propertiesLocation)); } } } else { result.add("Must specify a config XML file."); } return result; } /** * Do execution of CheckStyle based on Command line options. * @param commandLine command line object * @param filesToProcess List of files to process found from the command line. * @return number of violations * @throws IOException if a file could not be read. * @throws CheckstyleException if something happens processing the files. */ private static int runCli(CommandLine commandLine, List<File> filesToProcess) throws IOException, CheckstyleException { int result = 0; // create config helper object final CliOptions config = convertCliToPojo(commandLine, filesToProcess); if (commandLine.hasOption(OPTION_T_NAME)) { // print AST final File file = config.files.get(0); final String stringAst = AstTreeStringPrinter.printFileAst(file, false); System.out.print(stringAst); } else if (commandLine.hasOption(OPTION_CAPITAL_T_NAME)) { final File file = config.files.get(0); final String stringAst = AstTreeStringPrinter.printFileAst(file, true); System.out.print(stringAst); } else if (commandLine.hasOption(OPTION_J_NAME)) { final File file = config.files.get(0); final String stringAst = DetailNodeTreeStringPrinter.printFileAst(file); System.out.print(stringAst); } else if (commandLine.hasOption(OPTION_CAPITAL_J_NAME)) { final File file = config.files.get(0); final String stringAst = AstTreeStringPrinter.printJavaAndJavadocTree(file); System.out.print(stringAst); } else { if (commandLine.hasOption(OPTION_D_NAME)) { final Logger parentLogger = Logger.getLogger(Main.class.getName()).getParent(); final ConsoleHandler handler = new ConsoleHandler(); handler.setLevel(Level.FINEST); handler.setFilter(new Filter() { private final String packageName = Main.class.getPackage().getName(); @Override public boolean isLoggable(LogRecord record) { return record.getLoggerName().startsWith(packageName); } }); parentLogger.addHandler(handler); parentLogger.setLevel(Level.FINEST); } if (LOG.isDebugEnabled()) { LOG.debug("Checkstyle debug logging enabled"); LOG.debug("Running Checkstyle with version: " + Main.class.getPackage().getImplementationVersion()); } // run Checker result = runCheckstyle(config); } return result; } /** * Util method to convert CommandLine type to POJO object. * @param cmdLine command line object * @param filesToProcess List of files to process found from the command line. * @return command line option as POJO object */ private static CliOptions convertCliToPojo(CommandLine cmdLine, List<File> filesToProcess) { final CliOptions conf = new CliOptions(); conf.format = cmdLine.getOptionValue(OPTION_F_NAME); if (conf.format == null) { conf.format = PLAIN_FORMAT_NAME; } conf.outputLocation = cmdLine.getOptionValue(OPTION_O_NAME); conf.configLocation = cmdLine.getOptionValue(OPTION_C_NAME); conf.propertiesLocation = cmdLine.getOptionValue(OPTION_P_NAME); conf.files = filesToProcess; return conf; } /** * Executes required Checkstyle actions based on passed parameters. * @param cliOptions * pojo object that contains all options * @return number of violations of ERROR level * @throws FileNotFoundException * when output file could not be found * @throws CheckstyleException * when properties file could not be loaded */ private static int runCheckstyle(CliOptions cliOptions) throws CheckstyleException, FileNotFoundException { // setup the properties final Properties props; if (cliOptions.propertiesLocation == null) { props = System.getProperties(); } else { props = loadProperties(new File(cliOptions.propertiesLocation)); } // create a configuration final Configuration config = ConfigurationLoader.loadConfiguration( cliOptions.configLocation, new PropertiesExpander(props)); // create a listener for output final AuditListener listener = createListener(cliOptions.format, cliOptions.outputLocation); // create RootModule object and run it final int errorCounter; final ClassLoader moduleClassLoader = Checker.class.getClassLoader(); final RootModule rootModule = getRootModule(config.getName(), moduleClassLoader); try { rootModule.setModuleClassLoader(moduleClassLoader); rootModule.configure(config); rootModule.addListener(listener); // run RootModule errorCounter = rootModule.process(cliOptions.files); } finally { rootModule.destroy(); } return errorCounter; } /** * Creates a new instance of the root module that will control and run * Checkstyle. * @param name The name of the module. This will either be a short name that * will have to be found or the complete package name. * @param moduleClassLoader Class loader used to load the root module. * @return The new instance of the root module. * @throws CheckstyleException if no module can be instantiated from name */ private static RootModule getRootModule(String name, ClassLoader moduleClassLoader) throws CheckstyleException { final ModuleFactory factory = new PackageObjectFactory( Checker.class.getPackage().getName() + ".", moduleClassLoader); return (RootModule) factory.createModule(name); } /** * Loads properties from a File. * @param file * the properties file * @return the properties in file * @throws CheckstyleException * when could not load properties file */ private static Properties loadProperties(File file) throws CheckstyleException { final Properties properties = new Properties(); FileInputStream fis = null; try { fis = new FileInputStream(file); properties.load(fis); } catch (final IOException ex) { throw new CheckstyleException(String.format( "Unable to load properties from file '%s'.", file.getAbsolutePath()), ex); } finally { Closeables.closeQuietly(fis); } return properties; } /** * Creates the audit listener. * * @param format format of the audit listener * @param outputLocation the location of output * @return a fresh new {@code AuditListener} * @exception FileNotFoundException when provided output location is not found */ private static AuditListener createListener(String format, String outputLocation) throws FileNotFoundException { // setup the output stream final OutputStream out; final boolean closeOutputStream; if (outputLocation == null) { out = System.out; closeOutputStream = false; } else { out = new FileOutputStream(outputLocation); closeOutputStream = true; } // setup a listener final AuditListener listener; if (XML_FORMAT_NAME.equals(format)) { listener = new XMLLogger(out, closeOutputStream); } else if (PLAIN_FORMAT_NAME.equals(format)) { listener = new DefaultLogger(out, closeOutputStream, out, false); } else { if (closeOutputStream) { CommonUtils.close(out); } throw new IllegalStateException(String.format( "Invalid output format. Found '%s' but expected '%s' or '%s'.", format, PLAIN_FORMAT_NAME, XML_FORMAT_NAME)); } return listener; } /** * Determines the files to process. * @param patternsToExclude The list of directory patterns to exclude from searching. * @param filesToProcess * arguments that were not processed yet but shall be * @return list of files to process */ private static List<File> getFilesToProcess(List<Pattern> patternsToExclude, String... filesToProcess) { final List<File> files = new LinkedList<>(); for (String element : filesToProcess) { files.addAll(listFiles(new File(element), patternsToExclude)); } return files; } /** * Traverses a specified node looking for files to check. Found files are added to a specified * list. Subdirectories are also traversed. * @param node * the node to process * @param patternsToExclude The list of directory patterns to exclude from searching. * @return found files */ private static List<File> listFiles(File node, List<Pattern> patternsToExclude) { // could be replaced with org.apache.commons.io.FileUtils.list() method // if only we add commons-io library final List<File> result = new LinkedList<>(); if (node.canRead()) { if (node.isDirectory()) { if (!isDirectoryExcluded(node.getAbsolutePath(), patternsToExclude)) { final File[] files = node.listFiles(); // listFiles() can return null, so we need to check it if (files != null) { for (File element : files) { result.addAll(listFiles(element, patternsToExclude)); } } } } else if (node.isFile()) { result.add(node); } } return result; } /** * Checks if a directory {@code path} should be excluded based on if it matches one of the * patterns supplied. * @param path The path of the directory to check * @param patternsToExclude The list of directory patterns to exclude from searching. * @return True if the directory matches one of the patterns. */ private static boolean isDirectoryExcluded(String path, List<Pattern> patternsToExclude) { boolean result = false; for (Pattern pattern : patternsToExclude) { if (pattern.matcher(path).find()) { result = true; break; } } return result; } /** Prints the usage information. **/ private static void printUsage() { final HelpFormatter formatter = new HelpFormatter(); formatter.setWidth(HELP_WIDTH); formatter.printHelp(String.format("java %s [options] -c <config.xml> file...", Main.class.getName()), buildOptions()); } /** * Builds and returns list of parameters supported by cli Checkstyle. * @return available options */ private static Options buildOptions() { final Options options = new Options(); options.addOption(OPTION_C_NAME, true, "Sets the check configuration file to use."); options.addOption(OPTION_O_NAME, true, "Sets the output file. Defaults to stdout"); options.addOption(OPTION_P_NAME, true, "Loads the properties file"); options.addOption(OPTION_F_NAME, true, String.format( "Sets the output format. (%s|%s). Defaults to %s", PLAIN_FORMAT_NAME, XML_FORMAT_NAME, PLAIN_FORMAT_NAME)); options.addOption(OPTION_V_NAME, false, "Print product version and exit"); options.addOption(OPTION_T_NAME, OPTION_TREE_NAME, false, "Print Abstract Syntax Tree(AST) of the file"); options.addOption(OPTION_CAPITAL_T_NAME, OPTION_TREE_COMMENT_NAME, false, "Print Abstract Syntax Tree(AST) of the file including comments"); options.addOption(OPTION_J_NAME, OPTION_JAVADOC_TREE_NAME, false, "Print Parse tree of the Javadoc comment"); options.addOption(OPTION_CAPITAL_J_NAME, OPTION_TREE_JAVADOC_NAME, false, "Print full Abstract Syntax Tree of the file"); options.addOption(OPTION_D_NAME, OPTION_DEBUG_NAME, false, "Print all debug logging of CheckStyle utility"); options.addOption(OPTION_E_NAME, OPTION_EXCLUDE_NAME, true, "Directory path to exclude from CheckStyle"); options.addOption(OPTION_X_NAME, OPTION_EXCLUDE_REGEXP_NAME, true, "Regular expression of directory to exclude from CheckStyle"); return options; } /** Helper structure to clear show what is required for Checker to run. **/ private static class CliOptions { /** Properties file location. */ private String propertiesLocation; /** Config file location. */ private String configLocation; /** Output format. */ private String format; /** Output file location. */ private String outputLocation; /** List of file to validate. */ private List<File> files; } }