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