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.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
40 import org.apache.commons.logging.Log;
41 import org.apache.commons.logging.LogFactory;
42
43 import com.puppycrawl.tools.checkstyle.api.AuditEvent;
44 import com.puppycrawl.tools.checkstyle.api.AuditListener;
45 import com.puppycrawl.tools.checkstyle.api.AutomaticBean;
46 import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
47 import com.puppycrawl.tools.checkstyle.api.Configuration;
48 import com.puppycrawl.tools.checkstyle.api.LocalizedMessage;
49 import com.puppycrawl.tools.checkstyle.api.RootModule;
50 import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
51 import picocli.CommandLine;
52 import picocli.CommandLine.Command;
53 import picocli.CommandLine.Option;
54 import picocli.CommandLine.ParameterException;
55 import picocli.CommandLine.Parameters;
56 import picocli.CommandLine.ParseResult;
57
58
59
60
61 public final class Main {
62
63
64
65
66
67 public static final String ERROR_COUNTER = "Main.errorCounter";
68
69
70
71
72 public static final String LOAD_PROPERTIES_EXCEPTION = "Main.loadProperties";
73
74
75
76
77 public static final String CREATE_LISTENER_EXCEPTION = "Main.createListener";
78
79
80 private static final Log LOG = LogFactory.getLog(Main.class);
81
82
83 private static final int EXIT_WITH_INVALID_USER_INPUT_CODE = -1;
84
85
86 private static final int EXIT_WITH_CHECKSTYLE_EXCEPTION_CODE = -2;
87
88
89
90 private Main() {
91 }
92
93
94
95
96
97
98
99
100 public static void main(String... args) throws IOException {
101
102 final CliOptions cliOptions = new CliOptions();
103 final CommandLine commandLine = new CommandLine(cliOptions);
104 commandLine.setUsageHelpWidth(CliOptions.HELP_WIDTH);
105 commandLine.setCaseInsensitiveEnumValuesAllowed(true);
106
107
108 int exitStatus = 0;
109 int errorCounter = 0;
110 try {
111 final ParseResult parseResult = commandLine.parseArgs(args);
112 if (parseResult.isVersionHelpRequested()) {
113 System.out.println(getVersionString());
114 }
115 else if (parseResult.isUsageHelpRequested()) {
116 commandLine.usage(System.out);
117 }
118 else {
119 exitStatus = execute(parseResult, cliOptions);
120 errorCounter = exitStatus;
121 }
122 }
123 catch (ParameterException ex) {
124 exitStatus = EXIT_WITH_INVALID_USER_INPUT_CODE;
125 System.err.println(ex.getMessage());
126 System.err.println("Usage: checkstyle [OPTIONS]... FILES...");
127 System.err.println("Try 'checkstyle --help' for more information.");
128 }
129 catch (CheckstyleException ex) {
130 exitStatus = EXIT_WITH_CHECKSTYLE_EXCEPTION_CODE;
131 errorCounter = 1;
132 ex.printStackTrace();
133 }
134 finally {
135
136 if (errorCounter > 0) {
137 final LocalizedMessage errorCounterMessage = new LocalizedMessage(1,
138 Definitions.CHECKSTYLE_BUNDLE, ERROR_COUNTER,
139 new String[] {String.valueOf(errorCounter)}, null, Main.class, null);
140 System.out.println(errorCounterMessage.getMessage());
141 }
142 if (exitStatus != 0) {
143 System.exit(exitStatus);
144 }
145 }
146 }
147
148
149
150
151
152 private static String getVersionString() {
153 return "Checkstyle version: " + Main.class.getPackage().getImplementationVersion();
154 }
155
156
157
158
159
160
161
162
163
164
165
166 private static int execute(ParseResult parseResult, CliOptions options)
167 throws IOException, CheckstyleException {
168
169 final int exitStatus;
170
171
172 final List<File> filesToProcess = getFilesToProcess(options);
173 final List<String> messages = options.validateCli(parseResult, filesToProcess);
174 final boolean hasMessages = !messages.isEmpty();
175 if (hasMessages) {
176 messages.forEach(System.out::println);
177 exitStatus = EXIT_WITH_INVALID_USER_INPUT_CODE;
178 }
179 else {
180 exitStatus = runCli(options, filesToProcess);
181 }
182 return exitStatus;
183 }
184
185
186
187
188
189
190 private static List<File> getFilesToProcess(CliOptions options) {
191 final List<Pattern> patternsToExclude = options.getExclusions();
192
193 final List<File> result = new LinkedList<>();
194 for (File file : options.files) {
195 result.addAll(listFiles(file, patternsToExclude));
196 }
197 return result;
198 }
199
200
201
202
203
204
205
206
207
208
209 private static List<File> listFiles(File node, List<Pattern> patternsToExclude) {
210
211
212 final List<File> result = new LinkedList<>();
213
214 if (node.canRead()) {
215 if (!isPathExcluded(node.getAbsolutePath(), patternsToExclude)) {
216 if (node.isDirectory()) {
217 final File[] files = node.listFiles();
218
219 if (files != null) {
220 for (File element : files) {
221 result.addAll(listFiles(element, patternsToExclude));
222 }
223 }
224 }
225 else if (node.isFile()) {
226 result.add(node);
227 }
228 }
229 }
230 return result;
231 }
232
233
234
235
236
237
238
239
240
241 private static boolean isPathExcluded(String path, List<Pattern> patternsToExclude) {
242 boolean result = false;
243
244 for (Pattern pattern : patternsToExclude) {
245 if (pattern.matcher(path).find()) {
246 result = true;
247 break;
248 }
249 }
250
251 return result;
252 }
253
254
255
256
257
258
259
260
261
262
263 private static int runCli(CliOptions options, List<File> filesToProcess)
264 throws IOException, CheckstyleException {
265 int result = 0;
266 final boolean hasSuppressionLineColumnNumber = options.suppressionLineColumnNumber != null;
267
268
269 if (options.printAst) {
270
271 final File file = filesToProcess.get(0);
272 final String stringAst = AstTreeStringPrinter.printFileAst(file,
273 JavaParser.Options.WITHOUT_COMMENTS);
274 System.out.print(stringAst);
275 }
276 else if (options.printAstWithComments) {
277 final File file = filesToProcess.get(0);
278 final String stringAst = AstTreeStringPrinter.printFileAst(file,
279 JavaParser.Options.WITH_COMMENTS);
280 System.out.print(stringAst);
281 }
282 else if (options.printJavadocTree) {
283 final File file = filesToProcess.get(0);
284 final String stringAst = DetailNodeTreeStringPrinter.printFileAst(file);
285 System.out.print(stringAst);
286 }
287 else if (options.printTreeWithJavadoc) {
288 final File file = filesToProcess.get(0);
289 final String stringAst = AstTreeStringPrinter.printJavaAndJavadocTree(file);
290 System.out.print(stringAst);
291 }
292 else if (hasSuppressionLineColumnNumber) {
293 final File file = filesToProcess.get(0);
294 final String stringSuppressions =
295 SuppressionsStringPrinter.printSuppressions(file,
296 options.suppressionLineColumnNumber, options.tabWidth);
297 System.out.print(stringSuppressions);
298 }
299 else {
300 if (options.debug) {
301 final Logger parentLogger = Logger.getLogger(Main.class.getName()).getParent();
302 final ConsoleHandler handler = new ConsoleHandler();
303 handler.setLevel(Level.FINEST);
304 handler.setFilter(new OnlyCheckstyleLoggersFilter());
305 parentLogger.addHandler(handler);
306 parentLogger.setLevel(Level.FINEST);
307 }
308 if (LOG.isDebugEnabled()) {
309 LOG.debug("Checkstyle debug logging enabled");
310 LOG.debug("Running Checkstyle with version: "
311 + Main.class.getPackage().getImplementationVersion());
312 }
313
314
315 result = runCheckstyle(options, filesToProcess);
316 }
317
318 return result;
319 }
320
321
322
323
324
325
326
327
328
329
330
331
332 private static int runCheckstyle(CliOptions options, List<File> filesToProcess)
333 throws CheckstyleException, IOException {
334
335 final Properties props;
336
337 if (options.propertiesFile == null) {
338 props = System.getProperties();
339 }
340 else {
341 props = loadProperties(options.propertiesFile);
342 }
343
344
345 final ThreadModeSettings multiThreadModeSettings =
346 new ThreadModeSettings(options.checkerThreadsNumber,
347 options.treeWalkerThreadsNumber);
348
349 final ConfigurationLoader.IgnoredModulesOptions ignoredModulesOptions;
350 if (options.executeIgnoredModules) {
351 ignoredModulesOptions = ConfigurationLoader.IgnoredModulesOptions.EXECUTE;
352 }
353 else {
354 ignoredModulesOptions = ConfigurationLoader.IgnoredModulesOptions.OMIT;
355 }
356
357 final Configuration config = ConfigurationLoader.loadConfiguration(
358 options.configurationFile, new PropertiesExpander(props),
359 ignoredModulesOptions, multiThreadModeSettings);
360
361
362 final int errorCounter;
363 final ClassLoader moduleClassLoader = Checker.class.getClassLoader();
364 final RootModule rootModule = getRootModule(config.getName(), moduleClassLoader);
365
366 try {
367 final AuditListener listener;
368 if (options.generateXpathSuppressionsFile) {
369
370 final Configuration treeWalkerConfig = getTreeWalkerConfig(config);
371 if (treeWalkerConfig != null) {
372 final DefaultConfiguration moduleConfig =
373 new DefaultConfiguration(
374 XpathFileGeneratorAstFilter.class.getName());
375 moduleConfig.addAttribute(CliOptions.ATTRIB_TAB_WIDTH_NAME,
376 String.valueOf(options.tabWidth));
377 ((DefaultConfiguration) treeWalkerConfig).addChild(moduleConfig);
378 }
379
380 listener = new XpathFileGeneratorAuditListener(System.out,
381 AutomaticBean.OutputStreamOptions.NONE);
382 }
383 else {
384 listener = createListener(options.format, options.outputPath);
385 }
386
387 rootModule.setModuleClassLoader(moduleClassLoader);
388 rootModule.configure(config);
389 rootModule.addListener(listener);
390
391
392 errorCounter = rootModule.process(filesToProcess);
393 }
394 finally {
395 rootModule.destroy();
396 }
397
398 return errorCounter;
399 }
400
401
402
403
404
405
406
407
408
409 private static Properties loadProperties(File file)
410 throws CheckstyleException {
411 final Properties properties = new Properties();
412
413 try (InputStream stream = Files.newInputStream(file.toPath())) {
414 properties.load(stream);
415 }
416 catch (final IOException ex) {
417 final LocalizedMessage loadPropertiesExceptionMessage = new LocalizedMessage(1,
418 Definitions.CHECKSTYLE_BUNDLE, LOAD_PROPERTIES_EXCEPTION,
419 new String[] {file.getAbsolutePath()}, null, Main.class, null);
420 throw new CheckstyleException(loadPropertiesExceptionMessage.getMessage(), ex);
421 }
422
423 return properties;
424 }
425
426
427
428
429
430
431
432
433
434
435 private static RootModule getRootModule(String name, ClassLoader moduleClassLoader)
436 throws CheckstyleException {
437 final ModuleFactory factory = new PackageObjectFactory(
438 Checker.class.getPackage().getName(), moduleClassLoader);
439
440 return (RootModule) factory.createModule(name);
441 }
442
443
444
445
446
447
448 private static Configuration getTreeWalkerConfig(Configuration config) {
449 Configuration result = null;
450
451 final Configuration[] children = config.getChildren();
452 for (Configuration child : children) {
453 if ("TreeWalker".equals(child.getName())) {
454 result = child;
455 break;
456 }
457 }
458 return result;
459 }
460
461
462
463
464
465
466
467
468
469
470 private static AuditListener createListener(OutputFormat format, Path outputLocation)
471 throws IOException {
472 final OutputStream out = getOutputStream(outputLocation);
473 final AutomaticBean.OutputStreamOptions closeOutputStreamOption =
474 getOutputStreamOptions(outputLocation);
475 return format.createListener(out, closeOutputStreamOption);
476 }
477
478
479
480
481
482
483
484
485 @SuppressWarnings("resource")
486 private static OutputStream getOutputStream(Path outputPath) throws IOException {
487 final OutputStream result;
488 if (outputPath == null) {
489 result = System.out;
490 }
491 else {
492 result = Files.newOutputStream(outputPath);
493 }
494 return result;
495 }
496
497
498
499
500
501
502 private static AutomaticBean.OutputStreamOptions getOutputStreamOptions(Path outputPath) {
503 final AutomaticBean.OutputStreamOptions result;
504 if (outputPath == null) {
505 result = AutomaticBean.OutputStreamOptions.NONE;
506 }
507 else {
508 result = AutomaticBean.OutputStreamOptions.CLOSE;
509 }
510 return result;
511 }
512
513
514
515
516
517 enum OutputFormat {
518
519 XML,
520
521 PLAIN;
522
523
524
525
526
527
528
529 public AuditListener createListener(OutputStream out,
530 AutomaticBean.OutputStreamOptions options) {
531 final AuditListener result;
532 if (this == XML) {
533 result = new XMLLogger(out, options);
534 }
535 else {
536 result = new DefaultLogger(out, options);
537 }
538 return result;
539 }
540
541
542
543
544 @Override
545 public String toString() {
546 return name().toLowerCase(Locale.ROOT);
547 }
548 }
549
550
551 private static final class OnlyCheckstyleLoggersFilter implements Filter {
552
553 private final String packageName = Main.class.getPackage().getName();
554
555
556
557
558
559
560 @Override
561 public boolean isLoggable(LogRecord record) {
562 return record.getLoggerName().startsWith(packageName);
563 }
564 }
565
566
567
568
569
570
571 @Command(name = "checkstyle", description = "Checkstyle verifies that the specified "
572 + "source code files adhere to the specified rules. By default errors are "
573 + "reported to standard out in plain format. Checkstyle requires a configuration "
574 + "XML file that configures the checks to apply.",
575 mixinStandardHelpOptions = true)
576 private static class CliOptions {
577
578
579 private static final int HELP_WIDTH = 100;
580
581
582 private static final int DEFAULT_THREAD_COUNT = 1;
583
584
585 private static final String ATTRIB_TAB_WIDTH_NAME = "tabWidth";
586
587
588 private static final OutputFormat DEFAULT_OUTPUT_FORMAT = OutputFormat.PLAIN;
589
590
591 private static final String OUTPUT_FORMAT_OPTION = "-f";
592
593
594 @Parameters(arity = "1..*", description = "One or more source files to verify")
595 private List<File> files;
596
597
598 @Option(names = "-c", description = "Sets the check configuration file to use.")
599 private String configurationFile;
600
601
602 @Option(names = "-o", description = "Sets the output file. Defaults to stdout")
603 private Path outputPath;
604
605
606 @Option(names = "-p", description = "Loads the properties file")
607 private File propertiesFile;
608
609
610 @Option(names = "-s",
611 description = "Print xpath suppressions at the file's line and column position. "
612 + "Argument is the line and column number (separated by a : ) in the file "
613 + "that the suppression should be generated for")
614 private String suppressionLineColumnNumber;
615
616
617
618
619
620 @Option(names = "--tabWidth", description = "Sets the length of the tab character. "
621 + "Used only with \"-s\" option. Default value is ${DEFAULT-VALUE}")
622 private int tabWidth = CommonUtil.DEFAULT_TAB_WIDTH;
623
624
625 @Option(names = {"-g", "--generate-xpath-suppression"},
626 description = "Generates to output a suppression.xml to use to suppress all"
627 + " violations from user's config")
628 private boolean generateXpathSuppressionsFile;
629
630
631
632
633
634 @Option(names = "-f", description = "Sets the output format. Valid values: "
635 + "${COMPLETION-CANDIDATES}. Defaults to ${DEFAULT-VALUE}")
636 private OutputFormat format = DEFAULT_OUTPUT_FORMAT;
637
638
639 @Option(names = {"-t", "--tree"},
640 description = "Print Abstract Syntax Tree(AST) of the file")
641 private boolean printAst;
642
643
644 @Option(names = {"-T", "--treeWithComments"},
645 description = "Print Abstract Syntax Tree(AST) of the file including comments")
646 private boolean printAstWithComments;
647
648
649 @Option(names = {"-j", "--javadocTree"},
650 description = "Print Parse tree of the Javadoc comment")
651 private boolean printJavadocTree;
652
653
654 @Option(names = {"-J", "--treeWithJavadoc"},
655 description = "Print full Abstract Syntax Tree of the file")
656 private boolean printTreeWithJavadoc;
657
658
659 @Option(names = {"-d", "--debug"},
660 description = "Print all debug logging of CheckStyle utility")
661 private boolean debug;
662
663
664
665
666
667 @Option(names = {"-e", "--exclude"},
668 description = "Directory/File path to exclude from CheckStyle")
669 private List<File> exclude = new ArrayList<>();
670
671
672
673
674
675 @Option(names = {"-x", "--exclude-regexp"},
676 description = "Regular expression of directory/file to exclude from CheckStyle")
677 private List<Pattern> excludeRegex = new ArrayList<>();
678
679
680 @Option(names = "--executeIgnoredModules",
681 description = "Allows ignored modules to be run.")
682 private boolean executeIgnoredModules;
683
684
685
686
687
688 @Option(names = {"-C", "--checker-threads-number"}, description = "(experimental) The "
689 + "number of Checker threads (must be greater than zero)")
690 private int checkerThreadsNumber = DEFAULT_THREAD_COUNT;
691
692
693
694
695
696 @Option(names = {"-W", "--tree-walker-threads-number"}, description = "(experimental) The "
697 + "number of TreeWalker threads (must be greater than zero)")
698 private int treeWalkerThreadsNumber = DEFAULT_THREAD_COUNT;
699
700
701
702
703
704 private List<Pattern> getExclusions() {
705 final List<Pattern> result = new ArrayList<>();
706 exclude.forEach(file -> result.add(
707 Pattern.compile("^" + Pattern.quote(file.getAbsolutePath()) + "$")));
708 result.addAll(excludeRegex);
709 return result;
710 }
711
712
713
714
715
716
717
718
719 private List<String> validateCli(ParseResult parseResult, List<File> filesToProcess) {
720 final List<String> result = new ArrayList<>();
721 final boolean hasConfigurationFile = configurationFile != null;
722 final boolean hasSuppressionLineColumnNumber = suppressionLineColumnNumber != null;
723
724 if (filesToProcess.isEmpty()) {
725 result.add("Files to process must be specified, found 0.");
726 }
727
728 else if (printAst || printAstWithComments || printJavadocTree || printTreeWithJavadoc) {
729 if (suppressionLineColumnNumber != null || configurationFile != null
730 || propertiesFile != null || outputPath != null
731 || parseResult.hasMatchedOption(OUTPUT_FORMAT_OPTION)) {
732 result.add("Option '-t' cannot be used with other options.");
733 }
734 else if (filesToProcess.size() > 1) {
735 result.add("Printing AST is allowed for only one file.");
736 }
737 }
738 else if (hasSuppressionLineColumnNumber) {
739 if (configurationFile != null || propertiesFile != null
740 || outputPath != null
741 || parseResult.hasMatchedOption(OUTPUT_FORMAT_OPTION)) {
742 result.add("Option '-s' cannot be used with other options.");
743 }
744 else if (filesToProcess.size() > 1) {
745 result.add("Printing xpath suppressions is allowed for only one file.");
746 }
747 }
748 else if (hasConfigurationFile) {
749 try {
750
751 CommonUtil.getUriByFilename(configurationFile);
752 }
753 catch (CheckstyleException ignored) {
754 final String msg = "Could not find config XML file '%s'.";
755 result.add(String.format(Locale.ROOT, msg, configurationFile));
756 }
757
758
759 if (propertiesFile != null && !propertiesFile.exists()) {
760 result.add(String.format(Locale.ROOT,
761 "Could not find file '%s'.", propertiesFile));
762 }
763 if (checkerThreadsNumber < 1) {
764 result.add("Checker threads number must be greater than zero");
765 }
766 if (treeWalkerThreadsNumber < 1) {
767 result.add("TreeWalker threads number must be greater than zero");
768 }
769 }
770 else {
771 result.add("Must specify a config XML file.");
772 }
773
774 return result;
775 }
776 }
777 }