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}