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