001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2017 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.FileInputStream; 024import java.io.FileNotFoundException; 025import java.io.FileOutputStream; 026import java.io.IOException; 027import java.io.OutputStream; 028import java.util.ArrayList; 029import java.util.LinkedList; 030import java.util.List; 031import java.util.Properties; 032import java.util.logging.ConsoleHandler; 033import java.util.logging.Filter; 034import java.util.logging.Level; 035import java.util.logging.LogRecord; 036import java.util.logging.Logger; 037import java.util.regex.Pattern; 038 039import org.apache.commons.cli.CommandLine; 040import org.apache.commons.cli.CommandLineParser; 041import org.apache.commons.cli.DefaultParser; 042import org.apache.commons.cli.HelpFormatter; 043import org.apache.commons.cli.Options; 044import org.apache.commons.cli.ParseException; 045import org.apache.commons.logging.Log; 046import org.apache.commons.logging.LogFactory; 047 048import com.google.common.io.Closeables; 049import com.puppycrawl.tools.checkstyle.api.AuditListener; 050import com.puppycrawl.tools.checkstyle.api.CheckstyleException; 051import com.puppycrawl.tools.checkstyle.api.Configuration; 052import com.puppycrawl.tools.checkstyle.api.RootModule; 053import com.puppycrawl.tools.checkstyle.utils.CommonUtils; 054 055/** 056 * Wrapper command line program for the Checker. 057 * @author the original author or authors. 058 * 059 **/ 060public final class Main { 061 /** Logger for Main. */ 062 private static final Log LOG = LogFactory.getLog(Main.class); 063 064 /** Width of CLI help option. */ 065 private static final int HELP_WIDTH = 100; 066 067 /** Exit code returned when execution finishes with {@link CheckstyleException}. */ 068 private static final int EXIT_WITH_CHECKSTYLE_EXCEPTION_CODE = -2; 069 070 /** Name for the option 'v'. */ 071 private static final String OPTION_V_NAME = "v"; 072 073 /** Name for the option 'c'. */ 074 private static final String OPTION_C_NAME = "c"; 075 076 /** Name for the option 'f'. */ 077 private static final String OPTION_F_NAME = "f"; 078 079 /** Name for the option 'p'. */ 080 private static final String OPTION_P_NAME = "p"; 081 082 /** Name for the option 'o'. */ 083 private static final String OPTION_O_NAME = "o"; 084 085 /** Name for the option 't'. */ 086 private static final String OPTION_T_NAME = "t"; 087 088 /** Name for the option '--tree'. */ 089 private static final String OPTION_TREE_NAME = "tree"; 090 091 /** Name for the option '-T'. */ 092 private static final String OPTION_CAPITAL_T_NAME = "T"; 093 094 /** Name for the option '--treeWithComments'. */ 095 private static final String OPTION_TREE_COMMENT_NAME = "treeWithComments"; 096 097 /** Name for the option '-j'. */ 098 private static final String OPTION_J_NAME = "j"; 099 100 /** Name for the option '--javadocTree'. */ 101 private static final String OPTION_JAVADOC_TREE_NAME = "javadocTree"; 102 103 /** Name for the option '-J'. */ 104 private static final String OPTION_CAPITAL_J_NAME = "J"; 105 106 /** Name for the option '--treeWithJavadoc'. */ 107 private static final String OPTION_TREE_JAVADOC_NAME = "treeWithJavadoc"; 108 109 /** Name for the option '-d'. */ 110 private static final String OPTION_D_NAME = "d"; 111 112 /** Name for the option '--debug'. */ 113 private static final String OPTION_DEBUG_NAME = "debug"; 114 115 /** Name for the option 'e'. */ 116 private static final String OPTION_E_NAME = "e"; 117 118 /** Name for the option '--exclude'. */ 119 private static final String OPTION_EXCLUDE_NAME = "exclude"; 120 121 /** Name for the option '--executeIgnoredModules'. */ 122 private static final String OPTION_EXECUTE_IGNORED_MODULES_NAME = "executeIgnoredModules"; 123 124 /** Name for the option 'x'. */ 125 private static final String OPTION_X_NAME = "x"; 126 127 /** Name for the option '--exclude-regexp'. */ 128 private static final String OPTION_EXCLUDE_REGEXP_NAME = "exclude-regexp"; 129 130 /** Name for 'xml' format. */ 131 private static final String XML_FORMAT_NAME = "xml"; 132 133 /** Name for 'plain' format. */ 134 private static final String PLAIN_FORMAT_NAME = "plain"; 135 136 /** Don't create instance of this class, use {@link #main(String[])} method instead. */ 137 private Main() { 138 } 139 140 /** 141 * Loops over the files specified checking them for errors. The exit code 142 * is the number of errors found in all the files. 143 * @param args the command line arguments. 144 * @throws IOException if there is a problem with files access 145 * @noinspection CallToPrintStackTrace 146 **/ 147 public static void main(String... args) throws IOException { 148 int errorCounter = 0; 149 boolean cliViolations = false; 150 // provide proper exit code based on results. 151 final int exitWithCliViolation = -1; 152 int exitStatus = 0; 153 154 try { 155 //parse CLI arguments 156 final CommandLine commandLine = parseCli(args); 157 158 // show version and exit if it is requested 159 if (commandLine.hasOption(OPTION_V_NAME)) { 160 System.out.println("Checkstyle version: " 161 + Main.class.getPackage().getImplementationVersion()); 162 exitStatus = 0; 163 } 164 else { 165 final List<File> filesToProcess = getFilesToProcess(getExclusions(commandLine), 166 commandLine.getArgs()); 167 168 // return error if something is wrong in arguments 169 final List<String> messages = validateCli(commandLine, filesToProcess); 170 cliViolations = !messages.isEmpty(); 171 if (cliViolations) { 172 exitStatus = exitWithCliViolation; 173 errorCounter = 1; 174 messages.forEach(System.out::println); 175 } 176 else { 177 errorCounter = runCli(commandLine, filesToProcess); 178 exitStatus = errorCounter; 179 } 180 } 181 } 182 catch (ParseException pex) { 183 // something wrong with arguments - print error and manual 184 cliViolations = true; 185 exitStatus = exitWithCliViolation; 186 errorCounter = 1; 187 System.out.println(pex.getMessage()); 188 printUsage(); 189 } 190 catch (CheckstyleException ex) { 191 exitStatus = EXIT_WITH_CHECKSTYLE_EXCEPTION_CODE; 192 errorCounter = 1; 193 ex.printStackTrace(); 194 } 195 finally { 196 // return exit code base on validation of Checker 197 if (errorCounter != 0 && !cliViolations) { 198 System.out.println(String.format("Checkstyle ends with %d errors.", errorCounter)); 199 } 200 if (exitStatus != 0) { 201 System.exit(exitStatus); 202 } 203 } 204 } 205 206 /** 207 * Parses and executes Checkstyle based on passed arguments. 208 * @param args 209 * command line parameters 210 * @return parsed information about passed parameters 211 * @throws ParseException 212 * when passed arguments are not valid 213 */ 214 private static CommandLine parseCli(String... args) 215 throws ParseException { 216 // parse the parameters 217 final CommandLineParser clp = new DefaultParser(); 218 // always returns not null value 219 return clp.parse(buildOptions(), args); 220 } 221 222 /** 223 * Gets the list of exclusions provided through the command line argument. 224 * @param commandLine command line object 225 * @return List of exclusion patterns. 226 */ 227 private static List<Pattern> getExclusions(CommandLine commandLine) { 228 final List<Pattern> result = new ArrayList<>(); 229 230 if (commandLine.hasOption(OPTION_E_NAME)) { 231 for (String value : commandLine.getOptionValues(OPTION_E_NAME)) { 232 result.add(Pattern.compile("^" + Pattern.quote(new File(value).getAbsolutePath()) 233 + "$")); 234 } 235 } 236 if (commandLine.hasOption(OPTION_X_NAME)) { 237 for (String value : commandLine.getOptionValues(OPTION_X_NAME)) { 238 result.add(Pattern.compile(value)); 239 } 240 } 241 242 return result; 243 } 244 245 /** 246 * Do validation of Command line options. 247 * @param cmdLine command line object 248 * @param filesToProcess List of files to process found from the command line. 249 * @return list of violations 250 */ 251 // -@cs[CyclomaticComplexity] Breaking apart will damage encapsulation 252 private static List<String> validateCli(CommandLine cmdLine, List<File> filesToProcess) { 253 final List<String> result = new ArrayList<>(); 254 255 if (filesToProcess.isEmpty()) { 256 result.add("Files to process must be specified, found 0."); 257 } 258 // ensure there is no conflicting options 259 else if (cmdLine.hasOption(OPTION_T_NAME) || cmdLine.hasOption(OPTION_CAPITAL_T_NAME) 260 || cmdLine.hasOption(OPTION_J_NAME) || cmdLine.hasOption(OPTION_CAPITAL_J_NAME)) { 261 if (cmdLine.hasOption(OPTION_C_NAME) || cmdLine.hasOption(OPTION_P_NAME) 262 || cmdLine.hasOption(OPTION_F_NAME) || cmdLine.hasOption(OPTION_O_NAME)) { 263 result.add("Option '-t' cannot be used with other options."); 264 } 265 else if (filesToProcess.size() > 1) { 266 result.add("Printing AST is allowed for only one file."); 267 } 268 } 269 // ensure a configuration file is specified 270 else if (cmdLine.hasOption(OPTION_C_NAME)) { 271 final String configLocation = cmdLine.getOptionValue(OPTION_C_NAME); 272 try { 273 // test location only 274 CommonUtils.getUriByFilename(configLocation); 275 } 276 catch (CheckstyleException ignored) { 277 result.add(String.format("Could not find config XML file '%s'.", configLocation)); 278 } 279 280 // validate optional parameters 281 if (cmdLine.hasOption(OPTION_F_NAME)) { 282 final String format = cmdLine.getOptionValue(OPTION_F_NAME); 283 if (!PLAIN_FORMAT_NAME.equals(format) && !XML_FORMAT_NAME.equals(format)) { 284 result.add(String.format("Invalid output format." 285 + " Found '%s' but expected '%s' or '%s'.", 286 format, PLAIN_FORMAT_NAME, XML_FORMAT_NAME)); 287 } 288 } 289 if (cmdLine.hasOption(OPTION_P_NAME)) { 290 final String propertiesLocation = cmdLine.getOptionValue(OPTION_P_NAME); 291 final File file = new File(propertiesLocation); 292 if (!file.exists()) { 293 result.add(String.format("Could not find file '%s'.", propertiesLocation)); 294 } 295 } 296 } 297 else { 298 result.add("Must specify a config XML file."); 299 } 300 301 return result; 302 } 303 304 /** 305 * Do execution of CheckStyle based on Command line options. 306 * @param commandLine command line object 307 * @param filesToProcess List of files to process found from the command line. 308 * @return number of violations 309 * @throws IOException if a file could not be read. 310 * @throws CheckstyleException if something happens processing the files. 311 */ 312 private static int runCli(CommandLine commandLine, List<File> filesToProcess) 313 throws IOException, CheckstyleException { 314 int result = 0; 315 316 // create config helper object 317 final CliOptions config = convertCliToPojo(commandLine, filesToProcess); 318 if (commandLine.hasOption(OPTION_T_NAME)) { 319 // print AST 320 final File file = config.files.get(0); 321 final String stringAst = AstTreeStringPrinter.printFileAst(file, false); 322 System.out.print(stringAst); 323 } 324 else if (commandLine.hasOption(OPTION_CAPITAL_T_NAME)) { 325 final File file = config.files.get(0); 326 final String stringAst = AstTreeStringPrinter.printFileAst(file, true); 327 System.out.print(stringAst); 328 } 329 else if (commandLine.hasOption(OPTION_J_NAME)) { 330 final File file = config.files.get(0); 331 final String stringAst = DetailNodeTreeStringPrinter.printFileAst(file); 332 System.out.print(stringAst); 333 } 334 else if (commandLine.hasOption(OPTION_CAPITAL_J_NAME)) { 335 final File file = config.files.get(0); 336 final String stringAst = AstTreeStringPrinter.printJavaAndJavadocTree(file); 337 System.out.print(stringAst); 338 } 339 else { 340 if (commandLine.hasOption(OPTION_D_NAME)) { 341 final Logger parentLogger = Logger.getLogger(Main.class.getName()).getParent(); 342 final ConsoleHandler handler = new ConsoleHandler(); 343 handler.setLevel(Level.FINEST); 344 handler.setFilter(new Filter() { 345 private final String packageName = Main.class.getPackage().getName(); 346 347 @Override 348 public boolean isLoggable(LogRecord record) { 349 return record.getLoggerName().startsWith(packageName); 350 } 351 }); 352 parentLogger.addHandler(handler); 353 parentLogger.setLevel(Level.FINEST); 354 } 355 if (LOG.isDebugEnabled()) { 356 LOG.debug("Checkstyle debug logging enabled"); 357 LOG.debug("Running Checkstyle with version: " 358 + Main.class.getPackage().getImplementationVersion()); 359 } 360 361 // run Checker 362 result = runCheckstyle(config); 363 } 364 365 return result; 366 } 367 368 /** 369 * Util method to convert CommandLine type to POJO object. 370 * @param cmdLine command line object 371 * @param filesToProcess List of files to process found from the command line. 372 * @return command line option as POJO object 373 */ 374 private static CliOptions convertCliToPojo(CommandLine cmdLine, List<File> filesToProcess) { 375 final CliOptions conf = new CliOptions(); 376 conf.format = cmdLine.getOptionValue(OPTION_F_NAME); 377 if (conf.format == null) { 378 conf.format = PLAIN_FORMAT_NAME; 379 } 380 conf.outputLocation = cmdLine.getOptionValue(OPTION_O_NAME); 381 conf.configLocation = cmdLine.getOptionValue(OPTION_C_NAME); 382 conf.propertiesLocation = cmdLine.getOptionValue(OPTION_P_NAME); 383 conf.files = filesToProcess; 384 conf.executeIgnoredModules = cmdLine.hasOption(OPTION_EXECUTE_IGNORED_MODULES_NAME); 385 return conf; 386 } 387 388 /** 389 * Executes required Checkstyle actions based on passed parameters. 390 * @param cliOptions 391 * pojo object that contains all options 392 * @return number of violations of ERROR level 393 * @throws FileNotFoundException 394 * when output file could not be found 395 * @throws CheckstyleException 396 * when properties file could not be loaded 397 */ 398 private static int runCheckstyle(CliOptions cliOptions) 399 throws CheckstyleException, FileNotFoundException { 400 // setup the properties 401 final Properties props; 402 403 if (cliOptions.propertiesLocation == null) { 404 props = System.getProperties(); 405 } 406 else { 407 props = loadProperties(new File(cliOptions.propertiesLocation)); 408 } 409 410 // create a configuration 411 final Configuration config = ConfigurationLoader.loadConfiguration( 412 cliOptions.configLocation, new PropertiesExpander(props), 413 !cliOptions.executeIgnoredModules); 414 415 // create a listener for output 416 final AuditListener listener = createListener(cliOptions.format, cliOptions.outputLocation); 417 418 // create RootModule object and run it 419 final int errorCounter; 420 final ClassLoader moduleClassLoader = Checker.class.getClassLoader(); 421 final RootModule rootModule = getRootModule(config.getName(), moduleClassLoader); 422 423 try { 424 425 rootModule.setModuleClassLoader(moduleClassLoader); 426 rootModule.configure(config); 427 rootModule.addListener(listener); 428 429 // run RootModule 430 errorCounter = rootModule.process(cliOptions.files); 431 432 } 433 finally { 434 rootModule.destroy(); 435 } 436 437 return errorCounter; 438 } 439 440 /** 441 * Creates a new instance of the root module that will control and run 442 * Checkstyle. 443 * @param name The name of the module. This will either be a short name that 444 * will have to be found or the complete package name. 445 * @param moduleClassLoader Class loader used to load the root module. 446 * @return The new instance of the root module. 447 * @throws CheckstyleException if no module can be instantiated from name 448 */ 449 private static RootModule getRootModule(String name, ClassLoader moduleClassLoader) 450 throws CheckstyleException { 451 final ModuleFactory factory = new PackageObjectFactory( 452 Checker.class.getPackage().getName(), moduleClassLoader); 453 454 return (RootModule) factory.createModule(name); 455 } 456 457 /** 458 * Loads properties from a File. 459 * @param file 460 * the properties file 461 * @return the properties in file 462 * @throws CheckstyleException 463 * when could not load properties file 464 */ 465 private static Properties loadProperties(File file) 466 throws CheckstyleException { 467 final Properties properties = new Properties(); 468 469 FileInputStream fis = null; 470 try { 471 fis = new FileInputStream(file); 472 properties.load(fis); 473 } 474 catch (final IOException ex) { 475 throw new CheckstyleException(String.format( 476 "Unable to load properties from file '%s'.", file.getAbsolutePath()), ex); 477 } 478 finally { 479 Closeables.closeQuietly(fis); 480 } 481 482 return properties; 483 } 484 485 /** 486 * Creates the audit listener. 487 * 488 * @param format format of the audit listener 489 * @param outputLocation the location of output 490 * @return a fresh new {@code AuditListener} 491 * @exception FileNotFoundException when provided output location is not found 492 */ 493 private static AuditListener createListener(String format, 494 String outputLocation) 495 throws FileNotFoundException { 496 497 // setup the output stream 498 final OutputStream out; 499 final boolean closeOutputStream; 500 if (outputLocation == null) { 501 out = System.out; 502 closeOutputStream = false; 503 } 504 else { 505 out = new FileOutputStream(outputLocation); 506 closeOutputStream = true; 507 } 508 509 // setup a listener 510 final AuditListener listener; 511 if (XML_FORMAT_NAME.equals(format)) { 512 listener = new XMLLogger(out, closeOutputStream); 513 514 } 515 else if (PLAIN_FORMAT_NAME.equals(format)) { 516 listener = new DefaultLogger(out, closeOutputStream, out, false); 517 518 } 519 else { 520 if (closeOutputStream) { 521 CommonUtils.close(out); 522 } 523 throw new IllegalStateException(String.format( 524 "Invalid output format. Found '%s' but expected '%s' or '%s'.", 525 format, PLAIN_FORMAT_NAME, XML_FORMAT_NAME)); 526 } 527 528 return listener; 529 } 530 531 /** 532 * Determines the files to process. 533 * @param patternsToExclude The list of directory patterns to exclude from searching. 534 * @param filesToProcess 535 * arguments that were not processed yet but shall be 536 * @return list of files to process 537 */ 538 private static List<File> getFilesToProcess(List<Pattern> patternsToExclude, 539 String... filesToProcess) { 540 final List<File> files = new LinkedList<>(); 541 for (String element : filesToProcess) { 542 files.addAll(listFiles(new File(element), patternsToExclude)); 543 } 544 545 return files; 546 } 547 548 /** 549 * Traverses a specified node looking for files to check. Found files are added to a specified 550 * list. Subdirectories are also traversed. 551 * @param node 552 * the node to process 553 * @param patternsToExclude The list of directory patterns to exclude from searching. 554 * @return found files 555 */ 556 private static List<File> listFiles(File node, List<Pattern> patternsToExclude) { 557 // could be replaced with org.apache.commons.io.FileUtils.list() method 558 // if only we add commons-io library 559 final List<File> result = new LinkedList<>(); 560 561 if (node.canRead()) { 562 if (node.isDirectory()) { 563 if (!isDirectoryExcluded(node.getAbsolutePath(), patternsToExclude)) { 564 final File[] files = node.listFiles(); 565 // listFiles() can return null, so we need to check it 566 if (files != null) { 567 for (File element : files) { 568 result.addAll(listFiles(element, patternsToExclude)); 569 } 570 } 571 } 572 } 573 else if (node.isFile()) { 574 result.add(node); 575 } 576 } 577 return result; 578 } 579 580 /** 581 * Checks if a directory {@code path} should be excluded based on if it matches one of the 582 * patterns supplied. 583 * @param path The path of the directory to check 584 * @param patternsToExclude The list of directory patterns to exclude from searching. 585 * @return True if the directory matches one of the patterns. 586 */ 587 private static boolean isDirectoryExcluded(String path, List<Pattern> patternsToExclude) { 588 boolean result = false; 589 590 for (Pattern pattern : patternsToExclude) { 591 if (pattern.matcher(path).find()) { 592 result = true; 593 break; 594 } 595 } 596 597 return result; 598 } 599 600 /** Prints the usage information. **/ 601 private static void printUsage() { 602 final HelpFormatter formatter = new HelpFormatter(); 603 formatter.setWidth(HELP_WIDTH); 604 formatter.printHelp(String.format("java %s [options] -c <config.xml> file...", 605 Main.class.getName()), buildOptions()); 606 } 607 608 /** 609 * Builds and returns list of parameters supported by cli Checkstyle. 610 * @return available options 611 */ 612 private static Options buildOptions() { 613 final Options options = new Options(); 614 options.addOption(OPTION_C_NAME, true, "Sets the check configuration file to use."); 615 options.addOption(OPTION_O_NAME, true, "Sets the output file. Defaults to stdout"); 616 options.addOption(OPTION_P_NAME, true, "Loads the properties file"); 617 options.addOption(OPTION_F_NAME, true, String.format( 618 "Sets the output format. (%s|%s). Defaults to %s", 619 PLAIN_FORMAT_NAME, XML_FORMAT_NAME, PLAIN_FORMAT_NAME)); 620 options.addOption(OPTION_V_NAME, false, "Print product version and exit"); 621 options.addOption(OPTION_T_NAME, OPTION_TREE_NAME, false, 622 "Print Abstract Syntax Tree(AST) of the file"); 623 options.addOption(OPTION_CAPITAL_T_NAME, OPTION_TREE_COMMENT_NAME, false, 624 "Print Abstract Syntax Tree(AST) of the file including comments"); 625 options.addOption(OPTION_J_NAME, OPTION_JAVADOC_TREE_NAME, false, 626 "Print Parse tree of the Javadoc comment"); 627 options.addOption(OPTION_CAPITAL_J_NAME, OPTION_TREE_JAVADOC_NAME, false, 628 "Print full Abstract Syntax Tree of the file"); 629 options.addOption(OPTION_D_NAME, OPTION_DEBUG_NAME, false, 630 "Print all debug logging of CheckStyle utility"); 631 options.addOption(OPTION_E_NAME, OPTION_EXCLUDE_NAME, true, 632 "Directory path to exclude from CheckStyle"); 633 options.addOption(OPTION_X_NAME, OPTION_EXCLUDE_REGEXP_NAME, true, 634 "Regular expression of directory to exclude from CheckStyle"); 635 options.addOption(OPTION_EXECUTE_IGNORED_MODULES_NAME, false, 636 "Allows ignored modules to be run."); 637 return options; 638 } 639 640 /** Helper structure to clear show what is required for Checker to run. **/ 641 private static class CliOptions { 642 /** Properties file location. */ 643 private String propertiesLocation; 644 /** Config file location. */ 645 private String configLocation; 646 /** Output format. */ 647 private String format; 648 /** Output file location. */ 649 private String outputLocation; 650 /** List of file to validate. */ 651 private List<File> files; 652 /** Switch whether to execute ignored modules or not. */ 653 private boolean executeIgnoredModules; 654 } 655}