001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2025 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 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(exitStatus);
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 IOException if a file could not be read.
187 * @throws CheckstyleException if something happens processing the files.
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 LinkedList<>();
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 LinkedList<>();
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 IOException if a file could not be read.
288 * @throws CheckstyleException if something happens processing the files.
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.get(0);
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 = XpathUtil.printXpathBranch(options.xpath, filesToProcess.get(0));
308 System.out.print(branch);
309 }
310 else if (options.printAstWithComments) {
311 final File file = filesToProcess.get(0);
312 final String stringAst = AstTreeStringPrinter.printFileAst(file,
313 JavaParser.Options.WITH_COMMENTS);
314 System.out.print(stringAst);
315 }
316 else if (options.printJavadocTree) {
317 final File file = filesToProcess.get(0);
318 final String stringAst = DetailNodeTreeStringPrinter.printFileAst(file);
319 System.out.print(stringAst);
320 }
321 else if (options.printTreeWithJavadoc) {
322 final File file = filesToProcess.get(0);
323 final String stringAst = AstTreeStringPrinter.printJavaAndJavadocTree(file);
324 System.out.print(stringAst);
325 }
326 else if (hasSuppressionLineColumnNumber) {
327 final File file = filesToProcess.get(0);
328 final String stringSuppressions =
329 SuppressionsStringPrinter.printSuppressions(file,
330 options.suppressionLineColumnNumber, options.tabWidth);
331 System.out.print(stringSuppressions);
332 }
333 else {
334 if (options.debug) {
335 final Logger parentLogger = Logger.getLogger(Main.class.getName()).getParent();
336 final ConsoleHandler handler = new ConsoleHandler();
337 handler.setLevel(Level.FINEST);
338 handler.setFilter(new OnlyCheckstyleLoggersFilter());
339 parentLogger.addHandler(handler);
340 parentLogger.setLevel(Level.FINEST);
341 }
342 if (LOG.isDebugEnabled()) {
343 LOG.debug("Checkstyle debug logging enabled");
344 }
345
346 // run Checker
347 result = runCheckstyle(options, filesToProcess);
348 }
349
350 return result;
351 }
352
353 /**
354 * Executes required Checkstyle actions based on passed parameters.
355 *
356 * @param options user-specified options
357 * @param filesToProcess the list of files whose style to check
358 * @return number of violations of ERROR level
359 * @throws IOException
360 * when output file could not be found
361 * @throws CheckstyleException
362 * when properties file could not be loaded
363 */
364 private static int runCheckstyle(CliOptions options, List<File> filesToProcess)
365 throws CheckstyleException, IOException {
366 // setup the properties
367 final Properties props;
368
369 if (options.propertiesFile == null) {
370 props = System.getProperties();
371 }
372 else {
373 props = loadProperties(options.propertiesFile);
374 }
375
376 // create a configuration
377
378 final ConfigurationLoader.IgnoredModulesOptions ignoredModulesOptions;
379 if (options.executeIgnoredModules) {
380 ignoredModulesOptions = ConfigurationLoader.IgnoredModulesOptions.EXECUTE;
381 }
382 else {
383 ignoredModulesOptions = ConfigurationLoader.IgnoredModulesOptions.OMIT;
384 }
385
386 final ThreadModeSettings multiThreadModeSettings =
387 new ThreadModeSettings(CliOptions.CHECKER_THREADS_NUMBER,
388 CliOptions.TREE_WALKER_THREADS_NUMBER);
389 final Configuration config = ConfigurationLoader.loadConfiguration(
390 options.configurationFile, new PropertiesExpander(props),
391 ignoredModulesOptions, multiThreadModeSettings);
392
393 // create RootModule object and run it
394 final int errorCounter;
395 final ClassLoader moduleClassLoader = Checker.class.getClassLoader();
396 final RootModule rootModule = getRootModule(config.getName(), moduleClassLoader);
397
398 try {
399 final AuditListener listener;
400 if (options.generateXpathSuppressionsFile) {
401 // create filter to print generated xpath suppressions file
402 final Configuration treeWalkerConfig = getTreeWalkerConfig(config);
403 if (treeWalkerConfig != null) {
404 final DefaultConfiguration moduleConfig =
405 new DefaultConfiguration(
406 XpathFileGeneratorAstFilter.class.getName());
407 moduleConfig.addProperty(CliOptions.ATTRIB_TAB_WIDTH_NAME,
408 String.valueOf(options.tabWidth));
409 ((DefaultConfiguration) treeWalkerConfig).addChild(moduleConfig);
410 }
411
412 listener = new XpathFileGeneratorAuditListener(getOutputStream(options.outputPath),
413 getOutputStreamOptions(options.outputPath));
414 }
415 else if (options.generateCheckAndFileSuppressionsFile) {
416 listener = new ChecksAndFilesSuppressionFileGeneratorAuditListener(
417 getOutputStream(options.outputPath),
418 getOutputStreamOptions(options.outputPath));
419 }
420 else {
421 listener = createListener(options.format, options.outputPath);
422 }
423
424 rootModule.setModuleClassLoader(moduleClassLoader);
425 rootModule.configure(config);
426 rootModule.addListener(listener);
427
428 // run RootModule
429 errorCounter = rootModule.process(filesToProcess);
430 }
431 finally {
432 rootModule.destroy();
433 }
434
435 return errorCounter;
436 }
437
438 /**
439 * Loads properties from a File.
440 *
441 * @param file
442 * the properties file
443 * @return the properties in file
444 * @throws CheckstyleException
445 * when could not load properties file
446 */
447 private static Properties loadProperties(File file)
448 throws CheckstyleException {
449 final Properties properties = new Properties();
450
451 try (InputStream stream = Files.newInputStream(file.toPath())) {
452 properties.load(stream);
453 }
454 catch (final IOException exc) {
455 final LocalizedMessage loadPropertiesExceptionMessage = new LocalizedMessage(
456 Definitions.CHECKSTYLE_BUNDLE, Main.class,
457 LOAD_PROPERTIES_EXCEPTION, file.getAbsolutePath());
458 throw new CheckstyleException(loadPropertiesExceptionMessage.getMessage(), exc);
459 }
460
461 return ChainedPropertyUtil.getResolvedProperties(properties);
462 }
463
464 /**
465 * Creates a new instance of the root module that will control and run
466 * 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 * @exception 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 enum OutputFormat {
565 /** XML output format. */
566 XML,
567 /** SARIF output format. */
568 SARIF,
569 /** Plain output format. */
570 PLAIN;
571
572 /**
573 * Returns a new AuditListener for this OutputFormat.
574 *
575 * @param out the output stream
576 * @param options the output stream options
577 * @return a new AuditListener for this OutputFormat
578 * @throws IOException if there is any IO exception during logger initialization
579 */
580 public AuditListener createListener(
581 OutputStream out,
582 OutputStreamOptions options) throws IOException {
583 final AuditListener result;
584 if (this == XML) {
585 result = new XMLLogger(out, options);
586 }
587 else if (this == SARIF) {
588 result = new SarifLogger(out, options);
589 }
590 else {
591 result = new DefaultLogger(out, options);
592 }
593 return result;
594 }
595
596 /**
597 * Returns the name in lowercase.
598 *
599 * @return the enum name in lowercase
600 */
601 @Override
602 public String toString() {
603 return name().toLowerCase(Locale.ROOT);
604 }
605 }
606
607 /** Log Filter used in debug mode. */
608 private static final class OnlyCheckstyleLoggersFilter implements Filter {
609 /** Name of the package used to filter on. */
610 private final String packageName = Main.class.getPackage().getName();
611
612 /**
613 * Returns whether the specified logRecord should be logged.
614 *
615 * @param logRecord the logRecord to log
616 * @return true if the logger name is in the package of this class or a subpackage
617 */
618 @Override
619 public boolean isLoggable(LogRecord logRecord) {
620 return logRecord.getLoggerName().startsWith(packageName);
621 }
622 }
623
624 /**
625 * Command line options.
626 *
627 * @noinspection unused, FieldMayBeFinal, CanBeFinal,
628 * MismatchedQueryAndUpdateOfCollection, LocalCanBeFinal
629 * @noinspectionreason FieldMayBeFinal - usage of picocli requires
630 * suppression of above inspections
631 * @noinspectionreason CanBeFinal - usage of picocli requires
632 * suppression of above inspections
633 * @noinspectionreason MismatchedQueryAndUpdateOfCollection - list of files is gathered and used
634 * via reflection by picocli library
635 * @noinspectionreason LocalCanBeFinal - usage of picocli requires
636 * suppression of above inspections
637 */
638 @Command(name = "checkstyle", description = "Checkstyle verifies that the specified "
639 + "source code files adhere to the specified rules. By default, violations are "
640 + "reported to standard out in plain format. Checkstyle requires a configuration "
641 + "XML file that configures the checks to apply.",
642 mixinStandardHelpOptions = true)
643 private static final class CliOptions {
644
645 /** Width of CLI help option. */
646 private static final int HELP_WIDTH = 100;
647
648 /** The default number of threads to use for checker and the tree walker. */
649 private static final int DEFAULT_THREAD_COUNT = 1;
650
651 /** Name for the moduleConfig attribute 'tabWidth'. */
652 private static final String ATTRIB_TAB_WIDTH_NAME = "tabWidth";
653
654 /** Default output format. */
655 private static final OutputFormat DEFAULT_OUTPUT_FORMAT = OutputFormat.PLAIN;
656
657 /** Option name for output format. */
658 private static final String OUTPUT_FORMAT_OPTION = "-f";
659
660 /**
661 * The checker threads number.
662 * This option has been skipped for CLI options intentionally.
663 *
664 */
665 private static final int CHECKER_THREADS_NUMBER = DEFAULT_THREAD_COUNT;
666
667 /**
668 * The tree walker threads number.
669 *
670 */
671 private static final int TREE_WALKER_THREADS_NUMBER = DEFAULT_THREAD_COUNT;
672
673 /** List of file to validate. */
674 @Parameters(arity = "1..*", paramLabel = "<files or folders>",
675 description = "One or more source files to verify")
676 private List<File> files;
677
678 /** Config file location. */
679 @Option(names = "-c", description = "Specifies the location of the file that defines"
680 + " the configuration modules. The location can either be a filesystem location"
681 + ", or a name passed to the ClassLoader.getResource() method.")
682 private String configurationFile;
683
684 /** Output file location. */
685 @Option(names = "-o", description = "Sets the output file. Defaults to stdout.")
686 private Path outputPath;
687
688 /** Properties file location. */
689 @Option(names = "-p", description = "Sets the property files to load.")
690 private File propertiesFile;
691
692 /** LineNo and columnNo for the suppression. */
693 @Option(names = "-s",
694 description = "Prints xpath suppressions at the file's line and column position. "
695 + "Argument is the line and column number (separated by a : ) in the file "
696 + "that the suppression should be generated for. The option cannot be used "
697 + "with other options and requires exactly one file to run on to be "
698 + "specified. Note that the generated result will have few queries, joined "
699 + "by pipe(|). Together they will match all AST nodes on "
700 + "specified line and column. You need to choose only one and recheck "
701 + "that it works. Usage of all of them is also ok, but might result in "
702 + "undesirable matching and suppress other issues.")
703 private String suppressionLineColumnNumber;
704
705 /**
706 * Tab character length.
707 *
708 * @noinspection CanBeFinal
709 * @noinspectionreason CanBeFinal - we use picocli, and it uses
710 * reflection to manage such fields
711 */
712 @Option(names = {"-w", "--tabWidth"},
713 description = "Sets the length of the tab character. "
714 + "Used only with -s option. Default value is ${DEFAULT-VALUE}.")
715 private int tabWidth = CommonUtil.DEFAULT_TAB_WIDTH;
716
717 /** Switch whether to generate xpath suppressions file or not. */
718 @Option(names = {"-g", "--generate-xpath-suppression"},
719 description = "Generates to output a xpath suppression xml to use to suppress all "
720 + "violations from user's config. Instead of printing every violation, "
721 + "all violations will be catched and single suppressions xml file will "
722 + "be printed out. Used only with -c option. Output "
723 + "location can be specified with -o option.")
724 private boolean generateXpathSuppressionsFile;
725
726 /** Switch whether to generate check and file suppressions file or not. */
727 @Option(names = {"-G", "--generate-checks-and-files-suppression"},
728 description = "Generates to output a suppression xml that will have suppress "
729 + "elements with \"checks\" and \"files\" attributes only to use to "
730 + "suppress all violations from user's config. Instead of printing every "
731 + "violation, all violations will be catched and single suppressions xml "
732 + "file will be printed out. Used only with -c option. Output "
733 + "location can be specified with -o option.")
734 private boolean generateCheckAndFileSuppressionsFile;
735
736 /**
737 * Output format.
738 *
739 * @noinspection CanBeFinal
740 * @noinspectionreason CanBeFinal - we use picocli, and it uses
741 * reflection to manage such fields
742 */
743 @Option(names = "-f",
744 description = "Specifies the output format. Valid values: "
745 + "${COMPLETION-CANDIDATES} for XMLLogger, SarifLogger, "
746 + "and DefaultLogger respectively. Defaults to ${DEFAULT-VALUE}.")
747 private OutputFormat format = DEFAULT_OUTPUT_FORMAT;
748
749 /** Option that controls whether to print the AST of the file. */
750 @Option(names = {"-t", "--tree"},
751 description = "This option is used to display the Abstract Syntax Tree (AST) "
752 + "without any comments of the specified file. It can only be used on "
753 + "a single file and cannot be combined with other options.")
754 private boolean printAst;
755
756 /** Option that controls whether to print the AST of the file including comments. */
757 @Option(names = {"-T", "--treeWithComments"},
758 description = "This option is used to display the Abstract Syntax Tree (AST) "
759 + "with comment nodes excluding Javadoc of the specified file. It can only"
760 + " be used on a single file and cannot be combined with other options.")
761 private boolean printAstWithComments;
762
763 /** Option that controls whether to print the parse tree of the javadoc comment. */
764 @Option(names = {"-j", "--javadocTree"},
765 description = "This option is used to print the Parse Tree of the Javadoc comment."
766 + " The file has to contain only Javadoc comment content "
767 + "excluding '/**' and '*/' at the beginning and at the end respectively. "
768 + "It can only be used on a single file and cannot be combined "
769 + "with other options.")
770 private boolean printJavadocTree;
771
772 /** Option that controls whether to print the full AST of the file. */
773 @Option(names = {"-J", "--treeWithJavadoc"},
774 description = "This option is used to display the Abstract Syntax Tree (AST) "
775 + "with Javadoc nodes of the specified file. It can only be used on a "
776 + "single file and cannot be combined with other options.")
777 private boolean printTreeWithJavadoc;
778
779 /** Option that controls whether to print debug info. */
780 @Option(names = {"-d", "--debug"},
781 description = "Prints all debug logging of CheckStyle utility.")
782 private boolean debug;
783
784 /**
785 * Option that allows users to specify a list of paths to exclude.
786 *
787 * @noinspection CanBeFinal
788 * @noinspectionreason CanBeFinal - we use picocli, and it uses
789 * reflection to manage such fields
790 */
791 @Option(names = {"-e", "--exclude"},
792 description = "Directory/file to exclude from CheckStyle. The path can be the "
793 + "full, absolute path, or relative to the current path. Multiple "
794 + "excludes are allowed.")
795 private List<File> exclude = new ArrayList<>();
796
797 /**
798 * Option that allows users to specify a regex of paths to exclude.
799 *
800 * @noinspection CanBeFinal
801 * @noinspectionreason CanBeFinal - we use picocli, and it uses
802 * reflection to manage such fields
803 */
804 @Option(names = {"-x", "--exclude-regexp"},
805 description = "Directory/file pattern to exclude from CheckStyle. Multiple "
806 + "excludes are allowed.")
807 private List<Pattern> excludeRegex = new ArrayList<>();
808
809 /** Switch whether to execute ignored modules or not. */
810 @Option(names = {"-E", "--executeIgnoredModules"},
811 description = "Allows ignored modules to be run.")
812 private boolean executeIgnoredModules;
813
814 /** Show AST branches that match xpath. */
815 @Option(names = {"-b", "--branch-matching-xpath"},
816 description = "Shows Abstract Syntax Tree(AST) branches that match given XPath query.")
817 private String xpath;
818
819 /**
820 * Gets the list of exclusions provided through the command line arguments.
821 *
822 * @return List of exclusion patterns.
823 */
824 private List<Pattern> getExclusions() {
825 final List<Pattern> result = exclude.stream()
826 .map(File::getAbsolutePath)
827 .map(Pattern::quote)
828 .map(pattern -> Pattern.compile("^" + pattern + "$"))
829 .collect(Collectors.toCollection(ArrayList::new));
830 result.addAll(excludeRegex);
831 return result;
832 }
833
834 /**
835 * Validates the user-specified command line options.
836 *
837 * @param parseResult used to verify if the format option was specified on the command line
838 * @param filesToProcess the list of files whose style to check
839 * @return list of violations
840 */
841 // -@cs[CyclomaticComplexity] Breaking apart will damage encapsulation
842 private List<String> validateCli(ParseResult parseResult, List<File> filesToProcess) {
843 final List<String> result = new ArrayList<>();
844 final boolean hasConfigurationFile = configurationFile != null;
845 final boolean hasSuppressionLineColumnNumber = suppressionLineColumnNumber != null;
846
847 if (filesToProcess.isEmpty()) {
848 result.add("Files to process must be specified, found 0.");
849 }
850 // ensure there is no conflicting options
851 else if (printAst || printAstWithComments || printJavadocTree || printTreeWithJavadoc
852 || xpath != null) {
853 if (suppressionLineColumnNumber != null || configurationFile != null
854 || propertiesFile != null || outputPath != null
855 || parseResult.hasMatchedOption(OUTPUT_FORMAT_OPTION)) {
856 result.add("Option '-t' cannot be used with other options.");
857 }
858 else if (filesToProcess.size() > 1) {
859 result.add("Printing AST is allowed for only one file.");
860 }
861 }
862 else if (hasSuppressionLineColumnNumber) {
863 if (configurationFile != null || propertiesFile != null
864 || outputPath != null
865 || parseResult.hasMatchedOption(OUTPUT_FORMAT_OPTION)) {
866 result.add("Option '-s' cannot be used with other options.");
867 }
868 else if (filesToProcess.size() > 1) {
869 result.add("Printing xpath suppressions is allowed for only one file.");
870 }
871 }
872 else if (hasConfigurationFile) {
873 try {
874 // test location only
875 CommonUtil.getUriByFilename(configurationFile);
876 }
877 catch (CheckstyleException ignored) {
878 final String msg = "Could not find config XML file '%s'.";
879 result.add(String.format(Locale.ROOT, msg, configurationFile));
880 }
881 result.addAll(validateOptionalCliParametersIfConfigDefined());
882 }
883 else {
884 result.add("Must specify a config XML file.");
885 }
886
887 return result;
888 }
889
890 /**
891 * Validates optional command line parameters that might be used with config file.
892 *
893 * @return list of violations
894 */
895 private List<String> validateOptionalCliParametersIfConfigDefined() {
896 final List<String> result = new ArrayList<>();
897 if (propertiesFile != null && !propertiesFile.exists()) {
898 result.add(String.format(Locale.ROOT,
899 "Could not find file '%s'.", propertiesFile));
900 }
901 return result;
902 }
903 }
904
905}