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