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.LinkedList;
30 import java.util.List;
31 import java.util.Locale;
32 import java.util.Objects;
33 import java.util.Properties;
34 import java.util.logging.ConsoleHandler;
35 import java.util.logging.Filter;
36 import java.util.logging.Level;
37 import java.util.logging.LogRecord;
38 import java.util.logging.Logger;
39 import java.util.regex.Pattern;
40 import java.util.stream.Collectors;
41
42 import org.apache.commons.logging.Log;
43 import org.apache.commons.logging.LogFactory;
44
45 import com.puppycrawl.tools.checkstyle.AbstractAutomaticBean.OutputStreamOptions;
46 import com.puppycrawl.tools.checkstyle.api.AuditEvent;
47 import com.puppycrawl.tools.checkstyle.api.AuditListener;
48 import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
49 import com.puppycrawl.tools.checkstyle.api.Configuration;
50 import com.puppycrawl.tools.checkstyle.api.RootModule;
51 import com.puppycrawl.tools.checkstyle.utils.ChainedPropertyUtil;
52 import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
53 import com.puppycrawl.tools.checkstyle.utils.XpathUtil;
54 import picocli.CommandLine;
55 import picocli.CommandLine.Command;
56 import picocli.CommandLine.Option;
57 import picocli.CommandLine.ParameterException;
58 import picocli.CommandLine.Parameters;
59 import picocli.CommandLine.ParseResult;
60
61
62
63
64 public final class Main {
65
66
67
68
69
70 public static final String ERROR_COUNTER = "Main.errorCounter";
71
72
73
74
75 public static final String LOAD_PROPERTIES_EXCEPTION = "Main.loadProperties";
76
77
78
79
80 public static final String CREATE_LISTENER_EXCEPTION = "Main.createListener";
81
82
83 private static final Log LOG = LogFactory.getLog(Main.class);
84
85
86 private static final int EXIT_WITH_INVALID_USER_INPUT_CODE = -1;
87
88
89 private static final int EXIT_WITH_CHECKSTYLE_EXCEPTION_CODE = -2;
90
91
92
93
94
95 private Main() {
96 }
97
98
99
100
101
102
103
104
105
106
107
108
109
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
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
147 if (errorCounter > 0) {
148 final LocalizedMessage errorCounterViolation = new LocalizedMessage(
149 Definitions.CHECKSTYLE_BUNDLE, Main.class,
150 ERROR_COUNTER, String.valueOf(errorCounter));
151
152
153 System.err.println(errorCounterViolation.getMessage());
154 }
155 }
156 Runtime.getRuntime().exit(exitStatus);
157 }
158
159
160
161
162
163
164 private static String getVersionString() {
165 return "Checkstyle version: " + Main.class.getPackage().getImplementationVersion();
166 }
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181 private static int execute(ParseResult parseResult, CliOptions options)
182 throws IOException, CheckstyleException {
183
184 final int exitStatus;
185
186
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
202
203
204
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
218
219
220
221
222
223
224
225
226 private static List<File> listFiles(File node, List<Pattern> patternsToExclude) {
227
228
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
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
250
251
252
253
254
255
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
272
273
274
275
276
277
278
279
280
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
288 if (options.printAst) {
289
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
338 result = runCheckstyle(options, filesToProcess);
339 }
340
341 return result;
342 }
343
344
345
346
347
348
349
350
351
352
353
354
355 private static int runCheckstyle(CliOptions options, List<File> filesToProcess)
356 throws CheckstyleException, IOException {
357
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
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
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
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
415 errorCounter = rootModule.process(filesToProcess);
416 }
417 finally {
418 rootModule.destroy();
419 }
420
421 return errorCounter;
422 }
423
424
425
426
427
428
429
430
431
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
452
453
454
455
456
457
458
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
470
471
472
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
489
490
491
492
493
494
495
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
507
508
509
510
511
512
513
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
529
530
531
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
546
547
548
549
550 enum OutputFormat {
551
552 XML,
553
554 SARIF,
555
556 PLAIN;
557
558
559
560
561
562
563
564
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
584
585
586
587 @Override
588 public String toString() {
589 return name().toLowerCase(Locale.ROOT);
590 }
591 }
592
593
594 private static final class OnlyCheckstyleLoggersFilter implements Filter {
595
596 private final String packageName = Main.class.getPackage().getName();
597
598
599
600
601
602
603
604 @Override
605 public boolean isLoggable(LogRecord logRecord) {
606 return logRecord.getLoggerName().startsWith(packageName);
607 }
608 }
609
610
611
612
613
614
615
616
617
618
619
620
621
622
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
632 private static final int HELP_WIDTH = 100;
633
634
635 private static final int DEFAULT_THREAD_COUNT = 1;
636
637
638 private static final String ATTRIB_TAB_WIDTH_NAME = "tabWidth";
639
640
641 private static final OutputFormat DEFAULT_OUTPUT_FORMAT = OutputFormat.PLAIN;
642
643
644 private static final String OUTPUT_FORMAT_OPTION = "-f";
645
646
647
648
649
650
651 private static final int CHECKER_THREADS_NUMBER = DEFAULT_THREAD_COUNT;
652
653
654
655
656
657 private static final int TREE_WALKER_THREADS_NUMBER = DEFAULT_THREAD_COUNT;
658
659
660 @Parameters(arity = "1..*", description = "One or more source files to verify")
661 private List<File> files;
662
663
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
670 @Option(names = "-o", description = "Sets the output file. Defaults to stdout.")
671 private Path outputPath;
672
673
674 @Option(names = "-p", description = "Sets the property files to load.")
675 private File propertiesFile;
676
677
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
692
693
694
695
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
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
713
714
715
716
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
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
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
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
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
755 @Option(names = {"-d", "--debug"},
756 description = "Prints all debug logging of CheckStyle utility.")
757 private boolean debug;
758
759
760
761
762
763
764
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
774
775
776
777
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
785 @Option(names = {"-E", "--executeIgnoredModules"},
786 description = "Allows ignored modules to be run.")
787 private boolean executeIgnoredModules;
788
789
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
796
797
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
811
812
813
814
815
816
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
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
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
867
868
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 }