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