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