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