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