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)
583                     throws IOException {
584             final AuditListener result;
585             if (this == XML) {
586                 result = new XMLLogger(out, options);
587             }
588             else if (this == SARIF) {
589                 result = new SarifLogger(out, options);
590             }
591             else {
592                 result = new DefaultLogger(out, options);
593             }
594             return result;
595         }
596 
597         /**
598          * Returns the name in lowercase.
599          *
600          * @return the enum name in lowercase
601          */
602         @Override
603         public String toString() {
604             return name().toLowerCase(Locale.ROOT);
605         }
606     }
607 
608     /** Log Filter used in debug mode. */
609     private static final class OnlyCheckstyleLoggersFilter implements Filter {
610         /** Name of the package used to filter on. */
611         private final String packageName = Main.class.getPackage().getName();
612 
613         /**
614          * Creates a new {@code OnlyCheckstyleLoggersFilter} instance.
615          */
616         private OnlyCheckstyleLoggersFilter() {
617             // no code by default
618         }
619 
620         /**
621          * Returns whether the specified logRecord should be logged.
622          *
623          * @param logRecord the logRecord to log
624          * @return true if the logger name is in the package of this class or a subpackage
625          */
626         @Override
627         public boolean isLoggable(LogRecord logRecord) {
628             return logRecord.getLoggerName().startsWith(packageName);
629         }
630     }
631 
632     /**
633      * Command line options.
634      *
635      * @noinspection unused, FieldMayBeFinal, CanBeFinal,
636      *              MismatchedQueryAndUpdateOfCollection, LocalCanBeFinal
637      * @noinspectionreason FieldMayBeFinal - usage of picocli requires
638      *      suppression of above inspections
639      * @noinspectionreason CanBeFinal - usage of picocli requires
640      *      suppression of above inspections
641      * @noinspectionreason MismatchedQueryAndUpdateOfCollection - list of files is gathered and used
642      *      via reflection by picocli library
643      * @noinspectionreason LocalCanBeFinal - usage of picocli requires
644      *      suppression of above inspections
645      */
646     @Command(name = "checkstyle", description = "Checkstyle verifies that the specified "
647             + "source code files adhere to the specified rules. By default, violations are "
648             + "reported to standard out in plain format. Checkstyle requires a configuration "
649             + "XML file that configures the checks to apply.",
650             mixinStandardHelpOptions = true)
651     private static final class CliOptions {
652 
653         /** Width of CLI help option. */
654         private static final int HELP_WIDTH = 100;
655 
656         /** The default number of threads to use for checker and the tree walker. */
657         private static final int DEFAULT_THREAD_COUNT = 1;
658 
659         /** Name for the moduleConfig attribute 'tabWidth'. */
660         private static final String ATTRIB_TAB_WIDTH_NAME = "tabWidth";
661 
662         /** Default output format. */
663         private static final OutputFormat DEFAULT_OUTPUT_FORMAT = OutputFormat.PLAIN;
664 
665         /** Option name for output format. */
666         private static final String OUTPUT_FORMAT_OPTION = "-f";
667 
668         /**
669          * The checker threads number.
670          * This option has been skipped for CLI options intentionally.
671          *
672          */
673         private static final int CHECKER_THREADS_NUMBER = DEFAULT_THREAD_COUNT;
674 
675         /**
676          * The tree walker threads number.
677          *
678          */
679         private static final int TREE_WALKER_THREADS_NUMBER = DEFAULT_THREAD_COUNT;
680 
681         /** List of file to validate. */
682         @Parameters(arity = "1..*", paramLabel = "<files or folders>",
683                 description = "One or more source files to verify")
684         private List<File> files;
685 
686         /** Config file location. */
687         @Option(names = "-c", description = "Specifies the location of the file that defines"
688                 + " the configuration modules. The location can either be a filesystem location"
689                 + ", or a name passed to the ClassLoader.getResource() method.")
690         private String configurationFile;
691 
692         /** Output file location. */
693         @Option(names = "-o", description = "Sets the output file. Defaults to stdout.")
694         private Path outputPath;
695 
696         /** Properties file location. */
697         @Option(names = "-p", description = "Sets the property files to load.")
698         private File propertiesFile;
699 
700         /** LineNo and columnNo for the suppression. */
701         @Option(names = "-s",
702                 description = "Prints xpath suppressions at the file's line and column position. "
703                         + "Argument is the line and column number (separated by a : ) in the file "
704                         + "that the suppression should be generated for. The option cannot be used "
705                         + "with other options and requires exactly one file to run on to be "
706                         + "specified. Note that the generated result will have few queries, joined "
707                         + "by pipe(|). Together they will match all AST nodes on "
708                         + "specified line and column. You need to choose only one and recheck "
709                         + "that it works. Usage of all of them is also ok, but might result in "
710                         + "undesirable matching and suppress other issues.")
711         private String suppressionLineColumnNumber;
712 
713         /**
714          * Tab character length.
715          *
716          * @noinspection CanBeFinal
717          * @noinspectionreason CanBeFinal - we use picocli, and it uses
718          *      reflection to manage such fields
719          */
720         @Option(names = {"-w", "--tabWidth"},
721                 description = "Sets the length of the tab character. "
722                 + "Used only with -s option. Default value is ${DEFAULT-VALUE}.")
723         private int tabWidth = CommonUtil.DEFAULT_TAB_WIDTH;
724 
725         /** Switch whether to generate xpath suppressions file or not. */
726         @Option(names = {"-g", "--generate-xpath-suppression"},
727                 description = "Generates an output xpath suppression XML to use to suppress all "
728                         + "violations from user's config. Instead of printing every violation, "
729                         + "all violations will be caught and single suppressions xml file will "
730                         + "be printed out. Used only with -c option. Output "
731                         + "location can be specified with -o option.")
732         private boolean generateXpathSuppressionsFile;
733 
734         /** Switch whether to generate check and file suppressions file or not. */
735         @Option(names = {"-G", "--generate-checks-and-files-suppression"},
736                 description = "Generates an output suppression XML that will have suppress "
737                         + "elements with \"checks\" and \"files\" attributes only to use to "
738                         + "suppress all violations from user's config. Instead of printing every "
739                         + "violation, all violations will be caught and single suppressions xml "
740                         + "file will be printed out. Used only with -c option. Output "
741                         + "location can be specified with -o option.")
742         private boolean generateCheckAndFileSuppressionsFile;
743 
744         /**
745          * Output format.
746          *
747          * @noinspection CanBeFinal
748          * @noinspectionreason CanBeFinal - we use picocli, and it uses
749          *      reflection to manage such fields
750          */
751         @Option(names = "-f",
752                 description = "Specifies the output format. Valid values: "
753                 + "${COMPLETION-CANDIDATES} for XMLLogger, SarifLogger, "
754                 + "and DefaultLogger respectively. Defaults to ${DEFAULT-VALUE}.")
755         private OutputFormat format = DEFAULT_OUTPUT_FORMAT;
756 
757         /** Option that controls whether to print the AST of the file. */
758         @Option(names = {"-t", "--tree"},
759                 description = "This option is used to display the Abstract Syntax Tree (AST) "
760                         + "without any comments of the specified file. It can only be used on "
761                         + "a single file and cannot be combined with other options.")
762         private boolean printAst;
763 
764         /** Option that controls whether to print the AST of the file including comments. */
765         @Option(names = {"-T", "--treeWithComments"},
766                 description = "This option is used to display the Abstract Syntax Tree (AST) "
767                         + "with comment nodes excluding Javadoc of the specified file. It can only"
768                         + " be used on a single file and cannot be combined with other options.")
769         private boolean printAstWithComments;
770 
771         /** Option that controls whether to print the parse tree of the javadoc comment. */
772         @Option(names = {"-j", "--javadocTree"},
773                 description = "This option is used to print the Parse Tree of the Javadoc comment."
774                         + " The file has to contain only Javadoc comment content "
775                         + "excluding '/**' and '*/' at the beginning and at the end respectively. "
776                         + "It can only be used on a single file and cannot be combined "
777                         + "with other options.")
778         private boolean printJavadocTree;
779 
780         /** Option that controls whether to print the full AST of the file. */
781         @Option(names = {"-J", "--treeWithJavadoc"},
782                 description = "This option is used to display the Abstract Syntax Tree (AST) "
783                         + "with Javadoc nodes of the specified file. It can only be used on a "
784                         + "single file and cannot be combined with other options.")
785         private boolean printTreeWithJavadoc;
786 
787         /** Option that controls whether to print debug info. */
788         @Option(names = {"-d", "--debug"},
789                 description = "Prints all debug logging of CheckStyle utility.")
790         private boolean debug;
791 
792         /**
793          * Option that allows users to specify a list of paths to exclude.
794          *
795          * @noinspection CanBeFinal
796          * @noinspectionreason CanBeFinal - we use picocli, and it uses
797          *      reflection to manage such fields
798          */
799         @Option(names = {"-e", "--exclude"},
800                 description = "Directory/file to exclude from CheckStyle. The path can be the "
801                         + "full, absolute path, or relative to the current path. Multiple "
802                         + "excludes are allowed.")
803         private List<File> exclude = new ArrayList<>();
804 
805         /**
806          * Option that allows users to specify a regex of paths to exclude.
807          *
808          * @noinspection CanBeFinal
809          * @noinspectionreason CanBeFinal - we use picocli, and it uses
810          *      reflection to manage such fields
811          */
812         @Option(names = {"-x", "--exclude-regexp"},
813                 description = "Directory/file pattern to exclude from CheckStyle. Multiple "
814                         + "excludes are allowed.")
815         private List<Pattern> excludeRegex = new ArrayList<>();
816 
817         /** Switch whether to execute ignored modules or not. */
818         @Option(names = {"-E", "--executeIgnoredModules"},
819                 description = "Allows ignored modules to be run.")
820         private boolean executeIgnoredModules;
821 
822         /** Show AST branches that match xpath. */
823         @Option(names = {"-b", "--branch-matching-xpath"},
824             description = "Shows Abstract Syntax Tree(AST) branches that match given XPath query.")
825         private String xpath;
826 
827         /**
828          * Creates a new {@code CliOptions} instance.
829          */
830         private CliOptions() {
831             // no code by default
832         }
833 
834         /**
835          * Gets the list of exclusions provided through the command line arguments.
836          *
837          * @return List of exclusion patterns.
838          */
839         private List<Pattern> getExclusions() {
840             final List<Pattern> result = exclude.stream()
841                     .map(File::getAbsolutePath)
842                     .map(Pattern::quote)
843                     .map(pattern -> Pattern.compile("^" + pattern + "$"))
844                     .collect(Collectors.toCollection(ArrayList::new));
845             result.addAll(excludeRegex);
846             return result;
847         }
848 
849         /**
850          * Validates the user-specified command line options.
851          *
852          * @param parseResult used to verify if the format option was specified on the command line
853          * @param filesToProcess the list of files whose style to check
854          * @return list of violations
855          */
856         // -@cs[CyclomaticComplexity] Breaking apart will damage encapsulation
857         private List<String> validateCli(ParseResult parseResult, List<File> filesToProcess) {
858             final List<String> result = new ArrayList<>();
859             final boolean hasConfigurationFile = configurationFile != null;
860             final boolean hasSuppressionLineColumnNumber = suppressionLineColumnNumber != null;
861 
862             if (filesToProcess.isEmpty()) {
863                 result.add("Files to process must be specified, found 0.");
864             }
865             // ensure there is no conflicting options
866             else if (printAst || printAstWithComments || printJavadocTree || printTreeWithJavadoc
867                 || xpath != null) {
868                 if (suppressionLineColumnNumber != null || configurationFile != null
869                         || propertiesFile != null || outputPath != null
870                         || parseResult.hasMatchedOption(OUTPUT_FORMAT_OPTION)) {
871                     result.add("Option '-t' cannot be used with other options.");
872                 }
873                 else if (filesToProcess.size() > 1) {
874                     result.add("Printing AST is allowed for only one file.");
875                 }
876             }
877             else if (hasSuppressionLineColumnNumber) {
878                 if (configurationFile != null || propertiesFile != null
879                         || outputPath != null
880                         || parseResult.hasMatchedOption(OUTPUT_FORMAT_OPTION)) {
881                     result.add("Option '-s' cannot be used with other options.");
882                 }
883                 else if (filesToProcess.size() > 1) {
884                     result.add("Printing xpath suppressions is allowed for only one file.");
885                 }
886             }
887             else if (hasConfigurationFile) {
888                 try {
889                     // test location only
890                     CommonUtil.getUriByFilename(configurationFile);
891                 }
892                 catch (CheckstyleException ignored) {
893                     final String msg = "Could not find config XML file '%s'.";
894                     result.add(String.format(Locale.ROOT, msg, configurationFile));
895                 }
896                 result.addAll(validateOptionalCliParametersIfConfigDefined());
897             }
898             else {
899                 result.add("Must specify a config XML file.");
900             }
901 
902             return result;
903         }
904 
905         /**
906          * Validates optional command line parameters that might be used with config file.
907          *
908          * @return list of violations
909          */
910         private List<String> validateOptionalCliParametersIfConfigDefined() {
911             final List<String> result = new ArrayList<>();
912             if (propertiesFile != null && !propertiesFile.exists()) {
913                 result.add(String.format(Locale.ROOT,
914                         "Could not find file '%s'.", propertiesFile));
915             }
916             return result;
917         }
918     }
919 
920 }