1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 package com.puppycrawl.tools.checkstyle;
21
22 import java.io.File;
23 import java.io.IOException;
24 import java.io.InputStream;
25 import java.io.OutputStream;
26 import java.nio.file.Files;
27 import java.nio.file.Path;
28 import java.util.ArrayList;
29 import java.util.List;
30 import java.util.Locale;
31 import java.util.Objects;
32 import java.util.Properties;
33 import java.util.logging.ConsoleHandler;
34 import java.util.logging.Filter;
35 import java.util.logging.Level;
36 import java.util.logging.LogRecord;
37 import java.util.logging.Logger;
38 import java.util.regex.Pattern;
39 import java.util.stream.Collectors;
40
41 import org.apache.commons.logging.Log;
42 import org.apache.commons.logging.LogFactory;
43
44 import com.puppycrawl.tools.checkstyle.AbstractAutomaticBean.OutputStreamOptions;
45 import com.puppycrawl.tools.checkstyle.api.AuditEvent;
46 import com.puppycrawl.tools.checkstyle.api.AuditListener;
47 import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
48 import com.puppycrawl.tools.checkstyle.api.Configuration;
49 import com.puppycrawl.tools.checkstyle.api.RootModule;
50 import com.puppycrawl.tools.checkstyle.utils.ChainedPropertyUtil;
51 import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
52 import com.puppycrawl.tools.checkstyle.utils.XpathUtil;
53 import picocli.CommandLine;
54 import picocli.CommandLine.Command;
55 import picocli.CommandLine.Option;
56 import picocli.CommandLine.ParameterException;
57 import picocli.CommandLine.Parameters;
58 import picocli.CommandLine.ParseResult;
59
60
61
62
63 public final class Main {
64
65
66
67
68
69 public static final String ERROR_COUNTER = "Main.errorCounter";
70
71
72
73
74 public static final String LOAD_PROPERTIES_EXCEPTION = "Main.loadProperties";
75
76
77
78
79 public static final String CREATE_LISTENER_EXCEPTION = "Main.createListener";
80
81
82 private static final Log LOG = LogFactory.getLog(Main.class);
83
84
85 private static final int EXIT_WITH_INVALID_USER_INPUT_CODE = -1;
86
87
88 private static final int EXIT_WITH_CHECKSTYLE_EXCEPTION_CODE = -2;
89
90
91
92
93
94 private Main() {
95 }
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110 public static void main(String... args) throws IOException {
111
112 final CliOptions cliOptions = new CliOptions();
113 final CommandLine commandLine = new CommandLine(cliOptions);
114 commandLine.setUsageHelpWidth(CliOptions.HELP_WIDTH);
115 commandLine.setCaseInsensitiveEnumValuesAllowed(true);
116
117
118 int exitStatus = 0;
119 int errorCounter = 0;
120 try {
121 final ParseResult parseResult = commandLine.parseArgs(args);
122 if (parseResult.isVersionHelpRequested()) {
123 printVersionToSystemOutput();
124 }
125 else if (parseResult.isUsageHelpRequested()) {
126 commandLine.usage(System.out);
127 }
128 else {
129 exitStatus = execute(parseResult, cliOptions);
130 errorCounter = exitStatus;
131 }
132 }
133 catch (ParameterException exc) {
134 exitStatus = EXIT_WITH_INVALID_USER_INPUT_CODE;
135 System.err.println(exc.getMessage());
136 System.err.println("Usage: checkstyle [OPTIONS]... file(s) or folder(s) ...");
137 System.err.println("Try 'checkstyle --help' for more information.");
138 }
139 catch (CheckstyleException exc) {
140 exitStatus = EXIT_WITH_CHECKSTYLE_EXCEPTION_CODE;
141 errorCounter = 1;
142 exc.printStackTrace();
143 }
144 finally {
145
146 if (errorCounter > 0) {
147 final LocalizedMessage errorCounterViolation = new LocalizedMessage(
148 Definitions.CHECKSTYLE_BUNDLE, Main.class,
149 ERROR_COUNTER, String.valueOf(errorCounter));
150
151
152 System.err.println(errorCounterViolation.getMessage());
153 }
154 }
155 Runtime.getRuntime().exit(exitStatus);
156 }
157
158
159
160
161
162
163
164
165 private static void printVersionToSystemOutput() {
166 System.out.println("Checkstyle version: " + getVersionString());
167 }
168
169
170
171
172
173
174 private static String getVersionString() {
175 return Main.class.getPackage().getImplementationVersion();
176 }
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191 private static int execute(ParseResult parseResult, CliOptions options)
192 throws IOException, CheckstyleException {
193
194 final int exitStatus;
195
196
197 final List<File> filesToProcess = getFilesToProcess(options);
198 final List<String> messages = options.validateCli(parseResult, filesToProcess);
199 final boolean hasMessages = !messages.isEmpty();
200 if (hasMessages) {
201 messages.forEach(System.out::println);
202 exitStatus = EXIT_WITH_INVALID_USER_INPUT_CODE;
203 }
204 else {
205 exitStatus = runCli(options, filesToProcess);
206 }
207 return exitStatus;
208 }
209
210
211
212
213
214
215
216 private static List<File> getFilesToProcess(CliOptions options) {
217 final List<Pattern> patternsToExclude = options.getExclusions();
218
219 final List<File> result = new ArrayList<>();
220 for (File file : options.files) {
221 result.addAll(listFiles(file, patternsToExclude));
222 }
223 return result;
224 }
225
226
227
228
229
230
231
232
233
234
235
236 private static List<File> listFiles(File node, List<Pattern> patternsToExclude) {
237
238
239 final List<File> result = new ArrayList<>();
240
241 if (node.canRead() && !isPathExcluded(node.getAbsolutePath(), patternsToExclude)) {
242 if (node.isDirectory()) {
243 final File[] files = node.listFiles();
244
245 if (files != null) {
246 for (File element : files) {
247 result.addAll(listFiles(element, patternsToExclude));
248 }
249 }
250 }
251 else if (node.isFile()) {
252 result.add(node);
253 }
254 }
255 return result;
256 }
257
258
259
260
261
262
263
264
265
266
267 private static boolean isPathExcluded(String path, Iterable<Pattern> patternsToExclude) {
268 boolean result = false;
269
270 for (Pattern pattern : patternsToExclude) {
271 if (pattern.matcher(path).find()) {
272 result = true;
273 break;
274 }
275 }
276
277 return result;
278 }
279
280
281
282
283
284
285
286
287
288
289
290
291
292 private static int runCli(CliOptions options, List<File> filesToProcess)
293 throws IOException, CheckstyleException {
294 int result = 0;
295 final boolean hasSuppressionLineColumnNumber = options.suppressionLineColumnNumber != null;
296
297
298 if (options.printAst) {
299
300 final File file = filesToProcess.getFirst();
301 final String stringAst = AstTreeStringPrinter.printFileAst(file,
302 JavaParser.Options.WITHOUT_COMMENTS);
303 System.out.print(stringAst);
304 }
305 else if (Objects.nonNull(options.xpath)) {
306 final String branch =
307 XpathUtil.printXpathBranch(options.xpath, filesToProcess.getFirst());
308 System.out.print(branch);
309 }
310 else if (options.printAstWithComments) {
311 final File file = filesToProcess.getFirst();
312 final String stringAst = AstTreeStringPrinter.printFileAst(file,
313 JavaParser.Options.WITH_COMMENTS);
314 System.out.print(stringAst);
315 }
316 else if (options.printJavadocTree) {
317 final File file = filesToProcess.getFirst();
318 final String stringAst = DetailNodeTreeStringPrinter.printFileAst(file);
319 System.out.print(stringAst);
320 }
321 else if (options.printTreeWithJavadoc) {
322 final File file = filesToProcess.getFirst();
323 final String stringAst = AstTreeStringPrinter.printJavaAndJavadocTree(file);
324 System.out.print(stringAst);
325 }
326 else if (hasSuppressionLineColumnNumber) {
327 final File file = filesToProcess.getFirst();
328 final String stringSuppressions =
329 SuppressionsStringPrinter.printSuppressions(file,
330 options.suppressionLineColumnNumber, options.tabWidth);
331 System.out.print(stringSuppressions);
332 }
333 else {
334 if (options.debug) {
335 final Logger parentLogger = Logger.getLogger(Main.class.getName()).getParent();
336 final ConsoleHandler handler = new ConsoleHandler();
337 handler.setLevel(Level.FINEST);
338 handler.setFilter(new OnlyCheckstyleLoggersFilter());
339 parentLogger.addHandler(handler);
340 parentLogger.setLevel(Level.FINEST);
341 }
342 if (LOG.isDebugEnabled()) {
343 LOG.debug("Checkstyle debug logging enabled");
344 }
345
346
347 result = runCheckstyle(options, filesToProcess);
348 }
349
350 return result;
351 }
352
353
354
355
356
357
358
359
360
361
362
363
364 private static int runCheckstyle(CliOptions options, List<File> filesToProcess)
365 throws CheckstyleException, IOException {
366
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
377
378 final ConfigurationLoader.IgnoredModulesOptions ignoredModulesOptions;
379 if (options.executeIgnoredModules) {
380 ignoredModulesOptions = ConfigurationLoader.IgnoredModulesOptions.EXECUTE;
381 }
382 else {
383 ignoredModulesOptions = ConfigurationLoader.IgnoredModulesOptions.OMIT;
384 }
385
386 final ThreadModeSettings multiThreadModeSettings =
387 new ThreadModeSettings(CliOptions.CHECKER_THREADS_NUMBER,
388 CliOptions.TREE_WALKER_THREADS_NUMBER);
389 final Configuration config = ConfigurationLoader.loadConfiguration(
390 options.configurationFile, new PropertiesExpander(props),
391 ignoredModulesOptions, multiThreadModeSettings);
392
393
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
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
429 errorCounter = rootModule.process(filesToProcess);
430 }
431 finally {
432 rootModule.destroy();
433 }
434
435 return errorCounter;
436 }
437
438
439
440
441
442
443
444
445
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
466
467
468
469
470
471
472
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
484
485
486
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
503
504
505
506
507
508
509
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
521
522
523
524
525
526
527
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
543
544
545
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
560
561
562
563
564 enum OutputFormat {
565
566 XML,
567
568 SARIF,
569
570 PLAIN;
571
572
573
574
575
576
577
578
579
580 AuditListener createListener(
581 OutputStream out,
582 OutputStreamOptions options)
583 throws IOException {
584 final AuditListener result;
585 if (this == XML) {
586 result = new XMLLogger(out, options);
587 }
588 else if (this == SARIF) {
589 result = new SarifLogger(out, options);
590 }
591 else {
592 result = new DefaultLogger(out, options);
593 }
594 return result;
595 }
596
597
598
599
600
601
602 @Override
603 public String toString() {
604 return name().toLowerCase(Locale.ROOT);
605 }
606 }
607
608
609 private static final class OnlyCheckstyleLoggersFilter implements Filter {
610
611 private final String packageName = Main.class.getPackage().getName();
612
613
614
615
616 private OnlyCheckstyleLoggersFilter() {
617
618 }
619
620
621
622
623
624
625
626 @Override
627 public boolean isLoggable(LogRecord logRecord) {
628 return logRecord.getLoggerName().startsWith(packageName);
629 }
630 }
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646 @Command(name = "checkstyle", description = "Checkstyle verifies that the specified "
647 + "source code files adhere to the specified rules. By default, violations are "
648 + "reported to standard out in plain format. Checkstyle requires a configuration "
649 + "XML file that configures the checks to apply.",
650 mixinStandardHelpOptions = true)
651 private static final class CliOptions {
652
653
654 private static final int HELP_WIDTH = 100;
655
656
657 private static final int DEFAULT_THREAD_COUNT = 1;
658
659
660 private static final String ATTRIB_TAB_WIDTH_NAME = "tabWidth";
661
662
663 private static final OutputFormat DEFAULT_OUTPUT_FORMAT = OutputFormat.PLAIN;
664
665
666 private static final String OUTPUT_FORMAT_OPTION = "-f";
667
668
669
670
671
672
673 private static final int CHECKER_THREADS_NUMBER = DEFAULT_THREAD_COUNT;
674
675
676
677
678
679 private static final int TREE_WALKER_THREADS_NUMBER = DEFAULT_THREAD_COUNT;
680
681
682 @Parameters(arity = "1..*", paramLabel = "<files or folders>",
683 description = "One or more source files to verify")
684 private List<File> files;
685
686
687 @Option(names = "-c", description = "Specifies the location of the file that defines"
688 + " the configuration modules. The location can either be a filesystem location"
689 + ", or a name passed to the ClassLoader.getResource() method.")
690 private String configurationFile;
691
692
693 @Option(names = "-o", description = "Sets the output file. Defaults to stdout.")
694 private Path outputPath;
695
696
697 @Option(names = "-p", description = "Sets the property files to load.")
698 private File propertiesFile;
699
700
701 @Option(names = "-s",
702 description = "Prints xpath suppressions at the file's line and column position. "
703 + "Argument is the line and column number (separated by a : ) in the file "
704 + "that the suppression should be generated for. The option cannot be used "
705 + "with other options and requires exactly one file to run on to be "
706 + "specified. Note that the generated result will have few queries, joined "
707 + "by pipe(|). Together they will match all AST nodes on "
708 + "specified line and column. You need to choose only one and recheck "
709 + "that it works. Usage of all of them is also ok, but might result in "
710 + "undesirable matching and suppress other issues.")
711 private String suppressionLineColumnNumber;
712
713
714
715
716
717
718
719
720 @Option(names = {"-w", "--tabWidth"},
721 description = "Sets the length of the tab character. "
722 + "Used only with -s option. Default value is ${DEFAULT-VALUE}.")
723 private int tabWidth = CommonUtil.DEFAULT_TAB_WIDTH;
724
725
726 @Option(names = {"-g", "--generate-xpath-suppression"},
727 description = "Generates an output xpath suppression XML to use to suppress all "
728 + "violations from user's config. Instead of printing every violation, "
729 + "all violations will be caught and single suppressions xml file will "
730 + "be printed out. Used only with -c option. Output "
731 + "location can be specified with -o option.")
732 private boolean generateXpathSuppressionsFile;
733
734
735 @Option(names = {"-G", "--generate-checks-and-files-suppression"},
736 description = "Generates an output suppression XML that will have suppress "
737 + "elements with \"checks\" and \"files\" attributes only to use to "
738 + "suppress all violations from user's config. Instead of printing every "
739 + "violation, all violations will be caught and single suppressions xml "
740 + "file will be printed out. Used only with -c option. Output "
741 + "location can be specified with -o option.")
742 private boolean generateCheckAndFileSuppressionsFile;
743
744
745
746
747
748
749
750
751 @Option(names = "-f",
752 description = "Specifies the output format. Valid values: "
753 + "${COMPLETION-CANDIDATES} for XMLLogger, SarifLogger, "
754 + "and DefaultLogger respectively. Defaults to ${DEFAULT-VALUE}.")
755 private OutputFormat format = DEFAULT_OUTPUT_FORMAT;
756
757
758 @Option(names = {"-t", "--tree"},
759 description = "This option is used to display the Abstract Syntax Tree (AST) "
760 + "without any comments of the specified file. It can only be used on "
761 + "a single file and cannot be combined with other options.")
762 private boolean printAst;
763
764
765 @Option(names = {"-T", "--treeWithComments"},
766 description = "This option is used to display the Abstract Syntax Tree (AST) "
767 + "with comment nodes excluding Javadoc of the specified file. It can only"
768 + " be used on a single file and cannot be combined with other options.")
769 private boolean printAstWithComments;
770
771
772 @Option(names = {"-j", "--javadocTree"},
773 description = "This option is used to print the Parse Tree of the Javadoc comment."
774 + " The file has to contain only Javadoc comment content "
775 + "excluding '/**' and '*/' at the beginning and at the end respectively. "
776 + "It can only be used on a single file and cannot be combined "
777 + "with other options.")
778 private boolean printJavadocTree;
779
780
781 @Option(names = {"-J", "--treeWithJavadoc"},
782 description = "This option is used to display the Abstract Syntax Tree (AST) "
783 + "with Javadoc nodes of the specified file. It can only be used on a "
784 + "single file and cannot be combined with other options.")
785 private boolean printTreeWithJavadoc;
786
787
788 @Option(names = {"-d", "--debug"},
789 description = "Prints all debug logging of CheckStyle utility.")
790 private boolean debug;
791
792
793
794
795
796
797
798
799 @Option(names = {"-e", "--exclude"},
800 description = "Directory/file to exclude from CheckStyle. The path can be the "
801 + "full, absolute path, or relative to the current path. Multiple "
802 + "excludes are allowed.")
803 private List<File> exclude = new ArrayList<>();
804
805
806
807
808
809
810
811
812 @Option(names = {"-x", "--exclude-regexp"},
813 description = "Directory/file pattern to exclude from CheckStyle. Multiple "
814 + "excludes are allowed.")
815 private List<Pattern> excludeRegex = new ArrayList<>();
816
817
818 @Option(names = {"-E", "--executeIgnoredModules"},
819 description = "Allows ignored modules to be run.")
820 private boolean executeIgnoredModules;
821
822
823 @Option(names = {"-b", "--branch-matching-xpath"},
824 description = "Shows Abstract Syntax Tree(AST) branches that match given XPath query.")
825 private String xpath;
826
827
828
829
830 private CliOptions() {
831
832 }
833
834
835
836
837
838
839 private List<Pattern> getExclusions() {
840 final List<Pattern> result = exclude.stream()
841 .map(File::getAbsolutePath)
842 .map(Pattern::quote)
843 .map(pattern -> Pattern.compile("^" + pattern + "$"))
844 .collect(Collectors.toCollection(ArrayList::new));
845 result.addAll(excludeRegex);
846 return result;
847 }
848
849
850
851
852
853
854
855
856
857 private List<String> validateCli(ParseResult parseResult, List<File> filesToProcess) {
858 final List<String> result = new ArrayList<>();
859 final boolean hasConfigurationFile = configurationFile != null;
860 final boolean hasSuppressionLineColumnNumber = suppressionLineColumnNumber != null;
861
862 if (filesToProcess.isEmpty()) {
863 result.add("Files to process must be specified, found 0.");
864 }
865
866 else if (printAst || printAstWithComments || printJavadocTree || printTreeWithJavadoc
867 || xpath != null) {
868 if (suppressionLineColumnNumber != null || configurationFile != null
869 || propertiesFile != null || outputPath != null
870 || parseResult.hasMatchedOption(OUTPUT_FORMAT_OPTION)) {
871 result.add("Option '-t' cannot be used with other options.");
872 }
873 else if (filesToProcess.size() > 1) {
874 result.add("Printing AST is allowed for only one file.");
875 }
876 }
877 else if (hasSuppressionLineColumnNumber) {
878 if (configurationFile != null || propertiesFile != null
879 || outputPath != null
880 || parseResult.hasMatchedOption(OUTPUT_FORMAT_OPTION)) {
881 result.add("Option '-s' cannot be used with other options.");
882 }
883 else if (filesToProcess.size() > 1) {
884 result.add("Printing xpath suppressions is allowed for only one file.");
885 }
886 }
887 else if (hasConfigurationFile) {
888 try {
889
890 CommonUtil.getUriByFilename(configurationFile);
891 }
892 catch (CheckstyleException ignored) {
893 final String msg = "Could not find config XML file '%s'.";
894 result.add(String.format(Locale.ROOT, msg, configurationFile));
895 }
896 result.addAll(validateOptionalCliParametersIfConfigDefined());
897 }
898 else {
899 result.add("Must specify a config XML file.");
900 }
901
902 return result;
903 }
904
905
906
907
908
909
910 private List<String> validateOptionalCliParametersIfConfigDefined() {
911 final List<String> result = new ArrayList<>();
912 if (propertiesFile != null && !propertiesFile.exists()) {
913 result.add(String.format(Locale.ROOT,
914 "Could not find file '%s'.", propertiesFile));
915 }
916 return result;
917 }
918 }
919
920 }