View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2026 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ///////////////////////////////////////////////////////////////////////////////////////////////
19  
20  package com.puppycrawl.tools.checkstyle;
21  
22  import java.io.File;
23  import java.io.IOException;
24  import java.io.InputStream;
25  import java.io.OutputStream;
26  import java.nio.file.Files;
27  import java.nio.file.Path;
28  import java.util.ArrayList;
29  import java.util.List;
30  import java.util.Locale;
31  import java.util.Objects;
32  import java.util.Properties;
33  import java.util.logging.ConsoleHandler;
34  import java.util.logging.Filter;
35  import java.util.logging.Level;
36  import java.util.logging.LogRecord;
37  import java.util.logging.Logger;
38  import java.util.regex.Pattern;
39  import java.util.stream.Collectors;
40  
41  import org.apache.commons.logging.Log;
42  import org.apache.commons.logging.LogFactory;
43  
44  import com.puppycrawl.tools.checkstyle.AbstractAutomaticBean.OutputStreamOptions;
45  import com.puppycrawl.tools.checkstyle.api.AuditEvent;
46  import com.puppycrawl.tools.checkstyle.api.AuditListener;
47  import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
48  import com.puppycrawl.tools.checkstyle.api.Configuration;
49  import com.puppycrawl.tools.checkstyle.api.RootModule;
50  import com.puppycrawl.tools.checkstyle.utils.ChainedPropertyUtil;
51  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
52  import com.puppycrawl.tools.checkstyle.utils.XpathUtil;
53  import picocli.CommandLine;
54  import picocli.CommandLine.Command;
55  import picocli.CommandLine.Option;
56  import picocli.CommandLine.ParameterException;
57  import picocli.CommandLine.Parameters;
58  import picocli.CommandLine.ParseResult;
59  
60  /**
61   * Wrapper command line program for the Checker.
62   */
63  public final class Main {
64  
65      /**
66       * A key pointing to the error counter
67       * message in the "messages.properties" file.
68       */
69      public static final String ERROR_COUNTER = "Main.errorCounter";
70      /**
71       * A key pointing to the load properties exception
72       * message in the "messages.properties" file.
73       */
74      public static final String LOAD_PROPERTIES_EXCEPTION = "Main.loadProperties";
75      /**
76       * A key pointing to the create listener exception
77       * message in the "messages.properties" file.
78       */
79      public static final String CREATE_LISTENER_EXCEPTION = "Main.createListener";
80  
81      /** Logger for Main. */
82      private static final Log LOG = LogFactory.getLog(Main.class);
83  
84      /** Exit code returned when user specified invalid command line arguments. */
85      private static final int EXIT_WITH_INVALID_USER_INPUT_CODE = -1;
86  
87      /** Exit code returned when execution finishes with {@link CheckstyleException}. */
88      private static final int EXIT_WITH_CHECKSTYLE_EXCEPTION_CODE = -2;
89  
90      /**
91       * Client code should not create instances of this class, but use
92       * {@link #main(String[])} method instead.
93       */
94      private Main() {
95      }
96  
97      /**
98       * Loops over the files specified checking them for errors. The exit code
99       * is the number of errors found in all the files.
100      *
101      * @param args the command line arguments.
102      * @throws IOException if there is a problem with files access
103      * @noinspection UseOfSystemOutOrSystemErr, CallToPrintStackTrace, CallToSystemExit
104      * @noinspectionreason UseOfSystemOutOrSystemErr - driver class for Checkstyle requires
105      *      usage of System.out and System.err
106      * @noinspectionreason CallToPrintStackTrace - driver class for Checkstyle must be able to
107      *      show all details in case of failure
108      * @noinspectionreason CallToSystemExit - driver class must call exit
109      */
110     public static void main(String... args) throws IOException {
111 
112         final CliOptions cliOptions = new CliOptions();
113         final CommandLine commandLine = new CommandLine(cliOptions);
114         commandLine.setUsageHelpWidth(CliOptions.HELP_WIDTH);
115         commandLine.setCaseInsensitiveEnumValuesAllowed(true);
116 
117         // provide proper exit code based on results.
118         int exitStatus = 0;
119         int errorCounter = 0;
120         try {
121             final ParseResult parseResult = commandLine.parseArgs(args);
122             if (parseResult.isVersionHelpRequested()) {
123                 printVersionToSystemOutput();
124             }
125             else if (parseResult.isUsageHelpRequested()) {
126                 commandLine.usage(System.out);
127             }
128             else {
129                 exitStatus = execute(parseResult, cliOptions);
130                 errorCounter = exitStatus;
131             }
132         }
133         catch (ParameterException exc) {
134             exitStatus = EXIT_WITH_INVALID_USER_INPUT_CODE;
135             System.err.println(exc.getMessage());
136             System.err.println("Usage: checkstyle [OPTIONS]... file(s) or folder(s) ...");
137             System.err.println("Try 'checkstyle --help' for more information.");
138         }
139         catch (CheckstyleException exc) {
140             exitStatus = EXIT_WITH_CHECKSTYLE_EXCEPTION_CODE;
141             errorCounter = 1;
142             exc.printStackTrace();
143         }
144         finally {
145             // return exit code base on validation of Checker
146             if (errorCounter > 0) {
147                 final LocalizedMessage errorCounterViolation = new LocalizedMessage(
148                         Definitions.CHECKSTYLE_BUNDLE, Main.class,
149                         ERROR_COUNTER, String.valueOf(errorCounter));
150                 // print error count statistic to error output stream,
151                 // output stream might be used by validation report content
152                 System.err.println(errorCounterViolation.getMessage());
153             }
154         }
155         Runtime.getRuntime().exit(exitStatus);
156     }
157 
158     /**
159      * Prints version string when the user requests version help (--version or -V).
160      *
161      * @noinspection UseOfSystemOutOrSystemErr
162      * @noinspectionreason UseOfSystemOutOrSystemErr - driver class for Checkstyle requires
163      *      usage of System.out and System.err
164      */
165     private static void printVersionToSystemOutput() {
166         System.out.println("Checkstyle version: " + getVersionString());
167     }
168 
169     /**
170      * Returns the version string printed when the user requests version help (--version or -V).
171      *
172      * @return a version string based on the package implementation version
173      */
174     private static String getVersionString() {
175         return Main.class.getPackage().getImplementationVersion();
176     }
177 
178     /**
179      * Validates the user input and returns {@value #EXIT_WITH_INVALID_USER_INPUT_CODE} if
180      * invalid, otherwise executes CheckStyle and returns the number of violations.
181      *
182      * @param parseResult generic access to options and parameters found on the command line
183      * @param options encapsulates options and parameters specified on the command line
184      * @return number of violations
185      * @throws CheckstyleException if something happens processing the files.
186      * @throws IOException if a file could not be read.
187      * @noinspection UseOfSystemOutOrSystemErr
188      * @noinspectionreason UseOfSystemOutOrSystemErr - driver class for Checkstyle requires
189      *      usage of System.out and System.err
190      */
191     private static int execute(ParseResult parseResult, CliOptions options)
192             throws IOException, CheckstyleException {
193 
194         final int exitStatus;
195 
196         // return error if something is wrong in arguments
197         final List<File> filesToProcess = getFilesToProcess(options);
198         final List<String> messages = options.validateCli(parseResult, filesToProcess);
199         final boolean hasMessages = !messages.isEmpty();
200         if (hasMessages) {
201             messages.forEach(System.out::println);
202             exitStatus = EXIT_WITH_INVALID_USER_INPUT_CODE;
203         }
204         else {
205             exitStatus = runCli(options, filesToProcess);
206         }
207         return exitStatus;
208     }
209 
210     /**
211      * Determines the files to process.
212      *
213      * @param options the user-specified options
214      * @return list of files to process
215      */
216     private static List<File> getFilesToProcess(CliOptions options) {
217         final List<Pattern> patternsToExclude = options.getExclusions();
218 
219         final List<File> result = new ArrayList<>();
220         for (File file : options.files) {
221             result.addAll(listFiles(file, patternsToExclude));
222         }
223         return result;
224     }
225 
226     /**
227      * Traverses a specified node looking for files to check. Found files are added to
228      * a specified list. Subdirectories are also traversed.
229      *
230      * @param node
231      *        the node to process
232      * @param patternsToExclude The list of patterns to exclude from searching or being added as
233      *        files.
234      * @return found files
235      */
236     private static List<File> listFiles(File node, List<Pattern> patternsToExclude) {
237         // could be replaced with org.apache.commons.io.FileUtils.list() method
238         // if only we add commons-io library
239         final List<File> result = new ArrayList<>();
240 
241         if (node.canRead() && !isPathExcluded(node.getAbsolutePath(), patternsToExclude)) {
242             if (node.isDirectory()) {
243                 final File[] files = node.listFiles();
244                 // listFiles() can return null, so we need to check it
245                 if (files != null) {
246                     for (File element : files) {
247                         result.addAll(listFiles(element, patternsToExclude));
248                     }
249                 }
250             }
251             else if (node.isFile()) {
252                 result.add(node);
253             }
254         }
255         return result;
256     }
257 
258     /**
259      * Checks if a directory/file {@code path} should be excluded based on if it matches one of the
260      * patterns supplied.
261      *
262      * @param path The path of the directory/file to check
263      * @param patternsToExclude The collection of patterns to exclude from searching
264      *        or being added as files.
265      * @return True if the directory/file matches one of the patterns.
266      */
267     private static boolean isPathExcluded(String path, Iterable<Pattern> patternsToExclude) {
268         boolean result = false;
269 
270         for (Pattern pattern : patternsToExclude) {
271             if (pattern.matcher(path).find()) {
272                 result = true;
273                 break;
274             }
275         }
276 
277         return result;
278     }
279 
280     /**
281      * Do execution of CheckStyle based on Command line options.
282      *
283      * @param options user-specified options
284      * @param filesToProcess the list of files whose style to check
285      * @return number of violations
286      * @throws CheckstyleException if something happens processing the files.
287      * @throws IOException if a file could not be read.
288      * @noinspection UseOfSystemOutOrSystemErr
289      * @noinspectionreason UseOfSystemOutOrSystemErr - driver class for Checkstyle requires
290      *      usage of System.out and System.err
291      */
292     private static int runCli(CliOptions options, List<File> filesToProcess)
293             throws IOException, CheckstyleException {
294         int result = 0;
295         final boolean hasSuppressionLineColumnNumber = options.suppressionLineColumnNumber != null;
296 
297         // create config helper object
298         if (options.printAst) {
299             // print AST
300             final File file = filesToProcess.getFirst();
301             final String stringAst = AstTreeStringPrinter.printFileAst(file,
302                     JavaParser.Options.WITHOUT_COMMENTS);
303             System.out.print(stringAst);
304         }
305         else if (Objects.nonNull(options.xpath)) {
306             final String branch =
307                     XpathUtil.printXpathBranch(options.xpath, filesToProcess.getFirst());
308             System.out.print(branch);
309         }
310         else if (options.printAstWithComments) {
311             final File file = filesToProcess.getFirst();
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.getFirst();
318             final String stringAst = DetailNodeTreeStringPrinter.printFileAst(file);
319             System.out.print(stringAst);
320         }
321         else if (options.printTreeWithJavadoc) {
322             final File file = filesToProcess.getFirst();
323             final String stringAst = AstTreeStringPrinter.printJavaAndJavadocTree(file);
324             System.out.print(stringAst);
325         }
326         else if (hasSuppressionLineColumnNumber) {
327             final File file = filesToProcess.getFirst();
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 CheckstyleException
360      *         when properties file could not be loaded
361      * @throws IOException
362      *         when output file could not be found
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      * @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         /** 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         /* package */ 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          * Creates a new {@code OnlyCheckstyleLoggersFilter} instance.
614          */
615         private OnlyCheckstyleLoggersFilter() {
616             // no code by default
617         }
618 
619         /**
620          * Returns whether the specified logRecord should be logged.
621          *
622          * @param logRecord the logRecord to log
623          * @return true if the logger name is in the package of this class or a subpackage
624          */
625         @Override
626         public boolean isLoggable(LogRecord logRecord) {
627             return logRecord.getLoggerName().startsWith(packageName);
628         }
629     }
630 
631     /**
632      * Command line options.
633      *
634      * @noinspection unused, FieldMayBeFinal, CanBeFinal,
635      *              MismatchedQueryAndUpdateOfCollection, LocalCanBeFinal
636      * @noinspectionreason FieldMayBeFinal - usage of picocli requires
637      *      suppression of above inspections
638      * @noinspectionreason CanBeFinal - usage of picocli requires
639      *      suppression of above inspections
640      * @noinspectionreason MismatchedQueryAndUpdateOfCollection - list of files is gathered and used
641      *      via reflection by picocli library
642      * @noinspectionreason LocalCanBeFinal - usage of picocli requires
643      *      suppression of above inspections
644      */
645     @Command(name = "checkstyle", description = "Checkstyle verifies that the specified "
646             + "source code files adhere to the specified rules. By default, violations are "
647             + "reported to standard out in plain format. Checkstyle requires a configuration "
648             + "XML file that configures the checks to apply.",
649             mixinStandardHelpOptions = true)
650     private static final class CliOptions {
651 
652         /** Width of CLI help option. */
653         private static final int HELP_WIDTH = 100;
654 
655         /** The default number of threads to use for checker and the tree walker. */
656         private static final int DEFAULT_THREAD_COUNT = 1;
657 
658         /** Name for the moduleConfig attribute 'tabWidth'. */
659         private static final String ATTRIB_TAB_WIDTH_NAME = "tabWidth";
660 
661         /** Default output format. */
662         private static final OutputFormat DEFAULT_OUTPUT_FORMAT = OutputFormat.PLAIN;
663 
664         /** Option name for output format. */
665         private static final String OUTPUT_FORMAT_OPTION = "-f";
666 
667         /**
668          * The checker threads number.
669          * This option has been skipped for CLI options intentionally.
670          *
671          */
672         private static final int CHECKER_THREADS_NUMBER = DEFAULT_THREAD_COUNT;
673 
674         /**
675          * The tree walker threads number.
676          *
677          */
678         private static final int TREE_WALKER_THREADS_NUMBER = DEFAULT_THREAD_COUNT;
679 
680         /** List of file to validate. */
681         @Parameters(arity = "1..*", paramLabel = "<files or folders>",
682                 description = "One or more source files to verify")
683         private List<File> files;
684 
685         /** Config file location. */
686         @Option(names = "-c", description = "Specifies the location of the file that defines"
687                 + " the configuration modules. The location can either be a filesystem location"
688                 + ", or a name passed to the ClassLoader.getResource() method.")
689         private String configurationFile;
690 
691         /** Output file location. */
692         @Option(names = "-o", description = "Sets the output file. Defaults to stdout.")
693         private Path outputPath;
694 
695         /** Properties file location. */
696         @Option(names = "-p", description = "Sets the property files to load.")
697         private File propertiesFile;
698 
699         /** LineNo and columnNo for the suppression. */
700         @Option(names = "-s",
701                 description = "Prints xpath suppressions at the file's line and column position. "
702                         + "Argument is the line and column number (separated by a : ) in the file "
703                         + "that the suppression should be generated for. The option cannot be used "
704                         + "with other options and requires exactly one file to run on to be "
705                         + "specified. Note that the generated result will have few queries, joined "
706                         + "by pipe(|). Together they will match all AST nodes on "
707                         + "specified line and column. You need to choose only one and recheck "
708                         + "that it works. Usage of all of them is also ok, but might result in "
709                         + "undesirable matching and suppress other issues.")
710         private String suppressionLineColumnNumber;
711 
712         /**
713          * Tab character length.
714          *
715          * @noinspection CanBeFinal
716          * @noinspectionreason CanBeFinal - we use picocli, and it uses
717          *      reflection to manage such fields
718          */
719         @Option(names = {"-w", "--tabWidth"},
720                 description = "Sets the length of the tab character. "
721                 + "Used only with -s option. Default value is ${DEFAULT-VALUE}.")
722         private int tabWidth = CommonUtil.DEFAULT_TAB_WIDTH;
723 
724         /** Switch whether to generate xpath suppressions file or not. */
725         @Option(names = {"-g", "--generate-xpath-suppression"},
726                 description = "Generates an output xpath suppression XML to use to suppress all "
727                         + "violations from user's config. Instead of printing every violation, "
728                         + "all violations will be caught and single suppressions xml file will "
729                         + "be printed out. Used only with -c option. Output "
730                         + "location can be specified with -o option.")
731         private boolean generateXpathSuppressionsFile;
732 
733         /** Switch whether to generate check and file suppressions file or not. */
734         @Option(names = {"-G", "--generate-checks-and-files-suppression"},
735                 description = "Generates an output suppression XML that will have suppress "
736                         + "elements with \"checks\" and \"files\" attributes only to use to "
737                         + "suppress all violations from user's config. Instead of printing every "
738                         + "violation, all violations will be caught and single suppressions xml "
739                         + "file will be printed out. Used only with -c option. Output "
740                         + "location can be specified with -o option.")
741         private boolean generateCheckAndFileSuppressionsFile;
742 
743         /**
744          * Output format.
745          *
746          * @noinspection CanBeFinal
747          * @noinspectionreason CanBeFinal - we use picocli, and it uses
748          *      reflection to manage such fields
749          */
750         @Option(names = "-f",
751                 description = "Specifies the output format. Valid values: "
752                 + "${COMPLETION-CANDIDATES} for XMLLogger, SarifLogger, "
753                 + "and DefaultLogger respectively. Defaults to ${DEFAULT-VALUE}.")
754         private OutputFormat format = DEFAULT_OUTPUT_FORMAT;
755 
756         /** Option that controls whether to print the AST of the file. */
757         @Option(names = {"-t", "--tree"},
758                 description = "This option is used to display the Abstract Syntax Tree (AST) "
759                         + "without any comments of the specified file. It can only be used on "
760                         + "a single file and cannot be combined with other options.")
761         private boolean printAst;
762 
763         /** Option that controls whether to print the AST of the file including comments. */
764         @Option(names = {"-T", "--treeWithComments"},
765                 description = "This option is used to display the Abstract Syntax Tree (AST) "
766                         + "with comment nodes excluding Javadoc of the specified file. It can only"
767                         + " be used on a single file and cannot be combined with other options.")
768         private boolean printAstWithComments;
769 
770         /** Option that controls whether to print the parse tree of the javadoc comment. */
771         @Option(names = {"-j", "--javadocTree"},
772                 description = "This option is used to print the Parse Tree of the Javadoc comment."
773                         + " The file has to contain only Javadoc comment content "
774                         + "excluding '/**' and '*/' at the beginning and at the end respectively. "
775                         + "It can only be used on a single file and cannot be combined "
776                         + "with other options.")
777         private boolean printJavadocTree;
778 
779         /** Option that controls whether to print the full AST of the file. */
780         @Option(names = {"-J", "--treeWithJavadoc"},
781                 description = "This option is used to display the Abstract Syntax Tree (AST) "
782                         + "with Javadoc nodes of the specified file. It can only be used on a "
783                         + "single file and cannot be combined with other options.")
784         private boolean printTreeWithJavadoc;
785 
786         /** Option that controls whether to print debug info. */
787         @Option(names = {"-d", "--debug"},
788                 description = "Prints all debug logging of CheckStyle utility.")
789         private boolean debug;
790 
791         /**
792          * Option that allows users to specify a list of paths to exclude.
793          *
794          * @noinspection CanBeFinal
795          * @noinspectionreason CanBeFinal - we use picocli, and it uses
796          *      reflection to manage such fields
797          */
798         @Option(names = {"-e", "--exclude"},
799                 description = "Directory/file to exclude from CheckStyle. The path can be the "
800                         + "full, absolute path, or relative to the current path. Multiple "
801                         + "excludes are allowed.")
802         private List<File> exclude = new ArrayList<>();
803 
804         /**
805          * Option that allows users to specify a regex of paths to exclude.
806          *
807          * @noinspection CanBeFinal
808          * @noinspectionreason CanBeFinal - we use picocli, and it uses
809          *      reflection to manage such fields
810          */
811         @Option(names = {"-x", "--exclude-regexp"},
812                 description = "Directory/file pattern to exclude from CheckStyle. Multiple "
813                         + "excludes are allowed.")
814         private List<Pattern> excludeRegex = new ArrayList<>();
815 
816         /** Switch whether to execute ignored modules or not. */
817         @Option(names = {"-E", "--executeIgnoredModules"},
818                 description = "Allows ignored modules to be run.")
819         private boolean executeIgnoredModules;
820 
821         /** Show AST branches that match xpath. */
822         @Option(names = {"-b", "--branch-matching-xpath"},
823             description = "Shows Abstract Syntax Tree(AST) branches that match given XPath query.")
824         private String xpath;
825 
826         /**
827          * Creates a new {@code CliOptions} instance.
828          */
829         private CliOptions() {
830             // no code by default
831         }
832 
833         /**
834          * Gets the list of exclusions provided through the command line arguments.
835          *
836          * @return List of exclusion patterns.
837          */
838         private List<Pattern> getExclusions() {
839             final List<Pattern> result = exclude.stream()
840                     .map(File::getAbsolutePath)
841                     .map(Pattern::quote)
842                     .map(pattern -> Pattern.compile("^" + pattern + "$"))
843                     .collect(Collectors.toCollection(ArrayList::new));
844             result.addAll(excludeRegex);
845             return result;
846         }
847 
848         /**
849          * Validates the user-specified command line options.
850          *
851          * @param parseResult used to verify if the format option was specified on the command line
852          * @param filesToProcess the list of files whose style to check
853          * @return list of violations
854          */
855         // -@cs[CyclomaticComplexity] Breaking apart will damage encapsulation
856         private List<String> validateCli(ParseResult parseResult, List<File> filesToProcess) {
857             final List<String> result = new ArrayList<>();
858             final boolean hasConfigurationFile = configurationFile != null;
859             final boolean hasSuppressionLineColumnNumber = suppressionLineColumnNumber != null;
860 
861             if (filesToProcess.isEmpty()) {
862                 result.add("Files to process must be specified, found 0.");
863             }
864             // ensure there is no conflicting options
865             else if (printAst || printAstWithComments || printJavadocTree || printTreeWithJavadoc
866                 || xpath != null) {
867                 if (suppressionLineColumnNumber != null || configurationFile != null
868                         || propertiesFile != null || outputPath != null
869                         || parseResult.hasMatchedOption(OUTPUT_FORMAT_OPTION)) {
870                     result.add("Option '-t' cannot be used with other options.");
871                 }
872                 else if (filesToProcess.size() > 1) {
873                     result.add("Printing AST is allowed for only one file.");
874                 }
875             }
876             else if (hasSuppressionLineColumnNumber) {
877                 if (configurationFile != null || propertiesFile != null
878                         || outputPath != null
879                         || parseResult.hasMatchedOption(OUTPUT_FORMAT_OPTION)) {
880                     result.add("Option '-s' cannot be used with other options.");
881                 }
882                 else if (filesToProcess.size() > 1) {
883                     result.add("Printing xpath suppressions is allowed for only one file.");
884                 }
885             }
886             else if (hasConfigurationFile) {
887                 try {
888                     // test location only
889                     CommonUtil.getUriByFilename(configurationFile);
890                 }
891                 catch (CheckstyleException ignored) {
892                     final String msg = "Could not find config XML file '%s'.";
893                     result.add(String.format(Locale.ROOT, msg, configurationFile));
894                 }
895                 result.addAll(validateOptionalCliParametersIfConfigDefined());
896             }
897             else {
898                 result.add("Must specify a config XML file.");
899             }
900 
901             return result;
902         }
903 
904         /**
905          * Validates optional command line parameters that might be used with config file.
906          *
907          * @return list of violations
908          */
909         private List<String> validateOptionalCliParametersIfConfigDefined() {
910             final List<String> result = new ArrayList<>();
911             if (propertiesFile != null && !propertiesFile.exists()) {
912                 result.add(String.format(Locale.ROOT,
913                         "Could not find file '%s'.", propertiesFile));
914             }
915             return result;
916         }
917     }
918 
919 }