1
2
3
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.DefaultConfiguration;
11 import com.puppycrawl.tools.checkstyle.PropertiesExpander;
12 import com.puppycrawl.tools.checkstyle.api.AuditEvent;
13 import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
14 import com.puppycrawl.tools.checkstyle.api.Configuration;
15 import com.qulice.spi.Environment;
16 import com.qulice.spi.Ignored;
17 import com.qulice.spi.Relative;
18 import com.qulice.spi.ResourceValidator;
19 import com.qulice.spi.Violation;
20 import java.io.File;
21 import java.io.IOException;
22 import java.io.InputStream;
23 import java.util.ArrayList;
24 import java.util.Collection;
25 import java.util.List;
26 import java.util.Locale;
27 import java.util.Properties;
28 import java.util.Set;
29 import org.xml.sax.InputSource;
30
31
32
33
34
35
36 public final class CheckstyleValidator implements ResourceValidator {
37
38
39
40
41
42
43 private static final Set<String> EXTENSIONS = Set.of(
44 "java", "txt", "xml", "xsl", "xsd", "properties", "groovy", "vm",
45 "mf", "sh", "sql", "tokens", "g", "spec", "css", "csv", "js", "json",
46 "md", "yml", "yaml", "gradle", "dtd", "scss", "html"
47 );
48
49
50
51
52
53
54
55
56
57 private static final Set<String> MODERN = Set.of("UseEnhancedSwitch");
58
59
60
61
62
63 private static final int MINIMUM = 14;
64
65
66
67
68 private final Checker checker;
69
70
71
72
73 private final CheckstyleListener listener;
74
75
76
77
78 private final Environment env;
79
80
81
82
83
84
85 public CheckstyleValidator(final Environment env) {
86 this.env = env;
87 this.checker = new Checker();
88 this.listener = new CheckstyleListener(this.env);
89 }
90
91 @Override
92 public Collection<Violation> validate(final Collection<File> files) {
93 this.checker.setModuleClassLoader(
94 Thread.currentThread().getContextClassLoader()
95 );
96 try {
97 this.checker.configure(this.configuration());
98 } catch (final CheckstyleException ex) {
99 throw new IllegalStateException("Failed to configure checker", ex);
100 }
101 this.checker.addListener(this.listener);
102 final List<File> sources = this.getNonExcludedFiles(files);
103 final Collection<Violation> results = new ArrayList<>(0);
104 if (sources.isEmpty()) {
105 Logger.debug(
106 this,
107 "No files to check with Checkstyle, all %d are excluded",
108 files.size()
109 );
110 } else {
111 try {
112 Logger.debug(this, "Checkstyle processing %d files", sources.size());
113 this.checker.process(sources);
114 Logger.debug(this, "Checkstyle processed %d files", sources.size());
115 } catch (final CheckstyleException ex) {
116 throw new IllegalStateException("Failed to process files", ex);
117 }
118 for (final AuditEvent event : this.listener.events()) {
119 final String check = event.getSourceName();
120 results.add(
121 new Violation.Default(
122 this.name(),
123 check.substring(check.lastIndexOf('.') + 1),
124 event.getFileName(),
125 String.valueOf(event.getLine()),
126 event.getMessage()
127 )
128 );
129 }
130 results.addAll(
131 new UnusedSuppressions(this.env).validate(this.listener.processed())
132 );
133 }
134 return results;
135 }
136
137 @Override public String name() {
138 return "Checkstyle";
139 }
140
141 @Override
142 public int rules() {
143 return CheckstyleValidator.count(this.configuration());
144 }
145
146
147
148
149
150
151
152 public List<File> getNonExcludedFiles(final Collection<File> files) {
153 final List<File> relevant = new ArrayList<>(files.size());
154 for (final File file : files) {
155 final String name = new Relative(this.env.basedir(), file).path();
156 if (this.env.exclude("checkstyle", name)) {
157 continue;
158 }
159 if (new Ignored(name).yes()) {
160 continue;
161 }
162 final int dot = name.lastIndexOf('.');
163 if (dot < 0) {
164 continue;
165 }
166 final String ext = name.substring(dot + 1).toLowerCase(Locale.ROOT);
167 if (!CheckstyleValidator.EXTENSIONS.contains(ext)) {
168 continue;
169 }
170 relevant.add(file);
171 }
172 return relevant;
173 }
174
175 private Configuration configuration() {
176 final File cache =
177 new File(this.env.tempdir(), "checkstyle/checkstyle.cache");
178 final File parent = cache.getParentFile();
179 if (!parent.exists() && !parent.mkdirs()) {
180 throw new IllegalStateException(
181 String.format(
182 "Unable to create directories needed for %s",
183 cache.getPath()
184 )
185 );
186 }
187 if (!parent.canWrite()) {
188 throw new IllegalStateException(
189 String.format(
190 "Cannot write to %s, check filesystem permissions",
191 parent.getAbsolutePath()
192 )
193 );
194 }
195 final Properties props = new Properties();
196 props.setProperty("cache.file", cache.getPath());
197 final Configuration config;
198 try (InputStream stream = this.getClass().getResourceAsStream("checks.xml")) {
199 if (stream == null) {
200 throw new IllegalStateException(
201 "Checkstyle configuration file 'checks.xml' not found in classpath."
202 );
203 }
204 config = ConfigurationLoader.loadConfiguration(
205 new InputSource(stream),
206 new PropertiesExpander(props),
207 ConfigurationLoader.IgnoredModulesOptions.OMIT
208 );
209 } catch (final CheckstyleException | IOException ex) {
210 throw new IllegalStateException("Failed to load config", ex);
211 }
212 if (this.level() < CheckstyleValidator.MINIMUM) {
213 CheckstyleValidator.strip(config, CheckstyleValidator.MODERN);
214 }
215 return config;
216 }
217
218 private int level() {
219 int level = CheckstyleValidator.parse(
220 this.env.param("maven.compiler.release", "")
221 );
222 if (level < 0) {
223 level = CheckstyleValidator.parse(
224 this.env.param("maven.compiler.source", "")
225 );
226 }
227 final int result;
228 if (level < 0) {
229 result = CheckstyleValidator.MINIMUM - 1;
230 } else {
231 result = level;
232 }
233 return result;
234 }
235
236 private static int parse(final String value) {
237 int result = -1;
238 if (value != null) {
239 String txt = value.trim();
240 if (txt.startsWith("1.")) {
241 txt = txt.substring(2);
242 }
243 try {
244 result = Integer.parseInt(txt);
245 } catch (final NumberFormatException ex) {
246 result = -1;
247 }
248 }
249 return result;
250 }
251
252 private static int count(final Configuration config) {
253 int total = 0;
254 for (final Configuration child : config.getChildren()) {
255 final String name = child.getName();
256 if (child.getChildren().length > 0) {
257 total += CheckstyleValidator.count(child);
258 } else if (!name.endsWith("Filter") && !name.endsWith("Holder")) {
259 total += 1;
260 }
261 }
262 return total;
263 }
264
265 private static void strip(final Configuration config,
266 final Set<String> names) {
267 for (final Configuration child : config.getChildren()) {
268 final String name = child.getName();
269 final String simple = name.substring(name.lastIndexOf('.') + 1);
270 if (names.contains(simple)) {
271 ((DefaultConfiguration) config).removeChild(child);
272 } else {
273 CheckstyleValidator.strip(child, names);
274 }
275 }
276 }
277 }