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