001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2026 the original author or authors.
004//
005// This library is free software; you can redistribute it and/or
006// modify it under the terms of the GNU Lesser General Public
007// License as published by the Free Software Foundation; either
008// version 2.1 of the License, or (at your option) any later version.
009//
010// This library is distributed in the hope that it will be useful,
011// but WITHOUT ANY WARRANTY; without even the implied warranty of
012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013// Lesser General Public License for more details.
014//
015// You should have received a copy of the GNU Lesser General Public
016// License along with this library; if not, write to the Free Software
017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
018///////////////////////////////////////////////////////////////////////////////////////////////
019
020package com.puppycrawl.tools.checkstyle;
021
022import java.io.File;
023import java.io.IOException;
024import java.io.InputStream;
025import java.io.OutputStream;
026import java.nio.file.Files;
027import java.nio.file.Path;
028import java.util.ArrayList;
029import java.util.List;
030import java.util.Locale;
031import java.util.Objects;
032import java.util.Properties;
033import java.util.logging.ConsoleHandler;
034import java.util.logging.Filter;
035import java.util.logging.Level;
036import java.util.logging.LogRecord;
037import java.util.logging.Logger;
038import java.util.regex.Pattern;
039import java.util.stream.Collectors;
040
041import org.apache.commons.logging.Log;
042import org.apache.commons.logging.LogFactory;
043
044import com.puppycrawl.tools.checkstyle.AbstractAutomaticBean.OutputStreamOptions;
045import com.puppycrawl.tools.checkstyle.api.AuditEvent;
046import com.puppycrawl.tools.checkstyle.api.AuditListener;
047import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
048import com.puppycrawl.tools.checkstyle.api.Configuration;
049import com.puppycrawl.tools.checkstyle.api.RootModule;
050import com.puppycrawl.tools.checkstyle.utils.ChainedPropertyUtil;
051import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
052import com.puppycrawl.tools.checkstyle.utils.XpathUtil;
053import picocli.CommandLine;
054import picocli.CommandLine.Command;
055import picocli.CommandLine.Option;
056import picocli.CommandLine.ParameterException;
057import picocli.CommandLine.Parameters;
058import picocli.CommandLine.ParseResult;
059
060/**
061 * Wrapper command line program for the Checker.
062 */
063public final class Main {
064
065    /**
066     * A key pointing to the error counter message in the "messages.properties" file.
067     */
068    public static final String ERROR_COUNTER = "Main.errorCounter";
069    /**
070     * A key pointing to the load properties exception message in the "messages.properties" file.
071     */
072    public static final String LOAD_PROPERTIES_EXCEPTION = "Main.loadProperties";
073    /**
074     * A key pointing to the create listener exception message in the "messages.properties" file.
075     */
076    public static final String CREATE_LISTENER_EXCEPTION = "Main.createListener";
077
078    /** Logger for Main. */
079    private static final Log LOG = LogFactory.getLog(Main.class);
080
081    /** Exit code returned when user specified invalid command line arguments. */
082    private static final int EXIT_WITH_INVALID_USER_INPUT_CODE = -1;
083
084    /** Exit code returned when execution finishes with {@link CheckstyleException}. */
085    private static final int EXIT_WITH_CHECKSTYLE_EXCEPTION_CODE = -2;
086
087    /** Maximum exit code, so the number of errors can not wrap around to 0. */
088    private static final int MAX_EXIT_CODE = 255;
089
090    /**
091     * Client code should not create instances of this class, but use
092     * {@link #main(String[])} method instead.
093     */
094    private Main() {
095    }
096
097    /**
098     * Loops over the files specified checking them for errors. The exit code
099     * is the number of errors found in all the files, but no more than 255,
100     * so it can not wrap around to 0 on a multiple of 256 errors.
101     *
102     * @param args the command line arguments.
103     * @throws IOException if there is a problem with files access
104     * @noinspection UseOfSystemOutOrSystemErr, CallToPrintStackTrace, CallToSystemExit
105     * @noinspectionreason UseOfSystemOutOrSystemErr - driver class for Checkstyle requires
106     *      usage of System.out and System.err
107     * @noinspectionreason CallToPrintStackTrace - driver class for Checkstyle must be able to
108     *      show all details in case of failure
109     * @noinspectionreason CallToSystemExit - driver class must call exit
110     */
111    public static void main(String... args) throws IOException {
112
113        final CliOptions cliOptions = new CliOptions();
114        final CommandLine commandLine = new CommandLine(cliOptions);
115        commandLine.setUsageHelpWidth(CliOptions.HELP_WIDTH);
116        commandLine.setCaseInsensitiveEnumValuesAllowed(true);
117
118        // provide proper exit code based on results.
119        int exitStatus = 0;
120        int errorCounter = 0;
121        try {
122            final ParseResult parseResult = commandLine.parseArgs(args);
123            if (parseResult.isVersionHelpRequested()) {
124                printVersionToSystemOutput();
125            }
126            else if (parseResult.isUsageHelpRequested()) {
127                commandLine.usage(System.out);
128            }
129            else {
130                exitStatus = execute(parseResult, cliOptions);
131                errorCounter = exitStatus;
132            }
133        }
134        catch (ParameterException exc) {
135            exitStatus = EXIT_WITH_INVALID_USER_INPUT_CODE;
136            System.err.println(exc.getMessage());
137            System.err.println("Usage: checkstyle [OPTIONS]... file(s) or folder(s) ...");
138            System.err.println("Try 'checkstyle --help' for more information.");
139        }
140        catch (CheckstyleException exc) {
141            exitStatus = EXIT_WITH_CHECKSTYLE_EXCEPTION_CODE;
142            errorCounter = 1;
143            exc.printStackTrace();
144        }
145        finally {
146            // return exit code base on validation of Checker
147            if (errorCounter > 0) {
148                final LocalizedMessage errorCounterViolation = new LocalizedMessage(
149                        Definitions.CHECKSTYLE_BUNDLE, Main.class,
150                        ERROR_COUNTER, String.valueOf(errorCounter));
151                // print error count statistic to error output stream,
152                // output stream might be used by validation report content
153                System.err.println(errorCounterViolation.getMessage());
154            }
155        }
156        Runtime.getRuntime().exit(Math.min(exitStatus, MAX_EXIT_CODE));
157    }
158
159    /**
160     * Prints version string when the user requests version help (--version or -V).
161     *
162     * @noinspection UseOfSystemOutOrSystemErr
163     * @noinspectionreason UseOfSystemOutOrSystemErr - driver class for Checkstyle requires
164     *      usage of System.out and System.err
165     */
166    private static void printVersionToSystemOutput() {
167        System.out.println("Checkstyle version: " + getVersionString());
168    }
169
170    /**
171     * Returns the version string printed when the user requests version help (--version or -V).
172     *
173     * @return a version string based on the package implementation version
174     */
175    private static String getVersionString() {
176        return Main.class.getPackage().getImplementationVersion();
177    }
178
179    /**
180     * Validates the user input and returns {@value #EXIT_WITH_INVALID_USER_INPUT_CODE} if
181     * invalid, otherwise executes CheckStyle and returns the number of violations.
182     *
183     * @param parseResult generic access to options and parameters found on the command line
184     * @param options encapsulates options and parameters specified on the command line
185     * @return number of violations
186     * @throws CheckstyleException if something happens processing the files.
187     * @throws IOException if a file could not be read.
188     * @noinspection UseOfSystemOutOrSystemErr
189     * @noinspectionreason UseOfSystemOutOrSystemErr - driver class for Checkstyle requires
190     *      usage of System.out and System.err
191     */
192    private static int execute(ParseResult parseResult, CliOptions options)
193            throws IOException, CheckstyleException {
194
195        final int exitStatus;
196
197        // return error if something is wrong in arguments
198        final List<File> filesToProcess = getFilesToProcess(options);
199        final List<String> messages = options.validateCli(parseResult, filesToProcess);
200        final boolean hasMessages = !messages.isEmpty();
201        if (hasMessages) {
202            messages.forEach(System.out::println);
203            exitStatus = EXIT_WITH_INVALID_USER_INPUT_CODE;
204        }
205        else {
206            exitStatus = runCli(options, filesToProcess);
207        }
208        return exitStatus;
209    }
210
211    /**
212     * Determines the files to process.
213     *
214     * @param options the user-specified options
215     * @return list of files to process
216     */
217    private static List<File> getFilesToProcess(CliOptions options) {
218        final List<Pattern> patternsToExclude = options.getExclusions();
219
220        final List<File> result = new ArrayList<>();
221        for (File file : options.files) {
222            result.addAll(listFiles(file, patternsToExclude));
223        }
224        return result;
225    }
226
227    /**
228     * Traverses a specified node looking for files to check. Found files are added to
229     * a specified list. Subdirectories are also traversed.
230     *
231     * @param node
232     *        the node to process
233     * @param patternsToExclude The list of patterns to exclude from searching or being added as
234     *        files.
235     * @return found files
236     */
237    private static List<File> listFiles(File node, List<Pattern> patternsToExclude) {
238        // could be replaced with org.apache.commons.io.FileUtils.list() method
239        // if only we add commons-io library
240        final List<File> result = new ArrayList<>();
241
242        if (node.canRead() && !isPathExcluded(node.getAbsolutePath(), patternsToExclude)) {
243            if (node.isDirectory()) {
244                final File[] files = node.listFiles();
245                // listFiles() can return null, so we need to check it
246                if (files != null) {
247                    for (File element : files) {
248                        result.addAll(listFiles(element, patternsToExclude));
249                    }
250                }
251            }
252            else if (node.isFile()) {
253                result.add(node);
254            }
255        }
256        return result;
257    }
258
259    /**
260     * Checks if a directory/file {@code path} should be excluded based on if it matches one of the
261     * patterns supplied.
262     *
263     * @param path The path of the directory/file to check
264     * @param patternsToExclude The collection of patterns to exclude from searching
265     *        or being added as files.
266     * @return True if the directory/file matches one of the patterns.
267     */
268    private static boolean isPathExcluded(String path, Iterable<Pattern> patternsToExclude) {
269        boolean result = false;
270
271        for (Pattern pattern : patternsToExclude) {
272            if (pattern.matcher(path).find()) {
273                result = true;
274                break;
275            }
276        }
277
278        return result;
279    }
280
281    /**
282     * Do execution of CheckStyle based on Command line options.
283     *
284     * @param options user-specified options
285     * @param filesToProcess the list of files whose style to check
286     * @return number of violations
287     * @throws CheckstyleException if something happens processing the files.
288     * @throws IOException if a file could not be read.
289     * @noinspection UseOfSystemOutOrSystemErr
290     * @noinspectionreason UseOfSystemOutOrSystemErr - driver class for Checkstyle requires
291     *      usage of System.out and System.err
292     */
293    private static int runCli(CliOptions options, List<File> filesToProcess)
294            throws IOException, CheckstyleException {
295        int result = 0;
296        final boolean hasSuppressionLineColumnNumber = options.suppressionLineColumnNumber != null;
297
298        // create config helper object
299        if (options.printAst) {
300            // print AST
301            final File file = filesToProcess.getFirst();
302            final String stringAst = AstTreeStringPrinter.printFileAst(file,
303                    JavaParser.Options.WITHOUT_COMMENTS);
304            System.out.print(stringAst);
305        }
306        else if (Objects.nonNull(options.xpath)) {
307            final String branch =
308                    XpathUtil.printXpathBranch(options.xpath, filesToProcess.getFirst());
309            System.out.print(branch);
310        }
311        else if (options.printAstWithComments) {
312            final File file = filesToProcess.getFirst();
313            final String stringAst = AstTreeStringPrinter.printFileAst(file,
314                    JavaParser.Options.WITH_COMMENTS);
315            System.out.print(stringAst);
316        }
317        else if (options.printJavadocTree) {
318            final File file = filesToProcess.getFirst();
319            final String stringAst = DetailNodeTreeStringPrinter.printFileAst(file);
320            System.out.print(stringAst);
321        }
322        else if (options.printTreeWithJavadoc) {
323            final File file = filesToProcess.getFirst();
324            final String stringAst = AstTreeStringPrinter.printJavaAndJavadocTree(file);
325            System.out.print(stringAst);
326        }
327        else if (hasSuppressionLineColumnNumber) {
328            final File file = filesToProcess.getFirst();
329            final String stringSuppressions =
330                    SuppressionsStringPrinter.printSuppressions(file,
331                            options.suppressionLineColumnNumber, options.tabWidth);
332            System.out.print(stringSuppressions);
333        }
334        else {
335            if (options.debug) {
336                final Logger parentLogger = Logger.getLogger(Main.class.getName()).getParent();
337                final ConsoleHandler handler = new ConsoleHandler();
338                handler.setLevel(Level.FINEST);
339                handler.setFilter(new OnlyCheckstyleLoggersFilter());
340                parentLogger.addHandler(handler);
341                parentLogger.setLevel(Level.FINEST);
342            }
343            if (LOG.isDebugEnabled()) {
344                LOG.debug("Checkstyle debug logging enabled");
345            }
346
347            // run Checker
348            result = runCheckstyle(options, filesToProcess);
349        }
350
351        return result;
352    }
353
354    /**
355     * Executes required Checkstyle actions based on passed parameters.
356     *
357     * @param options user-specified options
358     * @param filesToProcess the list of files whose style to check
359     * @return number of violations of ERROR level
360     * @throws CheckstyleException
361     *         when properties file could not be loaded
362     * @throws IOException
363     *         when output file could not be found
364     */
365    private static int runCheckstyle(CliOptions options, List<File> filesToProcess)
366            throws CheckstyleException, IOException {
367        // setup the properties
368        final Properties props;
369
370        if (options.propertiesFile == null) {
371            props = System.getProperties();
372        }
373        else {
374            props = loadProperties(options.propertiesFile);
375        }
376
377        // create a configuration
378
379        final ConfigurationLoader.IgnoredModulesOptions ignoredModulesOptions;
380        if (options.executeIgnoredModules) {
381            ignoredModulesOptions = ConfigurationLoader.IgnoredModulesOptions.EXECUTE;
382        }
383        else {
384            ignoredModulesOptions = ConfigurationLoader.IgnoredModulesOptions.OMIT;
385        }
386
387        final ThreadModeSettings multiThreadModeSettings =
388                new ThreadModeSettings(CliOptions.CHECKER_THREADS_NUMBER,
389                CliOptions.TREE_WALKER_THREADS_NUMBER);
390        final Configuration config = ConfigurationLoader.loadConfiguration(
391                options.configurationFile, new PropertiesExpander(props),
392                ignoredModulesOptions, multiThreadModeSettings);
393
394        // create RootModule object and run it
395        final int errorCounter;
396        final ClassLoader moduleClassLoader = Checker.class.getClassLoader();
397        final RootModule rootModule = getRootModule(config.getName(), moduleClassLoader);
398
399        try {
400            final AuditListener listener;
401            if (options.generateXpathSuppressionsFile) {
402                // create filter to print generated xpath suppressions file
403                final Configuration treeWalkerConfig = getTreeWalkerConfig(config);
404                if (treeWalkerConfig != null) {
405                    final DefaultConfiguration moduleConfig =
406                            new DefaultConfiguration(
407                                    XpathFileGeneratorAstFilter.class.getName());
408                    moduleConfig.addProperty(CliOptions.ATTRIB_TAB_WIDTH_NAME,
409                            String.valueOf(options.tabWidth));
410                    ((DefaultConfiguration) treeWalkerConfig).addChild(moduleConfig);
411                }
412
413                listener = new XpathFileGeneratorAuditListener(getOutputStream(options.outputPath),
414                        getOutputStreamOptions(options.outputPath));
415            }
416            else if (options.generateCheckAndFileSuppressionsFile) {
417                listener = new ChecksAndFilesSuppressionFileGeneratorAuditListener(
418                        getOutputStream(options.outputPath),
419                        getOutputStreamOptions(options.outputPath));
420            }
421            else {
422                listener = createListener(options.format, options.outputPath);
423            }
424
425            rootModule.setModuleClassLoader(moduleClassLoader);
426            rootModule.configure(config);
427            rootModule.addListener(listener);
428
429            // run RootModule
430            errorCounter = rootModule.process(filesToProcess);
431        }
432        finally {
433            rootModule.destroy();
434        }
435
436        return errorCounter;
437    }
438
439    /**
440     * Loads properties from a File.
441     *
442     * @param file
443     *        the properties file
444     * @return the properties in file
445     * @throws CheckstyleException
446     *         when could not load properties file
447     */
448    private static Properties loadProperties(File file)
449            throws CheckstyleException {
450        final Properties properties = new Properties();
451
452        try (InputStream stream = Files.newInputStream(file.toPath())) {
453            properties.load(stream);
454        }
455        catch (final IOException exc) {
456            final LocalizedMessage loadPropertiesExceptionMessage = new LocalizedMessage(
457                    Definitions.CHECKSTYLE_BUNDLE, Main.class,
458                    LOAD_PROPERTIES_EXCEPTION, file.getAbsolutePath());
459            throw new CheckstyleException(loadPropertiesExceptionMessage.getMessage(), exc);
460        }
461
462        return ChainedPropertyUtil.getResolvedProperties(properties);
463    }
464
465    /**
466     * Creates a new instance of the root module that will control and run 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
566        /** XML output format. */
567        XML,
568        /** SARIF output format. */
569        SARIF,
570        /** Plain output format. */
571        PLAIN;
572
573        /**
574         * Returns a new AuditListener for this OutputFormat.
575         *
576         * @param out the output stream
577         * @param options the output stream options
578         * @return a new AuditListener for this OutputFormat
579         * @throws IOException if there is any IO exception during logger initialization
580         */
581        /* package */ AuditListener createListener(
582            OutputStream out,
583            OutputStreamOptions options)
584                    throws IOException {
585            final AuditListener result;
586            if (this == XML) {
587                result = new XMLLogger(out, options);
588            }
589            else if (this == SARIF) {
590                result = new SarifLogger(out, options);
591            }
592            else {
593                result = new DefaultLogger(out, options);
594            }
595            return result;
596        }
597
598        /**
599         * Returns the name in lowercase.
600         *
601         * @return the enum name in lowercase
602         */
603        @Override
604        public String toString() {
605            return name().toLowerCase(Locale.ROOT);
606        }
607
608    }
609
610    /** Log Filter used in debug mode. */
611    private static final class OnlyCheckstyleLoggersFilter implements Filter {
612
613        /** Name of the package used to filter on. */
614        private final String packageName = Main.class.getPackage().getName();
615
616        /**
617         * Creates a new {@code OnlyCheckstyleLoggersFilter} instance.
618         */
619        private OnlyCheckstyleLoggersFilter() {
620            // no code by default
621        }
622
623        /**
624         * Returns whether the specified logRecord should be logged.
625         *
626         * @param logRecord the logRecord to log
627         * @return true if the logger name is in the package of this class or a subpackage
628         */
629        @Override
630        public boolean isLoggable(LogRecord logRecord) {
631            return logRecord.getLoggerName().startsWith(packageName);
632        }
633
634    }
635
636    /**
637     * Command line options.
638     *
639     * @noinspection unused, FieldMayBeFinal, CanBeFinal,
640     *              MismatchedQueryAndUpdateOfCollection, LocalCanBeFinal
641     * @noinspectionreason FieldMayBeFinal - usage of picocli requires
642     *      suppression of above inspections
643     * @noinspectionreason CanBeFinal - usage of picocli requires suppression of above inspections
644     * @noinspectionreason MismatchedQueryAndUpdateOfCollection - list of files is gathered and used
645     *      via reflection by picocli library
646     * @noinspectionreason LocalCanBeFinal - usage of picocli requires
647     *      suppression of above inspections
648     */
649    @Command(name = "checkstyle", description = "Checkstyle verifies that the specified "
650            + "source code files adhere to the specified rules. By default, violations are "
651            + "reported to standard out in plain format. Checkstyle requires a configuration "
652            + "XML file that configures the checks to apply.",
653            mixinStandardHelpOptions = true)
654    private static final class CliOptions {
655
656        /** Width of CLI help option. */
657        private static final int HELP_WIDTH = 100;
658
659        /** The default number of threads to use for checker and the tree walker. */
660        private static final int DEFAULT_THREAD_COUNT = 1;
661
662        /** Name for the moduleConfig attribute 'tabWidth'. */
663        private static final String ATTRIB_TAB_WIDTH_NAME = "tabWidth";
664
665        /** Default output format. */
666        private static final OutputFormat DEFAULT_OUTPUT_FORMAT = OutputFormat.PLAIN;
667
668        /** Option name for output format. */
669        private static final String OUTPUT_FORMAT_OPTION = "-f";
670
671        /**
672         * The checker threads number.
673         * This option has been skipped for CLI options intentionally.
674         *
675         */
676        private static final int CHECKER_THREADS_NUMBER = DEFAULT_THREAD_COUNT;
677
678        /**
679         * The tree walker threads number.
680         *
681         */
682        private static final int TREE_WALKER_THREADS_NUMBER = DEFAULT_THREAD_COUNT;
683
684        /** List of file to validate. */
685        @Parameters(arity = "1..*", paramLabel = "<files or folders>",
686                description = "One or more source files to verify")
687        private List<File> files;
688
689        /** Config file location. */
690        @Option(names = "-c", description = "Specifies the location of the file that defines"
691                + " the configuration modules. The location can either be a filesystem location"
692                + ", or a name passed to the ClassLoader.getResource() method.")
693        private String configurationFile;
694
695        /** Output file location. */
696        @Option(names = "-o", description = "Sets the output file. Defaults to stdout.")
697        private Path outputPath;
698
699        /** Properties file location. */
700        @Option(names = "-p", description = "Sets the property files to load.")
701        private File propertiesFile;
702
703        /** LineNo and columnNo for the suppression. */
704        @Option(names = "-s",
705                description = "Prints xpath suppressions at the file's line and column position. "
706                        + "Argument is the line and column number (separated by a : ) in the file "
707                        + "that the suppression should be generated for. The option cannot be used "
708                        + "with other options and requires exactly one file to run on to be "
709                        + "specified. Note that the generated result will have few queries, joined "
710                        + "by pipe(|). Together they will match all AST nodes on "
711                        + "specified line and column. You need to choose only one and recheck "
712                        + "that it works. Usage of all of them is also ok, but might result in "
713                        + "undesirable matching and suppress other issues.")
714        private String suppressionLineColumnNumber;
715
716        /**
717         * Tab character length.
718         *
719         * @noinspection CanBeFinal
720         * @noinspectionreason CanBeFinal - we use picocli, and it uses
721         *      reflection to manage such fields
722         */
723        @Option(names = {"-w", "--tabWidth"},
724                description = "Sets the length of the tab character. "
725                + "Used only with -s option. Default value is ${DEFAULT-VALUE}.")
726        private int tabWidth = CommonUtil.DEFAULT_TAB_WIDTH;
727
728        /** Switch whether to generate xpath suppressions file or not. */
729        @Option(names = {"-g", "--generate-xpath-suppression"},
730                description = "Generates an output xpath suppression XML to use to suppress all "
731                        + "violations from user's config. Instead of printing every violation, "
732                        + "all violations will be caught and single suppressions xml file will "
733                        + "be printed out. Used only with -c option. Output "
734                        + "location can be specified with -o option.")
735        private boolean generateXpathSuppressionsFile;
736
737        /** Switch whether to generate check and file suppressions file or not. */
738        @Option(names = {"-G", "--generate-checks-and-files-suppression"},
739                description = "Generates an output suppression XML that will have suppress "
740                        + "elements with \"checks\" and \"files\" attributes only to use to "
741                        + "suppress all violations from user's config. Instead of printing every "
742                        + "violation, all violations will be caught and single suppressions xml "
743                        + "file will be printed out. Used only with -c option. Output "
744                        + "location can be specified with -o option.")
745        private boolean generateCheckAndFileSuppressionsFile;
746
747        /**
748         * Output format.
749         *
750         * @noinspection CanBeFinal
751         * @noinspectionreason CanBeFinal - we use picocli, and it uses
752         *      reflection to manage such fields
753         */
754        @Option(names = "-f",
755                description = "Specifies the output format. Valid values: "
756                + "${COMPLETION-CANDIDATES} for XMLLogger, SarifLogger, "
757                + "and DefaultLogger respectively. Defaults to ${DEFAULT-VALUE}.")
758        private OutputFormat format = DEFAULT_OUTPUT_FORMAT;
759
760        /** Option that controls whether to print the AST of the file. */
761        @Option(names = {"-t", "--tree"},
762                description = "This option is used to display the Abstract Syntax Tree (AST) "
763                        + "without any comments of the specified file. It can only be used on "
764                        + "a single file and cannot be combined with other options.")
765        private boolean printAst;
766
767        /** Option that controls whether to print the AST of the file including comments. */
768        @Option(names = {"-T", "--treeWithComments"},
769                description = "This option is used to display the Abstract Syntax Tree (AST) "
770                        + "with comment nodes excluding Javadoc of the specified file. It can only"
771                        + " be used on a single file and cannot be combined with other options.")
772        private boolean printAstWithComments;
773
774        /** Option that controls whether to print the parse tree of the javadoc comment. */
775        @Option(names = {"-j", "--javadocTree"},
776                description = "This option is used to print the Parse Tree of the Javadoc comment."
777                        + " The file has to contain only Javadoc comment content "
778                        + "excluding '/**' and '*/' at the beginning and at the end respectively. "
779                        + "It can only be used on a single file and cannot be combined "
780                        + "with other options.")
781        private boolean printJavadocTree;
782
783        /** Option that controls whether to print the full AST of the file. */
784        @Option(names = {"-J", "--treeWithJavadoc"},
785                description = "This option is used to display the Abstract Syntax Tree (AST) "
786                        + "with Javadoc nodes of the specified file. It can only be used on a "
787                        + "single file and cannot be combined with other options.")
788        private boolean printTreeWithJavadoc;
789
790        /** Option that controls whether to print debug info. */
791        @Option(names = {"-d", "--debug"},
792                description = "Prints all debug logging of CheckStyle utility.")
793        private boolean debug;
794
795        /**
796         * Option that allows users to specify a list of paths to exclude.
797         *
798         * @noinspection CanBeFinal
799         * @noinspectionreason CanBeFinal - we use picocli, and it uses
800         *      reflection to manage such fields
801         */
802        @Option(names = {"-e", "--exclude"},
803                description = "Directory/file to exclude from CheckStyle. The path can be the "
804                        + "full, absolute path, or relative to the current path. Multiple "
805                        + "excludes are allowed.")
806        private List<File> exclude = new ArrayList<>();
807
808        /**
809         * Option that allows users to specify a regex of paths to exclude.
810         *
811         * @noinspection CanBeFinal
812         * @noinspectionreason CanBeFinal - we use picocli, and it uses
813         *      reflection to manage such fields
814         */
815        @Option(names = {"-x", "--exclude-regexp"},
816                description = "Directory/file pattern to exclude from CheckStyle. Multiple "
817                        + "excludes are allowed.")
818        private List<Pattern> excludeRegex = new ArrayList<>();
819
820        /** Switch whether to execute ignored modules or not. */
821        @Option(names = {"-E", "--executeIgnoredModules"},
822                description = "Allows ignored modules to be run.")
823        private boolean executeIgnoredModules;
824
825        /** Show AST branches that match xpath. */
826        @Option(names = {"-b", "--branch-matching-xpath"},
827            description = "Shows Abstract Syntax Tree(AST) branches that match given XPath query.")
828        private String xpath;
829
830        /**
831         * Creates a new {@code CliOptions} instance.
832         */
833        private CliOptions() {
834            // no code by default
835        }
836
837        /**
838         * Gets the list of exclusions provided through the command line arguments.
839         *
840         * @return List of exclusion patterns.
841         */
842        private List<Pattern> getExclusions() {
843            final List<Pattern> result = exclude.stream()
844                    .map(File::getAbsolutePath)
845                    .map(Pattern::quote)
846                    .map(pattern -> Pattern.compile("^" + pattern + "$"))
847                    .collect(Collectors.toCollection(ArrayList::new));
848            result.addAll(excludeRegex);
849            return result;
850        }
851
852        /**
853         * Validates the user-specified command line options.
854         *
855         * @param parseResult used to verify if the format option was specified on the command line
856         * @param filesToProcess the list of files whose style to check
857         * @return list of violations
858         */
859        // -@cs[CyclomaticComplexity] Breaking apart will damage encapsulation
860        private List<String> validateCli(ParseResult parseResult, List<File> filesToProcess) {
861            final List<String> result = new ArrayList<>();
862            final boolean hasConfigurationFile = configurationFile != null;
863            final boolean hasSuppressionLineColumnNumber = suppressionLineColumnNumber != null;
864
865            if (filesToProcess.isEmpty()) {
866                result.add("Files to process must be specified, found 0.");
867            }
868            // ensure there is no conflicting options
869            else if (printAst || printAstWithComments || printJavadocTree || printTreeWithJavadoc
870                || xpath != null) {
871                if (suppressionLineColumnNumber != null || configurationFile != null
872                        || propertiesFile != null || outputPath != null
873                        || parseResult.hasMatchedOption(OUTPUT_FORMAT_OPTION)) {
874                    result.add("Option '-t' cannot be used with other options.");
875                }
876                else if (filesToProcess.size() > 1) {
877                    result.add("Printing AST is allowed for only one file.");
878                }
879            }
880            else if (hasSuppressionLineColumnNumber) {
881                if (configurationFile != null || propertiesFile != null
882                        || outputPath != null
883                        || parseResult.hasMatchedOption(OUTPUT_FORMAT_OPTION)) {
884                    result.add("Option '-s' cannot be used with other options.");
885                }
886                else if (filesToProcess.size() > 1) {
887                    result.add("Printing xpath suppressions is allowed for only one file.");
888                }
889            }
890            else if (hasConfigurationFile) {
891                try {
892                    // test location only
893                    CommonUtil.getUriByFilename(configurationFile);
894                }
895                catch (CheckstyleException ignored) {
896                    final String msg = "Could not find config XML file '%s'.";
897                    result.add(String.format(Locale.ROOT, msg, configurationFile));
898                }
899                result.addAll(validateOptionalCliParametersIfConfigDefined());
900            }
901            else {
902                result.add("Must specify a config XML file.");
903            }
904
905            return result;
906        }
907
908        /**
909         * Validates optional command line parameters that might be used with config file.
910         *
911         * @return list of violations
912         */
913        private List<String> validateOptionalCliParametersIfConfigDefined() {
914            final List<String> result = new ArrayList<>();
915            if (propertiesFile != null && !propertiesFile.exists()) {
916                result.add(String.format(Locale.ROOT,
917                        "Could not find file '%s'.", propertiesFile));
918            }
919            return result;
920        }
921
922    }
923
924}