001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2022 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.ant; 021 022import java.io.File; 023import java.io.IOException; 024import java.io.InputStream; 025import java.io.OutputStream; 026import java.nio.file.Files; 027import java.util.ArrayList; 028import java.util.Arrays; 029import java.util.List; 030import java.util.Locale; 031import java.util.Map; 032import java.util.Objects; 033import java.util.Properties; 034import java.util.stream.Collectors; 035 036import org.apache.tools.ant.BuildException; 037import org.apache.tools.ant.DirectoryScanner; 038import org.apache.tools.ant.Project; 039import org.apache.tools.ant.Task; 040import org.apache.tools.ant.taskdefs.LogOutputStream; 041import org.apache.tools.ant.types.EnumeratedAttribute; 042import org.apache.tools.ant.types.FileSet; 043import org.apache.tools.ant.types.Path; 044import org.apache.tools.ant.types.Reference; 045 046import com.puppycrawl.tools.checkstyle.Checker; 047import com.puppycrawl.tools.checkstyle.ConfigurationLoader; 048import com.puppycrawl.tools.checkstyle.DefaultLogger; 049import com.puppycrawl.tools.checkstyle.ModuleFactory; 050import com.puppycrawl.tools.checkstyle.PackageObjectFactory; 051import com.puppycrawl.tools.checkstyle.PropertiesExpander; 052import com.puppycrawl.tools.checkstyle.ThreadModeSettings; 053import com.puppycrawl.tools.checkstyle.XMLLogger; 054import com.puppycrawl.tools.checkstyle.api.AuditListener; 055import com.puppycrawl.tools.checkstyle.api.AutomaticBean; 056import com.puppycrawl.tools.checkstyle.api.CheckstyleException; 057import com.puppycrawl.tools.checkstyle.api.Configuration; 058import com.puppycrawl.tools.checkstyle.api.RootModule; 059import com.puppycrawl.tools.checkstyle.api.SeverityLevel; 060import com.puppycrawl.tools.checkstyle.api.SeverityLevelCounter; 061 062/** 063 * An implementation of a ANT task for calling checkstyle. See the documentation 064 * of the task for usage. 065 */ 066public class CheckstyleAntTask extends Task { 067 068 /** Poor man's enum for an xml formatter. */ 069 private static final String E_XML = "xml"; 070 /** Poor man's enum for an plain formatter. */ 071 private static final String E_PLAIN = "plain"; 072 073 /** Suffix for time string. */ 074 private static final String TIME_SUFFIX = " ms."; 075 076 /** Contains the paths to process. */ 077 private final List<Path> paths = new ArrayList<>(); 078 079 /** Contains the filesets to process. */ 080 private final List<FileSet> fileSets = new ArrayList<>(); 081 082 /** Contains the formatters to log to. */ 083 private final List<Formatter> formatters = new ArrayList<>(); 084 085 /** Contains the Properties to override. */ 086 private final List<Property> overrideProps = new ArrayList<>(); 087 088 /** Class path to locate class files. */ 089 private Path classpath; 090 091 /** Name of file to check. */ 092 private String fileName; 093 094 /** Config file containing configuration. */ 095 private String config; 096 097 /** Whether to fail build on violations. */ 098 private boolean failOnViolation = true; 099 100 /** Property to set on violations. */ 101 private String failureProperty; 102 103 /** The name of the properties file. */ 104 private File properties; 105 106 /** The maximum number of errors that are tolerated. */ 107 private int maxErrors; 108 109 /** The maximum number of warnings that are tolerated. */ 110 private int maxWarnings = Integer.MAX_VALUE; 111 112 /** 113 * Whether to execute ignored modules - some modules may log above 114 * their severity depending on their configuration (e.g. WriteTag) so 115 * need to be included 116 */ 117 private boolean executeIgnoredModules; 118 119 //////////////////////////////////////////////////////////////////////////// 120 // Setters for ANT specific attributes 121 //////////////////////////////////////////////////////////////////////////// 122 123 /** 124 * Tells this task to write failure message to the named property when there 125 * is a violation. 126 * 127 * @param propertyName the name of the property to set 128 * in the event of an failure. 129 */ 130 public void setFailureProperty(String propertyName) { 131 failureProperty = propertyName; 132 } 133 134 /** 135 * Sets flag - whether to fail if a violation is found. 136 * 137 * @param fail whether to fail if a violation is found 138 */ 139 public void setFailOnViolation(boolean fail) { 140 failOnViolation = fail; 141 } 142 143 /** 144 * Sets the maximum number of errors allowed. Default is 0. 145 * 146 * @param maxErrors the maximum number of errors allowed. 147 */ 148 public void setMaxErrors(int maxErrors) { 149 this.maxErrors = maxErrors; 150 } 151 152 /** 153 * Sets the maximum number of warnings allowed. Default is 154 * {@link Integer#MAX_VALUE}. 155 * 156 * @param maxWarnings the maximum number of warnings allowed. 157 */ 158 public void setMaxWarnings(int maxWarnings) { 159 this.maxWarnings = maxWarnings; 160 } 161 162 /** 163 * Adds a path. 164 * 165 * @param path the path to add. 166 */ 167 public void addPath(Path path) { 168 paths.add(path); 169 } 170 171 /** 172 * Adds set of files (nested fileset attribute). 173 * 174 * @param fileSet the file set to add 175 */ 176 public void addFileset(FileSet fileSet) { 177 fileSets.add(fileSet); 178 } 179 180 /** 181 * Add a formatter. 182 * 183 * @param formatter the formatter to add for logging. 184 */ 185 public void addFormatter(Formatter formatter) { 186 formatters.add(formatter); 187 } 188 189 /** 190 * Add an override property. 191 * 192 * @param property the property to add 193 */ 194 public void addProperty(Property property) { 195 overrideProps.add(property); 196 } 197 198 /** 199 * Set the class path. 200 * 201 * @param classpath the path to locate classes 202 */ 203 public void setClasspath(Path classpath) { 204 if (this.classpath == null) { 205 this.classpath = classpath; 206 } 207 else { 208 this.classpath.append(classpath); 209 } 210 } 211 212 /** 213 * Set the class path from a reference defined elsewhere. 214 * 215 * @param classpathRef the reference to an instance defining the classpath 216 */ 217 public void setClasspathRef(Reference classpathRef) { 218 createClasspath().setRefid(classpathRef); 219 } 220 221 /** 222 * Creates classpath. 223 * 224 * @return a created path for locating classes 225 */ 226 public Path createClasspath() { 227 if (classpath == null) { 228 classpath = new Path(getProject()); 229 } 230 return classpath.createPath(); 231 } 232 233 /** 234 * Sets file to be checked. 235 * 236 * @param file the file to be checked 237 */ 238 public void setFile(File file) { 239 fileName = file.getAbsolutePath(); 240 } 241 242 /** 243 * Sets configuration file. 244 * 245 * @param configuration the configuration file, URL, or resource to use 246 * @throws BuildException when config was already set 247 */ 248 public void setConfig(String configuration) { 249 if (config != null) { 250 throw new BuildException("Attribute 'config' has already been set"); 251 } 252 config = configuration; 253 } 254 255 /** 256 * Sets flag - whether to execute ignored modules. 257 * 258 * @param omit whether to execute ignored modules 259 */ 260 public void setExecuteIgnoredModules(boolean omit) { 261 executeIgnoredModules = omit; 262 } 263 264 //////////////////////////////////////////////////////////////////////////// 265 // Setters for Root Module's configuration attributes 266 //////////////////////////////////////////////////////////////////////////// 267 268 /** 269 * Sets a properties file for use instead 270 * of individually setting them. 271 * 272 * @param props the properties File to use 273 */ 274 public void setProperties(File props) { 275 properties = props; 276 } 277 278 //////////////////////////////////////////////////////////////////////////// 279 // The doers 280 //////////////////////////////////////////////////////////////////////////// 281 282 @Override 283 public void execute() { 284 final long startTime = System.currentTimeMillis(); 285 286 try { 287 final String version = CheckstyleAntTask.class.getPackage().getImplementationVersion(); 288 289 log("checkstyle version " + version, Project.MSG_VERBOSE); 290 291 // Check for no arguments 292 if (fileName == null 293 && fileSets.isEmpty() 294 && paths.isEmpty()) { 295 throw new BuildException( 296 "Must specify at least one of 'file' or nested 'fileset' or 'path'.", 297 getLocation()); 298 } 299 if (config == null) { 300 throw new BuildException("Must specify 'config'.", getLocation()); 301 } 302 realExecute(version); 303 } 304 finally { 305 final long endTime = System.currentTimeMillis(); 306 log("Total execution took " + (endTime - startTime) + TIME_SUFFIX, 307 Project.MSG_VERBOSE); 308 } 309 } 310 311 /** 312 * Helper implementation to perform execution. 313 * 314 * @param checkstyleVersion Checkstyle compile version. 315 */ 316 private void realExecute(String checkstyleVersion) { 317 // Create the root module 318 RootModule rootModule = null; 319 try { 320 rootModule = createRootModule(); 321 322 // setup the listeners 323 final AuditListener[] listeners = getListeners(); 324 for (AuditListener element : listeners) { 325 rootModule.addListener(element); 326 } 327 final SeverityLevelCounter warningCounter = 328 new SeverityLevelCounter(SeverityLevel.WARNING); 329 rootModule.addListener(warningCounter); 330 331 processFiles(rootModule, warningCounter, checkstyleVersion); 332 } 333 finally { 334 if (rootModule != null) { 335 rootModule.destroy(); 336 } 337 } 338 } 339 340 /** 341 * Scans and processes files by means given root module. 342 * 343 * @param rootModule Root module to process files 344 * @param warningCounter Root Module's counter of warnings 345 * @param checkstyleVersion Checkstyle compile version 346 * @throws BuildException if the files could not be processed, 347 * or if the build failed due to violations. 348 */ 349 private void processFiles(RootModule rootModule, final SeverityLevelCounter warningCounter, 350 final String checkstyleVersion) { 351 final long startTime = System.currentTimeMillis(); 352 final List<File> files = getFilesToCheck(); 353 final long endTime = System.currentTimeMillis(); 354 log("To locate the files took " + (endTime - startTime) + TIME_SUFFIX, 355 Project.MSG_VERBOSE); 356 357 log("Running Checkstyle " 358 + Objects.toString(checkstyleVersion, "") 359 + " on " + files.size() 360 + " files", Project.MSG_INFO); 361 log("Using configuration " + config, Project.MSG_VERBOSE); 362 363 final int numErrs; 364 365 try { 366 final long processingStartTime = System.currentTimeMillis(); 367 numErrs = rootModule.process(files); 368 final long processingEndTime = System.currentTimeMillis(); 369 log("To process the files took " + (processingEndTime - processingStartTime) 370 + TIME_SUFFIX, Project.MSG_VERBOSE); 371 } 372 catch (CheckstyleException ex) { 373 throw new BuildException("Unable to process files: " + files, ex); 374 } 375 final int numWarnings = warningCounter.getCount(); 376 final boolean okStatus = numErrs <= maxErrors && numWarnings <= maxWarnings; 377 378 // Handle the return status 379 if (!okStatus) { 380 final String failureMsg = 381 "Got " + numErrs + " errors and " + numWarnings 382 + " warnings."; 383 if (failureProperty != null) { 384 getProject().setProperty(failureProperty, failureMsg); 385 } 386 387 if (failOnViolation) { 388 throw new BuildException(failureMsg, getLocation()); 389 } 390 } 391 } 392 393 /** 394 * Creates new instance of the root module. 395 * 396 * @return new instance of the root module 397 * @throws BuildException if the root module could not be created. 398 */ 399 private RootModule createRootModule() { 400 final RootModule rootModule; 401 try { 402 final Properties props = createOverridingProperties(); 403 final ThreadModeSettings threadModeSettings = 404 ThreadModeSettings.SINGLE_THREAD_MODE_INSTANCE; 405 final ConfigurationLoader.IgnoredModulesOptions ignoredModulesOptions; 406 if (executeIgnoredModules) { 407 ignoredModulesOptions = ConfigurationLoader.IgnoredModulesOptions.EXECUTE; 408 } 409 else { 410 ignoredModulesOptions = ConfigurationLoader.IgnoredModulesOptions.OMIT; 411 } 412 413 final Configuration configuration = ConfigurationLoader.loadConfiguration(config, 414 new PropertiesExpander(props), ignoredModulesOptions, threadModeSettings); 415 416 final ClassLoader moduleClassLoader = 417 Checker.class.getClassLoader(); 418 419 final ModuleFactory factory = new PackageObjectFactory( 420 Checker.class.getPackage().getName() + ".", moduleClassLoader); 421 422 rootModule = (RootModule) factory.createModule(configuration.getName()); 423 rootModule.setModuleClassLoader(moduleClassLoader); 424 rootModule.configure(configuration); 425 } 426 catch (final CheckstyleException ex) { 427 throw new BuildException(String.format(Locale.ROOT, "Unable to create Root Module: " 428 + "config {%s}, classpath {%s}.", config, classpath), ex); 429 } 430 return rootModule; 431 } 432 433 /** 434 * Create the Properties object based on the arguments specified 435 * to the ANT task. 436 * 437 * @return the properties for property expansion expansion 438 * @throws BuildException if the properties file could not be loaded. 439 */ 440 private Properties createOverridingProperties() { 441 final Properties returnValue = new Properties(); 442 443 // Load the properties file if specified 444 if (properties != null) { 445 try (InputStream inStream = Files.newInputStream(properties.toPath())) { 446 returnValue.load(inStream); 447 } 448 catch (final IOException ex) { 449 throw new BuildException("Error loading Properties file '" 450 + properties + "'", ex, getLocation()); 451 } 452 } 453 454 // override with Ant properties like ${basedir} 455 final Map<String, Object> antProps = getProject().getProperties(); 456 for (Map.Entry<String, Object> entry : antProps.entrySet()) { 457 final String value = String.valueOf(entry.getValue()); 458 returnValue.setProperty(entry.getKey(), value); 459 } 460 461 // override with properties specified in subelements 462 for (Property p : overrideProps) { 463 returnValue.setProperty(p.getKey(), p.getValue()); 464 } 465 466 return returnValue; 467 } 468 469 /** 470 * Return the list of listeners set in this task. 471 * 472 * @return the list of listeners. 473 * @throws BuildException if the listeners could not be created. 474 */ 475 private AuditListener[] getListeners() { 476 final int formatterCount = Math.max(1, formatters.size()); 477 478 final AuditListener[] listeners = new AuditListener[formatterCount]; 479 480 // formatters 481 try { 482 if (formatters.isEmpty()) { 483 final OutputStream debug = new LogOutputStream(this, Project.MSG_DEBUG); 484 final OutputStream err = new LogOutputStream(this, Project.MSG_ERR); 485 listeners[0] = new DefaultLogger(debug, AutomaticBean.OutputStreamOptions.CLOSE, 486 err, AutomaticBean.OutputStreamOptions.CLOSE); 487 } 488 else { 489 for (int i = 0; i < formatterCount; i++) { 490 final Formatter formatter = formatters.get(i); 491 listeners[i] = formatter.createListener(this); 492 } 493 } 494 } 495 catch (IOException ex) { 496 throw new BuildException(String.format(Locale.ROOT, "Unable to create listeners: " 497 + "formatters {%s}.", formatters), ex); 498 } 499 return listeners; 500 } 501 502 /** 503 * Returns the list of files (full path name) to process. 504 * 505 * @return the list of files included via the fileName, filesets and paths. 506 */ 507 private List<File> getFilesToCheck() { 508 final List<File> allFiles = new ArrayList<>(); 509 if (fileName != null) { 510 // oops we've got an additional one to process, don't 511 // forget it. No sweat, it's fully resolved via the setter. 512 log("Adding standalone file for audit", Project.MSG_VERBOSE); 513 allFiles.add(new File(fileName)); 514 } 515 516 final List<File> filesFromFileSets = scanFileSets(); 517 allFiles.addAll(filesFromFileSets); 518 519 final List<File> filesFromPaths = scanPaths(); 520 allFiles.addAll(filesFromPaths); 521 522 return allFiles; 523 } 524 525 /** 526 * Retrieves all files from the defined paths. 527 * 528 * @return a list of files defined via paths. 529 */ 530 private List<File> scanPaths() { 531 final List<File> allFiles = new ArrayList<>(); 532 533 for (int i = 0; i < paths.size(); i++) { 534 final Path currentPath = paths.get(i); 535 final List<File> pathFiles = scanPath(currentPath, i + 1); 536 allFiles.addAll(pathFiles); 537 } 538 539 return allFiles; 540 } 541 542 /** 543 * Scans the given path and retrieves all files for the given path. 544 * 545 * @param path A path to scan. 546 * @param pathIndex The index of the given path. Used in log messages only. 547 * @return A list of files, extracted from the given path. 548 */ 549 private List<File> scanPath(Path path, int pathIndex) { 550 final String[] resources = path.list(); 551 log(pathIndex + ") Scanning path " + path, Project.MSG_VERBOSE); 552 final List<File> allFiles = new ArrayList<>(); 553 int concreteFilesCount = 0; 554 555 for (String resource : resources) { 556 final File file = new File(resource); 557 if (file.isFile()) { 558 concreteFilesCount++; 559 allFiles.add(file); 560 } 561 else { 562 final DirectoryScanner scanner = new DirectoryScanner(); 563 scanner.setBasedir(file); 564 scanner.scan(); 565 final List<File> scannedFiles = retrieveAllScannedFiles(scanner, pathIndex); 566 allFiles.addAll(scannedFiles); 567 } 568 } 569 570 if (concreteFilesCount > 0) { 571 log(String.format(Locale.ROOT, "%d) Adding %d files from path %s", 572 pathIndex, concreteFilesCount, path), Project.MSG_VERBOSE); 573 } 574 575 return allFiles; 576 } 577 578 /** 579 * Returns the list of files (full path name) to process. 580 * 581 * @return the list of files included via the filesets. 582 */ 583 protected List<File> scanFileSets() { 584 final List<File> allFiles = new ArrayList<>(); 585 586 for (int i = 0; i < fileSets.size(); i++) { 587 final FileSet fileSet = fileSets.get(i); 588 final DirectoryScanner scanner = fileSet.getDirectoryScanner(getProject()); 589 final List<File> scannedFiles = retrieveAllScannedFiles(scanner, i); 590 allFiles.addAll(scannedFiles); 591 } 592 593 return allFiles; 594 } 595 596 /** 597 * Retrieves all matched files from the given scanner. 598 * 599 * @param scanner A directory scanner. Note, that {@link DirectoryScanner#scan()} 600 * must be called before calling this method. 601 * @param logIndex A log entry index. Used only for log messages. 602 * @return A list of files, retrieved from the given scanner. 603 */ 604 private List<File> retrieveAllScannedFiles(DirectoryScanner scanner, int logIndex) { 605 final String[] fileNames = scanner.getIncludedFiles(); 606 log(String.format(Locale.ROOT, "%d) Adding %d files from directory %s", 607 logIndex, fileNames.length, scanner.getBasedir()), Project.MSG_VERBOSE); 608 609 return Arrays.stream(fileNames) 610 .map(name -> scanner.getBasedir() + File.separator + name) 611 .map(File::new) 612 .collect(Collectors.toList()); 613 } 614 615 /** 616 * Poor mans enumeration for the formatter types. 617 */ 618 public static class FormatterType extends EnumeratedAttribute { 619 620 /** My possible values. */ 621 private static final String[] VALUES = {E_XML, E_PLAIN}; 622 623 @Override 624 public String[] getValues() { 625 return VALUES.clone(); 626 } 627 628 } 629 630 /** 631 * Details about a formatter to be used. 632 */ 633 public static class Formatter { 634 635 /** The formatter type. */ 636 private FormatterType type; 637 /** The file to output to. */ 638 private File toFile; 639 /** Whether or not the write to the named file. */ 640 private boolean useFile = true; 641 642 /** 643 * Set the type of the formatter. 644 * 645 * @param type the type 646 */ 647 public void setType(FormatterType type) { 648 this.type = type; 649 } 650 651 /** 652 * Set the file to output to. 653 * 654 * @param destination destination the file to output to 655 */ 656 public void setTofile(File destination) { 657 toFile = destination; 658 } 659 660 /** 661 * Sets whether or not we write to a file if it is provided. 662 * 663 * @param use whether not not to use provided file. 664 */ 665 public void setUseFile(boolean use) { 666 useFile = use; 667 } 668 669 /** 670 * Creates a listener for the formatter. 671 * 672 * @param task the task running 673 * @return a listener 674 * @throws IOException if an error occurs 675 */ 676 public AuditListener createListener(Task task) throws IOException { 677 final AuditListener listener; 678 if (type != null 679 && E_XML.equals(type.getValue())) { 680 listener = createXmlLogger(task); 681 } 682 else { 683 listener = createDefaultLogger(task); 684 } 685 return listener; 686 } 687 688 /** 689 * Creates default logger. 690 * 691 * @param task the task to possibly log to 692 * @return a DefaultLogger instance 693 * @throws IOException if an error occurs 694 */ 695 private AuditListener createDefaultLogger(Task task) 696 throws IOException { 697 final AuditListener defaultLogger; 698 if (toFile == null || !useFile) { 699 defaultLogger = new DefaultLogger( 700 new LogOutputStream(task, Project.MSG_DEBUG), 701 AutomaticBean.OutputStreamOptions.CLOSE, 702 new LogOutputStream(task, Project.MSG_ERR), 703 AutomaticBean.OutputStreamOptions.CLOSE 704 ); 705 } 706 else { 707 final OutputStream infoStream = Files.newOutputStream(toFile.toPath()); 708 defaultLogger = 709 new DefaultLogger(infoStream, AutomaticBean.OutputStreamOptions.CLOSE, 710 infoStream, AutomaticBean.OutputStreamOptions.NONE); 711 } 712 return defaultLogger; 713 } 714 715 /** 716 * Creates XML logger. 717 * 718 * @param task the task to possibly log to 719 * @return an XMLLogger instance 720 * @throws IOException if an error occurs 721 */ 722 private AuditListener createXmlLogger(Task task) throws IOException { 723 final AuditListener xmlLogger; 724 if (toFile == null || !useFile) { 725 xmlLogger = new XMLLogger(new LogOutputStream(task, Project.MSG_INFO), 726 AutomaticBean.OutputStreamOptions.CLOSE); 727 } 728 else { 729 xmlLogger = new XMLLogger(Files.newOutputStream(toFile.toPath()), 730 AutomaticBean.OutputStreamOptions.CLOSE); 731 } 732 return xmlLogger; 733 } 734 735 } 736 737 /** 738 * Represents a property that consists of a key and value. 739 */ 740 public static class Property { 741 742 /** The property key. */ 743 private String key; 744 /** The property value. */ 745 private String value; 746 747 /** 748 * Gets key. 749 * 750 * @return the property key 751 */ 752 public String getKey() { 753 return key; 754 } 755 756 /** 757 * Sets key. 758 * 759 * @param key sets the property key 760 */ 761 public void setKey(String key) { 762 this.key = key; 763 } 764 765 /** 766 * Gets value. 767 * 768 * @return the property value 769 */ 770 public String getValue() { 771 return value; 772 } 773 774 /** 775 * Sets value. 776 * 777 * @param value set the property value 778 */ 779 public void setValue(String value) { 780 this.value = value; 781 } 782 783 /** 784 * Sets the property value from a File. 785 * 786 * @param file set the property value from a File 787 */ 788 public void setFile(File file) { 789 value = file.getAbsolutePath(); 790 } 791 792 } 793 794}