001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2025 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.AbstractAutomaticBean.OutputStreamOptions;
046import com.puppycrawl.tools.checkstyle.api.AuditEvent;
047import com.puppycrawl.tools.checkstyle.api.AuditListener;
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.utils.ChainedPropertyUtil;
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     * @noinspectionreason UseOfSystemOutOrSystemErr - driver class for Checkstyle requires
106     *      usage of System.out and System.err
107     * @noinspectionreason CallToPrintStackTrace - driver class for Checkstyle must be able to
108     *      show all details in case of failure
109     * @noinspectionreason CallToSystemExit - driver class must call exit
110     **/
111    public static void main(String... args) throws IOException {
112
113        final CliOptions cliOptions = new CliOptions();
114        final CommandLine commandLine = new CommandLine(cliOptions);
115        commandLine.setUsageHelpWidth(CliOptions.HELP_WIDTH);
116        commandLine.setCaseInsensitiveEnumValuesAllowed(true);
117
118        // provide proper exit code based on results.
119        int exitStatus = 0;
120        int errorCounter = 0;
121        try {
122            final ParseResult parseResult = commandLine.parseArgs(args);
123            if (parseResult.isVersionHelpRequested()) {
124                printVersionToSystemOutput();
125            }
126            else if (parseResult.isUsageHelpRequested()) {
127                commandLine.usage(System.out);
128            }
129            else {
130                exitStatus = execute(parseResult, cliOptions);
131                errorCounter = exitStatus;
132            }
133        }
134        catch (ParameterException exc) {
135            exitStatus = EXIT_WITH_INVALID_USER_INPUT_CODE;
136            System.err.println(exc.getMessage());
137            System.err.println("Usage: checkstyle [OPTIONS]... file(s) or folder(s) ...");
138            System.err.println("Try 'checkstyle --help' for more information.");
139        }
140        catch (CheckstyleException exc) {
141            exitStatus = EXIT_WITH_CHECKSTYLE_EXCEPTION_CODE;
142            errorCounter = 1;
143            exc.printStackTrace();
144        }
145        finally {
146            // return exit code base on validation of Checker
147            if (errorCounter > 0) {
148                final LocalizedMessage errorCounterViolation = new LocalizedMessage(
149                        Definitions.CHECKSTYLE_BUNDLE, Main.class,
150                        ERROR_COUNTER, String.valueOf(errorCounter));
151                // print error count statistic to error output stream,
152                // output stream might be used by validation report content
153                System.err.println(errorCounterViolation.getMessage());
154            }
155        }
156        Runtime.getRuntime().exit(exitStatus);
157    }
158
159    /**
160     * Prints version string when the user requests version help (--version or -V).
161     *
162     * @noinspection UseOfSystemOutOrSystemErr
163     * @noinspectionreason UseOfSystemOutOrSystemErr - driver class for Checkstyle requires
164     *      usage of System.out and System.err
165     */
166    private static void printVersionToSystemOutput() {
167        System.out.println("Checkstyle version: " + getVersionString());
168    }
169
170    /**
171     * Returns the version string printed when the user requests version help (--version or -V).
172     *
173     * @return a version string based on the package implementation version
174     */
175    private static String getVersionString() {
176        return Main.class.getPackage().getImplementationVersion();
177    }
178
179    /**
180     * Validates the user input and returns {@value #EXIT_WITH_INVALID_USER_INPUT_CODE} if
181     * invalid, otherwise executes CheckStyle and returns the number of violations.
182     *
183     * @param parseResult generic access to options and parameters found on the command line
184     * @param options encapsulates options and parameters specified on the command line
185     * @return number of violations
186     * @throws IOException if a file could not be read.
187     * @throws CheckstyleException if something happens processing the files.
188     * @noinspection UseOfSystemOutOrSystemErr
189     * @noinspectionreason UseOfSystemOutOrSystemErr - driver class for Checkstyle requires
190     *      usage of System.out and System.err
191     */
192    private static int execute(ParseResult parseResult, CliOptions options)
193            throws IOException, CheckstyleException {
194
195        final int exitStatus;
196
197        // return error if something is wrong in arguments
198        final List<File> filesToProcess = getFilesToProcess(options);
199        final List<String> messages = options.validateCli(parseResult, filesToProcess);
200        final boolean hasMessages = !messages.isEmpty();
201        if (hasMessages) {
202            messages.forEach(System.out::println);
203            exitStatus = EXIT_WITH_INVALID_USER_INPUT_CODE;
204        }
205        else {
206            exitStatus = runCli(options, filesToProcess);
207        }
208        return exitStatus;
209    }
210
211    /**
212     * Determines the files to process.
213     *
214     * @param options the user-specified options
215     * @return list of files to process
216     */
217    private static List<File> getFilesToProcess(CliOptions options) {
218        final List<Pattern> patternsToExclude = options.getExclusions();
219
220        final List<File> result = new LinkedList<>();
221        for (File file : options.files) {
222            result.addAll(listFiles(file, patternsToExclude));
223        }
224        return result;
225    }
226
227    /**
228     * Traverses a specified node looking for files to check. Found files are added to
229     * a specified list. Subdirectories are also traversed.
230     *
231     * @param node
232     *        the node to process
233     * @param patternsToExclude The list of patterns to exclude from searching or being added as
234     *        files.
235     * @return found files
236     */
237    private static List<File> listFiles(File node, List<Pattern> patternsToExclude) {
238        // could be replaced with org.apache.commons.io.FileUtils.list() method
239        // if only we add commons-io library
240        final List<File> result = new LinkedList<>();
241
242        if (node.canRead() && !isPathExcluded(node.getAbsolutePath(), patternsToExclude)) {
243            if (node.isDirectory()) {
244                final File[] files = node.listFiles();
245                // listFiles() can return null, so we need to check it
246                if (files != null) {
247                    for (File element : files) {
248                        result.addAll(listFiles(element, patternsToExclude));
249                    }
250                }
251            }
252            else if (node.isFile()) {
253                result.add(node);
254            }
255        }
256        return result;
257    }
258
259    /**
260     * Checks if a directory/file {@code path} should be excluded based on if it matches one of the
261     * patterns supplied.
262     *
263     * @param path The path of the directory/file to check
264     * @param patternsToExclude The collection of patterns to exclude from searching
265     *        or being added as files.
266     * @return True if the directory/file matches one of the patterns.
267     */
268    private static boolean isPathExcluded(String path, Iterable<Pattern> patternsToExclude) {
269        boolean result = false;
270
271        for (Pattern pattern : patternsToExclude) {
272            if (pattern.matcher(path).find()) {
273                result = true;
274                break;
275            }
276        }
277
278        return result;
279    }
280
281    /**
282     * Do execution of CheckStyle based on Command line options.
283     *
284     * @param options user-specified options
285     * @param filesToProcess the list of files whose style to check
286     * @return number of violations
287     * @throws IOException if a file could not be read.
288     * @throws CheckstyleException if something happens processing the files.
289     * @noinspection UseOfSystemOutOrSystemErr
290     * @noinspectionreason UseOfSystemOutOrSystemErr - driver class for Checkstyle requires
291     *      usage of System.out and System.err
292     */
293    private static int runCli(CliOptions options, List<File> filesToProcess)
294            throws IOException, CheckstyleException {
295        int result = 0;
296        final boolean hasSuppressionLineColumnNumber = options.suppressionLineColumnNumber != null;
297
298        // create config helper object
299        if (options.printAst) {
300            // print AST
301            final File file = filesToProcess.get(0);
302            final String stringAst = AstTreeStringPrinter.printFileAst(file,
303                    JavaParser.Options.WITHOUT_COMMENTS);
304            System.out.print(stringAst);
305        }
306        else if (Objects.nonNull(options.xpath)) {
307            final String branch = XpathUtil.printXpathBranch(options.xpath, filesToProcess.get(0));
308            System.out.print(branch);
309        }
310        else if (options.printAstWithComments) {
311            final File file = filesToProcess.get(0);
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.get(0);
318            final String stringAst = DetailNodeTreeStringPrinter.printFileAst(file);
319            System.out.print(stringAst);
320        }
321        else if (options.printTreeWithJavadoc) {
322            final File file = filesToProcess.get(0);
323            final String stringAst = AstTreeStringPrinter.printJavaAndJavadocTree(file);
324            System.out.print(stringAst);
325        }
326        else if (hasSuppressionLineColumnNumber) {
327            final File file = filesToProcess.get(0);
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        final ThreadModeSettings multiThreadModeSettings =
378                new ThreadModeSettings(CliOptions.CHECKER_THREADS_NUMBER,
379                        CliOptions.TREE_WALKER_THREADS_NUMBER);
380
381        final ConfigurationLoader.IgnoredModulesOptions ignoredModulesOptions;
382        if (options.executeIgnoredModules) {
383            ignoredModulesOptions = ConfigurationLoader.IgnoredModulesOptions.EXECUTE;
384        }
385        else {
386            ignoredModulesOptions = ConfigurationLoader.IgnoredModulesOptions.OMIT;
387        }
388
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     * @exception 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    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        public 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         * Returns whether the specified logRecord should be logged.
614         *
615         * @param logRecord the logRecord to log
616         * @return true if the logger name is in the package of this class or a subpackage
617         */
618        @Override
619        public boolean isLoggable(LogRecord logRecord) {
620            return logRecord.getLoggerName().startsWith(packageName);
621        }
622    }
623
624    /**
625     * Command line options.
626     *
627     * @noinspection unused, FieldMayBeFinal, CanBeFinal,
628     *              MismatchedQueryAndUpdateOfCollection, LocalCanBeFinal
629     * @noinspectionreason FieldMayBeFinal - usage of picocli requires
630     *      suppression of above inspections
631     * @noinspectionreason CanBeFinal - usage of picocli requires
632     *      suppression of above inspections
633     * @noinspectionreason MismatchedQueryAndUpdateOfCollection - list of files is gathered and used
634     *      via reflection by picocli library
635     * @noinspectionreason LocalCanBeFinal - usage of picocli requires
636     *      suppression of above inspections
637     */
638    @Command(name = "checkstyle", description = "Checkstyle verifies that the specified "
639            + "source code files adhere to the specified rules. By default, violations are "
640            + "reported to standard out in plain format. Checkstyle requires a configuration "
641            + "XML file that configures the checks to apply.",
642            mixinStandardHelpOptions = true)
643    private static final class CliOptions {
644
645        /** Width of CLI help option. */
646        private static final int HELP_WIDTH = 100;
647
648        /** The default number of threads to use for checker and the tree walker. */
649        private static final int DEFAULT_THREAD_COUNT = 1;
650
651        /** Name for the moduleConfig attribute 'tabWidth'. */
652        private static final String ATTRIB_TAB_WIDTH_NAME = "tabWidth";
653
654        /** Default output format. */
655        private static final OutputFormat DEFAULT_OUTPUT_FORMAT = OutputFormat.PLAIN;
656
657        /** Option name for output format. */
658        private static final String OUTPUT_FORMAT_OPTION = "-f";
659
660        /**
661         * The checker threads number.
662         * This option has been skipped for CLI options intentionally.
663         *
664         */
665        private static final int CHECKER_THREADS_NUMBER = DEFAULT_THREAD_COUNT;
666
667        /**
668         * The tree walker threads number.
669         *
670         */
671        private static final int TREE_WALKER_THREADS_NUMBER = DEFAULT_THREAD_COUNT;
672
673        /** List of file to validate. */
674        @Parameters(arity = "1..*", paramLabel = "<files or folders>",
675                description = "One or more source files to verify")
676        private List<File> files;
677
678        /** Config file location. */
679        @Option(names = "-c", description = "Specifies the location of the file that defines"
680                + " the configuration modules. The location can either be a filesystem location"
681                + ", or a name passed to the ClassLoader.getResource() method.")
682        private String configurationFile;
683
684        /** Output file location. */
685        @Option(names = "-o", description = "Sets the output file. Defaults to stdout.")
686        private Path outputPath;
687
688        /** Properties file location. */
689        @Option(names = "-p", description = "Sets the property files to load.")
690        private File propertiesFile;
691
692        /** LineNo and columnNo for the suppression. */
693        @Option(names = "-s",
694                description = "Prints xpath suppressions at the file's line and column position. "
695                        + "Argument is the line and column number (separated by a : ) in the file "
696                        + "that the suppression should be generated for. The option cannot be used "
697                        + "with other options and requires exactly one file to run on to be "
698                        + "specified. Note that the generated result will have few queries, joined "
699                        + "by pipe(|). Together they will match all AST nodes on "
700                        + "specified line and column. You need to choose only one and recheck "
701                        + "that it works. Usage of all of them is also ok, but might result in "
702                        + "undesirable matching and suppress other issues.")
703        private String suppressionLineColumnNumber;
704
705        /**
706         * Tab character length.
707         *
708         * @noinspection CanBeFinal
709         * @noinspectionreason CanBeFinal - we use picocli, and it uses
710         *      reflection to manage such fields
711         */
712        @Option(names = {"-w", "--tabWidth"},
713                description = "Sets the length of the tab character. "
714                + "Used only with -s option. Default value is ${DEFAULT-VALUE}.")
715        private int tabWidth = CommonUtil.DEFAULT_TAB_WIDTH;
716
717        /** Switch whether to generate xpath suppressions file or not. */
718        @Option(names = {"-g", "--generate-xpath-suppression"},
719                description = "Generates to output a xpath suppression xml to use to suppress all "
720                        + "violations from user's config. Instead of printing every violation, "
721                        + "all violations will be catched and single suppressions xml file will "
722                        + "be printed out. Used only with -c option. Output "
723                        + "location can be specified with -o option.")
724        private boolean generateXpathSuppressionsFile;
725
726        /** Switch whether to generate check and file suppressions file or not. */
727        @Option(names = {"-G", "--generate-checks-and-files-suppression"},
728                description = "Generates to output a suppression xml that will have suppress "
729                        + "elements with \"checks\" and \"files\" attributes only to use to "
730                        + "suppress all violations from user's config. Instead of printing every "
731                        + "violation, all violations will be catched and single suppressions xml "
732                        + "file will be printed out. Used only with -c option. Output "
733                        + "location can be specified with -o option.")
734        private boolean generateCheckAndFileSuppressionsFile;
735
736        /**
737         * Output format.
738         *
739         * @noinspection CanBeFinal
740         * @noinspectionreason CanBeFinal - we use picocli, and it uses
741         *      reflection to manage such fields
742         */
743        @Option(names = "-f",
744                description = "Specifies the output format. Valid values: "
745                + "${COMPLETION-CANDIDATES} for XMLLogger, SarifLogger, "
746                + "and DefaultLogger respectively. Defaults to ${DEFAULT-VALUE}.")
747        private OutputFormat format = DEFAULT_OUTPUT_FORMAT;
748
749        /** Option that controls whether to print the AST of the file. */
750        @Option(names = {"-t", "--tree"},
751                description = "This option is used to display the Abstract Syntax Tree (AST) "
752                        + "without any comments of the specified file. It can only be used on "
753                        + "a single file and cannot be combined with other options.")
754        private boolean printAst;
755
756        /** Option that controls whether to print the AST of the file including comments. */
757        @Option(names = {"-T", "--treeWithComments"},
758                description = "This option is used to display the Abstract Syntax Tree (AST) "
759                        + "with comment nodes excluding Javadoc of the specified file. It can only"
760                        + " be used on a single file and cannot be combined with other options.")
761        private boolean printAstWithComments;
762
763        /** Option that controls whether to print the parse tree of the javadoc comment. */
764        @Option(names = {"-j", "--javadocTree"},
765                description = "This option is used to print the Parse Tree of the Javadoc comment."
766                        + " The file has to contain only Javadoc comment content "
767                        + "excluding '/**' and '*/' at the beginning and at the end respectively. "
768                        + "It can only be used on a single file and cannot be combined "
769                        + "with other options.")
770        private boolean printJavadocTree;
771
772        /** Option that controls whether to print the full AST of the file. */
773        @Option(names = {"-J", "--treeWithJavadoc"},
774                description = "This option is used to display the Abstract Syntax Tree (AST) "
775                        + "with Javadoc nodes of the specified file. It can only be used on a "
776                        + "single file and cannot be combined with other options.")
777        private boolean printTreeWithJavadoc;
778
779        /** Option that controls whether to print debug info. */
780        @Option(names = {"-d", "--debug"},
781                description = "Prints all debug logging of CheckStyle utility.")
782        private boolean debug;
783
784        /**
785         * Option that allows users to specify a list of paths to exclude.
786         *
787         * @noinspection CanBeFinal
788         * @noinspectionreason CanBeFinal - we use picocli, and it uses
789         *      reflection to manage such fields
790         */
791        @Option(names = {"-e", "--exclude"},
792                description = "Directory/file to exclude from CheckStyle. The path can be the "
793                        + "full, absolute path, or relative to the current path. Multiple "
794                        + "excludes are allowed.")
795        private List<File> exclude = new ArrayList<>();
796
797        /**
798         * Option that allows users to specify a regex of paths to exclude.
799         *
800         * @noinspection CanBeFinal
801         * @noinspectionreason CanBeFinal - we use picocli, and it uses
802         *      reflection to manage such fields
803         */
804        @Option(names = {"-x", "--exclude-regexp"},
805                description = "Directory/file pattern to exclude from CheckStyle. Multiple "
806                        + "excludes are allowed.")
807        private List<Pattern> excludeRegex = new ArrayList<>();
808
809        /** Switch whether to execute ignored modules or not. */
810        @Option(names = {"-E", "--executeIgnoredModules"},
811                description = "Allows ignored modules to be run.")
812        private boolean executeIgnoredModules;
813
814        /** Show AST branches that match xpath. */
815        @Option(names = {"-b", "--branch-matching-xpath"},
816            description = "Shows Abstract Syntax Tree(AST) branches that match given XPath query.")
817        private String xpath;
818
819        /**
820         * Gets the list of exclusions provided through the command line arguments.
821         *
822         * @return List of exclusion patterns.
823         */
824        private List<Pattern> getExclusions() {
825            final List<Pattern> result = exclude.stream()
826                    .map(File::getAbsolutePath)
827                    .map(Pattern::quote)
828                    .map(pattern -> Pattern.compile("^" + pattern + "$"))
829                    .collect(Collectors.toCollection(ArrayList::new));
830            result.addAll(excludeRegex);
831            return result;
832        }
833
834        /**
835         * Validates the user-specified command line options.
836         *
837         * @param parseResult used to verify if the format option was specified on the command line
838         * @param filesToProcess the list of files whose style to check
839         * @return list of violations
840         */
841        // -@cs[CyclomaticComplexity] Breaking apart will damage encapsulation
842        private List<String> validateCli(ParseResult parseResult, List<File> filesToProcess) {
843            final List<String> result = new ArrayList<>();
844            final boolean hasConfigurationFile = configurationFile != null;
845            final boolean hasSuppressionLineColumnNumber = suppressionLineColumnNumber != null;
846
847            if (filesToProcess.isEmpty()) {
848                result.add("Files to process must be specified, found 0.");
849            }
850            // ensure there is no conflicting options
851            else if (printAst || printAstWithComments || printJavadocTree || printTreeWithJavadoc
852                || xpath != null) {
853                if (suppressionLineColumnNumber != null || configurationFile != null
854                        || propertiesFile != null || outputPath != null
855                        || parseResult.hasMatchedOption(OUTPUT_FORMAT_OPTION)) {
856                    result.add("Option '-t' cannot be used with other options.");
857                }
858                else if (filesToProcess.size() > 1) {
859                    result.add("Printing AST is allowed for only one file.");
860                }
861            }
862            else if (hasSuppressionLineColumnNumber) {
863                if (configurationFile != null || propertiesFile != null
864                        || outputPath != null
865                        || parseResult.hasMatchedOption(OUTPUT_FORMAT_OPTION)) {
866                    result.add("Option '-s' cannot be used with other options.");
867                }
868                else if (filesToProcess.size() > 1) {
869                    result.add("Printing xpath suppressions is allowed for only one file.");
870                }
871            }
872            else if (hasConfigurationFile) {
873                try {
874                    // test location only
875                    CommonUtil.getUriByFilename(configurationFile);
876                }
877                catch (CheckstyleException ignored) {
878                    final String msg = "Could not find config XML file '%s'.";
879                    result.add(String.format(Locale.ROOT, msg, configurationFile));
880                }
881                result.addAll(validateOptionalCliParametersIfConfigDefined());
882            }
883            else {
884                result.add("Must specify a config XML file.");
885            }
886
887            return result;
888        }
889
890        /**
891         * Validates optional command line parameters that might be used with config file.
892         *
893         * @return list of violations
894         */
895        private List<String> validateOptionalCliParametersIfConfigDefined() {
896            final List<String> result = new ArrayList<>();
897            if (propertiesFile != null && !propertiesFile.exists()) {
898                result.add(String.format(Locale.ROOT,
899                        "Could not find file '%s'.", propertiesFile));
900            }
901            return result;
902        }
903    }
904
905}