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