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