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