001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2016 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.List; 030import java.util.Properties; 031import java.util.logging.ConsoleHandler; 032import java.util.logging.Filter; 033import java.util.logging.Level; 034import java.util.logging.LogRecord; 035import java.util.logging.Logger; 036import java.util.regex.Pattern; 037 038import org.apache.commons.cli.CommandLine; 039import org.apache.commons.cli.CommandLineParser; 040import org.apache.commons.cli.DefaultParser; 041import org.apache.commons.cli.HelpFormatter; 042import org.apache.commons.cli.Options; 043import org.apache.commons.cli.ParseException; 044import org.apache.commons.logging.Log; 045import org.apache.commons.logging.LogFactory; 046 047import com.google.common.collect.Lists; 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.utils.CommonUtils; 053 054/** 055 * Wrapper command line program for the Checker. 056 * @author the original author or authors. 057 * 058 **/ 059public final class Main { 060 /** Logger for Main. */ 061 private static final Log LOG = LogFactory.getLog(Main.class); 062 063 /** Width of CLI help option. */ 064 private static final int HELP_WIDTH = 100; 065 066 /** Exit code returned when execution finishes with {@link CheckstyleException}. */ 067 private static final int EXIT_WITH_CHECKSTYLE_EXCEPTION_CODE = -2; 068 069 /** Name for the option 'v'. */ 070 private static final String OPTION_V_NAME = "v"; 071 072 /** Name for the option 'c'. */ 073 private static final String OPTION_C_NAME = "c"; 074 075 /** Name for the option 'f'. */ 076 private static final String OPTION_F_NAME = "f"; 077 078 /** Name for the option 'p'. */ 079 private static final String OPTION_P_NAME = "p"; 080 081 /** Name for the option 'o'. */ 082 private static final String OPTION_O_NAME = "o"; 083 084 /** Name for the option 't'. */ 085 private static final String OPTION_T_NAME = "t"; 086 087 /** Name for the option '--tree'. */ 088 private static final String OPTION_TREE_NAME = "tree"; 089 090 /** Name for the option '-T'. */ 091 private static final String OPTION_CAPITAL_T_NAME = "T"; 092 093 /** Name for the option '--treeWithComments'. */ 094 private static final String OPTION_TREE_COMMENT_NAME = "treeWithComments"; 095 096 /** Name for the option '-j'. */ 097 private static final String OPTION_J_NAME = "j"; 098 099 /** NAme for the option '--javadocTree'. */ 100 private static final String OPTION_JAVADOC_TREE_NAME = "javadocTree"; 101 102 /** Name for the option '-J'. */ 103 private static final String OPTION_CAPITAL_J_NAME = "J"; 104 105 /** Name for the option '--treeWithJavadoc'. */ 106 private static final String OPTION_TREE_JAVADOC_NAME = "treeWithJavadoc"; 107 108 /** Name for the option '-d'. */ 109 private static final String OPTION_D_NAME = "d"; 110 111 /** Name for the option '--debug'. */ 112 private static final String OPTION_DEBUG_NAME = "debug"; 113 114 /** Name for the option 'e'. */ 115 private static final String OPTION_E_NAME = "e"; 116 117 /** Name for the option '--exclude'. */ 118 private static final String OPTION_EXCLUDE_NAME = "exclude"; 119 120 /** Name for the option 'x'. */ 121 private static final String OPTION_X_NAME = "x"; 122 123 /** Name for the option '--exclude-regexp'. */ 124 private static final String OPTION_EXCLUDE_REGEXP_NAME = "exclude-regexp"; 125 126 /** Name for 'xml' format. */ 127 private static final String XML_FORMAT_NAME = "xml"; 128 129 /** Name for 'plain' format. */ 130 private static final String PLAIN_FORMAT_NAME = "plain"; 131 132 /** Don't create instance of this class, use {@link #main(String[])} method instead. */ 133 private Main() { 134 } 135 136 /** 137 * Loops over the files specified checking them for errors. The exit code 138 * is the number of errors found in all the files. 139 * @param args the command line arguments. 140 * @throws IOException if there is a problem with files access 141 * @noinspection CallToPrintStackTrace 142 **/ 143 public static void main(String... args) throws IOException { 144 int errorCounter = 0; 145 boolean cliViolations = false; 146 // provide proper exit code based on results. 147 final int exitWithCliViolation = -1; 148 int exitStatus = 0; 149 150 try { 151 //parse CLI arguments 152 final CommandLine commandLine = parseCli(args); 153 154 // show version and exit if it is requested 155 if (commandLine.hasOption(OPTION_V_NAME)) { 156 System.out.println("Checkstyle version: " 157 + Main.class.getPackage().getImplementationVersion()); 158 exitStatus = 0; 159 } 160 else { 161 final List<File> filesToProcess = getFilesToProcess(getExclusions(commandLine), 162 commandLine.getArgs()); 163 164 // return error if something is wrong in arguments 165 final List<String> messages = validateCli(commandLine, filesToProcess); 166 cliViolations = !messages.isEmpty(); 167 if (cliViolations) { 168 exitStatus = exitWithCliViolation; 169 errorCounter = 1; 170 for (String message : messages) { 171 System.out.println(message); 172 } 173 } 174 else { 175 errorCounter = runCli(commandLine, filesToProcess); 176 exitStatus = errorCounter; 177 } 178 } 179 } 180 catch (ParseException pex) { 181 // something wrong with arguments - print error and manual 182 cliViolations = true; 183 exitStatus = exitWithCliViolation; 184 errorCounter = 1; 185 System.out.println(pex.getMessage()); 186 printUsage(); 187 } 188 catch (CheckstyleException ex) { 189 exitStatus = EXIT_WITH_CHECKSTYLE_EXCEPTION_CODE; 190 errorCounter = 1; 191 ex.printStackTrace(); 192 } 193 finally { 194 // return exit code base on validation of Checker 195 if (errorCounter != 0 && !cliViolations) { 196 System.out.println(String.format("Checkstyle ends with %d errors.", errorCounter)); 197 } 198 if (exitStatus != 0) { 199 System.exit(exitStatus); 200 } 201 } 202 } 203 204 /** 205 * Parses and executes Checkstyle based on passed arguments. 206 * @param args 207 * command line parameters 208 * @return parsed information about passed parameters 209 * @throws ParseException 210 * when passed arguments are not valid 211 */ 212 private static CommandLine parseCli(String... args) 213 throws ParseException { 214 // parse the parameters 215 final CommandLineParser clp = new DefaultParser(); 216 // always returns not null value 217 return clp.parse(buildOptions(), args); 218 } 219 220 /** 221 * Gets the list of exclusions provided through the command line argument. 222 * @param commandLine command line object 223 * @return List of exclusion patterns. 224 */ 225 private static List<Pattern> getExclusions(CommandLine commandLine) { 226 final List<Pattern> result = new ArrayList<>(); 227 228 if (commandLine.hasOption(OPTION_E_NAME)) { 229 for (String value : commandLine.getOptionValues(OPTION_E_NAME)) { 230 result.add(Pattern.compile("^" + Pattern.quote(new File(value).getAbsolutePath()) 231 + "$")); 232 } 233 } 234 if (commandLine.hasOption(OPTION_X_NAME)) { 235 for (String value : commandLine.getOptionValues(OPTION_X_NAME)) { 236 result.add(Pattern.compile(value)); 237 } 238 } 239 240 return result; 241 } 242 243 /** 244 * Do validation of Command line options. 245 * @param cmdLine command line object 246 * @param filesToProcess List of files to process found from the command line. 247 * @return list of violations 248 */ 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 Checker object and run it 414 int errorCounter = 0; 415 final Checker checker = new Checker(); 416 417 try { 418 419 final ClassLoader moduleClassLoader = Checker.class.getClassLoader(); 420 checker.setModuleClassLoader(moduleClassLoader); 421 checker.configure(config); 422 checker.addListener(listener); 423 424 // run Checker 425 errorCounter = checker.process(cliOptions.files); 426 427 } 428 finally { 429 checker.destroy(); 430 } 431 432 return errorCounter; 433 } 434 435 /** 436 * Loads properties from a File. 437 * @param file 438 * the properties file 439 * @return the properties in file 440 * @throws CheckstyleException 441 * when could not load properties file 442 */ 443 private static Properties loadProperties(File file) 444 throws CheckstyleException { 445 final Properties properties = new Properties(); 446 447 FileInputStream fis = null; 448 try { 449 fis = new FileInputStream(file); 450 properties.load(fis); 451 } 452 catch (final IOException ex) { 453 throw new CheckstyleException(String.format( 454 "Unable to load properties from file '%s'.", file.getAbsolutePath()), ex); 455 } 456 finally { 457 Closeables.closeQuietly(fis); 458 } 459 460 return properties; 461 } 462 463 /** 464 * Creates the audit listener. 465 * 466 * @param format format of the audit listener 467 * @param outputLocation the location of output 468 * @return a fresh new {@code AuditListener} 469 * @exception FileNotFoundException when provided output location is not found 470 */ 471 private static AuditListener createListener(String format, 472 String outputLocation) 473 throws FileNotFoundException { 474 475 // setup the output stream 476 final OutputStream out; 477 final boolean closeOutputStream; 478 if (outputLocation == null) { 479 out = System.out; 480 closeOutputStream = false; 481 } 482 else { 483 out = new FileOutputStream(outputLocation); 484 closeOutputStream = true; 485 } 486 487 // setup a listener 488 final AuditListener listener; 489 if (XML_FORMAT_NAME.equals(format)) { 490 listener = new XMLLogger(out, closeOutputStream); 491 492 } 493 else if (PLAIN_FORMAT_NAME.equals(format)) { 494 listener = new DefaultLogger(out, closeOutputStream, out, false); 495 496 } 497 else { 498 if (closeOutputStream) { 499 CommonUtils.close(out); 500 } 501 throw new IllegalStateException(String.format( 502 "Invalid output format. Found '%s' but expected '%s' or '%s'.", 503 format, PLAIN_FORMAT_NAME, XML_FORMAT_NAME)); 504 } 505 506 return listener; 507 } 508 509 /** 510 * Determines the files to process. 511 * @param patternsToExclude The list of directory patterns to exclude from searching. 512 * @param filesToProcess 513 * arguments that were not processed yet but shall be 514 * @return list of files to process 515 */ 516 private static List<File> getFilesToProcess(List<Pattern> patternsToExclude, 517 String... filesToProcess) { 518 final List<File> files = Lists.newLinkedList(); 519 for (String element : filesToProcess) { 520 files.addAll(listFiles(new File(element), patternsToExclude)); 521 } 522 523 return files; 524 } 525 526 /** 527 * Traverses a specified node looking for files to check. Found files are added to a specified 528 * list. Subdirectories are also traversed. 529 * @param node 530 * the node to process 531 * @param patternsToExclude The list of directory patterns to exclude from searching. 532 * @return found files 533 */ 534 private static List<File> listFiles(File node, List<Pattern> patternsToExclude) { 535 // could be replaced with org.apache.commons.io.FileUtils.list() method 536 // if only we add commons-io library 537 final List<File> result = Lists.newLinkedList(); 538 539 if (node.canRead()) { 540 if (node.isDirectory()) { 541 if (!isDirectoryExcluded(node.getAbsolutePath(), patternsToExclude)) { 542 final File[] files = node.listFiles(); 543 // listFiles() can return null, so we need to check it 544 if (files != null) { 545 for (File element : files) { 546 result.addAll(listFiles(element, patternsToExclude)); 547 } 548 } 549 } 550 } 551 else if (node.isFile()) { 552 result.add(node); 553 } 554 } 555 return result; 556 } 557 558 /** 559 * Checks if a directory {@code path} should be excluded based on if it matches one of the 560 * patterns supplied. 561 * @param path The path of the directory to check 562 * @param patternsToExclude The list of directory patterns to exclude from searching. 563 * @return True if the directory matches one of the patterns. 564 */ 565 private static boolean isDirectoryExcluded(String path, List<Pattern> patternsToExclude) { 566 boolean result = false; 567 568 for (Pattern pattern : patternsToExclude) { 569 if (pattern.matcher(path).find()) { 570 result = true; 571 break; 572 } 573 } 574 575 return result; 576 } 577 578 /** Prints the usage information. **/ 579 private static void printUsage() { 580 final HelpFormatter formatter = new HelpFormatter(); 581 formatter.setWidth(HELP_WIDTH); 582 formatter.printHelp(String.format("java %s [options] -c <config.xml> file...", 583 Main.class.getName()), buildOptions()); 584 } 585 586 /** 587 * Builds and returns list of parameters supported by cli Checkstyle. 588 * @return available options 589 */ 590 private static Options buildOptions() { 591 final Options options = new Options(); 592 options.addOption(OPTION_C_NAME, true, "Sets the check configuration file to use."); 593 options.addOption(OPTION_O_NAME, true, "Sets the output file. Defaults to stdout"); 594 options.addOption(OPTION_P_NAME, true, "Loads the properties file"); 595 options.addOption(OPTION_F_NAME, true, String.format( 596 "Sets the output format. (%s|%s). Defaults to %s", 597 PLAIN_FORMAT_NAME, XML_FORMAT_NAME, PLAIN_FORMAT_NAME)); 598 options.addOption(OPTION_V_NAME, false, "Print product version and exit"); 599 options.addOption(OPTION_T_NAME, OPTION_TREE_NAME, false, 600 "Print Abstract Syntax Tree(AST) of the file"); 601 options.addOption(OPTION_CAPITAL_T_NAME, OPTION_TREE_COMMENT_NAME, false, 602 "Print Abstract Syntax Tree(AST) of the file including comments"); 603 options.addOption(OPTION_J_NAME, OPTION_JAVADOC_TREE_NAME, false, 604 "Print Parse tree of the Javadoc comment"); 605 options.addOption(OPTION_CAPITAL_J_NAME, OPTION_TREE_JAVADOC_NAME, false, 606 "Print full Abstract Syntax Tree of the file"); 607 options.addOption(OPTION_D_NAME, OPTION_DEBUG_NAME, false, 608 "Print all debug logging of CheckStyle utility"); 609 options.addOption(OPTION_E_NAME, OPTION_EXCLUDE_NAME, true, 610 "Directory path to exclude from CheckStyle"); 611 options.addOption(OPTION_X_NAME, OPTION_EXCLUDE_REGEXP_NAME, true, 612 "Regular expression of directory to exclude from CheckStyle"); 613 return options; 614 } 615 616 /** Helper structure to clear show what is required for Checker to run. **/ 617 private static class CliOptions { 618 /** Properties file location. */ 619 private String propertiesLocation; 620 /** Config file location. */ 621 private String configLocation; 622 /** Output format. */ 623 private String format; 624 /** Output file location. */ 625 private String outputLocation; 626 /** List of file to validate. */ 627 private List<File> files; 628 } 629}