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