001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2026 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.IOException; 024import java.io.InputStream; 025import java.io.OutputStream; 026import java.nio.file.Files; 027import java.nio.file.Path; 028import java.util.ArrayList; 029import java.util.List; 030import java.util.Locale; 031import java.util.Objects; 032import java.util.Properties; 033import java.util.logging.ConsoleHandler; 034import java.util.logging.Filter; 035import java.util.logging.Level; 036import java.util.logging.LogRecord; 037import java.util.logging.Logger; 038import java.util.regex.Pattern; 039import java.util.stream.Collectors; 040 041import org.apache.commons.logging.Log; 042import org.apache.commons.logging.LogFactory; 043 044import com.puppycrawl.tools.checkstyle.AbstractAutomaticBean.OutputStreamOptions; 045import com.puppycrawl.tools.checkstyle.api.AuditEvent; 046import com.puppycrawl.tools.checkstyle.api.AuditListener; 047import com.puppycrawl.tools.checkstyle.api.CheckstyleException; 048import com.puppycrawl.tools.checkstyle.api.Configuration; 049import com.puppycrawl.tools.checkstyle.api.RootModule; 050import com.puppycrawl.tools.checkstyle.utils.ChainedPropertyUtil; 051import com.puppycrawl.tools.checkstyle.utils.CommonUtil; 052import com.puppycrawl.tools.checkstyle.utils.XpathUtil; 053import picocli.CommandLine; 054import picocli.CommandLine.Command; 055import picocli.CommandLine.Option; 056import picocli.CommandLine.ParameterException; 057import picocli.CommandLine.Parameters; 058import picocli.CommandLine.ParseResult; 059 060/** 061 * Wrapper command line program for the Checker. 062 */ 063public final class Main { 064 065 /** 066 * A key pointing to the error counter message in the "messages.properties" file. 067 */ 068 public static final String ERROR_COUNTER = "Main.errorCounter"; 069 /** 070 * A key pointing to the load properties exception message in the "messages.properties" file. 071 */ 072 public static final String LOAD_PROPERTIES_EXCEPTION = "Main.loadProperties"; 073 /** 074 * A key pointing to the create listener exception message in the "messages.properties" file. 075 */ 076 public static final String CREATE_LISTENER_EXCEPTION = "Main.createListener"; 077 078 /** Logger for Main. */ 079 private static final Log LOG = LogFactory.getLog(Main.class); 080 081 /** Exit code returned when user specified invalid command line arguments. */ 082 private static final int EXIT_WITH_INVALID_USER_INPUT_CODE = -1; 083 084 /** Exit code returned when execution finishes with {@link CheckstyleException}. */ 085 private static final int EXIT_WITH_CHECKSTYLE_EXCEPTION_CODE = -2; 086 087 /** Maximum exit code, so the number of errors can not wrap around to 0. */ 088 private static final int MAX_EXIT_CODE = 255; 089 090 /** 091 * Client code should not create instances of this class, but use 092 * {@link #main(String[])} method instead. 093 */ 094 private Main() { 095 } 096 097 /** 098 * Loops over the files specified checking them for errors. The exit code 099 * is the number of errors found in all the files, but no more than 255, 100 * so it can not wrap around to 0 on a multiple of 256 errors. 101 * 102 * @param args the command line arguments. 103 * @throws IOException if there is a problem with files access 104 * @noinspection UseOfSystemOutOrSystemErr, CallToPrintStackTrace, CallToSystemExit 105 * @noinspectionreason UseOfSystemOutOrSystemErr - driver class for Checkstyle requires 106 * usage of System.out and System.err 107 * @noinspectionreason CallToPrintStackTrace - driver class for Checkstyle must be able to 108 * show all details in case of failure 109 * @noinspectionreason CallToSystemExit - driver class must call exit 110 */ 111 public static void main(String... args) throws IOException { 112 113 final CliOptions cliOptions = new CliOptions(); 114 final CommandLine commandLine = new CommandLine(cliOptions); 115 commandLine.setUsageHelpWidth(CliOptions.HELP_WIDTH); 116 commandLine.setCaseInsensitiveEnumValuesAllowed(true); 117 118 // provide proper exit code based on results. 119 int exitStatus = 0; 120 int errorCounter = 0; 121 try { 122 final ParseResult parseResult = commandLine.parseArgs(args); 123 if (parseResult.isVersionHelpRequested()) { 124 printVersionToSystemOutput(); 125 } 126 else if (parseResult.isUsageHelpRequested()) { 127 commandLine.usage(System.out); 128 } 129 else { 130 exitStatus = execute(parseResult, cliOptions); 131 errorCounter = exitStatus; 132 } 133 } 134 catch (ParameterException exc) { 135 exitStatus = EXIT_WITH_INVALID_USER_INPUT_CODE; 136 System.err.println(exc.getMessage()); 137 System.err.println("Usage: checkstyle [OPTIONS]... file(s) or folder(s) ..."); 138 System.err.println("Try 'checkstyle --help' for more information."); 139 } 140 catch (CheckstyleException exc) { 141 exitStatus = EXIT_WITH_CHECKSTYLE_EXCEPTION_CODE; 142 errorCounter = 1; 143 exc.printStackTrace(); 144 } 145 finally { 146 // return exit code base on validation of Checker 147 if (errorCounter > 0) { 148 final LocalizedMessage errorCounterViolation = new LocalizedMessage( 149 Definitions.CHECKSTYLE_BUNDLE, Main.class, 150 ERROR_COUNTER, String.valueOf(errorCounter)); 151 // print error count statistic to error output stream, 152 // output stream might be used by validation report content 153 System.err.println(errorCounterViolation.getMessage()); 154 } 155 } 156 Runtime.getRuntime().exit(Math.min(exitStatus, MAX_EXIT_CODE)); 157 } 158 159 /** 160 * Prints version string when the user requests version help (--version or -V). 161 * 162 * @noinspection UseOfSystemOutOrSystemErr 163 * @noinspectionreason UseOfSystemOutOrSystemErr - driver class for Checkstyle requires 164 * usage of System.out and System.err 165 */ 166 private static void printVersionToSystemOutput() { 167 System.out.println("Checkstyle version: " + getVersionString()); 168 } 169 170 /** 171 * Returns the version string printed when the user requests version help (--version or -V). 172 * 173 * @return a version string based on the package implementation version 174 */ 175 private static String getVersionString() { 176 return Main.class.getPackage().getImplementationVersion(); 177 } 178 179 /** 180 * Validates the user input and returns {@value #EXIT_WITH_INVALID_USER_INPUT_CODE} if 181 * invalid, otherwise executes CheckStyle and returns the number of violations. 182 * 183 * @param parseResult generic access to options and parameters found on the command line 184 * @param options encapsulates options and parameters specified on the command line 185 * @return number of violations 186 * @throws CheckstyleException if something happens processing the files. 187 * @throws IOException if a file could not be read. 188 * @noinspection UseOfSystemOutOrSystemErr 189 * @noinspectionreason UseOfSystemOutOrSystemErr - driver class for Checkstyle requires 190 * usage of System.out and System.err 191 */ 192 private static int execute(ParseResult parseResult, CliOptions options) 193 throws IOException, CheckstyleException { 194 195 final int exitStatus; 196 197 // return error if something is wrong in arguments 198 final List<File> filesToProcess = getFilesToProcess(options); 199 final List<String> messages = options.validateCli(parseResult, filesToProcess); 200 final boolean hasMessages = !messages.isEmpty(); 201 if (hasMessages) { 202 messages.forEach(System.out::println); 203 exitStatus = EXIT_WITH_INVALID_USER_INPUT_CODE; 204 } 205 else { 206 exitStatus = runCli(options, filesToProcess); 207 } 208 return exitStatus; 209 } 210 211 /** 212 * Determines the files to process. 213 * 214 * @param options the user-specified options 215 * @return list of files to process 216 */ 217 private static List<File> getFilesToProcess(CliOptions options) { 218 final List<Pattern> patternsToExclude = options.getExclusions(); 219 220 final List<File> result = new ArrayList<>(); 221 for (File file : options.files) { 222 result.addAll(listFiles(file, patternsToExclude)); 223 } 224 return result; 225 } 226 227 /** 228 * Traverses a specified node looking for files to check. Found files are added to 229 * a specified list. Subdirectories are also traversed. 230 * 231 * @param node 232 * the node to process 233 * @param patternsToExclude The list of patterns to exclude from searching or being added as 234 * files. 235 * @return found files 236 */ 237 private static List<File> listFiles(File node, List<Pattern> patternsToExclude) { 238 // could be replaced with org.apache.commons.io.FileUtils.list() method 239 // if only we add commons-io library 240 final List<File> result = new ArrayList<>(); 241 242 if (node.canRead() && !isPathExcluded(node.getAbsolutePath(), patternsToExclude)) { 243 if (node.isDirectory()) { 244 final File[] files = node.listFiles(); 245 // listFiles() can return null, so we need to check it 246 if (files != null) { 247 for (File element : files) { 248 result.addAll(listFiles(element, patternsToExclude)); 249 } 250 } 251 } 252 else if (node.isFile()) { 253 result.add(node); 254 } 255 } 256 return result; 257 } 258 259 /** 260 * Checks if a directory/file {@code path} should be excluded based on if it matches one of the 261 * patterns supplied. 262 * 263 * @param path The path of the directory/file to check 264 * @param patternsToExclude The collection of patterns to exclude from searching 265 * or being added as files. 266 * @return True if the directory/file matches one of the patterns. 267 */ 268 private static boolean isPathExcluded(String path, Iterable<Pattern> patternsToExclude) { 269 boolean result = false; 270 271 for (Pattern pattern : patternsToExclude) { 272 if (pattern.matcher(path).find()) { 273 result = true; 274 break; 275 } 276 } 277 278 return result; 279 } 280 281 /** 282 * Do execution of CheckStyle based on Command line options. 283 * 284 * @param options user-specified options 285 * @param filesToProcess the list of files whose style to check 286 * @return number of violations 287 * @throws CheckstyleException if something happens processing the files. 288 * @throws IOException if a file could not be read. 289 * @noinspection UseOfSystemOutOrSystemErr 290 * @noinspectionreason UseOfSystemOutOrSystemErr - driver class for Checkstyle requires 291 * usage of System.out and System.err 292 */ 293 private static int runCli(CliOptions options, List<File> filesToProcess) 294 throws IOException, CheckstyleException { 295 int result = 0; 296 final boolean hasSuppressionLineColumnNumber = options.suppressionLineColumnNumber != null; 297 298 // create config helper object 299 if (options.printAst) { 300 // print AST 301 final File file = filesToProcess.getFirst(); 302 final String stringAst = AstTreeStringPrinter.printFileAst(file, 303 JavaParser.Options.WITHOUT_COMMENTS); 304 System.out.print(stringAst); 305 } 306 else if (Objects.nonNull(options.xpath)) { 307 final String branch = 308 XpathUtil.printXpathBranch(options.xpath, filesToProcess.getFirst()); 309 System.out.print(branch); 310 } 311 else if (options.printAstWithComments) { 312 final File file = filesToProcess.getFirst(); 313 final String stringAst = AstTreeStringPrinter.printFileAst(file, 314 JavaParser.Options.WITH_COMMENTS); 315 System.out.print(stringAst); 316 } 317 else if (options.printJavadocTree) { 318 final File file = filesToProcess.getFirst(); 319 final String stringAst = DetailNodeTreeStringPrinter.printFileAst(file); 320 System.out.print(stringAst); 321 } 322 else if (options.printTreeWithJavadoc) { 323 final File file = filesToProcess.getFirst(); 324 final String stringAst = AstTreeStringPrinter.printJavaAndJavadocTree(file); 325 System.out.print(stringAst); 326 } 327 else if (hasSuppressionLineColumnNumber) { 328 final File file = filesToProcess.getFirst(); 329 final String stringSuppressions = 330 SuppressionsStringPrinter.printSuppressions(file, 331 options.suppressionLineColumnNumber, options.tabWidth); 332 System.out.print(stringSuppressions); 333 } 334 else { 335 if (options.debug) { 336 final Logger parentLogger = Logger.getLogger(Main.class.getName()).getParent(); 337 final ConsoleHandler handler = new ConsoleHandler(); 338 handler.setLevel(Level.FINEST); 339 handler.setFilter(new OnlyCheckstyleLoggersFilter()); 340 parentLogger.addHandler(handler); 341 parentLogger.setLevel(Level.FINEST); 342 } 343 if (LOG.isDebugEnabled()) { 344 LOG.debug("Checkstyle debug logging enabled"); 345 } 346 347 // run Checker 348 result = runCheckstyle(options, filesToProcess); 349 } 350 351 return result; 352 } 353 354 /** 355 * Executes required Checkstyle actions based on passed parameters. 356 * 357 * @param options user-specified options 358 * @param filesToProcess the list of files whose style to check 359 * @return number of violations of ERROR level 360 * @throws CheckstyleException 361 * when properties file could not be loaded 362 * @throws IOException 363 * when output file could not be found 364 */ 365 private static int runCheckstyle(CliOptions options, List<File> filesToProcess) 366 throws CheckstyleException, IOException { 367 // setup the properties 368 final Properties props; 369 370 if (options.propertiesFile == null) { 371 props = System.getProperties(); 372 } 373 else { 374 props = loadProperties(options.propertiesFile); 375 } 376 377 // create a configuration 378 379 final ConfigurationLoader.IgnoredModulesOptions ignoredModulesOptions; 380 if (options.executeIgnoredModules) { 381 ignoredModulesOptions = ConfigurationLoader.IgnoredModulesOptions.EXECUTE; 382 } 383 else { 384 ignoredModulesOptions = ConfigurationLoader.IgnoredModulesOptions.OMIT; 385 } 386 387 final ThreadModeSettings multiThreadModeSettings = 388 new ThreadModeSettings(CliOptions.CHECKER_THREADS_NUMBER, 389 CliOptions.TREE_WALKER_THREADS_NUMBER); 390 final Configuration config = ConfigurationLoader.loadConfiguration( 391 options.configurationFile, new PropertiesExpander(props), 392 ignoredModulesOptions, multiThreadModeSettings); 393 394 // create RootModule object and run it 395 final int errorCounter; 396 final ClassLoader moduleClassLoader = Checker.class.getClassLoader(); 397 final RootModule rootModule = getRootModule(config.getName(), moduleClassLoader); 398 399 try { 400 final AuditListener listener; 401 if (options.generateXpathSuppressionsFile) { 402 // create filter to print generated xpath suppressions file 403 final Configuration treeWalkerConfig = getTreeWalkerConfig(config); 404 if (treeWalkerConfig != null) { 405 final DefaultConfiguration moduleConfig = 406 new DefaultConfiguration( 407 XpathFileGeneratorAstFilter.class.getName()); 408 moduleConfig.addProperty(CliOptions.ATTRIB_TAB_WIDTH_NAME, 409 String.valueOf(options.tabWidth)); 410 ((DefaultConfiguration) treeWalkerConfig).addChild(moduleConfig); 411 } 412 413 listener = new XpathFileGeneratorAuditListener(getOutputStream(options.outputPath), 414 getOutputStreamOptions(options.outputPath)); 415 } 416 else if (options.generateCheckAndFileSuppressionsFile) { 417 listener = new ChecksAndFilesSuppressionFileGeneratorAuditListener( 418 getOutputStream(options.outputPath), 419 getOutputStreamOptions(options.outputPath)); 420 } 421 else { 422 listener = createListener(options.format, options.outputPath); 423 } 424 425 rootModule.setModuleClassLoader(moduleClassLoader); 426 rootModule.configure(config); 427 rootModule.addListener(listener); 428 429 // run RootModule 430 errorCounter = rootModule.process(filesToProcess); 431 } 432 finally { 433 rootModule.destroy(); 434 } 435 436 return errorCounter; 437 } 438 439 /** 440 * Loads properties from a File. 441 * 442 * @param file 443 * the properties file 444 * @return the properties in file 445 * @throws CheckstyleException 446 * when could not load properties file 447 */ 448 private static Properties loadProperties(File file) 449 throws CheckstyleException { 450 final Properties properties = new Properties(); 451 452 try (InputStream stream = Files.newInputStream(file.toPath())) { 453 properties.load(stream); 454 } 455 catch (final IOException exc) { 456 final LocalizedMessage loadPropertiesExceptionMessage = new LocalizedMessage( 457 Definitions.CHECKSTYLE_BUNDLE, Main.class, 458 LOAD_PROPERTIES_EXCEPTION, file.getAbsolutePath()); 459 throw new CheckstyleException(loadPropertiesExceptionMessage.getMessage(), exc); 460 } 461 462 return ChainedPropertyUtil.getResolvedProperties(properties); 463 } 464 465 /** 466 * Creates a new instance of the root module that will control and run Checkstyle. 467 * 468 * @param name The name of the module. This will either be a short name that 469 * will have to be found or the complete package name. 470 * @param moduleClassLoader Class loader used to load the root module. 471 * @return The new instance of the root module. 472 * @throws CheckstyleException if no module can be instantiated from name 473 */ 474 private static RootModule getRootModule(String name, ClassLoader moduleClassLoader) 475 throws CheckstyleException { 476 final ModuleFactory factory = new PackageObjectFactory( 477 Checker.class.getPackage().getName(), moduleClassLoader); 478 479 return (RootModule) factory.createModule(name); 480 } 481 482 /** 483 * Returns {@code TreeWalker} module configuration. 484 * 485 * @param config The configuration object. 486 * @return The {@code TreeWalker} module configuration. 487 */ 488 private static Configuration getTreeWalkerConfig(Configuration config) { 489 Configuration result = null; 490 491 final Configuration[] children = config.getChildren(); 492 for (Configuration child : children) { 493 if ("TreeWalker".equals(child.getName())) { 494 result = child; 495 break; 496 } 497 } 498 return result; 499 } 500 501 /** 502 * This method creates in AuditListener an open stream for validation data, it must be 503 * closed by {@link RootModule} (default implementation is {@link Checker}) by calling 504 * {@link AuditListener#auditFinished(AuditEvent)}. 505 * 506 * @param format format of the audit listener 507 * @param outputLocation the location of output 508 * @return a fresh new {@code AuditListener} 509 * @throws IOException when provided output location is not found 510 */ 511 private static AuditListener createListener(OutputFormat format, Path outputLocation) 512 throws IOException { 513 final OutputStream out = getOutputStream(outputLocation); 514 final OutputStreamOptions closeOutputStreamOption = 515 getOutputStreamOptions(outputLocation); 516 return format.createListener(out, closeOutputStreamOption); 517 } 518 519 /** 520 * Create output stream or return System.out. 521 * 522 * @param outputPath output location 523 * @return output stream 524 * @throws IOException might happen 525 * @noinspection UseOfSystemOutOrSystemErr 526 * @noinspectionreason UseOfSystemOutOrSystemErr - driver class for Checkstyle requires 527 * usage of System.out and System.err 528 */ 529 @SuppressWarnings("resource") 530 private static OutputStream getOutputStream(Path outputPath) throws IOException { 531 final OutputStream result; 532 if (outputPath == null) { 533 result = System.out; 534 } 535 else { 536 result = Files.newOutputStream(outputPath); 537 } 538 return result; 539 } 540 541 /** 542 * Create {@link OutputStreamOptions} for the given location. 543 * 544 * @param outputPath output location 545 * @return output stream options 546 */ 547 private static OutputStreamOptions getOutputStreamOptions(Path outputPath) { 548 final OutputStreamOptions result; 549 if (outputPath == null) { 550 result = OutputStreamOptions.NONE; 551 } 552 else { 553 result = OutputStreamOptions.CLOSE; 554 } 555 return result; 556 } 557 558 /** 559 * Enumeration over the possible output formats. 560 * 561 * @noinspection PackageVisibleInnerClass 562 * @noinspectionreason PackageVisibleInnerClass - we keep this enum package visible for tests 563 */ 564 /* package */ enum OutputFormat { 565 566 /** XML output format. */ 567 XML, 568 /** SARIF output format. */ 569 SARIF, 570 /** Plain output format. */ 571 PLAIN; 572 573 /** 574 * Returns a new AuditListener for this OutputFormat. 575 * 576 * @param out the output stream 577 * @param options the output stream options 578 * @return a new AuditListener for this OutputFormat 579 * @throws IOException if there is any IO exception during logger initialization 580 */ 581 /* package */ AuditListener createListener( 582 OutputStream out, 583 OutputStreamOptions options) 584 throws IOException { 585 final AuditListener result; 586 if (this == XML) { 587 result = new XMLLogger(out, options); 588 } 589 else if (this == SARIF) { 590 result = new SarifLogger(out, options); 591 } 592 else { 593 result = new DefaultLogger(out, options); 594 } 595 return result; 596 } 597 598 /** 599 * Returns the name in lowercase. 600 * 601 * @return the enum name in lowercase 602 */ 603 @Override 604 public String toString() { 605 return name().toLowerCase(Locale.ROOT); 606 } 607 608 } 609 610 /** Log Filter used in debug mode. */ 611 private static final class OnlyCheckstyleLoggersFilter implements Filter { 612 613 /** Name of the package used to filter on. */ 614 private final String packageName = Main.class.getPackage().getName(); 615 616 /** 617 * Creates a new {@code OnlyCheckstyleLoggersFilter} instance. 618 */ 619 private OnlyCheckstyleLoggersFilter() { 620 // no code by default 621 } 622 623 /** 624 * Returns whether the specified logRecord should be logged. 625 * 626 * @param logRecord the logRecord to log 627 * @return true if the logger name is in the package of this class or a subpackage 628 */ 629 @Override 630 public boolean isLoggable(LogRecord logRecord) { 631 return logRecord.getLoggerName().startsWith(packageName); 632 } 633 634 } 635 636 /** 637 * Command line options. 638 * 639 * @noinspection unused, FieldMayBeFinal, CanBeFinal, 640 * MismatchedQueryAndUpdateOfCollection, LocalCanBeFinal 641 * @noinspectionreason FieldMayBeFinal - usage of picocli requires 642 * suppression of above inspections 643 * @noinspectionreason CanBeFinal - usage of picocli requires suppression of above inspections 644 * @noinspectionreason MismatchedQueryAndUpdateOfCollection - list of files is gathered and used 645 * via reflection by picocli library 646 * @noinspectionreason LocalCanBeFinal - usage of picocli requires 647 * suppression of above inspections 648 */ 649 @Command(name = "checkstyle", description = "Checkstyle verifies that the specified " 650 + "source code files adhere to the specified rules. By default, violations are " 651 + "reported to standard out in plain format. Checkstyle requires a configuration " 652 + "XML file that configures the checks to apply.", 653 mixinStandardHelpOptions = true) 654 private static final class CliOptions { 655 656 /** Width of CLI help option. */ 657 private static final int HELP_WIDTH = 100; 658 659 /** The default number of threads to use for checker and the tree walker. */ 660 private static final int DEFAULT_THREAD_COUNT = 1; 661 662 /** Name for the moduleConfig attribute 'tabWidth'. */ 663 private static final String ATTRIB_TAB_WIDTH_NAME = "tabWidth"; 664 665 /** Default output format. */ 666 private static final OutputFormat DEFAULT_OUTPUT_FORMAT = OutputFormat.PLAIN; 667 668 /** Option name for output format. */ 669 private static final String OUTPUT_FORMAT_OPTION = "-f"; 670 671 /** 672 * The checker threads number. 673 * This option has been skipped for CLI options intentionally. 674 * 675 */ 676 private static final int CHECKER_THREADS_NUMBER = DEFAULT_THREAD_COUNT; 677 678 /** 679 * The tree walker threads number. 680 * 681 */ 682 private static final int TREE_WALKER_THREADS_NUMBER = DEFAULT_THREAD_COUNT; 683 684 /** List of file to validate. */ 685 @Parameters(arity = "1..*", paramLabel = "<files or folders>", 686 description = "One or more source files to verify") 687 private List<File> files; 688 689 /** Config file location. */ 690 @Option(names = "-c", description = "Specifies the location of the file that defines" 691 + " the configuration modules. The location can either be a filesystem location" 692 + ", or a name passed to the ClassLoader.getResource() method.") 693 private String configurationFile; 694 695 /** Output file location. */ 696 @Option(names = "-o", description = "Sets the output file. Defaults to stdout.") 697 private Path outputPath; 698 699 /** Properties file location. */ 700 @Option(names = "-p", description = "Sets the property files to load.") 701 private File propertiesFile; 702 703 /** LineNo and columnNo for the suppression. */ 704 @Option(names = "-s", 705 description = "Prints xpath suppressions at the file's line and column position. " 706 + "Argument is the line and column number (separated by a : ) in the file " 707 + "that the suppression should be generated for. The option cannot be used " 708 + "with other options and requires exactly one file to run on to be " 709 + "specified. Note that the generated result will have few queries, joined " 710 + "by pipe(|). Together they will match all AST nodes on " 711 + "specified line and column. You need to choose only one and recheck " 712 + "that it works. Usage of all of them is also ok, but might result in " 713 + "undesirable matching and suppress other issues.") 714 private String suppressionLineColumnNumber; 715 716 /** 717 * Tab character length. 718 * 719 * @noinspection CanBeFinal 720 * @noinspectionreason CanBeFinal - we use picocli, and it uses 721 * reflection to manage such fields 722 */ 723 @Option(names = {"-w", "--tabWidth"}, 724 description = "Sets the length of the tab character. " 725 + "Used only with -s option. Default value is ${DEFAULT-VALUE}.") 726 private int tabWidth = CommonUtil.DEFAULT_TAB_WIDTH; 727 728 /** Switch whether to generate xpath suppressions file or not. */ 729 @Option(names = {"-g", "--generate-xpath-suppression"}, 730 description = "Generates an output xpath suppression XML to use to suppress all " 731 + "violations from user's config. Instead of printing every violation, " 732 + "all violations will be caught and single suppressions xml file will " 733 + "be printed out. Used only with -c option. Output " 734 + "location can be specified with -o option.") 735 private boolean generateXpathSuppressionsFile; 736 737 /** Switch whether to generate check and file suppressions file or not. */ 738 @Option(names = {"-G", "--generate-checks-and-files-suppression"}, 739 description = "Generates an output suppression XML that will have suppress " 740 + "elements with \"checks\" and \"files\" attributes only to use to " 741 + "suppress all violations from user's config. Instead of printing every " 742 + "violation, all violations will be caught and single suppressions xml " 743 + "file will be printed out. Used only with -c option. Output " 744 + "location can be specified with -o option.") 745 private boolean generateCheckAndFileSuppressionsFile; 746 747 /** 748 * Output format. 749 * 750 * @noinspection CanBeFinal 751 * @noinspectionreason CanBeFinal - we use picocli, and it uses 752 * reflection to manage such fields 753 */ 754 @Option(names = "-f", 755 description = "Specifies the output format. Valid values: " 756 + "${COMPLETION-CANDIDATES} for XMLLogger, SarifLogger, " 757 + "and DefaultLogger respectively. Defaults to ${DEFAULT-VALUE}.") 758 private OutputFormat format = DEFAULT_OUTPUT_FORMAT; 759 760 /** Option that controls whether to print the AST of the file. */ 761 @Option(names = {"-t", "--tree"}, 762 description = "This option is used to display the Abstract Syntax Tree (AST) " 763 + "without any comments of the specified file. It can only be used on " 764 + "a single file and cannot be combined with other options.") 765 private boolean printAst; 766 767 /** Option that controls whether to print the AST of the file including comments. */ 768 @Option(names = {"-T", "--treeWithComments"}, 769 description = "This option is used to display the Abstract Syntax Tree (AST) " 770 + "with comment nodes excluding Javadoc of the specified file. It can only" 771 + " be used on a single file and cannot be combined with other options.") 772 private boolean printAstWithComments; 773 774 /** Option that controls whether to print the parse tree of the javadoc comment. */ 775 @Option(names = {"-j", "--javadocTree"}, 776 description = "This option is used to print the Parse Tree of the Javadoc comment." 777 + " The file has to contain only Javadoc comment content " 778 + "excluding '/**' and '*/' at the beginning and at the end respectively. " 779 + "It can only be used on a single file and cannot be combined " 780 + "with other options.") 781 private boolean printJavadocTree; 782 783 /** Option that controls whether to print the full AST of the file. */ 784 @Option(names = {"-J", "--treeWithJavadoc"}, 785 description = "This option is used to display the Abstract Syntax Tree (AST) " 786 + "with Javadoc nodes of the specified file. It can only be used on a " 787 + "single file and cannot be combined with other options.") 788 private boolean printTreeWithJavadoc; 789 790 /** Option that controls whether to print debug info. */ 791 @Option(names = {"-d", "--debug"}, 792 description = "Prints all debug logging of CheckStyle utility.") 793 private boolean debug; 794 795 /** 796 * Option that allows users to specify a list of paths to exclude. 797 * 798 * @noinspection CanBeFinal 799 * @noinspectionreason CanBeFinal - we use picocli, and it uses 800 * reflection to manage such fields 801 */ 802 @Option(names = {"-e", "--exclude"}, 803 description = "Directory/file to exclude from CheckStyle. The path can be the " 804 + "full, absolute path, or relative to the current path. Multiple " 805 + "excludes are allowed.") 806 private List<File> exclude = new ArrayList<>(); 807 808 /** 809 * Option that allows users to specify a regex of paths to exclude. 810 * 811 * @noinspection CanBeFinal 812 * @noinspectionreason CanBeFinal - we use picocli, and it uses 813 * reflection to manage such fields 814 */ 815 @Option(names = {"-x", "--exclude-regexp"}, 816 description = "Directory/file pattern to exclude from CheckStyle. Multiple " 817 + "excludes are allowed.") 818 private List<Pattern> excludeRegex = new ArrayList<>(); 819 820 /** Switch whether to execute ignored modules or not. */ 821 @Option(names = {"-E", "--executeIgnoredModules"}, 822 description = "Allows ignored modules to be run.") 823 private boolean executeIgnoredModules; 824 825 /** Show AST branches that match xpath. */ 826 @Option(names = {"-b", "--branch-matching-xpath"}, 827 description = "Shows Abstract Syntax Tree(AST) branches that match given XPath query.") 828 private String xpath; 829 830 /** 831 * Creates a new {@code CliOptions} instance. 832 */ 833 private CliOptions() { 834 // no code by default 835 } 836 837 /** 838 * Gets the list of exclusions provided through the command line arguments. 839 * 840 * @return List of exclusion patterns. 841 */ 842 private List<Pattern> getExclusions() { 843 final List<Pattern> result = exclude.stream() 844 .map(File::getAbsolutePath) 845 .map(Pattern::quote) 846 .map(pattern -> Pattern.compile("^" + pattern + "$")) 847 .collect(Collectors.toCollection(ArrayList::new)); 848 result.addAll(excludeRegex); 849 return result; 850 } 851 852 /** 853 * Validates the user-specified command line options. 854 * 855 * @param parseResult used to verify if the format option was specified on the command line 856 * @param filesToProcess the list of files whose style to check 857 * @return list of violations 858 */ 859 // -@cs[CyclomaticComplexity] Breaking apart will damage encapsulation 860 private List<String> validateCli(ParseResult parseResult, List<File> filesToProcess) { 861 final List<String> result = new ArrayList<>(); 862 final boolean hasConfigurationFile = configurationFile != null; 863 final boolean hasSuppressionLineColumnNumber = suppressionLineColumnNumber != null; 864 865 if (filesToProcess.isEmpty()) { 866 result.add("Files to process must be specified, found 0."); 867 } 868 // ensure there is no conflicting options 869 else if (printAst || printAstWithComments || printJavadocTree || printTreeWithJavadoc 870 || xpath != null) { 871 if (suppressionLineColumnNumber != null || configurationFile != null 872 || propertiesFile != null || outputPath != null 873 || parseResult.hasMatchedOption(OUTPUT_FORMAT_OPTION)) { 874 result.add("Option '-t' cannot be used with other options."); 875 } 876 else if (filesToProcess.size() > 1) { 877 result.add("Printing AST is allowed for only one file."); 878 } 879 } 880 else if (hasSuppressionLineColumnNumber) { 881 if (configurationFile != null || propertiesFile != null 882 || outputPath != null 883 || parseResult.hasMatchedOption(OUTPUT_FORMAT_OPTION)) { 884 result.add("Option '-s' cannot be used with other options."); 885 } 886 else if (filesToProcess.size() > 1) { 887 result.add("Printing xpath suppressions is allowed for only one file."); 888 } 889 } 890 else if (hasConfigurationFile) { 891 try { 892 // test location only 893 CommonUtil.getUriByFilename(configurationFile); 894 } 895 catch (CheckstyleException ignored) { 896 final String msg = "Could not find config XML file '%s'."; 897 result.add(String.format(Locale.ROOT, msg, configurationFile)); 898 } 899 result.addAll(validateOptionalCliParametersIfConfigDefined()); 900 } 901 else { 902 result.add("Must specify a config XML file."); 903 } 904 905 return result; 906 } 907 908 /** 909 * Validates optional command line parameters that might be used with config file. 910 * 911 * @return list of violations 912 */ 913 private List<String> validateOptionalCliParametersIfConfigDefined() { 914 final List<String> result = new ArrayList<>(); 915 if (propertiesFile != null && !propertiesFile.exists()) { 916 result.add(String.format(Locale.ROOT, 917 "Could not find file '%s'.", propertiesFile)); 918 } 919 return result; 920 } 921 922 } 923 924}