View Javadoc
1   /*
2    * SPDX-FileCopyrightText: Copyright (c) 2011-2026 Yegor Bugayenko
3    * SPDX-License-Identifier: MIT
4    */
5   package com.qulice.checkstyle;
6   
7   import com.jcabi.log.Logger;
8   import com.puppycrawl.tools.checkstyle.Checker;
9   import com.puppycrawl.tools.checkstyle.ConfigurationLoader;
10  import com.puppycrawl.tools.checkstyle.PropertiesExpander;
11  import com.puppycrawl.tools.checkstyle.api.AuditEvent;
12  import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
13  import com.puppycrawl.tools.checkstyle.api.Configuration;
14  import com.qulice.spi.Environment;
15  import com.qulice.spi.Relative;
16  import com.qulice.spi.ResourceValidator;
17  import com.qulice.spi.Violation;
18  import java.io.File;
19  import java.util.Collection;
20  import java.util.LinkedList;
21  import java.util.List;
22  import java.util.Locale;
23  import java.util.Properties;
24  import java.util.Set;
25  import org.xml.sax.InputSource;
26  
27  /**
28   * Validator with Checkstyle.
29   * @since 0.3
30   * @checkstyle ClassDataAbstractionCoupling (260 lines)
31   */
32  public final class CheckstyleValidator implements ResourceValidator {
33  
34      /**
35       * Extensions of files that are passed to Checkstyle. These match the
36       * file extensions referenced by checks in {@code checks.xml}. Checkstyle
37       * itself filters further based on each module's {@code fileExtensions}.
38       */
39      private static final Set<String> EXTENSIONS = Set.of(
40          "java", "txt", "xml", "xsl", "xsd", "properties", "groovy", "vm",
41          "mf", "sh", "sql", "tokens", "g", "spec", "css", "csv", "js", "json",
42          "md", "yml", "yaml", "gradle", "dtd", "scss", "html"
43      );
44  
45      /**
46       * Checkstyle checker.
47       */
48      private final Checker checker;
49  
50      /**
51       * Listener of checkstyle messages.
52        */
53      private final CheckstyleListener listener;
54  
55      /**
56       * Environment to use.
57       */
58      private final Environment env;
59  
60      /**
61       * Constructor.
62       * @param env Environment to use
63       */
64      public CheckstyleValidator(final Environment env) {
65          this.env = env;
66          this.checker = new Checker();
67          this.listener = new CheckstyleListener(this.env);
68      }
69  
70      @Override
71      public Collection<Violation> validate(final Collection<File> files) {
72          this.checker.setModuleClassLoader(
73              Thread.currentThread().getContextClassLoader()
74          );
75          try {
76              this.checker.configure(this.configuration());
77          } catch (final CheckstyleException ex) {
78              throw new IllegalStateException("Failed to configure checker", ex);
79          }
80          this.checker.addListener(this.listener);
81          final List<File> sources = this.getNonExcludedFiles(files);
82          final Collection<Violation> results = new LinkedList<>();
83          if (sources.isEmpty()) {
84              Logger.debug(
85                  this,
86                  "No files to check with Checkstyle, all %d are excluded",
87                  files.size()
88              );
89          } else {
90              try {
91                  Logger.debug(this, "Checkstyle processing %d files", sources.size());
92                  this.checker.process(sources);
93                  Logger.debug(this, "Checkstyle processed %d files", sources.size());
94              } catch (final CheckstyleException ex) {
95                  throw new IllegalStateException("Failed to process files", ex);
96              }
97              for (final AuditEvent event : this.listener.events()) {
98                  final String check = event.getSourceName();
99                  results.add(
100                     new Violation.Default(
101                         this.name(),
102                         check.substring(check.lastIndexOf('.') + 1),
103                         event.getFileName(),
104                         String.valueOf(event.getLine()),
105                         event.getMessage()
106                     )
107                 );
108             }
109         }
110         return results;
111     }
112 
113     @Override public String name() {
114         return "Checkstyle";
115     }
116 
117     /**
118      * Filters out excluded files from further validation.
119      * @param files Files to validate
120      * @return List of relevant files
121      */
122     public List<File> getNonExcludedFiles(final Collection<File> files) {
123         final List<File> relevant = new LinkedList<>();
124         for (final File file : files) {
125             final String name = new Relative(this.env.basedir(), file).path();
126             if (this.env.exclude("checkstyle", name)) {
127                 continue;
128             }
129             final int dot = name.lastIndexOf('.');
130             if (dot < 0) {
131                 continue;
132             }
133             final String ext = name.substring(dot + 1).toLowerCase(Locale.ROOT);
134             if (!CheckstyleValidator.EXTENSIONS.contains(ext)) {
135                 continue;
136             }
137             relevant.add(file);
138         }
139         return relevant;
140     }
141 
142     /**
143      * Load checkstyle configuration.
144      * @return The configuration just loaded
145      * @see #validate(Collection)
146      */
147     private Configuration configuration() {
148         final File cache =
149             new File(this.env.tempdir(), "checkstyle/checkstyle.cache");
150         final File parent = cache.getParentFile();
151         if (!parent.exists() && !parent.mkdirs()) {
152             throw new IllegalStateException(
153                 String.format(
154                     "Unable to create directories needed for %s",
155                     cache.getPath()
156                 )
157             );
158         }
159         if (!parent.canWrite()) {
160             throw new IllegalStateException(
161                 String.format(
162                     "Cannot write to %s, check filesystem permissions",
163                     parent.getAbsolutePath()
164                 )
165             );
166         }
167         final Properties props = new Properties();
168         props.setProperty("cache.file", cache.getPath());
169         final Configuration config;
170         try (java.io.InputStream stream = this.getClass().getResourceAsStream("checks.xml")) {
171             if (stream == null) {
172                 throw new IllegalStateException(
173                     "Checkstyle configuration file 'checks.xml' not found in classpath."
174                 );
175             }
176             config = ConfigurationLoader.loadConfiguration(
177                 new InputSource(stream),
178                 new PropertiesExpander(props),
179                 ConfigurationLoader.IgnoredModulesOptions.OMIT
180             );
181         } catch (final CheckstyleException | java.io.IOException ex) {
182             throw new IllegalStateException("Failed to load config", ex);
183         }
184         return config;
185     }
186 }