1
2
3
4
5 package com.qulice.checkstyle;
6
7 import com.puppycrawl.tools.checkstyle.Checker;
8 import com.puppycrawl.tools.checkstyle.ConfigurationLoader;
9 import com.puppycrawl.tools.checkstyle.DefaultConfiguration;
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.Violation;
17 import java.io.File;
18 import java.io.IOException;
19 import java.io.InputStream;
20 import java.nio.file.Files;
21 import java.util.ArrayList;
22 import java.util.Collection;
23 import java.util.Properties;
24 import java.util.Set;
25 import org.xml.sax.InputSource;
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45 final class UnusedSuppressions {
46
47
48
49
50
51
52 private static final Set<String> FILTERS = Set.of(
53 "SuppressWithNearbyCommentFilter",
54 "SuppressWithNearbyTextFilter",
55 "SuppressWithPlainTextCommentFilter"
56 );
57
58
59
60
61 private final Environment env;
62
63
64
65
66
67
68 UnusedSuppressions(final Environment env) {
69 this.env = env;
70 }
71
72
73
74
75
76
77
78 Collection<Violation> validate(final Collection<File> files) {
79 final Collection<Violation> results = new ArrayList<>(0);
80 if (!files.isEmpty()) {
81 final CheckstyleListener sink = new CheckstyleListener(this.env);
82 this.collect(files, sink);
83 for (final File file : files) {
84 results.addAll(this.unused(file, sink.events()));
85 }
86 }
87 return results;
88 }
89
90 private void collect(final Collection<File> files,
91 final CheckstyleListener sink) {
92 final Checker checker = new Checker();
93 checker.setModuleClassLoader(
94 Thread.currentThread().getContextClassLoader()
95 );
96 final File cache = this.cache();
97 try {
98 checker.configure(this.configuration(cache));
99 checker.addListener(sink);
100 checker.process(new ArrayList<>(files));
101 } catch (final CheckstyleException ex) {
102 throw new IllegalStateException(
103 "Failed to re-run Checkstyle without suppression filters", ex
104 );
105 } finally {
106 checker.destroy();
107 cache.delete();
108 }
109 }
110
111 private Collection<Violation> unused(final File file,
112 final Collection<AuditEvent> events) {
113 final Collection<Violation> results = new ArrayList<>(0);
114 if (!this.env.exclude(
115 "checkstyle", new Relative(this.env.basedir(), file).path()
116 )) {
117 final Collection<AuditEvent> here = new ArrayList<>(0);
118 for (final AuditEvent event : events) {
119 if (file.getPath().equals(event.getFileName())) {
120 here.add(event);
121 }
122 }
123 for (final SuppressionTag tag : new Suppressions(this.read(file))) {
124 if (tag.unused(here)) {
125 results.add(
126 new Violation.Default(
127 "Checkstyle",
128 "UnusedSuppressionCheck",
129 file.getPath(),
130 String.valueOf(tag.line()),
131 String.format(
132 "This suppression covers no violation of \"%s\"",
133 tag.check()
134 )
135 )
136 );
137 }
138 }
139 }
140 return results;
141 }
142
143 private String read(final File file) {
144 try {
145 return Files.readString(file.toPath(), this.env.encoding());
146 } catch (final IOException ex) {
147 throw new IllegalStateException(
148 String.format("Failed to read %s", file), ex
149 );
150 }
151 }
152
153 private File cache() {
154 final File dir = new File(this.env.tempdir(), "checkstyle");
155 if (!dir.exists() && !dir.mkdirs()) {
156 throw new IllegalStateException(
157 String.format("Unable to create %s", dir)
158 );
159 }
160 try {
161 return Files.createTempFile(dir.toPath(), "unused", ".cache").toFile();
162 } catch (final IOException ex) {
163 throw new IllegalStateException(
164 "Failed to create a cache file for the second Checkstyle run", ex
165 );
166 }
167 }
168
169 private Configuration configuration(final File cache) {
170 final Properties props = new Properties();
171 props.setProperty("cache.file", cache.getPath());
172 final Configuration config;
173 try (InputStream stream = this.getClass().getResourceAsStream("checks.xml")) {
174 if (stream == null) {
175 throw new IllegalStateException(
176 "Checkstyle configuration file 'checks.xml' not found in classpath."
177 );
178 }
179 config = ConfigurationLoader.loadConfiguration(
180 new InputSource(stream),
181 new PropertiesExpander(props),
182 ConfigurationLoader.IgnoredModulesOptions.OMIT
183 );
184 } catch (final CheckstyleException | IOException ex) {
185 throw new IllegalStateException("Failed to load config", ex);
186 }
187 UnusedSuppressions.strip(config);
188 return config;
189 }
190
191 private static void strip(final Configuration config) {
192 for (final Configuration child : config.getChildren()) {
193 final String name = child.getName();
194 final String simple = name.substring(name.lastIndexOf('.') + 1);
195 if (UnusedSuppressions.FILTERS.contains(simple)) {
196 ((DefaultConfiguration) config).removeChild(child);
197 } else {
198 UnusedSuppressions.strip(child);
199 }
200 }
201 }
202 }