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